mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-24 03:16:20 +00:00
refactor: organize frontend project structure
- feat: add toggle for conversation sidebar - save sidebar state in localstorage.
This commit is contained in:
+5
-3
@@ -143,7 +143,7 @@ func main() {
|
||||
messageIncomingQWorkers = ko.MustDuration("message.incoming_queue_workers")
|
||||
messageOutgoingScanInterval = ko.MustDuration("message.message_outoing_scan_interval")
|
||||
slaEvaluationInterval = ko.MustDuration("sla.evaluation_interval")
|
||||
lo = initLogger("libredesk")
|
||||
lo = initLogger(appName)
|
||||
wsHub = ws.NewHub()
|
||||
rdb = initRedis()
|
||||
constants = initConstants()
|
||||
@@ -221,13 +221,15 @@ func main() {
|
||||
}
|
||||
|
||||
go func() {
|
||||
colorlog.Green("Server started at %s", ko.String("app.server.address"))
|
||||
if ko.String("server.socket") != "" {
|
||||
colorlog.Green("Unix socket created at %s", ko.String("server.socket"))
|
||||
}
|
||||
if err := g.ListenAndServe(ko.String("app.server.address"), ko.String("server.socket"), s); err != nil {
|
||||
log.Fatalf("error starting server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
colorlog.Green("🚀 listening on %s %s", ko.String("app.server.address"), ko.String("app.server.socket"))
|
||||
|
||||
// Wait for shutdown signal.
|
||||
<-ctx.Done()
|
||||
colorlog.Red("Shutting down HTTP server...")
|
||||
|
||||
+21
-20
@@ -1,20 +1,22 @@
|
||||
<template>
|
||||
<Sidebar
|
||||
:isLoading="false"
|
||||
:open="sidebarOpen"
|
||||
:userTeams="userStore.teams"
|
||||
:userViews="userViews"
|
||||
@update:open="sidebarOpen = $event"
|
||||
@create-view="openCreateViewForm = true"
|
||||
@edit-view="editView"
|
||||
@delete-view="deleteView"
|
||||
>
|
||||
<div class="w-full h-screen border-l">
|
||||
<PageHeader />
|
||||
<RouterView />
|
||||
</div>
|
||||
<ViewForm v-model:openDialog="openCreateViewForm" v-model:view="view" />
|
||||
</Sidebar>
|
||||
<div class="flex">
|
||||
<!-- Main sidebar -->
|
||||
<Sidebar
|
||||
class="w-full"
|
||||
:userTeams="userStore.teams"
|
||||
:userViews="userViews"
|
||||
@create-view="openCreateViewForm = true"
|
||||
@edit-view="editView"
|
||||
@delete-view="deleteView"
|
||||
>
|
||||
<div class="h-screen border-l">
|
||||
<PageHeader />
|
||||
<RouterView />
|
||||
</div>
|
||||
<ViewForm v-model:openDialog="openCreateViewForm" v-model:view="view" />
|
||||
</Sidebar>
|
||||
</div>
|
||||
<!-- Command box -->
|
||||
<Command />
|
||||
</template>
|
||||
|
||||
@@ -34,15 +36,14 @@ import { useTeamStore } from '@/stores/team'
|
||||
import { useSlaStore } from '@/stores/sla'
|
||||
import { useMacroStore } from '@/stores/macro'
|
||||
import { useTagStore } from '@/stores/tag'
|
||||
import PageHeader from './components/common/PageHeader.vue'
|
||||
import ViewForm from '@/components/ViewForm.vue'
|
||||
import PageHeader from './components/layout/PageHeader.vue'
|
||||
import ViewForm from '@/features/view/ViewForm.vue'
|
||||
import api from '@/api'
|
||||
import Sidebar from '@/components/sidebar/Sidebar.vue'
|
||||
import Command from '@/components/command/CommandBox.vue'
|
||||
import Command from '@/features/command/CommandBox.vue'
|
||||
|
||||
const { toast } = useToast()
|
||||
const emitter = useEmitter()
|
||||
const sidebarOpen = ref(true)
|
||||
const userStore = useUserStore()
|
||||
const conversationStore = useConversationStore()
|
||||
const usersStore = useUsersStore()
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,104 +0,0 @@
|
||||
<template>
|
||||
<form @submit="onLocalFsSubmit" class="space-y-6">
|
||||
<FormField v-slot="{ componentField }" name="provider">
|
||||
<FormItem>
|
||||
<FormLabel>Provider</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
v-bind="componentField"
|
||||
v-model="componentField.modelValue"
|
||||
@update:modelValue="handleProviderUpdate"
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="s3"> S3 </SelectItem>
|
||||
<SelectItem value="localfs"> Local filesystem </SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="upload_path">
|
||||
<FormItem v-auto-animate>
|
||||
<FormLabel>Upload path</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" placeholder="/home/ubuntu/uploads" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription> Path to the directory where files will be uploaded. </FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<Button type="submit"> {{ submitLabel }} </Button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { watch } from 'vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { localFsFormSchema } from './formSchema.js'
|
||||
import { vAutoAnimate } from '@formkit/auto-animate/vue'
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
FormDescription
|
||||
} from '@/components/ui/form'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
const props = defineProps({
|
||||
initialValues: {
|
||||
type: Object,
|
||||
required: false
|
||||
},
|
||||
submitForm: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
submitLabel: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: () => 'Submit'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['provider-update'])
|
||||
|
||||
const localFsForm = useForm({
|
||||
validationSchema: toTypedSchema(localFsFormSchema)
|
||||
})
|
||||
|
||||
const onLocalFsSubmit = localFsForm.handleSubmit((values) => {
|
||||
props.submitForm(values)
|
||||
})
|
||||
|
||||
const handleProviderUpdate = (value) => {
|
||||
emit('provider-update', value)
|
||||
}
|
||||
|
||||
// Watch for changes in initialValues and update the form.
|
||||
watch(
|
||||
() => props.initialValues,
|
||||
(newValues) => {
|
||||
localFsForm.setValues(newValues)
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
</script>
|
||||
@@ -1,191 +0,0 @@
|
||||
<template>
|
||||
<form @submit="onS3FormSubmit" class="space-y-6">
|
||||
<FormField v-slot="{ componentField }" name="provider">
|
||||
<FormItem>
|
||||
<FormLabel>Provider</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
v-bind="componentField"
|
||||
v-model="componentField.modelValue"
|
||||
@update:modelValue="handleProviderUpdate"
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="s3"> S3 </SelectItem>
|
||||
<SelectItem value="localfs"> Local filesystem </SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="region">
|
||||
<FormItem v-auto-animate>
|
||||
<FormLabel>Region</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" placeholder="ap-south-1" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="access_key">
|
||||
<FormItem v-auto-animate>
|
||||
<FormLabel>AWS access key</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" placeholder="AWS access key" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="access_secret">
|
||||
<FormItem v-auto-animate>
|
||||
<FormLabel>AWS access secret</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder="AWS access secret" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="bucket_type">
|
||||
<FormItem>
|
||||
<FormLabel>Bucket type</FormLabel>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField" v-model="componentField.modelValue">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select bucket type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="public"> Public </SelectItem>
|
||||
<SelectItem value="private"> Private </SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="bucket">
|
||||
<FormItem v-auto-animate>
|
||||
<FormLabel>Bucket</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" placeholder="Bucket" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="bucket_path">
|
||||
<FormItem v-auto-animate>
|
||||
<FormLabel>Bucket path</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" placeholder="Bucket path" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="upload_expiry">
|
||||
<FormItem v-auto-animate>
|
||||
<FormLabel>Upload expiry</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" placeholder="24h" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>Only applicable for private buckets.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="url">
|
||||
<FormItem v-auto-animate>
|
||||
<FormLabel>S3 backend URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="url"
|
||||
placeholder="https://ap-south-1.s3.amazonaws.com"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription
|
||||
>Only change if using a custom S3 compatible backend like Minio.</FormDescription
|
||||
>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<Button type="submit"> {{ submitLabel }} </Button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { watch, defineEmits } from 'vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { s3FormSchema } from './formSchema.js'
|
||||
import { vAutoAnimate } from '@formkit/auto-animate/vue'
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
FormDescription
|
||||
} from '@/components/ui/form'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
const props = defineProps({
|
||||
initialValues: {
|
||||
type: Object,
|
||||
required: false
|
||||
},
|
||||
submitForm: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
submitLabel: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: () => 'Submit'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['provider-update'])
|
||||
|
||||
const s3Form = useForm({
|
||||
validationSchema: toTypedSchema(s3FormSchema)
|
||||
})
|
||||
|
||||
const onS3FormSubmit = s3Form.handleSubmit((values) => {
|
||||
props.submitForm(values)
|
||||
})
|
||||
|
||||
const handleProviderUpdate = (value) => {
|
||||
emit('provider-update', value)
|
||||
}
|
||||
|
||||
// Watch for changes in initialValues and update the form.
|
||||
watch(
|
||||
() => props.initialValues,
|
||||
(newValues) => {
|
||||
s3Form.setValues(newValues)
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
</script>
|
||||
@@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<component
|
||||
:is="formProvider === 's3' ? S3Form : LocalFsForm"
|
||||
:submitForm="submitForm"
|
||||
:initialValues="initialValues"
|
||||
submitLabel="Save"
|
||||
@provider-update="handleProviderUpdate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import S3Form from './S3Form.vue'
|
||||
import LocalFsForm from './LocalFsForm.vue'
|
||||
import api from '@/api'
|
||||
|
||||
|
||||
const initialValues = ref({})
|
||||
|
||||
onMounted(async () => {
|
||||
const resp = await api.getSettings('upload')
|
||||
const config = resp.data.data
|
||||
const modifiedConfig = {}
|
||||
for (const key in config) {
|
||||
if (key.startsWith('upload.localfs.')) {
|
||||
const newKey = key.replace('upload.localfs.', '')
|
||||
modifiedConfig[newKey] = config[key]
|
||||
} else if (key.startsWith('upload.s3')) {
|
||||
const newKey = key.replace('upload.s3.', '')
|
||||
modifiedConfig[newKey] = config[key]
|
||||
} else if (key.startsWith('upload.')) {
|
||||
const newKey = key.replace('upload.', '')
|
||||
modifiedConfig[newKey] = config[key]
|
||||
}
|
||||
}
|
||||
initialValues.value = modifiedConfig
|
||||
})
|
||||
|
||||
const submitForm = async (values) => {
|
||||
const prefixedValues = {}
|
||||
for (const key in values) {
|
||||
if (values.provider === 'localfs') {
|
||||
prefixedValues[`upload.localfs.${key}`] = values[key]
|
||||
} else if (values.provider === 's3') {
|
||||
prefixedValues[`upload.s3.${key}`] = values[key]
|
||||
}
|
||||
}
|
||||
prefixedValues['upload.provider'] = values.provider
|
||||
await api.updateSettings('upload', prefixedValues)
|
||||
}
|
||||
|
||||
const formProvider = computed(() => {
|
||||
return initialValues.value.provider
|
||||
})
|
||||
|
||||
const handleProviderUpdate = (value) => {
|
||||
initialValues.value.provider = value
|
||||
}
|
||||
</script>
|
||||
@@ -1,44 +0,0 @@
|
||||
import * as z from 'zod'
|
||||
|
||||
export const s3FormSchema = z.object({
|
||||
provider: z.string({
|
||||
required_error: 'Provider is required.'
|
||||
}),
|
||||
region: z.string({
|
||||
required_error: 'Region is required.'
|
||||
}),
|
||||
access_key: z.string({
|
||||
required_error: 'AWS access key is required.'
|
||||
}),
|
||||
access_secret: z.string({
|
||||
required_error: 'AWS access secret is required.'
|
||||
}),
|
||||
bucket_type: z.string({
|
||||
required_error: 'Bucket type is required.'
|
||||
}),
|
||||
bucket: z.string({
|
||||
required_error: 'Bucket is required.'
|
||||
}),
|
||||
bucket_path: z.string({
|
||||
required_error: 'Bucket path is required.'
|
||||
}),
|
||||
upload_expiry: z.string({
|
||||
required_error: 'Upload expiry is required.'
|
||||
}),
|
||||
url: z
|
||||
.string({
|
||||
required_error: 'S3 backend URL is required.'
|
||||
})
|
||||
.url({
|
||||
message: 'S3 backend URL must be a valid URL.'
|
||||
})
|
||||
})
|
||||
|
||||
export const localFsFormSchema = z.object({
|
||||
provider: z.string({
|
||||
required_error: 'Provider is required.'
|
||||
}),
|
||||
upload_path: z.string({
|
||||
required_error: 'Upload path is required.'
|
||||
})
|
||||
})
|
||||
@@ -1,74 +0,0 @@
|
||||
<template>
|
||||
<div v-if="conversationStore.messages.data">
|
||||
<!-- Header -->
|
||||
<div class="p-2 border-b flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3 text-sm">
|
||||
<div class="font-medium">
|
||||
{{ conversationStore.current?.subject }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<div
|
||||
class="flex items-center space-x-1 cursor-pointer bg-primary px-2 py-1 rounded-md text-sm"
|
||||
>
|
||||
<span
|
||||
class="text-secondary font-medium inline-block"
|
||||
v-if="conversationStore.current?.status"
|
||||
>
|
||||
{{ conversationStore.current?.status }}
|
||||
</span>
|
||||
<span v-else class="text-secondary font-medium inline-block"> Loading... </span>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
v-for="status in conversationStore.statusOptions"
|
||||
:key="status.value"
|
||||
@click="handleUpdateStatus(status.label)"
|
||||
>
|
||||
{{ status.label }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Messages & reply box -->
|
||||
<div>
|
||||
<div class="flex flex-col h-[calc(100vh-theme(spacing.10))]">
|
||||
<MessageList class="flex-1 overflow-y-auto" />
|
||||
<div class="sticky bottom-0 bg-white">
|
||||
<ReplyBox class="h-max" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useConversationStore } from '@/stores/conversation'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import MessageList from '@/components/message/MessageList.vue'
|
||||
import ReplyBox from './ReplyBox.vue'
|
||||
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
|
||||
import { CONVERSATION_DEFAULT_STATUSES } from '@/constants/conversation'
|
||||
import { useEmitter } from '@/composables/useEmitter'
|
||||
|
||||
const conversationStore = useConversationStore()
|
||||
const emitter = useEmitter()
|
||||
|
||||
const handleUpdateStatus = (status) => {
|
||||
if (status === CONVERSATION_DEFAULT_STATUSES.SNOOZED) {
|
||||
emitter.emit(EMITTER_EVENTS.SET_NESTED_COMMAND, 'snooze')
|
||||
return
|
||||
}
|
||||
conversationStore.updateStatus(status)
|
||||
}
|
||||
</script>
|
||||
@@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<h3 class="h-screen flex items-center justify-center">
|
||||
<div class="flex flex-row items-center justify-center">
|
||||
<p>Select a conversation from the left panel.</p>
|
||||
</div>
|
||||
</h3>
|
||||
</template>
|
||||
@@ -1,90 +0,0 @@
|
||||
<template>
|
||||
<div class="flex justify-end px-2 py-2 border-b w-full">
|
||||
<Popover v-model:open="open">
|
||||
<PopoverTrigger as-child>
|
||||
<div class="flex items-center mr-2 relative">
|
||||
<span class="absolute inline-flex h-2 w-2 rounded-full bg-primary opacity-75 right-0 bottom-5 z-20"
|
||||
v-if="conversationStore.conversations.filters.length > 0" />
|
||||
<ListFilter size="27"
|
||||
class="mx-auto cursor-pointer transition-all transform hover:scale-110 hover:bg-secondary hover:bg-opacity-80 p-1 rounded-md z-10" />
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[450px]">
|
||||
<Filter v-model:modelValue="localFilters" :fields="fields" @apply="handleApply" @clear="handleClear" />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { ListFilter } from 'lucide-vue-next'
|
||||
import { useConversationStore } from '@/stores/conversation'
|
||||
import Filter from '@/components/common/FilterBuilder.vue'
|
||||
import api from '@/api'
|
||||
|
||||
const conversationStore = useConversationStore()
|
||||
const open = ref(false)
|
||||
const localFilters = ref([])
|
||||
const statuses = ref([])
|
||||
const priorities = ref([])
|
||||
const emit = defineEmits(['updateFilters'])
|
||||
|
||||
onMounted(() => {
|
||||
fetchInitialData()
|
||||
localFilters.value = [...conversationStore.conversations.filters]
|
||||
})
|
||||
|
||||
const fetchInitialData = async () => {
|
||||
const [statusesResp, prioritiesResp] = await Promise.all([
|
||||
api.getStatuses(),
|
||||
api.getPriorities()
|
||||
])
|
||||
|
||||
statuses.value = statusesResp.data.data.map(status => ({
|
||||
label: status.name,
|
||||
value: status.id.toString(),
|
||||
}))
|
||||
|
||||
priorities.value = prioritiesResp.data.data.map(priority => ({
|
||||
label: priority.name,
|
||||
value: priority.id.toString(),
|
||||
}))
|
||||
}
|
||||
|
||||
const fields = ref([
|
||||
{
|
||||
model: 'conversations',
|
||||
label: 'Status',
|
||||
value: 'status_id',
|
||||
type: 'select',
|
||||
options: statuses,
|
||||
},
|
||||
{
|
||||
model: 'conversations',
|
||||
label: 'Priority',
|
||||
value: 'priority_id',
|
||||
type: 'select',
|
||||
options: priorities,
|
||||
},
|
||||
{
|
||||
model: 'conversations',
|
||||
label: 'Reference number',
|
||||
value: 'reference_number',
|
||||
type: 'text',
|
||||
}
|
||||
])
|
||||
|
||||
const handleApply = (filters) => {
|
||||
emit('updateFilters', filters)
|
||||
open.value = false
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
localFilters.value = []
|
||||
emit('updateFilters', [])
|
||||
open.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -1,36 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Avatar class="size-20">
|
||||
<AvatarImage
|
||||
:src="conversation?.contact?.avatar_url"
|
||||
v-if="conversation?.contact?.avatar_url"
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{{ conversation?.contact.first_name.toUpperCase().substring(0, 2) }}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<h4 class="mt-3">
|
||||
{{ conversation?.contact.first_name + ' ' + conversation?.contact.last_name }}
|
||||
</h4>
|
||||
<p class="text-sm text-muted-foreground flex gap-2 mt-1" v-if="conversation?.contact.email">
|
||||
<Mail class="size-3 mt-1"></Mail>
|
||||
{{ conversation.contact.email }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground flex gap-2 mt-1">
|
||||
<Phone class="size-3 mt-1"></Phone>
|
||||
<span>
|
||||
{{
|
||||
conversation?.contact?.phone_number ? conversation.contact.phone_number : 'Not available'
|
||||
}}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Mail, Phone } from 'lucide-vue-next'
|
||||
defineProps({
|
||||
conversation: Object
|
||||
})
|
||||
</script>
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-if="!isHidden">
|
||||
<div class="flex items-center space-x-4 p-4">
|
||||
<SidebarTrigger class="cursor-pointer w-5 h-5" />
|
||||
<SidebarTrigger class="cursor-pointer w-4 h-4" />
|
||||
<span class="text-2xl font-semibold">
|
||||
{{ title }}
|
||||
</span>
|
||||
@@ -40,26 +40,20 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useConversationStore } from '@/stores/conversation'
|
||||
import ConversationSideBar from '@/components/conversation/sidebar/ConversationSideBar.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
defineProps({
|
||||
isLoading: Boolean,
|
||||
open: Boolean,
|
||||
activeItem: { type: Object, default: () => {} },
|
||||
activeGroup: { type: Object, default: () => {} },
|
||||
userTeams: { type: Array, default: () => [] },
|
||||
userViews: { type: Array, default: () => [] }
|
||||
})
|
||||
|
||||
const conversationStore = useConversationStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const emit = defineEmits(['createView', 'editView', 'deleteView', 'update:open'])
|
||||
const emit = defineEmits(['createView', 'editView', 'deleteView'])
|
||||
|
||||
const openCreateViewDialog = () => {
|
||||
emit('createView')
|
||||
@@ -153,7 +147,6 @@ const adminNavItems = [
|
||||
},
|
||||
{
|
||||
title: 'Conversations',
|
||||
description: 'Manage tags, macros, and statuses.',
|
||||
children: [
|
||||
{
|
||||
title: 'Tags',
|
||||
@@ -174,7 +167,6 @@ const adminNavItems = [
|
||||
},
|
||||
{
|
||||
title: 'Inboxes',
|
||||
description: 'Manage inboxes.',
|
||||
children: [
|
||||
{
|
||||
title: 'Inboxes',
|
||||
@@ -185,7 +177,6 @@ const adminNavItems = [
|
||||
},
|
||||
{
|
||||
title: 'Teammates',
|
||||
description: 'Manage users, teams, and roles.',
|
||||
children: [
|
||||
{
|
||||
title: 'Users',
|
||||
@@ -206,7 +197,6 @@ const adminNavItems = [
|
||||
},
|
||||
{
|
||||
title: 'Automations',
|
||||
description: 'Manage automation rules.',
|
||||
children: [
|
||||
{
|
||||
title: 'Automations',
|
||||
@@ -217,7 +207,6 @@ const adminNavItems = [
|
||||
},
|
||||
{
|
||||
title: 'Notifications',
|
||||
description: 'Manage email notifications.',
|
||||
children: [
|
||||
{
|
||||
title: 'Email',
|
||||
@@ -228,7 +217,6 @@ const adminNavItems = [
|
||||
},
|
||||
{
|
||||
title: 'Templates',
|
||||
description: 'Manage email templates.',
|
||||
children: [
|
||||
{
|
||||
title: 'Templates',
|
||||
@@ -239,7 +227,6 @@ const adminNavItems = [
|
||||
},
|
||||
{
|
||||
title: 'Security',
|
||||
description: 'Configure SSO and security.',
|
||||
children: [
|
||||
{
|
||||
title: 'OpenID Connect SSO',
|
||||
@@ -263,23 +250,21 @@ const isActiveParent = (parentHref) => {
|
||||
}
|
||||
|
||||
const isInboxRoute = (path) => {
|
||||
return path.startsWith('/inboxes') || path.startsWith('/teams') || path.startsWith('/views')
|
||||
return path.startsWith('/inboxes')
|
||||
}
|
||||
|
||||
const hasConversationOpen = computed(() => {
|
||||
return conversationStore.current || conversationStore.conversation.loading
|
||||
})
|
||||
const sidebarOpen = useStorage('sidebarOpen', true)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-row justify-between h-full">
|
||||
<div class="flex-1">
|
||||
<SidebarProvider
|
||||
:open="open"
|
||||
@update:open="($event) => emit('update:open', $event)"
|
||||
style="--sidebar-width: 16rem"
|
||||
:default-open="sidebarOpen"
|
||||
v-on:update:open="sidebarOpen = $event"
|
||||
>
|
||||
<!-- Flex Container that holds all the sidebar components -->
|
||||
|
||||
<Sidebar
|
||||
collapsible="icon"
|
||||
class="overflow-hidden [&>[data-sidebar=sidebar]]:flex-row !border-r-0"
|
||||
@@ -591,7 +576,7 @@ const hasConversationOpen = computed(() => {
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuSubButton
|
||||
size="sm"
|
||||
:isActive="isActiveParent(`/teams/${team.id}`)"
|
||||
:isActive="isActiveParent(`/inboxes/teams/${team.id}`)"
|
||||
asChild
|
||||
>
|
||||
<router-link
|
||||
@@ -637,7 +622,7 @@ const hasConversationOpen = computed(() => {
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuSubButton
|
||||
size="sm"
|
||||
:isActive="isActiveParent(`/views/${view.id}`)"
|
||||
:isActive="isActiveParent(`/inboxes/views/${view.id}`)"
|
||||
asChild
|
||||
>
|
||||
<router-link
|
||||
@@ -680,24 +665,5 @@ const hasConversationOpen = computed(() => {
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
|
||||
<!-- Right Sidebar with conversation details -->
|
||||
<div v-if="hasConversationOpen">
|
||||
<SidebarProvider :open="true" style="--sidebar-width: 20rem">
|
||||
<Sidebar collapsible="none" side="right">
|
||||
<SidebarSeparator />
|
||||
<SidebarContent>
|
||||
<SidebarGroup style="padding: 0">
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<ConversationSideBar />
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
// composables/useSla.js
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { calculateSla } from '@/utils/sla'
|
||||
|
||||
export function useSla (dueAt, actualAt) {
|
||||
const sla = ref(null)
|
||||
|
||||
|
||||
function updateSla () {
|
||||
if (!dueAt.value) {
|
||||
sla.value = null
|
||||
@@ -13,7 +10,6 @@ export function useSla (dueAt, actualAt) {
|
||||
}
|
||||
sla.value = calculateSla(dueAt.value, actualAt.value)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateSla()
|
||||
// Update the SLA every 30 seconds.
|
||||
@@ -22,6 +18,5 @@ export function useSla (dueAt, actualAt) {
|
||||
clearInterval(intervalId)
|
||||
})
|
||||
})
|
||||
|
||||
return { sla, updateSla }
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ref, onUnmounted, nextTick } from 'vue'
|
||||
|
||||
export function useTemporaryClass(containerID, className, timeMs = 300) {
|
||||
const container = ref(null)
|
||||
|
||||
const applyClass = async () => {
|
||||
await nextTick()
|
||||
container.value = document.getElementById(containerID)
|
||||
@@ -13,16 +12,13 @@ export function useTemporaryClass(containerID, className, timeMs = 300) {
|
||||
}, timeMs)
|
||||
}
|
||||
}
|
||||
|
||||
applyClass()
|
||||
|
||||
onUnmounted(() => {
|
||||
if (container.value) {
|
||||
container.value.classList.remove(className)
|
||||
container.value = null
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
container
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@ export const EMITTER_EVENTS = {
|
||||
SHOW_SOONER: 'show-sooner',
|
||||
NEW_MESSAGE: 'new-message',
|
||||
SET_NESTED_COMMAND: 'set-nested-command',
|
||||
CONVERSATION_SIDEBAR_TOGGLE: 'conversation-sidebar-toggle'
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
export const WS_EVENT = {
|
||||
NEW_MESSAGE: 'new_message',
|
||||
MESSAGE_PROP_UPDATE: 'message_prop_update',
|
||||
CONVERSATION_PROP_UPDATE: 'conversation_prop_update',
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -141,7 +141,7 @@ import { cn } from '@/lib/utils'
|
||||
import { format } from 'date-fns'
|
||||
import { WEEKDAYS } from '@/constants/date'
|
||||
import { Calendar as CalendarIcon } from 'lucide-vue-next'
|
||||
import SimpleTable from '@/components/common/SimpleTable.vue'
|
||||
import SimpleTable from '@/components/table/SimpleTable.vue'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
+1
-1
@@ -129,7 +129,7 @@ import { Spinner } from '@/components/ui/spinner'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||
import ActionBuilder from '@/components/common/ActionBuilder.vue'
|
||||
import ActionBuilder from '@/features/admin/macros/ActionBuilder.vue'
|
||||
import { useConversationFilters } from '@/composables/useConversationFilters'
|
||||
import { useUsersStore } from '@/stores/users'
|
||||
import { useTeamStore } from '@/stores/team'
|
||||
+3
-3
@@ -62,9 +62,9 @@
|
||||
|
||||
<FormField v-slot="{ componentField }" name="redirect_uri" v-if="!isNewForm">
|
||||
<FormItem v-auto-animate>
|
||||
<FormLabel>Redirect URI</FormLabel>
|
||||
<FormLabel>Callback URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" placeholder="Redirect URI" v-bind="componentField" readonly />
|
||||
<Input type="text" placeholder="Redirect URL" v-bind="componentField" readonly />
|
||||
</FormControl>
|
||||
<FormDescription>Set this URI for callback.</FormDescription>
|
||||
<FormMessage />
|
||||
@@ -135,7 +135,7 @@ const props = defineProps({
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const form = useForm({
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { h } from 'vue'
|
||||
import TeamDataTableDropdown from '@/components/admin/team/teams/TeamDataTableDropdown.vue'
|
||||
import TeamDataTableDropdown from '@/features/admin/teams/TeamDataTableDropdown.vue'
|
||||
import { format } from 'date-fns'
|
||||
|
||||
export const columns = [
|
||||
+1
-1
@@ -74,7 +74,7 @@ import {
|
||||
FormDescription
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import CodeEditor from '@/components/common/CodeEditor.vue'
|
||||
import CodeEditor from '@/components/editor/CodeEditor.vue'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Label } from '@/components/ui/label'
|
||||
const props = defineProps({
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { h } from 'vue'
|
||||
import UserDataTableDropDown from '@/components/admin/team/users/dataTableDropdown.vue'
|
||||
import UserDataTableDropDown from '@/features/admin/users/dataTableDropdown.vue'
|
||||
import { format } from 'date-fns'
|
||||
|
||||
export const columns = [
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="flex flex-col w-full">
|
||||
<div>
|
||||
<!-- Header -->
|
||||
<div class="p-2 border-b flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3 text-sm">
|
||||
<div class="font-medium">
|
||||
{{ conversationStore.current?.subject }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<div
|
||||
class="flex items-center space-x-1 cursor-pointer bg-primary px-2 py-1 rounded-md text-sm"
|
||||
>
|
||||
<span
|
||||
class="text-secondary font-medium inline-block"
|
||||
v-if="conversationStore.current?.status"
|
||||
>
|
||||
{{ conversationStore.current?.status }}
|
||||
</span>
|
||||
<span v-else class="text-secondary font-medium inline-block"> Loading... </span>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
v-for="status in conversationStore.statusOptions"
|
||||
:key="status.value"
|
||||
@click="handleUpdateStatus(status.label)"
|
||||
>
|
||||
{{ status.label }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Messages & reply box -->
|
||||
<div>
|
||||
<div class="flex flex-col h-[calc(100vh-theme(spacing.10))]">
|
||||
<MessageList class="flex-1 overflow-y-auto" />
|
||||
<div class="sticky bottom-0 bg-white">
|
||||
<ReplyBox class="h-max" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useConversationStore } from '@/stores/conversation'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import MessageList from '@/features/conversation/message/MessageList.vue'
|
||||
import ReplyBox from './ReplyBox.vue'
|
||||
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
|
||||
import { CONVERSATION_DEFAULT_STATUSES } from '@/constants/conversation'
|
||||
import { useEmitter } from '@/composables/useEmitter'
|
||||
const conversationStore = useConversationStore()
|
||||
const emitter = useEmitter()
|
||||
|
||||
const handleUpdateStatus = (status) => {
|
||||
if (status === CONVERSATION_DEFAULT_STATUSES.SNOOZED) {
|
||||
emitter.emit(EMITTER_EVENTS.SET_NESTED_COMMAND, 'snooze')
|
||||
return
|
||||
}
|
||||
conversationStore.updateStatus(status)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="h-screen w-full flex items-center justify-center">
|
||||
<p>Select a conversation from the left panel.</p>
|
||||
</div>
|
||||
</template>
|
||||
+3
-3
@@ -263,9 +263,9 @@ import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { useEmitter } from '@/composables/useEmitter'
|
||||
import AttachmentsPreview from '@/components/attachment/AttachmentsPreview.vue'
|
||||
import MacroActionsPreview from '../macro/MacroActionsPreview.vue'
|
||||
import ReplyBoxBottomMenuBar from '@/components/conversation/ReplyBoxMenuBar.vue'
|
||||
import AttachmentsPreview from '@/features/conversation/message/attachment/AttachmentsPreview.vue'
|
||||
import MacroActionsPreview from '@/features/conversation/MacroActionsPreview.vue'
|
||||
import ReplyBoxBottomMenuBar from '@/features/conversation/ReplyBoxMenuBar.vue'
|
||||
|
||||
const conversationStore = useConversationStore()
|
||||
const emitter = useEmitter()
|
||||
+4
-4
@@ -9,7 +9,7 @@
|
||||
</header>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="bg-white px-4 py-3 flex justify-between items-center">
|
||||
<div class="bg-white px-2 py-3 flex justify-between items-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" class="w-30">
|
||||
@@ -144,10 +144,10 @@ import {
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { SidebarTrigger } from '@/components/ui/sidebar'
|
||||
import EmptyList from '@/components/conversation/list/ConversationEmptyList.vue'
|
||||
import ConversationListItem from '@/components/conversation/list/ConversationListItem.vue'
|
||||
import EmptyList from '@/features/conversation/list/ConversationEmptyList.vue'
|
||||
import ConversationListItem from '@/features/conversation/list/ConversationListItem.vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import ConversationListItemSkeleton from '@/components/conversation/list/ConversationListItemSkeleton.vue'
|
||||
import ConversationListItemSkeleton from '@/features/conversation/list/ConversationListItemSkeleton.vue'
|
||||
|
||||
const conversationStore = useConversationStore()
|
||||
const route = useRoute()
|
||||
+1
-2
@@ -65,7 +65,7 @@ import { useRouter, useRoute } from 'vue-router'
|
||||
import { formatTime } from '@/utils/datetime'
|
||||
import { Mail, CheckCheck } from 'lucide-vue-next'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import SlaDisplay from '@/components/sla/SlaDisplay.vue'
|
||||
import SlaDisplay from '@/features/sla/SlaDisplay.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -82,7 +82,6 @@ const navigateToConversation = (uuid) => {
|
||||
: route.name.includes('view')
|
||||
? 'view-inbox-conversation'
|
||||
: 'inbox-conversation'
|
||||
|
||||
router.push({
|
||||
name: baseRoute,
|
||||
params: {
|
||||
+1
-1
@@ -74,7 +74,7 @@ import { revertCIDToImageSrc } from '@/utils/strings'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { Spinner } from '@/components/ui/spinner'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import MessageAttachmentPreview from '@/components/attachment/MessageAttachmentPreview.vue'
|
||||
import MessageAttachmentPreview from '@/features/conversation/message/attachment/MessageAttachmentPreview.vue'
|
||||
import api from '@/api'
|
||||
|
||||
const props = defineProps({
|
||||
+1
-1
@@ -73,7 +73,7 @@ import { useConversationStore } from '@/stores/conversation'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Letter } from 'vue-letter'
|
||||
import MessageAttachmentPreview from '@/components/attachment/MessageAttachmentPreview.vue'
|
||||
import MessageAttachmentPreview from '@/features/conversation/message/attachment/MessageAttachmentPreview.vue'
|
||||
|
||||
const props = defineProps({
|
||||
message: Object
|
||||
+2
-2
@@ -14,8 +14,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ImageAttachmentPreview from '@/components/attachment/ImageAttachmentPreview.vue'
|
||||
import FileAttachmentPreview from '@/components/attachment/FileAttachmentPreview.vue'
|
||||
import ImageAttachmentPreview from '@/features/conversation/message/attachment/ImageAttachmentPreview.vue'
|
||||
import FileAttachmentPreview from '@/features/conversation/message/attachment/FileAttachmentPreview.vue'
|
||||
|
||||
defineProps({
|
||||
attachments: {
|
||||
+1
-1
@@ -53,7 +53,7 @@
|
||||
|
||||
<script setup>
|
||||
import { format } from 'date-fns'
|
||||
import SlaDisplay from '@/components/sla/SlaDisplay.vue'
|
||||
import SlaDisplay from '@/features/sla/SlaDisplay.vue'
|
||||
defineProps({
|
||||
conversation: Object
|
||||
})
|
||||
+6
-7
@@ -1,11 +1,10 @@
|
||||
<template>
|
||||
<div v-if="conversationStore.current" class="h-screen overflow-y-auto">
|
||||
<div>
|
||||
<ConversationSideBarContact :conversation="conversationStore.current" class="p-4 border-b" />
|
||||
<Accordion
|
||||
type="multiple"
|
||||
collapsible
|
||||
:default-value="['Actions', 'Information', 'Previous conversations']"
|
||||
class="border-t"
|
||||
>
|
||||
<AccordionItem value="Actions">
|
||||
<AccordionTrigger class="bg-muted px-4 py-3 text-sm font-medium">
|
||||
@@ -191,12 +190,13 @@ import {
|
||||
AccordionTrigger
|
||||
} from '@/components/ui/accordion'
|
||||
import ConversationInfo from './ConversationInfo.vue'
|
||||
import ConversationSideBarContact from '@/components/conversation/sidebar/ConversationSideBarContact.vue'
|
||||
import ConversationSideBarContact from '@/features/conversation/sidebar/ConversationSideBarContact.vue'
|
||||
import ComboBox from '@/components/ui/combobox/ComboBox.vue'
|
||||
import { SelectTag } from '@/components/ui/select'
|
||||
import { useToast } from '@/components/ui/toast/use-toast'
|
||||
import { handleHTTPError } from '@/utils/http'
|
||||
import api from '@/api'
|
||||
import { SidebarTrigger } from '@/components/ui/sidebar'
|
||||
import { CircleAlert, SignalLow, SignalMedium, SignalHigh, Users } from 'lucide-vue-next'
|
||||
|
||||
const { toast } = useToast()
|
||||
@@ -219,9 +219,9 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
const assignedUserID = computed(() => String(conversationStore.current.assigned_user_id))
|
||||
const assignedTeamID = computed(() => String(conversationStore.current.assigned_team_id))
|
||||
const priorityID = computed(() => String(conversationStore.current.priority_id))
|
||||
const assignedUserID = computed(() => String(conversationStore.current?.assigned_user_id))
|
||||
const assignedTeamID = computed(() => String(conversationStore.current?.assigned_team_id))
|
||||
const priorityID = computed(() => String(conversationStore.current?.priority_id))
|
||||
const priorityOptions = computed(() => conversationStore.priorityOptions)
|
||||
|
||||
const fetchTags = async () => {
|
||||
@@ -285,7 +285,6 @@ const selectPriority = (priority) => {
|
||||
}
|
||||
|
||||
const getPriorityIcon = (value) => {
|
||||
console.log(value)
|
||||
switch (value) {
|
||||
case '1':
|
||||
return SignalLow
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex justify-between items-start">
|
||||
<Avatar class="size-20">
|
||||
<AvatarImage
|
||||
:src="conversation?.contact?.avatar_url"
|
||||
v-if="conversation?.contact?.avatar_url"
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{{ conversation?.contact?.first_name?.toUpperCase().substring(0, 2) }}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div class="mt-2">
|
||||
<PanelLeft
|
||||
class="cursor-pointer"
|
||||
@click="emitter.emit(EMITTER_EVENTS.CONVERSATION_SIDEBAR_TOGGLE)"
|
||||
size="16"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="mt-3">
|
||||
{{ conversation?.contact?.first_name + ' ' + conversation?.contact?.last_name }}
|
||||
</h4>
|
||||
<p class="text-sm text-muted-foreground flex gap-2 mt-1" v-if="conversation?.contact?.email">
|
||||
<Mail class="size-3 mt-1" />
|
||||
{{ conversation.contact.email }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground flex gap-2 mt-1">
|
||||
<Phone class="size-3 mt-1" />
|
||||
<span>
|
||||
{{ conversation?.contact?.phone_number || 'Not available' }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { PanelLeft } from 'lucide-vue-next'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Mail, Phone } from 'lucide-vue-next'
|
||||
import { useEmitter } from '@/composables/useEmitter'
|
||||
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
|
||||
|
||||
const emitter = useEmitter()
|
||||
|
||||
defineProps({
|
||||
conversation: Object
|
||||
})
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user