mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-21 10:03:23 +00:00
feat: emojis for teams
This commit is contained in:
+4
-2
@@ -55,13 +55,14 @@ func handleCreateTeam(r *fastglue.Request) error {
|
||||
app = r.Context.(*App)
|
||||
name = string(r.RequestCtx.PostArgs().Peek("name"))
|
||||
timezone = string(r.RequestCtx.PostArgs().Peek("timezone"))
|
||||
emoji = string(r.RequestCtx.PostArgs().Peek("emoji"))
|
||||
conversationAssignmentType = string(r.RequestCtx.PostArgs().Peek("conversation_assignment_type"))
|
||||
)
|
||||
businessHrsID, err := strconv.Atoi(string(r.RequestCtx.PostArgs().Peek("business_hours_id")))
|
||||
if err != nil || businessHrsID == 0 {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Invalid `business_hours_id`.", nil, envelope.InputError)
|
||||
}
|
||||
if err := app.team.Create(name, timezone, conversationAssignmentType, businessHrsID); err != nil {
|
||||
if err := app.team.Create(name, timezone, conversationAssignmentType, businessHrsID, emoji); err != nil {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
return r.SendEnvelope("Team created successfully.")
|
||||
@@ -73,6 +74,7 @@ func handleUpdateTeam(r *fastglue.Request) error {
|
||||
app = r.Context.(*App)
|
||||
name = string(r.RequestCtx.PostArgs().Peek("name"))
|
||||
timezone = string(r.RequestCtx.PostArgs().Peek("timezone"))
|
||||
emoji = string(r.RequestCtx.PostArgs().Peek("emoji"))
|
||||
conversationAssignmentType = string(r.RequestCtx.PostArgs().Peek("conversation_assignment_type"))
|
||||
)
|
||||
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
|
||||
@@ -86,7 +88,7 @@ func handleUpdateTeam(r *fastglue.Request) error {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Invalid `business_hours_id`.", nil, envelope.InputError)
|
||||
}
|
||||
|
||||
if err = app.team.Update(id, name, timezone, conversationAssignmentType, businessHrsID); err != nil {
|
||||
if err = app.team.Update(id, name, timezone, conversationAssignmentType, businessHrsID, emoji); err != nil {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
return r.SendEnvelope(true)
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
<template>
|
||||
<form @submit="onSubmit" class="space-y-6">
|
||||
|
||||
<FormField name="emoji" v-slot="{ componentField }">
|
||||
<FormItem ref="emojiPickerContainer" class="relative">
|
||||
<FormLabel>Emoji</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" v-bind="componentField" @click="toggleEmojiPicker" />
|
||||
<div v-if="isEmojiPickerVisible" class="absolute z-10 mt-2">
|
||||
<EmojiPicker :native="true" @select="onSelectEmoji" class="w-[300px]" />
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormDescription>Select an emoji.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="name">
|
||||
<FormItem v-auto-animate>
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" placeholder="Name" v-bind="componentField" />
|
||||
@@ -29,7 +44,7 @@
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Round robin: Conversations are assigned to team members in a round-robin fashion. <br>
|
||||
Manual: Conversations are manually assigned to team members.
|
||||
Manual: Conversations are to be picked by team members.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -84,12 +99,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { watch, computed, ref, onMounted } from 'vue'
|
||||
import { ref, watch, computed, onMounted } from 'vue'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { teamFormSchema } from './teamFormSchema.js'
|
||||
import { vAutoAnimate } from '@formkit/auto-animate/vue'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -109,41 +124,35 @@ import {
|
||||
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
|
||||
import { useEmitter } from '@/composables/useEmitter'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import EmojiPicker from 'vue3-emoji-picker'
|
||||
import 'vue3-emoji-picker/css'
|
||||
import { handleHTTPError } from '@/utils/http'
|
||||
import api from '@/api'
|
||||
|
||||
const emitter = useEmitter()
|
||||
const timezones = computed(() => {
|
||||
return Intl.supportedValuesOf('timeZone')
|
||||
})
|
||||
const timezones = computed(() => Intl.supportedValuesOf('timeZone'))
|
||||
const assignmentTypes = ['Round robin', 'Manual']
|
||||
const businessHours = ref([])
|
||||
|
||||
const props = defineProps({
|
||||
initialValues: {
|
||||
type: Object,
|
||||
required: false
|
||||
},
|
||||
submitForm: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
submitLabel: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: () => 'Submit'
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
}
|
||||
initialValues: { type: Object, required: false },
|
||||
submitForm: { type: Function, required: true },
|
||||
submitLabel: { type: String, default: 'Submit' },
|
||||
isLoading: { type: Boolean }
|
||||
})
|
||||
|
||||
const form = useForm({
|
||||
validationSchema: toTypedSchema(teamFormSchema)
|
||||
})
|
||||
|
||||
const isEmojiPickerVisible = ref(false)
|
||||
const emojiPickerContainer = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
fetchBusinessHours()
|
||||
onClickOutside(emojiPickerContainer, () => {
|
||||
isEmojiPickerVisible.value = false
|
||||
})
|
||||
})
|
||||
|
||||
const fetchBusinessHours = async () => {
|
||||
@@ -152,33 +161,39 @@ const fetchBusinessHours = async () => {
|
||||
businessHours.value = response.data.data
|
||||
} catch (error) {
|
||||
// If unauthorized (no permission), show a toast message.
|
||||
if (error.response.status === 403) {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
const toastPayload = error.response.status === 403
|
||||
? {
|
||||
title: 'Unauthorized',
|
||||
variant: 'destructive',
|
||||
description: 'You do not have permission to view business hours.'
|
||||
})
|
||||
} else {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
}
|
||||
: {
|
||||
title: 'Could not fetch business hours',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
}
|
||||
}
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, toastPayload)
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = form.handleSubmit((values) => {
|
||||
const onSubmit = form.handleSubmit(values => {
|
||||
props.submitForm(values)
|
||||
})
|
||||
|
||||
// Watch for changes in initialValues and update the form.
|
||||
watch(
|
||||
() => props.initialValues,
|
||||
(newValues) => {
|
||||
newValues => {
|
||||
if (Object.keys(newValues).length === 0) return
|
||||
form.setValues(newValues)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
function toggleEmojiPicker () {
|
||||
isEmojiPickerVisible.value = !isEmojiPickerVisible.value
|
||||
}
|
||||
|
||||
function onSelectEmoji (emoji) {
|
||||
form.setFieldValue('emoji', emoji.i || emoji)
|
||||
}
|
||||
</script>
|
||||
@@ -8,6 +8,7 @@ export const teamFormSchema = z.object({
|
||||
.min(2, {
|
||||
message: 'Team name must be at least 2 characters.'
|
||||
}),
|
||||
emoji: z.string({ required_error: 'Emoji is required.' }),
|
||||
conversation_assignment_type: z.string({ required_error: 'Conversation assignment type is required.' }),
|
||||
business_hours_id : z.number({ required_error: 'Business hours is required.' }),
|
||||
timezone: z.string().optional(),
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
<CommandEmpty>No team found.</CommandEmpty>
|
||||
<CommandList>
|
||||
<CommandGroup>
|
||||
<CommandItem v-for="team in filteredTeams" :key="team.id" :value="team.id" @select="handleSelectTeam(team.id)">
|
||||
{{ team.name }}
|
||||
<CommandItem v-for="team in filteredTeams" :key="team.id" :value="team.id"
|
||||
@select="handleSelectTeam(team.id)">
|
||||
{{ team.emoji }} {{ team.name }}
|
||||
<CheckIcon :class="cn(
|
||||
'ml-auto h-4 w-4',
|
||||
conversation.assigned_team_id === team.id ? 'opacity-100' : 'opacity-0'
|
||||
|
||||
@@ -590,7 +590,7 @@ const hasConversationOpen = computed(() => {
|
||||
<SidebarMenuSubItem v-for="team in userTeams" :key="team.id">
|
||||
<SidebarMenuButton :isActive="isActiveParent(`/teams/${team.id}`)" asChild>
|
||||
<router-link :to="{ name: 'team-inbox', params: { teamID: team.id } }">
|
||||
<span>{{ team.name }}</span>
|
||||
{{ team.emoji }}<span>{{ team.name }}</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuSubItem>
|
||||
|
||||
@@ -15,8 +15,8 @@ type Team struct {
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
Emoji null.String `db:"emoji" json:"emoji"`
|
||||
Name string `db:"name" json:"name"`
|
||||
ConversationAssignmentType string `db:"conversation_assignment_type" json:"conversation_assignment_type"`
|
||||
Timezone string `db:"timezone" json:"timezone"`
|
||||
ConversationAssignmentType string `db:"conversation_assignment_type" json:"conversation_assignment_type,omitempty"`
|
||||
Timezone string `db:"timezone" json:"timezone,omitempty"`
|
||||
BusinessHoursID int `db:"business_hours_id" json:"business_hours_id,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ JOIN teams t ON t.id = tm.team_id
|
||||
WHERE t.id = $1;
|
||||
|
||||
-- name: insert-team
|
||||
INSERT INTO teams (name, timezone, conversation_assignment_type, business_hours_id) VALUES ($1, $2, $3, $4) RETURNING id;
|
||||
INSERT INTO teams (name, timezone, conversation_assignment_type, business_hours_id, emoji) VALUES ($1, $2, $3, $4, $5) RETURNING id;
|
||||
|
||||
-- name: update-team
|
||||
UPDATE teams set name = $2, timezone = $3, conversation_assignment_type = $4, business_hours_id = $5 where id = $1;
|
||||
UPDATE teams set name = $2, timezone = $3, conversation_assignment_type = $4, business_hours_id = $5, emoji = $6 where id = $1;
|
||||
|
||||
-- name: upsert-user-teams
|
||||
WITH delete_old_teams AS (
|
||||
|
||||
@@ -102,8 +102,8 @@ func (u *Manager) Get(id int) (models.Team, error) {
|
||||
}
|
||||
|
||||
// Create creates a new team.
|
||||
func (u *Manager) Create(name, timezone, conversationAssignmentType string, businessHrsID int) error {
|
||||
if _, err := u.q.InsertTeam.Exec(name, timezone, conversationAssignmentType, businessHrsID); err != nil {
|
||||
func (u *Manager) Create(name, timezone, conversationAssignmentType string, businessHrsID int, emoji string) error {
|
||||
if _, err := u.q.InsertTeam.Exec(name, timezone, conversationAssignmentType, businessHrsID, emoji); err != nil {
|
||||
u.lo.Error("error inserting team", "error", err)
|
||||
return envelope.NewError(envelope.GeneralError, "Error creating team", nil)
|
||||
}
|
||||
@@ -111,8 +111,8 @@ func (u *Manager) Create(name, timezone, conversationAssignmentType string, busi
|
||||
}
|
||||
|
||||
// Update updates an existing team.
|
||||
func (u *Manager) Update(id int, name, timezone, conversationAssignmentType string, businessHrsID int) error {
|
||||
if _, err := u.q.UpdateTeam.Exec(id, name, timezone, conversationAssignmentType, businessHrsID); err != nil {
|
||||
func (u *Manager) Update(id int, name, timezone, conversationAssignmentType string, businessHrsID int, emoji string) error {
|
||||
if _, err := u.q.UpdateTeam.Exec(id, name, timezone, conversationAssignmentType, businessHrsID, emoji); err != nil {
|
||||
u.lo.Error("error updating team", "error", err)
|
||||
return envelope.NewError(envelope.GeneralError, "Error updating team", nil)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ SELECT
|
||||
u.last_name,
|
||||
u.roles,
|
||||
COALESCE(
|
||||
(SELECT json_agg(json_build_object('id', t.id, 'name', t.name))
|
||||
(SELECT json_agg(json_build_object('id', t.id, 'name', t.name, 'emoji', t.emoji))
|
||||
FROM team_members tm
|
||||
JOIN teams t ON tm.team_id = t.id
|
||||
WHERE tm.user_id = u.id),
|
||||
|
||||
Reference in New Issue
Block a user