fix: email channel content not being rendered in template

- fix: resolved_at, closed_at timestamps getting cleared.
- feat: generic code editor for Email templates
- feat: Quill editor for canned responses
- Refactor: to reuse forms across /admin/conversations components
- Update sample toml remove unused key.
- Fix: multiple system user pass prompts when setting up for the first time
- Remove unused file session.go
- Fix: Inline images showing up in reply box as an attachment
- Update vite config to proxy requests to `/uploads`
This commit is contained in:
Abhinav Raut
2024-10-24 19:19:01 +05:30
parent ee7be54c0d
commit f1b4007d7d
34 changed files with 570 additions and 316 deletions
-1
View File
@@ -78,7 +78,6 @@ func main() {
// Installer.
if ko.Bool("install") {
install(db, fs)
setSystemUserPass(db)
os.Exit(0)
}
-1
View File
@@ -1 +0,0 @@
package main
+1 -1
View File
@@ -5,7 +5,7 @@ env = "dev"
# HTTP server.
[app.server]
address = "0.0.0.0:9009"
address = "0.0.0.0:9000"
socket = ""
read_timeout = "5s"
write_timeout = "5s"
+2
View File
@@ -33,10 +33,12 @@
"@vee-validate/zod": "^4.13.2",
"@vue/reactivity": "^3.4.15",
"@vue/runtime-core": "^3.4.15",
"@vueup/vue-quill": "^1.2.0",
"@vueuse/core": "^10.11.1",
"add": "^2.0.6",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"codeflask": "^1.4.1",
"date-fns": "^3.6.0",
"install": "^0.13.0",
"lucide-vue-next": "^0.378.0",
+9 -9
View File
@@ -1,7 +1,7 @@
<template>
<Toaster />
<TooltipProvider :delay-duration="200">
<div class="font-poppins">
<div class="font-inter">
<div v-if="$route.path !== '/'">
<div class="flex">
<NavBar :is-collapsed="isCollapsed" :links="navLinks" :bottom-links="bottomLinks"
@@ -24,24 +24,24 @@
</template>
<script setup>
import { ref, reactive, onMounted, computed, onUnmounted } from 'vue'
import { ref, onMounted, computed, onUnmounted } from 'vue'
import { RouterView, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores/user'
import { initWS } from '@/websocket.js'
import { useEmitter } from '@/composables/useEmitter'
import { Toaster } from '@/components/ui/toast'
import NavBar from '@/components/NavBar.vue'
import { useToast } from '@/components/ui/toast/use-toast'
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'
import { TooltipProvider } from '@/components/ui/tooltip'
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
import NavBar from '@/components/NavBar.vue'
const { t } = useI18n()
const { toast } = useToast()
const emitter = useEmitter()
const isCollapsed = ref(true)
const allNavLinks = reactive([
const allNavLinks = [
{
title: t('navbar.dashboard'),
to: '/dashboard',
@@ -68,15 +68,15 @@ const allNavLinks = reactive([
icon: 'lucide:settings',
permission: 'admin:read'
}
])
]
const bottomLinks = ref([
const bottomLinks = [
{
to: '/logout',
icon: 'lucide:log-out',
title: 'Logout'
}
])
]
const userStore = useUserStore()
const router = useRouter()
@@ -93,7 +93,7 @@ onUnmounted(() => {
const getCurrentUser = () => {
userStore.getCurrentUser().catch((err) => {
if (err.response && err.response.status === 401) {
router.push('/login')
router.push('/')
}
})
}
@@ -104,7 +104,7 @@ const initToaster = () => {
const navLinks = computed(() =>
allNavLinks.filter((link) =>
link.permission ? userStore.hasPermission(link.permission) : true
!link.permission || (userStore.userPermissions.includes(link.permission) && link.permission)
)
)
</script>
+32
View File
@@ -173,3 +173,35 @@ body {
.admin-main-content {
@apply p-0;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background-color: #888;
border-radius: 4px;
border: 2px solid transparent;
}
::-webkit-scrollbar-thumb:hover {
background-color: #555;
}
* {
scrollbar-width: thin;
}
.code-editor {
@apply rounded-md border shadow h-[65vh] min-h-[250px] w-full relative
}
.ql-container {
margin-top: 0 !important;
height: 200px !important;
}
+3 -3
View File
@@ -44,9 +44,9 @@ const allNavItems = [
permission: null,
},
{
title: 'Templates',
title: 'Email templates',
href: '/admin/templates',
description: 'Manage email templates',
description: 'Manage outgoing email templates',
permission: null,
},
{
@@ -58,7 +58,7 @@ const allNavItems = [
]
const sidebarNavItems = computed(() =>
allNavItems.filter((item) => userStore.hasPermission(item.permission))
allNavItems.filter((item) => !item.permission || item.permission && userStore.userPermissions.includes(item.permission))
)
</script>
@@ -9,34 +9,16 @@
</DialogTrigger>
<DialogContent class="sm:max-w-[625px]">
<DialogHeader>
<DialogTitle>Add a canned response</DialogTitle>
<DialogDescription> Set canned response name. Click save when you're done. </DialogDescription>
</DialogHeader>
<form @submit.prevent="onSubmit">
<FormField v-slot="{ field }" name="title">
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input type="text" placeholder="" v-bind="field" />
</FormControl>
<FormDescription></FormDescription>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="content">
<FormItem>
<FormLabel>Content</FormLabel>
<FormControl>
<Textarea v-bind="componentField" class="h-52"></Textarea>
</FormControl>
<FormDescription></FormDescription>
<FormMessage />
</FormItem>
</FormField>
<DialogFooter class="mt-7">
<Button type="submit" size="sm">Save Changes</Button>
</DialogFooter>
</form>
<DialogTitle>New canned response</DialogTitle>
<DialogDescription>Set title and content, click save when you're done. </DialogDescription>
</DialogHeader>
<CannedResponsesForm @submit="onSubmit">
<template #footer>
<DialogFooter class="mt-7">
<Button type="submit" size="sm">Save Changes</Button>
</DialogFooter>
</template>
</CannedResponsesForm>
</DialogContent>
</Dialog>
</div>
@@ -54,17 +36,7 @@ import DataTable from '@/components/admin/DataTable.vue'
import { columns } from './dataTableColumns.js'
import { Button } from '@/components/ui/button'
import PageHeader from '@/components/admin/common/PageHeader.vue'
import { Textarea } from '@/components/ui/textarea'
import { Spinner } from '@/components/ui/spinner'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import {
Dialog,
DialogContent,
@@ -74,6 +46,7 @@ import {
DialogTitle,
DialogTrigger
} from '@/components/ui/dialog'
import CannedResponsesForm from './CannedResponsesForm.vue'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { formSchema } from './formSchema.js'
@@ -86,9 +59,17 @@ const cannedResponses = ref([])
const emit = useEmitter()
const dialogOpen = ref(false)
const form = useForm({
validationSchema: toTypedSchema(formSchema)
})
onMounted(() => {
getCannedResponses()
emit.on(EMITTER_EVENTS.REFRESH_LIST, refreshList)
form.setValues({
title: "",
content: "",
})
})
onUnmounted(() => {
@@ -99,10 +80,6 @@ const refreshList = (data) => {
if (data?.model === 'canned_responses') getCannedResponses()
}
const form = useForm({
validationSchema: toTypedSchema(formSchema)
})
const getCannedResponses = async () => {
try {
formLoading.value = true
@@ -0,0 +1,43 @@
<template>
<form class="space-y-6">
<FormField v-slot="{ componentField }" name="title">
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input type="text" placeholder="" v-bind="componentField" />
</FormControl>
<FormDescription></FormDescription>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField, handleInput }" name="content">
<FormItem>
<FormLabel>Content</FormLabel>
<FormControl>
<QuillEditor theme="snow" v-model:content="componentField.modelValue" contentType="html"
@update:content="handleInput"></QuillEditor>
</FormControl>
<FormDescription></FormDescription>
<FormMessage />
</FormItem>
</FormField>
<!-- Form submit button slot -->
<slot name="footer"></slot>
</form>
</template>
<script setup>
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css';
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
</script>
@@ -34,12 +34,12 @@ export const columns = [
id: 'actions',
enableHiding: false,
cell: ({ row }) => {
const canned_response = row.original
const cannedResponse = row.original
return h(
'div',
{ class: 'relative' },
h(dropdown, {
canned_response
cannedResponse
})
)
}
@@ -17,33 +17,15 @@
<DialogContent class="sm:max-w-[625px]">
<DialogHeader>
<DialogTitle>Edit canned response</DialogTitle>
<DialogDescription>Click save when you're done. </DialogDescription>
<DialogDescription>Edit title and content, click save when you're done. </DialogDescription>
</DialogHeader>
<form @submit.prevent="onSubmit">
<FormField v-slot="{ componentField }" name="title">
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input type="text" placeholder="" v-bind="componentField" />
</FormControl>
<FormDescription></FormDescription>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="content">
<FormItem>
<FormLabel>Content</FormLabel>
<FormControl>
<Textarea v-bind="componentField" class="h-52"></Textarea>
</FormControl>
<FormDescription></FormDescription>
<FormMessage />
</FormItem>
</FormField>
<DialogFooter>
<Button type="submit" size="sm"> Save changes </Button>
</DialogFooter>
</form>
<CannedResponsesForm @submit="onSubmit">
<template #footer>
<DialogFooter class="mt-7">
<Button type="submit" size="sm">Save Changes</Button>
</DialogFooter>
</template>
</CannedResponsesForm>
</DialogContent>
</Dialog>
</template>
@@ -60,15 +42,9 @@ import {
import { Button } from '@/components/ui/button'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import CannedResponsesForm from './CannedResponsesForm.vue'
import '@vueup/vue-quill/dist/vue-quill.snow.css';
import { formSchema } from './formSchema.js'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form'
import {
Dialog,
DialogContent,
@@ -78,8 +54,6 @@ import {
DialogTitle,
DialogTrigger
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { useEmitter } from '@/composables/useEmitter'
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
import api from '@/api/index.js'
@@ -88,13 +62,9 @@ const dialogOpen = ref(false)
const emit = useEmitter()
const props = defineProps({
canned_response: {
cannedResponse: {
type: Object,
required: true,
default: () => ({
id: '',
name: ''
})
}
})
@@ -103,13 +73,13 @@ const form = useForm({
})
const onSubmit = form.handleSubmit(async (values) => {
await api.updateCannedResponse(props.canned_response.id, values)
await api.updateCannedResponse(props.cannedResponse.id, values)
dialogOpen.value = false
emitRefreshCannedResponseList()
})
const deleteCannedResponse = async () => {
await api.deleteCannedResponse(props.canned_response.id)
await api.deleteCannedResponse(props.cannedResponse.id)
dialogOpen.value = false
emitRefreshCannedResponseList()
}
@@ -122,7 +92,7 @@ const emitRefreshCannedResponseList = () => {
// Watch for changes in initialValues and update the form.
watch(
() => props.canned_response,
() => props.cannedResponse,
(newValues) => {
form.setValues(newValues)
},
@@ -12,26 +12,17 @@
<DialogTitle>New status</DialogTitle>
<DialogDescription> Set status name. Click save when you're done. </DialogDescription>
</DialogHeader>
<form @submit.prevent="onSubmit">
<FormField v-slot="{ field }" name="name">
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input type="text" placeholder="Processing" v-bind="field" />
</FormControl>
<FormDescription></FormDescription>
<FormMessage />
</FormItem>
</FormField>
<DialogFooter class="mt-7">
<Button type="submit" size="sm">Save Changes</Button>
</DialogFooter>
</form>
<StatusForm @submit.prevent="onSubmit">
<template #footer>
<DialogFooter class="mt-10">
<Button type="submit" size="sm"> Save changes </Button>
</DialogFooter>
</template>
</StatusForm>
</DialogContent>
</Dialog>
</div>
</div>
<Spinner v-if="isLoading"></Spinner>
<div>
<DataTable :columns="columns" :data="statuses" />
@@ -46,15 +37,7 @@ import { columns } from './dataTableColumns.js'
import { Button } from '@/components/ui/button'
import PageHeader from '@/components/admin/common/PageHeader.vue'
import { Spinner } from '@/components/ui/spinner'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import StatusForm from './StatusForm.vue'
import {
Dialog,
DialogContent,
@@ -0,0 +1,28 @@
<template>
<form>
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input type="text" placeholder="Spam" v-bind="componentField" />
</FormControl>
<FormDescription></FormDescription>
<FormMessage />
</FormItem>
</FormField>
<!-- Form submit button slot -->
<slot name="footer"></slot>
</form>
</template>
<script setup>
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
</script>
@@ -19,21 +19,13 @@
<DialogTitle>Edit status</DialogTitle>
<DialogDescription> Change the status name. Click save when you're done. </DialogDescription>
</DialogHeader>
<form @submit.prevent="onSubmit">
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input type="text" placeholder="billing, tech" v-bind="componentField" />
</FormControl>
<FormDescription>Renaming the status will rename it across all conversations.</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<DialogFooter>
<Button type="submit" size="sm"> Save changes </Button>
</DialogFooter>
</form>
<StatusForm @submit.prevent="onSubmit">
<template #footer>
<DialogFooter class="mt-10">
<Button type="submit" size="sm"> Save changes </Button>
</DialogFooter>
</template>
</StatusForm>
</DialogContent>
</Dialog>
</template>
@@ -51,14 +43,7 @@ import { Button } from '@/components/ui/button'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { formSchema } from './formSchema.js'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form'
import StatusForm from './StatusForm.vue'
import {
Dialog,
DialogContent,
@@ -68,7 +53,6 @@ import {
DialogTitle,
DialogTrigger
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { useEmitter } from '@/composables/useEmitter'
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
import api from '@/api/index.js'
@@ -80,10 +64,6 @@ const props = defineProps({
status: {
type: Object,
required: true,
default: () => ({
id: '',
name: ''
})
}
})
@@ -1,40 +1,31 @@
<template>
<div class="flex justify-between mb-5">
<PageHeader title="Tags" description="Manage conversation tags" />
<div class="flex justify-end mb-4">
<Dialog v-model:open="dialogOpen">
<DialogTrigger as-child>
<Button size="sm">New Tag</Button>
</DialogTrigger>
<DialogContent class="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Add a Tag</DialogTitle>
<DialogDescription> Set tag name. Click save when you're done. </DialogDescription>
</DialogHeader>
<form @submit.prevent="onSubmit">
<FormField v-slot="{ field }" name="name">
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input type="text" placeholder="billing, tech" v-bind="field" />
</FormControl>
<FormDescription></FormDescription>
<FormMessage />
</FormItem>
</FormField>
<DialogFooter class="mt-7">
<Button type="submit" size="sm">Save Changes</Button>
<div class="flex justify-between mb-5">
<PageHeader title="Tags" description="Manage conversation tags" />
<div class="flex justify-end mb-4">
<Dialog v-model:open="dialogOpen">
<DialogTrigger as-child>
<Button size="sm">New Tag</Button>
</DialogTrigger>
<DialogContent class="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Create new tag</DialogTitle>
<DialogDescription> Set tag name. Click save when you're done. </DialogDescription>
</DialogHeader>
<TagsForm @submit.prevent="onSubmit">
<template #footer>
<DialogFooter class="mt-10">
<Button type="submit" size="sm"> Save changes </Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
</div>
<Spinner v-if="isLoading"></Spinner>
<div v-else>
<DataTable :columns="columns" :data="tags" />
</template>
</TagsForm>
</DialogContent>
</Dialog>
</div>
</div>
<Spinner v-if="isLoading"></Spinner>
<div v-else>
<DataTable :columns="columns" :data="tags" />
</div>
</template>
<script setup>
@@ -44,15 +35,7 @@ import { Spinner } from '@/components/ui/spinner'
import { columns } from '@/components/admin/conversation/tags/dataTableColumns.js'
import { Button } from '@/components/ui/button'
import PageHeader from '@/components/admin/common/PageHeader.vue'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import TagsForm from './TagsForm.vue'
import {
Dialog,
DialogContent,
@@ -0,0 +1,28 @@
<template>
<form>
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input type="text" placeholder="billing, order" v-bind="componentField" />
</FormControl>
<FormDescription></FormDescription>
<FormMessage />
</FormItem>
</FormField>
<!-- Form submit button slot -->
<slot name="footer" ></slot>
</form>
</template>
<script setup>
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
</script>
@@ -19,23 +19,13 @@
<DialogTitle>Edit tag</DialogTitle>
<DialogDescription> Change the tag name. Click save when you're done. </DialogDescription>
</DialogHeader>
<form @submit.prevent="onSubmit">
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input type="text" placeholder="billing, tech" v-bind="componentField" />
</FormControl>
<FormDescription
>Renaming the tag will rename it across all conversations.</FormDescription
>
<FormMessage />
</FormItem>
</FormField>
<DialogFooter>
<Button type="submit" size="sm"> Save changes </Button>
</DialogFooter>
</form>
<TagsForm @submit.prevent="onSubmit">
<template #footer>
<DialogFooter class="mt-10">
<Button type="submit" size="sm"> Save changes </Button>
</DialogFooter>
</template>
</TagsForm>
</DialogContent>
</Dialog>
</template>
@@ -53,14 +43,6 @@ import { Button } from '@/components/ui/button'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { formSchema } from './formSchema.js'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form'
import {
Dialog,
DialogContent,
@@ -70,9 +52,9 @@ import {
DialogTitle,
DialogTrigger
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { useEmitter } from '@/composables/useEmitter'
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
import TagsForm from './TagsForm.vue'
import api from '@/api/index.js'
const dialogOpen = ref(false)
@@ -142,18 +142,18 @@
</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 { smtpConfigSchema } from './formSchema.js';
import { watch } from 'vue'
import { Button } from '@/components/ui/button'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { smtpConfigSchema } from './formSchema.js'
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form';
} from '@/components/ui/form'
import {
Select,
SelectContent,
@@ -161,10 +161,10 @@ import {
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select';
} from '@/components/ui/select'
import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input';
import { Input } from '@/components/ui/input'
const props = defineProps({
initialValues: {
@@ -188,11 +188,11 @@ const props = defineProps({
const smtpForm = useForm({
validationSchema: toTypedSchema(smtpConfigSchema)
});
})
const onSmtpSubmit = smtpForm.handleSubmit((values) => {
props.submitForm(values);
});
})
// Watch for changes in initialValues and update the form.
watch(
@@ -201,5 +201,5 @@ watch(
smtpForm.setValues(newValues);
},
{ deep: true, immediate: true }
);
)
</script>
@@ -19,7 +19,7 @@
<Label>Auto assign conversations</Label>
</div>
</FormControl>
<FormDescription>Auto assign new conversations to agents in this team.</FormDescription>
<FormDescription>Automatically assign new conversations to agents in this team in a round-robin fashion.</FormDescription>
<FormMessage />
</FormItem>
</FormField>
@@ -1,5 +1,5 @@
<template>
<form @submit="onSubmit" class="space-y-6">
<form @submit.prevent="onSubmit" class="space-y-6">
<FormField v-slot="{ componentField }" name="name">
<FormItem v-auto-animate>
<FormLabel>Name</FormLabel>
@@ -10,13 +10,13 @@
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="body">
<FormItem v-auto-animate>
<FormLabel>HTML body</FormLabel>
<FormField v-slot="{ componentField, handleChange }" name="body">
<FormItem>
<FormLabel>Body</FormLabel>
<FormControl>
<Textarea placeholder="HTML here.." v-bind="componentField" class="h-52" />
<CodeEditor v-model="componentField.modelValue" @update:modelValue="handleChange"></CodeEditor>
</FormControl>
<FormDescription>{{ templateBodyDescription() }}</FormDescription>
<FormDescription>{{ `Make sure the template has \{\{ .Content \}\}` }}</FormDescription>
<FormMessage />
</FormItem>
</FormField>
@@ -54,10 +54,9 @@ import {
FormDescription
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import CodeEditor from '@/components/common/CodeEditor.vue';
import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
const props = defineProps({
initialValues: {
type: Object,
@@ -78,8 +77,6 @@ const props = defineProps({
}
})
const templateBodyDescription = () => 'Make sure the template has {{ .Content }}'
const form = useForm({
validationSchema: toTypedSchema(formSchema)
})
@@ -1,7 +1,7 @@
<template>
<div>
<div class="flex justify-between mb-5">
<PageHeader title="Templates" description="Manage email templates" />
<PageHeader title="Email Templates" description="Manage outgoing email templates" />
<div class="flex justify-end mb-4">
<Button @click="navigateToAddTemplate" size="sm"> New template </Button>
</div>
@@ -5,7 +5,7 @@ export const formSchema = z.object({
required_error: 'Template name is required.'
}),
body: z.string({
required_error: 'Template body is required.'
required_error: 'Template content is required.'
}),
is_default: z.boolean().optional()
})
@@ -1,12 +1,7 @@
<template>
<div
class="flex m-2 items-end text-sm overflow-hidden text-ellipsis whitespace-nowrap cursor-pointer"
>
<div
v-for="attachment in attachments"
:key="attachment.uuid"
class="flex items-center p-1 bg-[#F5F5F4] gap-1 rounded-md max-w-[15rem]"
>
<div class="flex m-2 items-end text-sm overflow-hidden text-ellipsis whitespace-nowrap cursor-pointer">
<div v-for="attachment in attachments" :key="attachment.uuid"
class="flex items-center p-1 bg-[#F5F5F4] gap-1 rounded-md max-w-[15rem]">
<!-- Filename tooltip -->
<Tooltip>
<TooltipTrigger as-child>
@@ -30,7 +25,6 @@
<script setup>
import { formatBytes } from '@/utils/file.js'
import { X } from 'lucide-vue-next'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
@@ -0,0 +1,67 @@
<template>
<div ref="codeEditor" id="code-editor" class="code-editor" />
</template>
<script setup>
import { ref, onMounted, watch, nextTick } from 'vue'
import CodeFlask from 'codeflask'
const props = defineProps({
modelValue: { type: String, default: '' },
language: { type: String, default: 'html' },
disabled: Boolean
})
const emit = defineEmits(['update:modelValue'])
const codeEditor = ref(null)
const data = ref('')
const flask = ref(null)
const initCodeEditor = (body) => {
const el = document.createElement('code-flask')
el.attachShadow({ mode: 'open' })
el.shadowRoot.innerHTML = `
<style>
.codeflask .codeflask__flatten {
font-size: 15px;
white-space: pre-wrap;
word-break: break-word;
}
.codeflask .codeflask__lines { background: #fafafa; z-index: 10; }
.codeflask .token.tag { font-weight: bold; }
.codeflask .token.attr-name { color: #111; }
.codeflask .token.attr-value { color: #000 !important; }
</style>
<div id="area"></div>
`
codeEditor.value.appendChild(el)
flask.value = new CodeFlask(el.shadowRoot.getElementById('area'), {
language: props.language,
lineNumbers: false,
styleParent: el.shadowRoot,
readonly: props.disabled
})
flask.value.onUpdate((v) => {
emit('update:modelValue', v)
data.value = v
})
flask.value.updateCode(body)
nextTick(() => {
document.querySelector('code-flask').shadowRoot.querySelector('textarea').focus()
})
}
onMounted(() => {
initCodeEditor(props.modelValue || '')
})
watch(() => props.modelValue, (newVal) => {
if (newVal !== data.value) {
flask.value.updateCode(newVal)
}
})
</script>
@@ -31,7 +31,7 @@
:messageType="messageType" :contentToSet="contentToSet" :cannedResponses="cannedResponses" />
<!-- Attachments preview -->
<AttachmentsPreview :attachments="uploadedFiles" :onDelete="handleOnFileDelete"></AttachmentsPreview>
<AttachmentsPreview :attachments="attachments" :onDelete="handleOnFileDelete"></AttachmentsPreview>
<!-- Bottom menu bar -->
<ReplyBoxBottomMenuBar :handleFileUpload="handleFileUpload" :handleInlineImageUpload="handleInlineImageUpload"
@@ -100,7 +100,11 @@ const toggleItalic = () => {
}
const editorPlaceholder = computed(() => {
return "Shift + Enter to add a new line; Press '/' to select a Canned Response."
return "Press enter to add a new line; Press '/' to select a Canned Response."
})
const attachments = computed(() => {
return uploadedFiles.value.filter(upload => upload.disposition === 'attachment')
})
const filterCannedResponses = (input) => {
@@ -187,8 +191,23 @@ const handleContentCleared = () => {
const handleSend = async () => {
try {
// Replace image source url with cid.
// Replace image url with cid.
const message = transformImageSrcToCID(editorHTML.value)
// Check which images are still in editor before sending.
const parser = new DOMParser()
const doc = parser.parseFromString(editorHTML.value, 'text/html')
const inlineImageUUIDs = Array.from(doc.querySelectorAll('img.inline-image'))
.map(img => img.getAttribute('title'))
.filter(Boolean)
uploadedFiles.value = uploadedFiles.value.filter(file =>
// Keep if:
// 1. Not an inline image OR
// 2. Is an inline image that exists in editor
file.disposition !== 'inline' || inlineImageUUIDs.includes(file.uuid)
)
await api.sendMessage(conversationStore.current.uuid, {
private: messageType.value === 'private_note',
message: message,
-7
View File
@@ -61,12 +61,6 @@ export const useUserStore = defineStore('user', () => {
}
}
// Check if user has a specific permission
const hasPermission = (permission) => {
if (!permission) return true
return userPermissions.value.includes(permission)
}
const clearAvatar = () => {
userAvatar.value = ''
}
@@ -87,7 +81,6 @@ export const useUserStore = defineStore('user', () => {
setFirstName,
setLastName,
getCurrentUser,
hasPermission,
clearAvatar
}
})
+1 -1
View File
@@ -23,7 +23,7 @@ module.exports = {
},
extend: {
fontFamily: {
poppins: ['Inter', 'sans-serif'],
inter: ['Inter', 'Helvetica Neue', 'sans-serif'],
},
colors: {
border: "hsl(var(--border))",
+4
View File
@@ -12,6 +12,10 @@ export default defineConfig({
target: 'http://127.0.0.1:9000',
changeOrigin: true,
},
'/uploads': {
target: 'http://127.0.0.1:9000',
changeOrigin: true,
},
'/ws': {
target: 'ws://127.0.0.1:9000',
ws: true,
+175 -5
View File
@@ -1782,6 +1782,11 @@
resolved "https://registry.yarnpkg.com/@types/pbf/-/pbf-3.0.5.tgz#a9495a58d8c75be4ffe9a0bd749a307715c07404"
integrity sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==
"@types/prismjs@^1.9.1":
version "1.26.5"
resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.5.tgz#72499abbb4c4ec9982446509d2f14fb8483869d6"
integrity sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==
"@types/sinonjs__fake-timers@8.1.1":
version "8.1.1"
resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3"
@@ -2112,6 +2117,14 @@
dependencies:
"@vue/compiler-core" "^3.0.0"
"@vueup/vue-quill@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@vueup/vue-quill/-/vue-quill-1.2.0.tgz#cd0d93559256d069f639723dd91c044e8162c72a"
integrity sha512-kd5QPSHMDpycklojPXno2Kw2JSiKMYduKYQckTm1RJoVDA557MnyUXgcuuDpry4HY/Rny9nGNcK+m3AHk94wag==
dependencies:
quill "^1.3.7"
quill-delta "^4.2.2"
"@vueuse/core@^10.11.0", "@vueuse/core@^10.11.1":
version "10.11.1"
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-10.11.1.tgz#15d2c0b6448d2212235b23a7ba29c27173e0c2c6"
@@ -2749,7 +2762,7 @@ cachedir@^2.3.0:
resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.4.0.tgz#7fef9cf7367233d7c88068fe6e34ed0d355a610d"
integrity sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==
call-bind@^1.0.5, call-bind@^1.0.7:
call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==
@@ -2972,6 +2985,11 @@ clone@^1.0.2:
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
clone@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
clsx@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b"
@@ -3010,6 +3028,14 @@ code-point-at@^1.0.0:
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==
codeflask@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/codeflask/-/codeflask-1.4.1.tgz#c5229854e3f648377922a75f1145f7316030d3db"
integrity sha512-4vb2IbE/iwvP0Uubhd2ixVeysm3KNC2pl7SoDaisxq1juhZzvap3qbaX7B2CtpQVvv5V9sjcQK8hO0eTcY0V9Q==
dependencies:
"@types/prismjs" "^1.9.1"
prismjs "^1.14.0"
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -3653,6 +3679,18 @@ decamelize@^1.1.1:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
deep-equal@^1.0.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.2.tgz#78a561b7830eef3134c7f6f3a3d6af272a678761"
integrity sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==
dependencies:
is-arguments "^1.1.1"
is-date-object "^1.0.5"
is-regex "^1.1.4"
object-is "^1.1.5"
object-keys "^1.1.1"
regexp.prototype.flags "^1.5.1"
deep-extend@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
@@ -3670,7 +3708,7 @@ defaults@^1.0.3:
dependencies:
clone "^1.0.2"
define-data-property@^1.1.4:
define-data-property@^1.0.1, define-data-property@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==
@@ -3679,6 +3717,15 @@ define-data-property@^1.1.4:
es-errors "^1.3.0"
gopd "^1.0.1"
define-properties@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c"
integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==
dependencies:
define-data-property "^1.0.1"
has-property-descriptors "^1.0.0"
object-keys "^1.1.1"
defu@^6.1.4:
version "6.1.4"
resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479"
@@ -4121,6 +4168,11 @@ eventemitter2@6.4.7:
resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d"
integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==
eventemitter3@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba"
integrity sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==
execa@4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a"
@@ -4204,7 +4256,7 @@ exponential-backoff@^3.1.1:
resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6"
integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==
extend@~3.0.0, extend@~3.0.2:
extend@^3.0.2, extend@~3.0.0, extend@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
@@ -4235,6 +4287,16 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-diff@1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.1.2.tgz#4b62c42b8e03de3f848460b639079920695d0154"
integrity sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==
fast-diff@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
fast-diff@^1.1.2:
version "1.3.0"
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0"
@@ -4513,6 +4575,11 @@ function-bind@^1.1.2:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
functions-have-names@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@@ -4796,7 +4863,7 @@ has-flag@^4.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
has-property-descriptors@^1.0.2:
has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
@@ -4813,6 +4880,13 @@ has-symbols@^1.0.3:
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
has-tostringtag@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
dependencies:
has-symbols "^1.0.3"
has-unicode@^2.0.0, has-unicode@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
@@ -5106,6 +5180,14 @@ ip@^1.1.4:
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.9.tgz#8dfbcc99a754d07f425310b86a99546b1151e396"
integrity sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==
is-arguments@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==
dependencies:
call-bind "^1.0.2"
has-tostringtag "^1.0.0"
is-arrayish@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
@@ -5153,6 +5235,13 @@ is-core-module@^2.13.0, is-core-module@^2.8.1:
dependencies:
hasown "^2.0.0"
is-date-object@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==
dependencies:
has-tostringtag "^1.0.0"
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
@@ -5240,6 +5329,14 @@ is-redirect@^1.0.0:
resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
integrity sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw==
is-regex@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
dependencies:
call-bind "^1.0.2"
has-tostringtag "^1.0.0"
is-retry-allowed@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4"
@@ -5751,11 +5848,16 @@ lodash.castarray@^4.4.0:
resolved "https://registry.yarnpkg.com/lodash.castarray/-/lodash.castarray-4.4.0.tgz#c02513515e309daddd4c24c60cfddcf5976d9115"
integrity sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==
lodash.clonedeep@~4.5.0:
lodash.clonedeep@^4.5.0, lodash.clonedeep@~4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==
lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
@@ -6763,6 +6865,14 @@ object-inspect@^1.13.1:
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2"
integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==
object-is@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07"
integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==
dependencies:
call-bind "^1.0.7"
define-properties "^1.2.1"
object-keys@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
@@ -6996,6 +7106,11 @@ parallel-transform@^1.1.0:
inherits "^2.0.3"
readable-stream "^2.1.5"
parchment@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/parchment/-/parchment-1.1.4.tgz#aeded7ab938fe921d4c34bc339ce1168bc2ffde5"
integrity sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
@@ -7293,6 +7408,11 @@ pretty-bytes@^5.6.0:
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
prismjs@^1.14.0:
version "1.29.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12"
integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==
proc-log@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-3.0.0.tgz#fb05ef83ccd64fd7b20bbe9c8c1070fc08338dd8"
@@ -7665,6 +7785,36 @@ quickselect@^2.0.0:
resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018"
integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==
quill-delta@^3.6.2:
version "3.6.3"
resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-3.6.3.tgz#b19fd2b89412301c60e1ff213d8d860eac0f1032"
integrity sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==
dependencies:
deep-equal "^1.0.1"
extend "^3.0.2"
fast-diff "1.1.2"
quill-delta@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-4.2.2.tgz#015397d046e0a3bed087cd8a51f98c11a1b8f351"
integrity sha512-qjbn82b/yJzOjstBgkhtBjN2TNK+ZHP/BgUQO+j6bRhWQQdmj2lH6hXG7+nwwLF41Xgn//7/83lxs9n2BkTtTg==
dependencies:
fast-diff "1.2.0"
lodash.clonedeep "^4.5.0"
lodash.isequal "^4.5.0"
quill@^1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/quill/-/quill-1.3.7.tgz#da5b2f3a2c470e932340cdbf3668c9f21f9286e8"
integrity sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==
dependencies:
clone "^2.1.1"
deep-equal "^1.0.1"
eventemitter3 "^2.0.3"
extend "^3.0.2"
parchment "^1.1.4"
quill-delta "^3.6.2"
radix-vue@^1.7.3:
version "1.8.5"
resolved "https://registry.yarnpkg.com/radix-vue/-/radix-vue-1.8.5.tgz#d16118470f318706a3b18726dbebcf9a67d111e8"
@@ -7850,6 +8000,16 @@ regenerator-runtime@^0.14.0:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
regexp.prototype.flags@^1.5.1:
version "1.5.3"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz#b3ae40b1d2499b8350ab2c3fe6ef3845d3a96f42"
integrity sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==
dependencies:
call-bind "^1.0.7"
define-properties "^1.2.1"
es-errors "^1.3.0"
set-function-name "^2.0.2"
registry-auth-token@^3.0.1:
version "3.4.0"
resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.4.0.tgz#d7446815433f5d5ed6431cd5dca21048f66b397e"
@@ -8148,6 +8308,16 @@ set-function-length@^1.2.1:
gopd "^1.0.1"
has-property-descriptors "^1.0.2"
set-function-name@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985"
integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==
dependencies:
define-data-property "^1.1.4"
es-errors "^1.3.0"
functions-have-names "^1.2.3"
has-property-descriptors "^1.0.2"
sha@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/sha/-/sha-2.0.1.tgz#6030822fbd2c9823949f8f72ed6411ee5cf25aae"
+5 -8
View File
@@ -48,7 +48,7 @@ func NewEnforcer(lo *logf.Logger) (*Enforcer, error) {
return &Enforcer{enforcer: e, lo: lo}, nil
}
// LoadPermissions adds the user's permissions to the Casbin enforcer if not already present
// LoadPermissions adds the user's permissions to the Casbin enforcer if not already present.
func (e *Enforcer) LoadPermissions(user umodels.User) error {
for _, perm := range user.Permissions {
parts := strings.Split(perm, ":")
@@ -57,11 +57,8 @@ func (e *Enforcer) LoadPermissions(user umodels.User) error {
}
userID, permObj, permAct := strconv.Itoa(user.ID), parts[0], parts[1]
ok, err := e.enforcer.HasPolicy(userID, permObj, permAct)
if err != nil || !ok {
if _, err := e.enforcer.AddPolicy(userID, permObj, permAct); err != nil {
return fmt.Errorf("failed to add policy: %v", err)
}
if _, err := e.enforcer.AddPolicy(userID, permObj, permAct); err != nil {
return fmt.Errorf("failed to add policy: %v", err)
}
}
return nil
@@ -105,8 +102,8 @@ func (e *Enforcer) EnforceConversationAccess(user umodels.User, conversation cmo
return true, nil
}
// Check for `read_assigned` permission
allowed, err = e.enforcer.Enforce(strconv.Itoa(user.ID), "conversations", "read_assigned")
// Check for `read_unassigned` permission
allowed, err = e.enforcer.Enforce(strconv.Itoa(user.ID), "conversations", "read_unassigned")
if err != nil {
return false, envelope.NewError(envelope.GeneralError, "Error checking permissions", nil)
}
+2 -2
View File
@@ -97,7 +97,7 @@ func (m *Manager) Run(ctx context.Context, dispatchConcurrency int, scanInterval
}
// Render content in template.
if err := m.RenderContentInTemplate(inb, message); err != nil {
if err := m.RenderContentInTemplate(inb, &message); err != nil {
m.lo.Error("error rendering content", "message_id", message.ID, "error", err)
continue
}
@@ -196,7 +196,7 @@ func (m *Manager) MessageDispatchWorker(ctx context.Context) {
}
// RenderContentInTemplate renders message content in the default template
func (m *Manager) RenderContentInTemplate(inb inbox.Inbox, message models.Message) error {
func (m *Manager) RenderContentInTemplate(inb inbox.Inbox, message *models.Message) error {
var (
channel = inb.Channel()
err error
+26 -29
View File
@@ -36,7 +36,6 @@ type Conversation struct {
AssignedTeamID null.Int `db:"assigned_team_id" json:"assigned_team_id"`
AssigneeLastSeenAt null.Time `db:"assignee_last_seen_at" json:"assignee_last_seen_at"`
cmodels.Contact
// Psuedo fields.
Subject string `db:"subject" json:"subject"`
UnreadMessageCount int `db:"unread_message_count" json:"unread_message_count"`
InboxName string `db:"inbox_name" json:"inbox_name"`
@@ -67,35 +66,33 @@ type NewConversationsStats struct {
}
// Message represents a message in a conversation
// TODO: Maybe diffentiate conversation message and a outgoing message.
type Message struct {
ID int `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
UUID string `db:"uuid" json:"uuid"`
Type string `db:"type" json:"type"`
Status string `db:"status" json:"status"`
ConversationID int `db:"conversation_id" json:"conversation_id"`
Content string `db:"content" json:"content"`
ContentType string `db:"content_type" json:"content_type"`
Private bool `db:"private" json:"private"`
SourceID null.String `db:"source_id" json:"-"`
SenderID int `db:"sender_id" json:"sender_id"`
SenderType string `db:"sender_type" json:"sender_type"`
InboxID int `db:"inbox_id" json:"-"`
Meta string `db:"meta" json:"meta"`
Attachments attachment.Attachments `db:"attachments" json:"attachments"`
// Psuedo fields.
ConversationUUID string `db:"conversation_uuid" json:"-"`
From string `db:"from" json:"-"`
To []string `db:"from" json:"-"`
AltContent string `db:"alt_content" json:"-"`
Subject string `db:"subject" json:"-"`
Channel string `db:"channel" json:"-"`
References []string `json:"-"`
InReplyTo string `json:"-"`
Headers textproto.MIMEHeader `json:"-"`
Media []mmodels.Media `db:"-" json:"-"`
ID int `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
UUID string `db:"uuid" json:"uuid"`
Type string `db:"type" json:"type"`
Status string `db:"status" json:"status"`
ConversationID int `db:"conversation_id" json:"conversation_id"`
Content string `db:"content" json:"content"`
ContentType string `db:"content_type" json:"content_type"`
Private bool `db:"private" json:"private"`
SourceID null.String `db:"source_id" json:"-"`
SenderID int `db:"sender_id" json:"sender_id"`
SenderType string `db:"sender_type" json:"sender_type"`
InboxID int `db:"inbox_id" json:"-"`
Meta string `db:"meta" json:"meta"`
Attachments attachment.Attachments `db:"attachments" json:"attachments"`
ConversationUUID string `db:"conversation_uuid" json:"-"`
From string `db:"from" json:"-"`
To []string `db:"from" json:"-"`
AltContent string `db:"alt_content" json:"-"`
Subject string `db:"subject" json:"-"`
Channel string `db:"channel" json:"-"`
References []string `json:"-"`
InReplyTo string `json:"-"`
Headers textproto.MIMEHeader `json:"-"`
Media []mmodels.Media `db:"-" json:"-"`
}
// IncomingMessage links a message with the contact information and inbox id.
+12 -2
View File
@@ -129,8 +129,18 @@ WHERE uuid = $1;
-- name: update-conversation-status
UPDATE conversations
SET status_id = (SELECT id FROM status WHERE name = $2),
resolved_at = CASE WHEN $2 = 'Resolved' THEN CURRENT_TIMESTAMP ELSE NULL END,
closed_at = CASE WHEN $2 = 'Closed' THEN CURRENT_TIMESTAMP ELSE NULL END,
resolved_at = CASE
WHEN $2 = 'Resolved' THEN
COALESCE(resolved_at, CURRENT_TIMESTAMP)
WHEN $2 != 'Resolved' THEN
resolved_at
END,
closed_at = CASE
WHEN $2 = 'Closed' THEN
COALESCE(closed_at, CURRENT_TIMESTAMP)
ELSE
closed_at
END,
updated_at = now()
WHERE uuid = $1;
+1 -1
View File
@@ -141,7 +141,7 @@ func (u *Manager) Create(user *models.User) error {
return nil
}
// Get retrieves a user by ID or UUID.
// Get retrieves a user by ID.
func (u *Manager) Get(id int) (models.User, error) {
var user models.User
if err := u.q.GetUser.Get(&user, id); err != nil {