mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-11-03 05:23:48 +00:00
fix: Ticks of bar charts.
- Update shadcn charts. - Refactors user store. - Fix: pagination incorrect total pages. - Comestic changes and cleanups. - Fixes toaster not working in OuterApp.vue. - Allow complete from address in notification settings from address form field.
This commit is contained in:
@@ -33,7 +33,7 @@ func handleGetAllConversations(r *fastglue.Request) error {
|
||||
Results: conversations,
|
||||
Total: total,
|
||||
PerPage: pageSize,
|
||||
TotalPages: total / pageSize,
|
||||
TotalPages: (total + pageSize - 1) / pageSize,
|
||||
Page: page,
|
||||
})
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func handleGetAssignedConversations(r *fastglue.Request) error {
|
||||
Results: conversations,
|
||||
Total: total,
|
||||
PerPage: pageSize,
|
||||
TotalPages: total / pageSize,
|
||||
TotalPages: (total + pageSize - 1) / pageSize,
|
||||
Page: page,
|
||||
})
|
||||
}
|
||||
@@ -89,7 +89,7 @@ func handleGetUnassignedConversations(r *fastglue.Request) error {
|
||||
Results: conversations,
|
||||
Total: total,
|
||||
PerPage: pageSize,
|
||||
TotalPages: total / pageSize,
|
||||
TotalPages: (total + pageSize - 1) / pageSize,
|
||||
Page: page,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ type constants struct {
|
||||
AppBaseURL string
|
||||
LogoURL string
|
||||
SiteName string
|
||||
Filestore string
|
||||
UploadProvider string
|
||||
AllowedUploadFileExtensions []string
|
||||
MaxFileUploadSizeMB float64
|
||||
}
|
||||
@@ -107,7 +107,7 @@ func initConstants() constants {
|
||||
AppBaseURL: ko.String("app.root_url"),
|
||||
LogoURL: ko.String("app.logo_url"),
|
||||
SiteName: ko.String("app.site_name"),
|
||||
Filestore: ko.MustString("upload.provider"),
|
||||
UploadProvider: ko.MustString("upload.provider"),
|
||||
AllowedUploadFileExtensions: ko.Strings("app.allowed_file_upload_extensions"),
|
||||
MaxFileUploadSizeMB: ko.Float64("app.max_file_upload_size"),
|
||||
}
|
||||
@@ -594,7 +594,6 @@ func initStatus(db *sqlx.DB) *status.Manager {
|
||||
return manager
|
||||
}
|
||||
|
||||
|
||||
// initPriority inits conversation priority manager.
|
||||
func initPriority(db *sqlx.DB) *priority.Manager {
|
||||
manager, err := priority.New(priority.Opts{
|
||||
|
||||
@@ -42,7 +42,7 @@ var (
|
||||
|
||||
// App is the global app context which is passed and injected in the http handlers.
|
||||
type App struct {
|
||||
constant constants
|
||||
consts constants
|
||||
fs stuffbin.FileSystem
|
||||
auth *auth_.Auth
|
||||
authz *authz.Enforcer
|
||||
@@ -167,7 +167,7 @@ func main() {
|
||||
conversation: conversation,
|
||||
automation: automation,
|
||||
oidc: oidc,
|
||||
constant: constants,
|
||||
consts: constants,
|
||||
notifier: notifier,
|
||||
authz: initAuthz(),
|
||||
status: initStatus(db),
|
||||
|
||||
12
cmd/media.go
12
cmd/media.go
@@ -63,17 +63,17 @@ func handleMediaUpload(r *fastglue.Request) error {
|
||||
srcExt := strings.TrimPrefix(strings.ToLower(filepath.Ext(srcFileName)), ".")
|
||||
|
||||
// Check file size
|
||||
if bytesToMegabytes(srcFileSize) > app.constant.MaxFileUploadSizeMB {
|
||||
app.lo.Error("error: uploaded file size is larger than max allowed", "size", bytesToMegabytes(srcFileSize), "max_allowed", app.constant.MaxFileUploadSizeMB)
|
||||
if bytesToMegabytes(srcFileSize) > app.consts.MaxFileUploadSizeMB {
|
||||
app.lo.Error("error: uploaded file size is larger than max allowed", "size", bytesToMegabytes(srcFileSize), "max_allowed", app.consts.MaxFileUploadSizeMB)
|
||||
return r.SendErrorEnvelope(
|
||||
http.StatusRequestEntityTooLarge,
|
||||
fmt.Sprintf("File size is too large. Please upload file lesser than %f MB", app.constant.MaxFileUploadSizeMB),
|
||||
fmt.Sprintf("File size is too large. Please upload file lesser than %f MB", app.consts.MaxFileUploadSizeMB),
|
||||
nil,
|
||||
envelope.GeneralError,
|
||||
)
|
||||
}
|
||||
|
||||
if !slices.Contains(app.constant.AllowedUploadFileExtensions, "*") && !slices.Contains(app.constant.AllowedUploadFileExtensions, srcExt) {
|
||||
if !slices.Contains(app.consts.AllowedUploadFileExtensions, "*") && !slices.Contains(app.consts.AllowedUploadFileExtensions, srcExt) {
|
||||
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Unsupported file type", nil, envelope.InputError)
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ func handleServeMedia(r *fastglue.Request) error {
|
||||
|
||||
// Check if the user has permission to access the linked model.
|
||||
allowed, err := app.authz.EnforceMediaAccess(user, media.Model.String)
|
||||
if err != nil {
|
||||
if err != nil {
|
||||
app.lo.Error("error checking media permission", "error", err, "model", media.Model.String, "model_id", media.ModelID)
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
@@ -172,7 +172,7 @@ func handleServeMedia(r *fastglue.Request) error {
|
||||
return r.SendErrorEnvelope(http.StatusUnauthorized, "Permission denied", nil, envelope.PermissionError)
|
||||
}
|
||||
|
||||
switch ko.String("upload.provider") {
|
||||
switch app.consts.UploadProvider {
|
||||
case "fs":
|
||||
fasthttp.ServeFile(r.RequestCtx, filepath.Join(ko.String("upload.fs.upload_path"), uuid))
|
||||
case "s3":
|
||||
|
||||
@@ -49,7 +49,7 @@ func handleGetMessages(r *fastglue.Request) error {
|
||||
Results: messages,
|
||||
Page: page,
|
||||
PerPage: pageSize,
|
||||
TotalPages: total / pageSize,
|
||||
TotalPages: (total + pageSize - 1) / pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ func handleGetOIDC(r *fastglue.Request) error {
|
||||
return sendErrorEnvelope(r, err)
|
||||
}
|
||||
|
||||
o.RedirectURI = fmt.Sprintf("%s%s", app.constant.AppBaseURL, fmt.Sprintf(redirectURI, o.ID))
|
||||
o.RedirectURI = fmt.Sprintf("%s%s", app.consts.AppBaseURL, fmt.Sprintf(redirectURI, o.ID))
|
||||
|
||||
return r.SendEnvelope(o)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
func ErrHandler(ctx *fasthttp.RequestCtx, status int, reason error) {
|
||||
fmt.Printf("error status %d - error %d", status, reason)
|
||||
fmt.Printf("error status %d: %s", status, reason)
|
||||
}
|
||||
|
||||
var upgrader = websocket.FastHTTPUpgrader{
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
"@tiptap/starter-kit": "^2.4.0",
|
||||
"@tiptap/suggestion": "^2.4.0",
|
||||
"@tiptap/vue-3": "^2.4.0",
|
||||
"@unovis/ts": "^1.4.3",
|
||||
"@unovis/vue": "^1.4.3",
|
||||
"@unovis/ts": "^1.4.4",
|
||||
"@unovis/vue": "^1.4.4",
|
||||
"@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",
|
||||
"@vueuse/core": "^11.2.0",
|
||||
"add": "^2.0.6",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
<template>
|
||||
<Toaster />
|
||||
<TooltipProvider :delay-duration="200">
|
||||
<div class="font-inter">
|
||||
<div class="flex">
|
||||
<NavBar :is-collapsed="isCollapsed" :links="navLinks" :bottom-links="bottomLinks"
|
||||
class="shadow shadow-gray-300 h-screen" />
|
||||
<ResizablePanelGroup direction="horizontal" auto-save-id="app.vue.resizable.panel">
|
||||
<ResizableHandle id="resize-handle-1" />
|
||||
<ResizablePanel id="resize-panel-2">
|
||||
<div class="w-full h-screen">
|
||||
<RouterView />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
<div class="flex">
|
||||
<NavBar :is-collapsed="isCollapsed" :links="navLinks" :bottom-links="bottomLinks"
|
||||
class="shadow shadow-gray-300 h-screen" />
|
||||
<ResizablePanelGroup direction="horizontal" auto-save-id="app.vue.resizable.panel">
|
||||
<ResizableHandle id="resize-handle-1" />
|
||||
<ResizablePanel id="resize-panel-2">
|
||||
<div class="w-full h-screen">
|
||||
<RouterView />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -25,10 +20,8 @@ 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 { 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'
|
||||
|
||||
@@ -99,7 +92,7 @@ const initToaster = () => {
|
||||
|
||||
const navLinks = computed(() =>
|
||||
allNavLinks.filter((link) =>
|
||||
!link.permission || (userStore.userPermissions.includes(link.permission) && link.permission)
|
||||
!link.permission || (userStore.permissions.includes(link.permission) && link.permission)
|
||||
)
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<template>
|
||||
<RouterView />
|
||||
<Toaster />
|
||||
<TooltipProvider :delay-duration="250">
|
||||
<div class="font-inter">
|
||||
<RouterView />
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { RouterView } from 'vue-router'
|
||||
import { Toaster } from '@/components/ui/toast'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
</script>
|
||||
@@ -11,6 +11,8 @@
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
@@ -24,62 +26,62 @@ body {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
|
||||
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
|
||||
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
|
||||
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border:240 5.9% 90%;
|
||||
--input:240 5.9% 90%;
|
||||
--ring:240 5.9% 10%;
|
||||
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 5.9% 10%;
|
||||
--radius: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
.dark {
|
||||
--background:240 10% 3.9%;
|
||||
--foreground:0 0% 98%;
|
||||
|
||||
--card:240 10% 3.9%;
|
||||
--card-foreground:0 0% 98%;
|
||||
|
||||
--popover:240 10% 3.9%;
|
||||
--popover-foreground:0 0% 98%;
|
||||
|
||||
--primary:0 0% 98%;
|
||||
--primary-foreground:240 5.9% 10%;
|
||||
|
||||
--secondary:240 3.7% 15.9%;
|
||||
--secondary-foreground:0 0% 98%;
|
||||
|
||||
--muted:240 3.7% 15.9%;
|
||||
--muted-foreground:240 5% 64.9%;
|
||||
|
||||
--accent:240 3.7% 15.9%;
|
||||
--accent-foreground:0 0% 98%;
|
||||
|
||||
--destructive:0 62.8% 30.6%;
|
||||
--destructive-foreground:0 0% 98%;
|
||||
|
||||
--border:240 3.7% 15.9%;
|
||||
--input:240 3.7% 15.9%;
|
||||
--ring:240 4.9% 83.9%;
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,23 +176,22 @@ body {
|
||||
@apply p-0;
|
||||
}
|
||||
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #888;
|
||||
background-color: #888;
|
||||
border-radius: 4px;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #555;
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -198,10 +199,16 @@ body {
|
||||
}
|
||||
|
||||
.code-editor {
|
||||
@apply rounded-md border shadow h-[65vh] min-h-[250px] w-full relative
|
||||
@apply rounded-md border shadow h-[65vh] min-h-[250px] w-full relative;
|
||||
}
|
||||
|
||||
.ql-container {
|
||||
.ql-container .ql-editor {
|
||||
margin-top: 0 !important;
|
||||
height: 200px !important;
|
||||
border-radius: var(--radius) !important;
|
||||
@apply rounded-lg rounded-t-none;
|
||||
}
|
||||
|
||||
.ql-toolbar {
|
||||
@apply rounded-t-lg;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</div>
|
||||
<div class="flex space-x-5">
|
||||
<Avatar class="size-28">
|
||||
<AvatarImage :src="userStore.userAvatar" alt="Cropped Image" />
|
||||
<AvatarImage :src="userStore.avatar" alt="Cropped Image" />
|
||||
<AvatarFallback>{{ userStore.getInitials }}</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
@@ -98,7 +98,7 @@ const getResult = async () => {
|
||||
if (!cropper) return
|
||||
croppedBlob = await cropper.getBlob()
|
||||
if (!croppedBlob) return
|
||||
userStore.userAvatar = URL.createObjectURL(croppedBlob)
|
||||
userStore.setAvatar(URL.createObjectURL(croppedBlob))
|
||||
showCropper.value = false
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ const allNavItems = [
|
||||
]
|
||||
|
||||
const sidebarNavItems = computed(() =>
|
||||
allNavItems.filter((item) => !item.permission || item.permission && userStore.userPermissions.includes(item.permission))
|
||||
allNavItems.filter((item) => !item.permission || item.permission && userStore.permissions.includes(item.permission))
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<CircleX size="21" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="action.type && conversationActions[action.type].inputType === 'richtext'" class="pl-0">
|
||||
<div v-if="action.type && conversationActions[action.type].inputType === 'richtext'" class="pl-0 shadow">
|
||||
<QuillEditor theme="snow" v-model:content="action.value" contentType="html"
|
||||
@update:content="(value) => handleValueChange(value, index)" class="h-32 mb-12" />
|
||||
</div>
|
||||
|
||||
@@ -115,8 +115,10 @@ import {
|
||||
} from '@/components/ui/form'
|
||||
import { Spinner } from '@/components/ui/spinner'
|
||||
import { CustomBreadcrumb } from '@/components/ui/breadcrumb'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const isLoading = ref(false)
|
||||
const router = useRouter()
|
||||
const emitter = useEmitter()
|
||||
const rule = ref({
|
||||
id: 0,
|
||||
@@ -250,8 +252,12 @@ const handleSave = async (values) => {
|
||||
// Delete fields not required.
|
||||
delete updatedRule.created_at
|
||||
delete updatedRule.updated_at
|
||||
if (props.id > 0) await api.updateAutomationRule(props.id, updatedRule)
|
||||
else await api.createAutomationRule(updatedRule)
|
||||
if (props.id > 0) {
|
||||
await api.updateAutomationRule(props.id, updatedRule)
|
||||
} else {
|
||||
await api.createAutomationRule(updatedRule)
|
||||
router.push('/admin/automations')
|
||||
}
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Saved',
|
||||
description: "Rule saved successfully"
|
||||
@@ -304,20 +310,7 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
firstRuleGroup.value = getFirstGroup()
|
||||
// Convert multi tag select values separated by commas to an array
|
||||
firstRuleGroup.value.rules.forEach(rule => {
|
||||
if (!Array.isArray(rule.value))
|
||||
if (["contains", "not contains"].includes(rule.operator)) {
|
||||
rule.value = rule.value ? rule.value.split(',') : []
|
||||
}
|
||||
})
|
||||
secondRuleGroup.value = getSecondGroup()
|
||||
secondRuleGroup.value?.rules?.forEach(rule => {
|
||||
if (!Array.isArray(rule.value))
|
||||
if (["contains", "not contains"].includes(rule.operator)) {
|
||||
rule.value = rule.value ? rule.value.split(',') : []
|
||||
}
|
||||
})
|
||||
groupOperator.value = getGroupOperator()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="flex flex-col box px-5 py-6 rounded-lg justify-center">
|
||||
<div class="flex justify-between space-y-3">
|
||||
<div class="flex flex-col box px-5 rounded-lg justify-center py-3">
|
||||
<div class="flex justify-between space-y-1">
|
||||
<div>
|
||||
<span class="sub-title space-x-3 flex justify-center items-center">
|
||||
<div class="text-base">
|
||||
{{ rule.name }}
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<Badge v-if="!rule.disabled">Enabled</Badge>
|
||||
<Badge v-if="!rule.disabled" class="text-[9px]">Enabled</Badge>
|
||||
<Badge v-else variant="secondary">Disabled</Badge>
|
||||
</div>
|
||||
</span>
|
||||
@@ -16,7 +16,7 @@
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<button>
|
||||
<EllipsisVertical size="21"></EllipsisVertical>
|
||||
<EllipsisVertical size="18"></EllipsisVertical>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
|
||||
@@ -120,9 +120,10 @@
|
||||
<FormItem>
|
||||
<FormLabel>From Email Address</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" placeholder="admin@yourcompany.com" v-bind="componentField" />
|
||||
<Input type="text" placeholder="From email address. e.g. My Support <mysupport@example.com>" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>From email address. e.g. My Support <mysupport@example.com></FormDescription>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
@@ -152,7 +153,8 @@ import {
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
FormMessage,
|
||||
FormDescription,
|
||||
} from '@/components/ui/form'
|
||||
import {
|
||||
Select,
|
||||
|
||||
@@ -45,7 +45,7 @@ export const smtpConfigSchema = z.object({
|
||||
.enum(['plain', 'login', 'cram', 'none'])
|
||||
.describe('Authentication protocol')
|
||||
.default('plain'),
|
||||
email_address: z.string().describe('Email address').email(),
|
||||
email_address: z.string().describe('Email address'),
|
||||
max_msg_retries: z
|
||||
.number({
|
||||
invalid_type_error: 'Must be a number.'
|
||||
|
||||
@@ -37,6 +37,6 @@ const getAttachmentName = (name) => {
|
||||
}
|
||||
|
||||
const downloadAttachment = () => {
|
||||
window.open(props.attachment.url, '_blank');
|
||||
window.open(props.attachment.url, '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -14,7 +14,6 @@ import Link from '@tiptap/extension-link'
|
||||
|
||||
const emit = defineEmits([
|
||||
'send',
|
||||
'input',
|
||||
'editorText',
|
||||
'updateBold',
|
||||
'updateItalic',
|
||||
@@ -25,7 +24,6 @@ const emit = defineEmits([
|
||||
|
||||
const props = defineProps({
|
||||
placeholder: String,
|
||||
messageType: String,
|
||||
isBold: Boolean,
|
||||
isItalic: Boolean,
|
||||
clearContent: Boolean,
|
||||
@@ -56,10 +54,6 @@ const editor = ref(
|
||||
// No outline for the editor.
|
||||
class: 'outline-none'
|
||||
},
|
||||
// Emit new input text.
|
||||
handleTextInput: (view, from, to, text) => {
|
||||
emit('input', text)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -153,6 +147,7 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
br.ProseMirror-trailingBreak {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
'cursor-pointer rounded p-1 hover:bg-secondary',
|
||||
{ 'bg-secondary': index === selectedResponseIndex }
|
||||
]" @click="selectCannedResponse(response.content)" @mouseenter="selectedResponseIndex = index">
|
||||
<span class="font-semibold">{{ response.title }}</span> - {{ getTextFromHTML(response.content).slice(0, 150) }}...
|
||||
<span class="font-semibold">{{ response.title }}</span> - {{ getTextFromHTML(response.content).slice(0, 150)
|
||||
}}...
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -24,11 +25,12 @@
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<!-- Editor -->
|
||||
<!-- Main Editor -->
|
||||
<Editor @keydown="handleKeydown" @editorText="handleEditorText" :placeholder="editorPlaceholder" :isBold="isBold"
|
||||
:clearContent="clearContent" :isItalic="isItalic" @updateBold="updateBold" @updateItalic="updateItalic"
|
||||
@contentCleared="handleContentCleared" @contentSet="clearContentToSet" @editorReady="onEditorReady"
|
||||
:messageType="messageType" :contentToSet="contentToSet" :cannedResponses="cannedResponses" />
|
||||
:contentToSet="contentToSet" :cannedResponses="cannedResponses" />
|
||||
|
||||
|
||||
<!-- Attachments preview -->
|
||||
<AttachmentsPreview :attachments="attachments" :onDelete="handleOnFileDelete"></AttachmentsPreview>
|
||||
@@ -71,7 +73,7 @@ const uploadedFiles = ref([])
|
||||
const messageType = ref('reply')
|
||||
const selectedResponseIndex = ref(-1)
|
||||
const responsesList = ref(null)
|
||||
let editorInstance = null
|
||||
let editorInstance = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
@@ -138,7 +140,7 @@ const hasText = computed(() => {
|
||||
})
|
||||
|
||||
const onEditorReady = (editor) => {
|
||||
editorInstance = editor
|
||||
editorInstance.value = editor
|
||||
}
|
||||
|
||||
const handleFileUpload = (event) => {
|
||||
@@ -169,7 +171,7 @@ const handleInlineImageUpload = (event) => {
|
||||
inline: true,
|
||||
})
|
||||
.then((resp) => {
|
||||
editorInstance.commands.setImage({
|
||||
editorInstance.value.commands.setImage({
|
||||
src: resp.data.data.url,
|
||||
alt: resp.data.data.filename,
|
||||
title: resp.data.data.uuid,
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
<template>
|
||||
<BarChart :data="data" index="status" :categories="priorities" :show-grid-line="true"
|
||||
:margin="{ top: 0, bottom: 0, left: 0, right: 0 }" />
|
||||
<BarChart :data="data" index="status" :categories="priorities" :show-grid-line="true" :show-x-axis="true"
|
||||
:show-y-axis="true" type="grouped" :x-formatter="xFormatter" :y-formatter="yFormatter" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BarChart } from '@/components/ui/chart-bar'
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const xFormatter = (tick) => {
|
||||
return props.data[tick]?.status ?? ''
|
||||
}
|
||||
|
||||
const yFormatter = (tick) => {
|
||||
return Number.isInteger(tick) ? tick : ''
|
||||
}
|
||||
|
||||
const priorities = ["Low", "Medium", "High"]
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col space-y-6" v-if="userStore.getFullName">
|
||||
<div>
|
||||
<span class="font-medium text-2xl space-y-1">
|
||||
<span class="font-medium text-3xl space-y-1">
|
||||
<p>Hi, {{ userStore.getFullName }}</p>
|
||||
<p class="text-sm-muted">🌤️ {{ format(new Date(), 'EEEE, MMMM d, HH:mm a') }}</p>
|
||||
</span>
|
||||
|
||||
@@ -34,12 +34,14 @@ import ActivityMessageBubble from './ActivityMessageBubble.vue'
|
||||
import AgentMessageBubble from './AgentMessageBubble.vue'
|
||||
import MessagesSkeleton from './MessagesSkeleton.vue'
|
||||
import { useConversationStore } from '@/stores/conversation'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { RefreshCw } from 'lucide-vue-next'
|
||||
import { useEmitter } from '@/composables/useEmitter'
|
||||
import { EMITTER_EVENTS } from '@/constants/emitterEvents'
|
||||
|
||||
const conversationStore = useConversationStore()
|
||||
const userStore = useUserStore()
|
||||
const threadEl = ref(null)
|
||||
const emitter = useEmitter()
|
||||
|
||||
@@ -55,9 +57,9 @@ const scrollToBottom = () => {
|
||||
|
||||
onMounted(() => {
|
||||
scrollToBottom()
|
||||
// On new outgoing message to the current conversation, scroll to the bottom.
|
||||
emitter.on(EMITTER_EVENTS.NEW_OUTGOING_MESSAGE, (data) => {
|
||||
if (data.conversation_uuid === conversationStore.current.uuid) {
|
||||
// Scroll to bottom on new message from logged in user
|
||||
emitter.on(EMITTER_EVENTS.NEW_MESSAGE, (data) => {
|
||||
if (data.conversation_uuid === conversationStore.current.uuid && data.message.sender_id === userStore.userID) {
|
||||
scrollToBottom()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
<script setup>
|
||||
import { VisAxis, VisGroupedBar, VisStackedBar, VisXYContainer } from '@unovis/vue'
|
||||
import { Axis, GroupedBar, StackedBar } from '@unovis/ts'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useMounted } from '@vueuse/core'
|
||||
import { ChartCrosshair, ChartLegend, defaultColors } from '@/components/ui/chart'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
ChartCrosshair,
|
||||
ChartLegend,
|
||||
defaultColors,
|
||||
} from '@/components/ui/chart';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Axis, GroupedBar, StackedBar } from '@unovis/ts';
|
||||
import {
|
||||
VisAxis,
|
||||
VisGroupedBar,
|
||||
VisStackedBar,
|
||||
VisXYContainer,
|
||||
} from '@unovis/vue';
|
||||
import { useMounted } from '@vueuse/core';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: { type: Array, required: true },
|
||||
@@ -12,9 +21,9 @@ const props = defineProps({
|
||||
index: { type: null, required: true },
|
||||
colors: { type: Array, required: false },
|
||||
margin: {
|
||||
type: null,
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({ top: 0, bottom: 0, left: 0, right: 0 })
|
||||
default: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
|
||||
},
|
||||
filterOpacity: { type: Number, required: false, default: 0.2 },
|
||||
xFormatter: { type: Function, required: false },
|
||||
@@ -26,43 +35,53 @@ const props = defineProps({
|
||||
showGridLine: { type: Boolean, required: false, default: true },
|
||||
customTooltip: { type: null, required: false },
|
||||
type: { type: String, required: false, default: 'grouped' },
|
||||
roundedCorners: { type: Number, required: false, default: 0 }
|
||||
})
|
||||
const emits = defineEmits(['legendItemClick'])
|
||||
roundedCorners: { type: Number, required: false, default: 0 },
|
||||
});
|
||||
const emits = defineEmits(['legendItemClick']);
|
||||
|
||||
const index = computed(() => props.index)
|
||||
const index = computed(() => props.index);
|
||||
const colors = computed(() =>
|
||||
props.colors?.length ? props.colors : defaultColors(props.categories.length)
|
||||
)
|
||||
props.colors?.length ? props.colors : defaultColors(props.categories.length),
|
||||
);
|
||||
const legendItems = ref(
|
||||
props.categories.map((category, i) => ({
|
||||
name: category,
|
||||
color: colors.value[i],
|
||||
inactive: false
|
||||
}))
|
||||
)
|
||||
inactive: false,
|
||||
})),
|
||||
);
|
||||
|
||||
const isMounted = useMounted()
|
||||
const isMounted = useMounted();
|
||||
|
||||
function handleLegendItemClick(d, i) {
|
||||
emits('legendItemClick', d, i)
|
||||
emits('legendItemClick', d, i);
|
||||
}
|
||||
|
||||
const VisBarComponent = computed(() => (props.type === 'grouped' ? VisGroupedBar : VisStackedBar))
|
||||
const VisBarComponent = computed(() =>
|
||||
props.type === 'grouped' ? VisGroupedBar : VisStackedBar,
|
||||
);
|
||||
const selectorsBar = computed(() =>
|
||||
props.type === 'grouped' ? GroupedBar.selectors.bar : StackedBar.selectors.bar
|
||||
)
|
||||
props.type === 'grouped'
|
||||
? GroupedBar.selectors.bar
|
||||
: StackedBar.selectors.bar,
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('w-full h-[400px] flex flex-col items-end', $attrs.class ?? '')">
|
||||
<div
|
||||
:class="cn('w-full h-[400px] flex flex-col items-end', $attrs.class ?? '')"
|
||||
>
|
||||
<ChartLegend
|
||||
v-if="showLegend"
|
||||
v-model:items="legendItems"
|
||||
@legend-item-click="handleLegendItemClick"
|
||||
/>
|
||||
|
||||
<VisXYContainer :data="data" :style="{ height: isMounted ? '100%' : 'auto' }" :margin="margin">
|
||||
<VisXYContainer
|
||||
:data="data"
|
||||
:style="{ height: isMounted ? '100%' : 'auto' }"
|
||||
:margin="margin"
|
||||
>
|
||||
<ChartCrosshair
|
||||
v-if="showTooltip"
|
||||
:colors="colors"
|
||||
@@ -80,10 +99,10 @@ const selectorsBar = computed(() =>
|
||||
:attributes="{
|
||||
[selectorsBar]: {
|
||||
opacity: (d, i) => {
|
||||
const pos = i % categories.length
|
||||
return legendItems[pos]?.inactive ? filterOpacity : 1
|
||||
}
|
||||
}
|
||||
const pos = i % categories.length;
|
||||
return legendItems[pos]?.inactive ? filterOpacity : 1;
|
||||
},
|
||||
},
|
||||
}"
|
||||
/>
|
||||
|
||||
@@ -104,8 +123,8 @@ const selectorsBar = computed(() =>
|
||||
:grid-line="showGridLine"
|
||||
:attributes="{
|
||||
[Axis.selectors.grid]: {
|
||||
class: 'text-muted'
|
||||
}
|
||||
class: 'text-muted',
|
||||
},
|
||||
}"
|
||||
tick-text-color="hsl(var(--vis-text-color))"
|
||||
/>
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as BarChart } from './BarChart.vue'
|
||||
export { default as BarChart } from './BarChart.vue';
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
<script setup>
|
||||
import { CurveType } from '@unovis/ts'
|
||||
import { VisAxis, VisLine, VisXYContainer } from '@unovis/vue'
|
||||
import { Axis, Line } from '@unovis/ts'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useMounted } from '@vueuse/core'
|
||||
import { ChartCrosshair, ChartLegend, defaultColors } from '@/components/ui/chart'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
ChartCrosshair,
|
||||
ChartLegend,
|
||||
defaultColors,
|
||||
} from '@/components/ui/chart';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CurveType } from '@unovis/ts';
|
||||
import { Axis, Line } from '@unovis/ts';
|
||||
import { VisAxis, VisLine, VisXYContainer } from '@unovis/vue';
|
||||
import { useMounted } from '@vueuse/core';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: { type: Array, required: true },
|
||||
@@ -13,9 +17,9 @@ const props = defineProps({
|
||||
index: { type: null, required: true },
|
||||
colors: { type: Array, required: false },
|
||||
margin: {
|
||||
type: null,
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({ top: 0, bottom: 0, left: 0, right: 0 })
|
||||
default: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
|
||||
},
|
||||
filterOpacity: { type: Number, required: false, default: 0.2 },
|
||||
xFormatter: { type: Function, required: false },
|
||||
@@ -26,33 +30,35 @@ const props = defineProps({
|
||||
showLegend: { type: Boolean, required: false, default: true },
|
||||
showGridLine: { type: Boolean, required: false, default: true },
|
||||
customTooltip: { type: null, required: false },
|
||||
curveType: { type: String, required: false, default: CurveType.MonotoneX }
|
||||
})
|
||||
curveType: { type: String, required: false, default: CurveType.MonotoneX },
|
||||
});
|
||||
|
||||
const emits = defineEmits(['legendItemClick'])
|
||||
const emits = defineEmits(['legendItemClick']);
|
||||
|
||||
const index = computed(() => props.index)
|
||||
const index = computed(() => props.index);
|
||||
const colors = computed(() =>
|
||||
props.colors?.length ? props.colors : defaultColors(props.categories.length)
|
||||
)
|
||||
props.colors?.length ? props.colors : defaultColors(props.categories.length),
|
||||
);
|
||||
|
||||
const legendItems = ref(
|
||||
props.categories.map((category, i) => ({
|
||||
name: category,
|
||||
color: colors.value[i],
|
||||
inactive: false
|
||||
}))
|
||||
)
|
||||
inactive: false,
|
||||
})),
|
||||
);
|
||||
|
||||
const isMounted = useMounted()
|
||||
const isMounted = useMounted();
|
||||
|
||||
function handleLegendItemClick(d, i) {
|
||||
emits('legendItemClick', d, i)
|
||||
emits('legendItemClick', d, i);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('w-full h-[400px] flex flex-col items-end', $attrs.class ?? '')">
|
||||
<div
|
||||
:class="cn('w-full h-[400px] flex flex-col items-end', $attrs.class ?? '')"
|
||||
>
|
||||
<ChartLegend
|
||||
v-if="showLegend"
|
||||
v-model:items="legendItems"
|
||||
@@ -80,10 +86,11 @@ function handleLegendItemClick(d, i) {
|
||||
:color="colors[i]"
|
||||
:attributes="{
|
||||
[Line.selectors.line]: {
|
||||
opacity: legendItems.find((item) => item.name === category)?.inactive
|
||||
opacity: legendItems.find((item) => item.name === category)
|
||||
?.inactive
|
||||
? filterOpacity
|
||||
: 1
|
||||
}
|
||||
: 1,
|
||||
},
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
@@ -105,8 +112,8 @@ function handleLegendItemClick(d, i) {
|
||||
:grid-line="showGridLine"
|
||||
:attributes="{
|
||||
[Axis.selectors.grid]: {
|
||||
class: 'text-muted'
|
||||
}
|
||||
class: 'text-muted',
|
||||
},
|
||||
}"
|
||||
tick-text-color="hsl(var(--vis-text-color))"
|
||||
/>
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as LineChart } from './LineChart.vue'
|
||||
export { default as LineChart } from './LineChart.vue';
|
||||
|
||||
@@ -8,4 +8,4 @@ export const CONVERSTION_WS_ACTIONS = {
|
||||
SUB_LIST: 'conversations_list_sub',
|
||||
SET_CURRENT: 'conversation_set_current',
|
||||
UNSET_CURRENT: 'conversation_unset_current'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export const EMITTER_EVENTS = {
|
||||
REFRESH_LIST: 'refresh-list',
|
||||
SHOW_TOAST: 'show-toast',
|
||||
NEW_OUTGOING_MESSAGE: 'new-outgoing-message',
|
||||
NEW_INCOMING_MESSAGE: 'new-incoming-message',
|
||||
NEW_MESSAGE: 'new-message',
|
||||
}
|
||||
@@ -186,10 +186,11 @@ export const useConversationStore = defineStore('conversation', () => {
|
||||
try {
|
||||
const response = await api.getConversationMessage(cuuid, uuid)
|
||||
if (response?.data?.data) {
|
||||
const message = response.data.data
|
||||
if (!messages.data.some((m) => m.uuid === message.uuid)) {
|
||||
messages.data.push(message)
|
||||
const newMsg = response.data.data
|
||||
if (!messages.data.some((m) => m.uuid === newMsg.uuid)) {
|
||||
messages.data.push(newMsg)
|
||||
}
|
||||
return newMsg
|
||||
}
|
||||
} catch (error) {
|
||||
messages.errorMessage = handleHTTPError(error).message
|
||||
@@ -354,23 +355,16 @@ export const useConversationStore = defineStore('conversation', () => {
|
||||
}
|
||||
|
||||
// Adds a new message to conversation.
|
||||
function updateConversationMessageList (message) {
|
||||
async function updateConversationMessageList (message) {
|
||||
// Fetch entire message only if the convesation is open and the message is not present in the list.
|
||||
if (conversation?.data?.uuid === message.conversation_uuid) {
|
||||
if (!messages.data.some((msg) => msg.uuid === message.uuid)) {
|
||||
fetchParticipants(message.conversation_uuid)
|
||||
fetchMessage(message.conversation_uuid, message.uuid)
|
||||
const fetchedMessage = await fetchMessage(message.conversation_uuid, message.uuid)
|
||||
updateAssigneeLastSeen(message.conversation_uuid)
|
||||
if (message.type === 'outgoing') {
|
||||
setTimeout(() => {
|
||||
emitter.emit(EMITTER_EVENTS.NEW_OUTGOING_MESSAGE, { conversation_uuid: message.conversation_uuid })
|
||||
}, 50)
|
||||
}
|
||||
if (message.type === 'incoming') {
|
||||
setTimeout(() => {
|
||||
emitter.emit(EMITTER_EVENTS.NEW_INCOMING_MESSAGE, { conversation_uuid: message.conversation_uuid })
|
||||
}, 50)
|
||||
}
|
||||
setTimeout(() => {
|
||||
emitter.emit(EMITTER_EVENTS.NEW_MESSAGE, { conversation_uuid: message.conversation_uuid, message: fetchedMessage })
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { computed, reactive } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { handleHTTPError } from '@/utils/http'
|
||||
import { useEmitter } from '@/composables/useEmitter'
|
||||
@@ -6,81 +6,74 @@ import { EMITTER_EVENTS } from '@/constants/emitterEvents'
|
||||
import api from '@/api'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const userAvatar = ref('')
|
||||
const userFirstName = ref('')
|
||||
const userLastName = ref('')
|
||||
const userPermissions = ref([])
|
||||
let user = reactive({
|
||||
id: null,
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
avatar_url: '',
|
||||
permissions: []
|
||||
})
|
||||
const emitter = useEmitter()
|
||||
|
||||
// Setters
|
||||
const setAvatar = (avatar) => {
|
||||
userAvatar.value = avatar
|
||||
}
|
||||
const userID = computed(() => user.id)
|
||||
const firstName = computed(() => user.first_name)
|
||||
const lastName = computed(() => user.last_name)
|
||||
const avatar = computed(() => user.avatar_url)
|
||||
const permissions = computed(() => user.permissions || [])
|
||||
|
||||
const setFirstName = (firstName) => {
|
||||
userFirstName.value = firstName
|
||||
}
|
||||
const getFullName = computed(() => {
|
||||
if (!user.first_name && !user.last_name) return ''
|
||||
return `${user.first_name || ''} ${user.last_name || ''}`.trim()
|
||||
})
|
||||
|
||||
const setLastName = (lastName) => {
|
||||
userLastName.value = lastName
|
||||
}
|
||||
|
||||
// Computed properties
|
||||
const getInitials = computed(() => {
|
||||
const firstInitial = userFirstName.value.charAt(0).toUpperCase()
|
||||
const lastInitial = userLastName.value.charAt(0).toUpperCase()
|
||||
const firstInitial = user.first_name?.charAt(0)?.toUpperCase() || ''
|
||||
const lastInitial = user.last_name?.charAt(0)?.toUpperCase() || ''
|
||||
return `${firstInitial}${lastInitial}`
|
||||
})
|
||||
|
||||
const getFullName = computed(() => {
|
||||
return `${userFirstName.value} ${userLastName.value}`
|
||||
})
|
||||
|
||||
// Fetches current user.
|
||||
const getCurrentUser = async () => {
|
||||
try {
|
||||
const response = await api.getCurrentUser()
|
||||
const userData = response?.data?.data
|
||||
if (userData) {
|
||||
const { avatar_url, first_name, last_name, permissions } = userData
|
||||
setAvatar(avatar_url)
|
||||
setFirstName(first_name)
|
||||
setLastName(last_name)
|
||||
userPermissions.value = permissions || []
|
||||
Object.assign(user, userData)
|
||||
} else {
|
||||
throw new Error('No user data found')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
if (error.response.status !== 401) {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Could not fetch current user',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
}
|
||||
if (error.response?.status !== 401) {
|
||||
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
|
||||
title: 'Could not fetch current user',
|
||||
variant: 'destructive',
|
||||
description: handleHTTPError(error).message
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const setAvatar = (avatarURL) => {
|
||||
if (typeof avatarURL !== 'string') {
|
||||
console.warn('Avatar URL must be a string')
|
||||
return
|
||||
}
|
||||
user.avatar_url = avatarURL
|
||||
}
|
||||
|
||||
const clearAvatar = () => {
|
||||
userAvatar.value = ''
|
||||
user.avatar_url = ''
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
userFirstName,
|
||||
userLastName,
|
||||
userAvatar,
|
||||
userPermissions,
|
||||
|
||||
// Computed
|
||||
userID,
|
||||
firstName,
|
||||
lastName,
|
||||
avatar,
|
||||
permissions,
|
||||
getFullName,
|
||||
getInitials,
|
||||
|
||||
// Actions
|
||||
setAvatar,
|
||||
setFirstName,
|
||||
setLastName,
|
||||
getCurrentUser,
|
||||
clearAvatar
|
||||
clearAvatar,
|
||||
setAvatar,
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<Spinner v-if="isLoading"></Spinner>
|
||||
<div class="page-content w-9/12" :class="{ 'soft-fade': isLoading }">
|
||||
<div>
|
||||
<DashboardGreet></DashboardGreet>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
<Spinner v-if="isLoading"></Spinner>
|
||||
<DashboardGreet></DashboardGreet>
|
||||
<div class="mt-7" v-auto-animate>
|
||||
<Card :counts="cardCounts" :labels="agentCountCardsLabels" />
|
||||
</div>
|
||||
<div class="flex my-7 justify-between items-center space-x-5">
|
||||
<div class="dashboard-card p-5">
|
||||
<LineChart :data="chartData.new_conversations"></LineChart>
|
||||
</div>
|
||||
<div class="dashboard-card p-5">
|
||||
<BarChart :data="chartData.status_summary"></BarChart>
|
||||
<div class="w-11/12" :class="{ 'soft-fade': isLoading }">
|
||||
<div class="flex my-7 justify-between items-center space-x-5">
|
||||
<div class="dashboard-card p-5">
|
||||
<LineChart :data="chartData.new_conversations"></LineChart>
|
||||
</div>
|
||||
<div class="dashboard-card p-5">
|
||||
<BarChart :data="chartData.status_summary"></BarChart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<CardHeader class="space-y-1">
|
||||
<CardTitle class="text-2xl text-center">Reset Password</CardTitle>
|
||||
<p class="text-sm text-muted-foreground text-center">
|
||||
Enter your email address and we'll send you a link to reset your password.
|
||||
Enter your email to receive a password reset link.
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent class="grid gap-4">
|
||||
|
||||
@@ -123,16 +123,8 @@ const loginAction = () => {
|
||||
email: loginForm.value.email,
|
||||
password: loginForm.value.password
|
||||
})
|
||||
.then((resp) => {
|
||||
const userData = resp.data.data
|
||||
if (userData) {
|
||||
userStore.$patch({
|
||||
userAvatar: userData.avatar_url,
|
||||
userFirstName: userData.first_name,
|
||||
userLastName: userData.last_name
|
||||
})
|
||||
.then(() => {
|
||||
router.push({ name: 'dashboard' })
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
errorMessage.value = handleHTTPError(error).message
|
||||
|
||||
@@ -10,15 +10,12 @@ export default defineConfig({
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:9000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/logout': {
|
||||
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',
|
||||
|
||||
@@ -1895,10 +1895,10 @@
|
||||
dependencies:
|
||||
lodash-es "^4.17.21"
|
||||
|
||||
"@unovis/ts@^1.4.3":
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@unovis/ts/-/ts-1.4.3.tgz#3e176e72d0db0c1c48ed5217bd35d32a082e37ba"
|
||||
integrity sha512-QYh1Qot1N9L6ZQg+uuhcsI3iuic9c6VVjlex3ipRqYDvrDDN6N+SG2555+9z+yAV6cbVsD1/EkMfK+o84PPjSw==
|
||||
"@unovis/ts@^1.4.4":
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@unovis/ts/-/ts-1.4.4.tgz#36464fdcb3cf3c9a6e6ffa4f3573500ed1c6e4ec"
|
||||
integrity sha512-OoAbCdxSq3tvEKDUDkWNIkPWJ1tnjklWR+8W3zhVqUkOvpqTYW8IjAl+07dPS9/qv+mtjZry5zOt1aBCMPCfdA==
|
||||
dependencies:
|
||||
"@emotion/css" "^11.7.1"
|
||||
"@juggle/resize-observer" "^3.3.1"
|
||||
@@ -1933,10 +1933,10 @@
|
||||
topojson-client "^3.1.0"
|
||||
tslib "^2.3.1"
|
||||
|
||||
"@unovis/vue@^1.4.3":
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@unovis/vue/-/vue-1.4.3.tgz#ddf2a8ef56cd6e7fce371a6ef0f220a72d26ff23"
|
||||
integrity sha512-L45ncN+e1dynA2cvHyq2ss+hLNBBPC1bl+i9JNveLufl+k7qybbt2IrioKBGQEbVmzXuJ80r2f3cD64i6ca9jg==
|
||||
"@unovis/vue@^1.4.4":
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@unovis/vue/-/vue-1.4.4.tgz#836396fdc3f92ee08c1e68f0862d4ad8557f08bd"
|
||||
integrity sha512-19xdi/0+18NzUyRh4sK0GCIv5JbqSMSEqXaMio4Vya8I/8JYU0Sgyo4jwr+O60Rl8fSOXScL9NJQHxPHJeTcMg==
|
||||
|
||||
"@vee-validate/zod@^4.13.2":
|
||||
version "4.13.2"
|
||||
@@ -2132,7 +2132,7 @@
|
||||
quill "^1.3.7"
|
||||
quill-delta "^4.2.2"
|
||||
|
||||
"@vueuse/core@^10.11.0", "@vueuse/core@^10.11.1":
|
||||
"@vueuse/core@^10.11.0":
|
||||
version "10.11.1"
|
||||
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-10.11.1.tgz#15d2c0b6448d2212235b23a7ba29c27173e0c2c6"
|
||||
integrity sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==
|
||||
@@ -2152,6 +2152,16 @@
|
||||
"@vueuse/shared" "10.9.0"
|
||||
vue-demi ">=0.14.7"
|
||||
|
||||
"@vueuse/core@^11.2.0":
|
||||
version "11.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-11.2.0.tgz#3fc6c0963051bb154dc4c08061889405e3fc745d"
|
||||
integrity sha512-JIUwRcOqOWzcdu1dGlfW04kaJhW3EXnnjJJfLTtddJanymTL7lF1C0+dVVZ/siLfc73mWn+cGP1PE1PKPruRSA==
|
||||
dependencies:
|
||||
"@types/web-bluetooth" "^0.0.20"
|
||||
"@vueuse/metadata" "11.2.0"
|
||||
"@vueuse/shared" "11.2.0"
|
||||
vue-demi ">=0.14.10"
|
||||
|
||||
"@vueuse/metadata@10.11.1":
|
||||
version "10.11.1"
|
||||
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-10.11.1.tgz#209db7bb5915aa172a87510b6de2ca01cadbd2a7"
|
||||
@@ -2162,6 +2172,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-10.9.0.tgz#769a1a9db65daac15cf98084cbf7819ed3758620"
|
||||
integrity sha512-iddNbg3yZM0X7qFY2sAotomgdHK7YJ6sKUvQqbvwnf7TmaVPxS4EJydcNsVejNdS8iWCtDk+fYXr7E32nyTnGA==
|
||||
|
||||
"@vueuse/metadata@11.2.0":
|
||||
version "11.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-11.2.0.tgz#fd02cbbc7d08cb4592fceea0486559b89ae38643"
|
||||
integrity sha512-L0ZmtRmNx+ZW95DmrgD6vn484gSpVeRbgpWevFKXwqqQxW9hnSi2Ppuh2BzMjnbv4aJRiIw8tQatXT9uOB23dQ==
|
||||
|
||||
"@vueuse/shared@10.11.1", "@vueuse/shared@^10.11.0":
|
||||
version "10.11.1"
|
||||
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-10.11.1.tgz#62b84e3118ae6e1f3ff38f4fbe71b0c5d0f10938"
|
||||
@@ -2176,6 +2191,13 @@
|
||||
dependencies:
|
||||
vue-demi ">=0.14.7"
|
||||
|
||||
"@vueuse/shared@11.2.0":
|
||||
version "11.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-11.2.0.tgz#7fb2f3cade6b6c00ef97e613f187ee9bdcfb9a3a"
|
||||
integrity sha512-VxFjie0EanOudYSgMErxXfq6fo8vhr5ICI+BuE3I9FnX7ePllEsVrRQ7O6Q1TLgApeLuPKcHQxAXpP+KnlrJsg==
|
||||
dependencies:
|
||||
vue-demi ">=0.14.10"
|
||||
|
||||
"@withtypes/mime@^0.1.2":
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@withtypes/mime/-/mime-0.1.2.tgz#956883251b0786f2c898ed76a23594ea6d9939e0"
|
||||
@@ -9421,6 +9443,11 @@ vue-demi@>=0.13.0, vue-demi@>=0.14.5, vue-demi@>=0.14.7:
|
||||
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.7.tgz#8317536b3ef74c5b09f268f7782e70194567d8f2"
|
||||
integrity sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==
|
||||
|
||||
vue-demi@>=0.14.10:
|
||||
version "0.14.10"
|
||||
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
|
||||
integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==
|
||||
|
||||
vue-demi@>=0.14.8:
|
||||
version "0.14.8"
|
||||
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.8.tgz#00335e9317b45e4a68d3528aaf58e0cec3d5640a"
|
||||
|
||||
@@ -12,15 +12,14 @@ type User 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"`
|
||||
FirstName string `db:"first_name" json:"first_name"`
|
||||
LastName string `db:"last_name" json:"last_name"`
|
||||
Email string `db:"email" json:"email,omitempty"`
|
||||
AvatarURL null.String `db:"avatar_url" json:"avatar_url"`
|
||||
Disabled bool `db:"disabled" json:"disabled"`
|
||||
Password string `db:"password" json:"-"`
|
||||
NewPassword string `db:"-" json:"new_password"`
|
||||
SendWelcomeEmail bool `db:"-" json:"send_welcome_email"`
|
||||
NewPassword string `db:"-" json:"new_password,omitempty"`
|
||||
SendWelcomeEmail bool `db:"-" json:"send_welcome_email,omitempty"`
|
||||
Roles pq.StringArray `db:"roles" json:"roles"`
|
||||
Permissions pq.StringArray `db:"permissions" json:"permissions"`
|
||||
Teams tmodels.Teams `db:"teams" json:"teams"`
|
||||
|
||||
Reference in New Issue
Block a user