mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-21 10:03:23 +00:00
fix: fixes to sidebar for smaller devices.
- fix: avatarless contact message bubble. - fix: set viewport width to device width. - revert[backend]: remove demo env check for starting backend services. i changed my mind.
This commit is contained in:
+8
-12
@@ -48,7 +48,6 @@ var (
|
||||
ctx = context.Background()
|
||||
appName = "libredesk"
|
||||
frontendDir = "frontend/dist"
|
||||
envDemo = "demo"
|
||||
|
||||
// Injected at build time.
|
||||
buildString = ""
|
||||
@@ -171,16 +170,14 @@ func main() {
|
||||
)
|
||||
automation.SetConversationStore(conversation)
|
||||
|
||||
if ko.MustString("app.env") != envDemo {
|
||||
startInboxes(ctx, inbox, conversation)
|
||||
go automation.Run(ctx, automationWorkers)
|
||||
go autoassigner.Run(ctx, autoAssignInterval)
|
||||
go conversation.Run(ctx, messageIncomingQWorkers, messageOutgoingQWorkers, messageOutgoingScanInterval)
|
||||
go conversation.RunUnsnoozer(ctx, unsnoozeInterval)
|
||||
go notifier.Run(ctx)
|
||||
go sla.Run(ctx, slaEvaluationInterval)
|
||||
go media.DeleteUnlinkedMedia(ctx)
|
||||
}
|
||||
startInboxes(ctx, inbox, conversation)
|
||||
go automation.Run(ctx, automationWorkers)
|
||||
go autoassigner.Run(ctx, autoAssignInterval)
|
||||
go conversation.Run(ctx, messageIncomingQWorkers, messageOutgoingQWorkers, messageOutgoingScanInterval)
|
||||
go conversation.RunUnsnoozer(ctx, unsnoozeInterval)
|
||||
go notifier.Run(ctx)
|
||||
go sla.Run(ctx, slaEvaluationInterval)
|
||||
go media.DeleteUnlinkedMedia(ctx)
|
||||
|
||||
var app = &App{
|
||||
lo: lo,
|
||||
@@ -227,7 +224,6 @@ func main() {
|
||||
}
|
||||
|
||||
go func() {
|
||||
colorlog.Green("Starting HTTP server in %s mode", ko.String("app.env"))
|
||||
colorlog.Green("Server started at %s", ko.String("app.server.address"))
|
||||
if ko.String("server.socket") != "" {
|
||||
colorlog.Green("Unix socket created at %s", ko.String("server.socket"))
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1366, initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
|
||||
+68
-20
@@ -1,27 +1,65 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<!-- Main sidebar -->
|
||||
<Sidebar
|
||||
class="w-full"
|
||||
:userTeams="userStore.teams"
|
||||
:userViews="userViews"
|
||||
@create-view="openCreateViewForm = true"
|
||||
@edit-view="editView"
|
||||
@delete-view="deleteView"
|
||||
>
|
||||
<div class="h-screen border-l">
|
||||
<div class="flex w-full h-screen">
|
||||
<!-- Icon sidebar always visible -->
|
||||
<SidebarProvider style="--sidebar-width: 3rem" class="w-auto z-50">
|
||||
<ShadcnSidebar collapsible="none" class="border-r">
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild :isActive="route.path.startsWith('/inboxes')">
|
||||
<router-link :to="{ name: 'inboxes' }">
|
||||
<Inbox />
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem v-if="userStore.hasAdminTabPermissions">
|
||||
<SidebarMenuButton asChild :isActive="route.path.startsWith('/admin')">
|
||||
<router-link :to="{ name: 'admin' }">
|
||||
<Shield />
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem v-if="userStore.hasReportTabPermissions">
|
||||
<SidebarMenuButton asChild :isActive="route.path.startsWith('/reports')">
|
||||
<router-link :to="{ name: 'reports' }">
|
||||
<FileLineChart />
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<SidebarNavUser />
|
||||
</SidebarFooter>
|
||||
</ShadcnSidebar>
|
||||
</SidebarProvider>
|
||||
|
||||
<!-- Main sidebar that collapses -->
|
||||
<div class="flex-1">
|
||||
<Sidebar
|
||||
:userTeams="userStore.teams"
|
||||
:userViews="userViews"
|
||||
@create-view="openCreateViewForm = true"
|
||||
@edit-view="editView"
|
||||
@delete-view="deleteView"
|
||||
>
|
||||
<PageHeader />
|
||||
<RouterView />
|
||||
</div>
|
||||
<ViewForm v-model:openDialog="openCreateViewForm" v-model:view="view" />
|
||||
</Sidebar>
|
||||
<ViewForm v-model:openDialog="openCreateViewForm" v-model:view="view" />
|
||||
</Sidebar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Command box -->
|
||||
<Command />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { RouterView } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { initWS } from '@/websocket.js'
|
||||
@@ -41,7 +79,22 @@ import api from '@/api'
|
||||
import { toast as sooner } from 'vue-sonner'
|
||||
import Sidebar from '@/components/sidebar/Sidebar.vue'
|
||||
import Command from '@/features/command/CommandBox.vue'
|
||||
import { Inbox, Shield, FileLineChart } from 'lucide-vue-next'
|
||||
import { useRoute } from 'vue-router'
|
||||
import {
|
||||
Sidebar as ShadcnSidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarMenu,
|
||||
SidebarGroupContent,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarProvider
|
||||
} from '@/components/ui/sidebar'
|
||||
import SidebarNavUser from '@/components/sidebar/SidebarNavUser.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const emitter = useEmitter()
|
||||
const userStore = useUserStore()
|
||||
const conversationStore = useConversationStore()
|
||||
@@ -62,11 +115,6 @@ onMounted(() => {
|
||||
initStores()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
emitter.off(EMITTER_EVENTS.SHOW_TOAST, toast)
|
||||
emitter.off(EMITTER_EVENTS.REFRESH_LIST, refreshViews)
|
||||
})
|
||||
|
||||
// initialize data stores
|
||||
const initStores = async () => {
|
||||
await Promise.allSettled([
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
}
|
||||
|
||||
.page-content {
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
overflow-y: scroll;
|
||||
padding-bottom: 100px;
|
||||
background-color: #fff;
|
||||
@@ -20,6 +20,10 @@
|
||||
min-height: 100%;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
<script setup>
|
||||
import { adminNavItems, reportsNavItems, accountNavItems } from '@/constants/navigation'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import SidebarNavUser from './SidebarNavUser.vue'
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarHeader,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarSeparator,
|
||||
SidebarGroupContent,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
@@ -22,16 +19,13 @@ import {
|
||||
SidebarRail
|
||||
} from '@/components/ui/sidebar'
|
||||
import {
|
||||
Users,
|
||||
Inbox,
|
||||
ChevronRight,
|
||||
SlidersHorizontal,
|
||||
Shield,
|
||||
FileLineChart,
|
||||
EllipsisVertical,
|
||||
Plus,
|
||||
Search,
|
||||
MessageCircle
|
||||
CircleUserRound,
|
||||
UserSearch,
|
||||
UsersRound,
|
||||
Search
|
||||
} from 'lucide-vue-next'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -39,19 +33,17 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { filterNavItems } from '@/utils/nav-permissions'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
defineProps({
|
||||
userTeams: { type: Array, default: () => [] },
|
||||
userViews: { type: Array, default: () => [] }
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const route = useRoute()
|
||||
const emit = defineEmits(['createView', 'editView', 'deleteView'])
|
||||
|
||||
const openCreateViewDialog = () => {
|
||||
@@ -66,183 +58,8 @@ const deleteView = (view) => {
|
||||
emit('deleteView', view)
|
||||
}
|
||||
|
||||
const filterNavItemsByPermissions = (navItems, userStore) => {
|
||||
return navItems
|
||||
.filter((item) => {
|
||||
// Check if the item has permissions and if all are satisfied
|
||||
const hasPermission =
|
||||
!item.permissions || item.permissions.every((permission) => userStore.can(permission))
|
||||
|
||||
// Check if the children have permissions
|
||||
const filteredChildren = item.children
|
||||
? item.children.filter(
|
||||
(child) =>
|
||||
!child.permissions ||
|
||||
child.permissions.every((permission) => userStore.can(permission))
|
||||
)
|
||||
: []
|
||||
|
||||
// Include item only if it has permission and either no children or children with permission
|
||||
return hasPermission && (filteredChildren.length > 0 || !item.children)
|
||||
})
|
||||
.map((item) => ({
|
||||
...item,
|
||||
children: item.children
|
||||
? item.children.filter(
|
||||
(child) =>
|
||||
!child.permissions ||
|
||||
child.permissions.every((permission) => userStore.can(permission))
|
||||
)
|
||||
: []
|
||||
}))
|
||||
}
|
||||
|
||||
const filteredAdminNavItems = computed(() => filterNavItemsByPermissions(adminNavItems, userStore))
|
||||
|
||||
const hasAdminAccess = computed(() => {
|
||||
return filterNavItemsByPermissions(adminNavItems, userStore).length > 0
|
||||
})
|
||||
|
||||
const filterReportsNavItemsByPermissions = (navItems, userStore) => {
|
||||
return navItems.filter(
|
||||
(item) => !item.permissions || item.permissions.every((permission) => userStore.can(permission))
|
||||
)
|
||||
}
|
||||
|
||||
const filteredReportsNavItems = computed(() =>
|
||||
filterReportsNavItemsByPermissions(reportsNavItems, userStore)
|
||||
)
|
||||
|
||||
const hasReportsAccess = computed(() => filteredReportsNavItems.value.length > 0)
|
||||
|
||||
const reportsNavItems = [
|
||||
{
|
||||
title: 'Overview',
|
||||
href: '/reports/overview',
|
||||
permissions: ['reports:manage']
|
||||
}
|
||||
]
|
||||
|
||||
const adminNavItems = [
|
||||
{
|
||||
title: 'Workspace',
|
||||
children: [
|
||||
{
|
||||
title: 'General',
|
||||
href: '/admin/general',
|
||||
permissions: ['general_settings:manage']
|
||||
},
|
||||
{
|
||||
title: 'Business Hours',
|
||||
href: '/admin/business-hours',
|
||||
permissions: ['business_hours:manage']
|
||||
},
|
||||
{
|
||||
title: 'SLA',
|
||||
href: '/admin/sla',
|
||||
permissions: ['sla:manage']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Conversations',
|
||||
children: [
|
||||
{
|
||||
title: 'Tags',
|
||||
href: '/admin/conversations/tags',
|
||||
permissions: ['tags:manage']
|
||||
},
|
||||
{
|
||||
title: 'Macros',
|
||||
href: '/admin/conversations/macros',
|
||||
permissions: ['macros:manage']
|
||||
},
|
||||
{
|
||||
title: 'Statuses',
|
||||
href: '/admin/conversations/statuses',
|
||||
permissions: ['status:manage']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Inboxes',
|
||||
children: [
|
||||
{
|
||||
title: 'Inboxes',
|
||||
href: '/admin/inboxes',
|
||||
permissions: ['inboxes:manage']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Teammates',
|
||||
children: [
|
||||
{
|
||||
title: 'Users',
|
||||
href: '/admin/teams/users',
|
||||
permissions: ['users:manage']
|
||||
},
|
||||
{
|
||||
title: 'Teams',
|
||||
href: '/admin/teams/teams',
|
||||
permissions: ['teams:manage']
|
||||
},
|
||||
{
|
||||
title: 'Roles',
|
||||
href: '/admin/teams/roles',
|
||||
permissions: ['roles:manage']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Automations',
|
||||
children: [
|
||||
{
|
||||
title: 'Automations',
|
||||
href: '/admin/automations',
|
||||
permissions: ['automations:manage']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Notifications',
|
||||
children: [
|
||||
{
|
||||
title: 'Email',
|
||||
href: '/admin/notification',
|
||||
permissions: ['notification_settings:manage']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Templates',
|
||||
children: [
|
||||
{
|
||||
title: 'Templates',
|
||||
href: '/admin/templates',
|
||||
permissions: ['templates:manage']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Security',
|
||||
children: [
|
||||
{
|
||||
title: 'SSO',
|
||||
href: '/admin/oidc',
|
||||
permissions: ['oidc:manage']
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const accountNavItems = [
|
||||
{
|
||||
title: 'Profile',
|
||||
href: '/account/profile',
|
||||
description: 'Update your profile'
|
||||
}
|
||||
]
|
||||
const filteredAdminNavItems = computed(() => filterNavItems(adminNavItems, userStore.can))
|
||||
const filteredReportsNavItems = computed(() => filterNavItems(reportsNavItems, userStore.can))
|
||||
|
||||
const isActiveParent = (parentHref) => {
|
||||
return route.path.startsWith(parentHref)
|
||||
@@ -252,394 +69,305 @@ const isInboxRoute = (path) => {
|
||||
return path.startsWith('/inboxes')
|
||||
}
|
||||
|
||||
const sidebarOpen = useStorage('sidebarOpen', true)
|
||||
const sidebarOpen = useStorage('mainSidebarOpen', true)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-row justify-between h-full">
|
||||
<div class="flex-1">
|
||||
<SidebarProvider
|
||||
style="--sidebar-width: 16rem"
|
||||
:default-open="sidebarOpen"
|
||||
v-on:update:open="sidebarOpen = $event"
|
||||
>
|
||||
<!-- Main sidebar holding other sidebars -->
|
||||
<Sidebar
|
||||
collapsible="icon"
|
||||
class="overflow-hidden [&>[data-sidebar=sidebar]]:flex-row !border-r-0"
|
||||
>
|
||||
<!-- Icon sidebar -->
|
||||
<Sidebar collapsible="none" class="!w-[calc(var(--sidebar-width-icon)_+_1px)] border-r">
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent class="px-1.5 md:px-0">
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton :isActive="route.path && isInboxRoute(route.path)" asChild>
|
||||
<router-link :to="{ name: 'inboxes' }">
|
||||
<Inbox class="w-5 h-5" />
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem v-if="hasAdminAccess">
|
||||
<SidebarMenuButton
|
||||
:isActive="route.path && route.path.startsWith('/admin')"
|
||||
asChild
|
||||
>
|
||||
<router-link :to="{ name: 'admin' }">
|
||||
<Shield class="w-5 h-5" />
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem v-if="hasReportsAccess">
|
||||
<SidebarMenuButton
|
||||
:isActive="route.path && route.path.startsWith('/reports')"
|
||||
asChild
|
||||
>
|
||||
<router-link :to="{ name: 'reports' }">
|
||||
<FileLineChart class="w-5 h-5" />
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<SidebarNavUser />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
<SidebarProvider
|
||||
style="--sidebar-width: 14rem"
|
||||
:default-open="sidebarOpen"
|
||||
v-on:update:open="sidebarOpen = $event"
|
||||
>
|
||||
<!-- Reports sidebar -->
|
||||
<template
|
||||
v-if="
|
||||
userStore.hasReportTabPermissions &&
|
||||
route.matched.some((record) => record.name && record.name.startsWith('reports'))
|
||||
"
|
||||
>
|
||||
<Sidebar collapsible="offcanvas" class="border-r ml-12">
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton :isActive="isActiveParent('/reports/overview')" asChild>
|
||||
<div>
|
||||
<span class="font-semibold text-xl">Reports</span>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarSeparator />
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem v-for="item in filteredReportsNavItems" :key="item.title">
|
||||
<SidebarMenuButton :isActive="isActiveParent(item.href)" asChild>
|
||||
<router-link :to="item.href">
|
||||
<span>{{ item.title }}</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
</template>
|
||||
|
||||
<!-- Reports sidebar -->
|
||||
<template
|
||||
v-if="
|
||||
hasReportsAccess &&
|
||||
route.matched.some((record) => record.name && record.name.startsWith('reports'))
|
||||
"
|
||||
>
|
||||
<Sidebar collapsible="none" class="!border-r-0 bg-white">
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton :isActive="isActiveParent('/reports/overview')" asChild>
|
||||
<div>
|
||||
<span class="font-semibold text-2xl">Reports</span>
|
||||
</div>
|
||||
<!-- Admin Sidebar -->
|
||||
<template v-if="route.matched.some((record) => record.name && record.name.startsWith('admin'))">
|
||||
<Sidebar collapsible="offcanvas" class="border-r ml-12">
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton :isActive="isActiveParent('/admin')" asChild>
|
||||
<div>
|
||||
<span class="font-semibold text-xl">Admin</span>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarSeparator />
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem v-for="item in filteredAdminNavItems" :key="item.title">
|
||||
<SidebarMenuButton
|
||||
v-if="!item.children"
|
||||
:isActive="isActiveParent(item.href)"
|
||||
asChild
|
||||
>
|
||||
<router-link :to="item.href">
|
||||
<span>{{ item.title }}</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
|
||||
<Collapsible
|
||||
v-else
|
||||
class="group/collapsible"
|
||||
:default-open="isActiveParent(item.href)"
|
||||
>
|
||||
<CollapsibleTrigger as-child>
|
||||
<SidebarMenuButton :isActive="isActiveParent(item.href)">
|
||||
<span>{{ item.title }}</span>
|
||||
<ChevronRight
|
||||
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
/>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarSeparator />
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem v-for="item in filteredReportsNavItems" :key="item.title">
|
||||
<SidebarMenuButton :isActive="isActiveParent(item.href)" asChild>
|
||||
<router-link :to="item.href">
|
||||
<span>{{ item.title }}</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
</template>
|
||||
|
||||
<!-- Admin Sidebar -->
|
||||
<template
|
||||
v-if="route.matched.some((record) => record.name && record.name.startsWith('admin'))"
|
||||
>
|
||||
<Sidebar collapsible="none" class="!border-r-0 bg-white">
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton :isActive="isActiveParent('/admin')" asChild>
|
||||
<div>
|
||||
<span class="font-semibold text-2xl">Admin</span>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarSeparator />
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem v-for="item in filteredAdminNavItems" :key="item.title">
|
||||
<SidebarMenuButton
|
||||
v-if="!item.children"
|
||||
:isActive="isActiveParent(item.href)"
|
||||
asChild
|
||||
>
|
||||
<router-link :to="item.href">
|
||||
<span>{{ item.title }}</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
|
||||
<Collapsible
|
||||
v-else
|
||||
class="group/collapsible"
|
||||
:default-open="isActiveParent(item.href)"
|
||||
>
|
||||
<CollapsibleTrigger as-child>
|
||||
<SidebarMenuButton :isActive="isActiveParent(item.href)">
|
||||
<span>{{ item.title }}</span>
|
||||
<ChevronRight
|
||||
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
/>
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
<SidebarMenuSubItem v-for="child in item.children" :key="child.title">
|
||||
<SidebarMenuButton
|
||||
size="sm"
|
||||
:isActive="isActiveParent(child.href)"
|
||||
asChild
|
||||
>
|
||||
<router-link :to="child.href">
|
||||
<span>{{ child.title }}</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuSubItem>
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
</template>
|
||||
|
||||
<!-- Account sidebar -->
|
||||
<template v-if="isActiveParent('/account')">
|
||||
<Sidebar collapsible="none" class="!border-r-0 bg-white">
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton :isActive="isActiveParent('/account/profile')" asChild>
|
||||
<div>
|
||||
<span class="font-semibold text-2xl">Account</span>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarSeparator />
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem v-for="item in accountNavItems" :key="item.title">
|
||||
<SidebarMenuButton :isActive="isActiveParent(item.href)" asChild>
|
||||
<router-link :to="item.href">
|
||||
<span>{{ item.title }}</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuAction>
|
||||
<span class="sr-only">{{ item.description }}</span>
|
||||
</SidebarMenuAction>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
</template>
|
||||
|
||||
<!-- Inbox sidebar -->
|
||||
<template v-if="route.path && isInboxRoute(route.path)">
|
||||
<Sidebar collapsible="none" class="!border-r-0 bg-white">
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="font-semibold text-2xl">Inbox</div>
|
||||
<div class="ml-auto">
|
||||
<router-link :to="{ name: 'search' }">
|
||||
<div class="flex items-center bg-accent p-2 rounded-full">
|
||||
<Search
|
||||
class="transition-transform duration-200 hover:scale-110 cursor-pointer"
|
||||
size="15"
|
||||
stroke-width="2.5"
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
<SidebarMenuSubItem v-for="child in item.children" :key="child.title">
|
||||
<SidebarMenuButton size="sm" :isActive="isActiveParent(child.href)" asChild>
|
||||
<router-link :to="child.href">
|
||||
<span>{{ child.title }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuSubItem>
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
</template>
|
||||
|
||||
<!-- Account sidebar -->
|
||||
<template v-if="isActiveParent('/account')">
|
||||
<Sidebar collapsible="offcanvas" class="border-r ml-12">
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton :isActive="isActiveParent('/account/profile')" asChild>
|
||||
<div>
|
||||
<span class="font-semibold text-xl">Account</span>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarSeparator />
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem v-for="item in accountNavItems" :key="item.title">
|
||||
<SidebarMenuButton :isActive="isActiveParent(item.href)" asChild>
|
||||
<router-link :to="item.href">
|
||||
<span>{{ item.title }}</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuAction>
|
||||
<span class="sr-only">{{ item.description }}</span>
|
||||
</SidebarMenuAction>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
</template>
|
||||
|
||||
<!-- Inbox sidebar -->
|
||||
<template v-if="route.path && isInboxRoute(route.path)">
|
||||
<Sidebar collapsible="offcanvas" class="border-r ml-12">
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="font-semibold text-xl">Inbox</div>
|
||||
<div class="ml-auto">
|
||||
<router-link :to="{ name: 'search' }">
|
||||
<div class="flex items-center bg-accent p-2 rounded-full">
|
||||
<Search
|
||||
class="transition-transform duration-200 hover:scale-110 cursor-pointer"
|
||||
size="15"
|
||||
stroke-width="2.5"
|
||||
/>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarSeparator />
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild :isActive="isActiveParent('/inboxes/assigned')">
|
||||
<router-link :to="{ name: 'inbox', params: { type: 'assigned' } }">
|
||||
<CircleUserRound />
|
||||
<span>My inbox</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild :isActive="isActiveParent('/inboxes/unassigned')">
|
||||
<router-link :to="{ name: 'inbox', params: { type: 'unassigned' } }">
|
||||
<UserSearch />
|
||||
<span>Unassigned</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild :isActive="isActiveParent('/inboxes/all')">
|
||||
<router-link :to="{ name: 'inbox', params: { type: 'all' } }">
|
||||
<UsersRound />
|
||||
<span>All</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
<!-- Team Inboxes -->
|
||||
<Collapsible defaultOpen class="group/collapsible" v-if="userTeams.length">
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger as-child>
|
||||
<SidebarMenuButton asChild>
|
||||
<router-link to="#">
|
||||
<!-- <Users /> -->
|
||||
<span>Team inboxes</span>
|
||||
<ChevronRight
|
||||
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
/>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarSeparator />
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarMenu>
|
||||
<!-- Inboxes Collapsible -->
|
||||
<Collapsible class="group/collapsible" defaultOpen>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger as-child>
|
||||
<SidebarMenuButton>
|
||||
<MessageCircle />
|
||||
<span>Inboxes</span>
|
||||
<ChevronRight
|
||||
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
/>
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuButton
|
||||
size="sm"
|
||||
:isActive="isActiveParent('/inboxes/assigned')"
|
||||
asChild
|
||||
>
|
||||
<router-link :to="{ name: 'inbox', params: { type: 'assigned' } }">
|
||||
<span>My inbox</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuSubItem>
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuButton
|
||||
size="sm"
|
||||
:isActive="isActiveParent('/inboxes/unassigned')"
|
||||
asChild
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: 'inbox', params: { type: 'unassigned' } }"
|
||||
>
|
||||
<span>Unassigned</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuSubItem>
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuButton
|
||||
size="sm"
|
||||
:isActive="isActiveParent('/inboxes/all')"
|
||||
asChild
|
||||
>
|
||||
<router-link :to="{ name: 'inbox', params: { type: 'all' } }">
|
||||
<span>All</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuSubItem>
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub v-for="team in userTeams" :key="team.id">
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuButton
|
||||
size="sm"
|
||||
:is-active="route.params.teamID == team.id"
|
||||
asChild
|
||||
>
|
||||
<router-link :to="{ name: 'team-inbox', params: { teamID: team.id } }">
|
||||
{{ team.emoji }}<span>{{ team.name }}</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuSubItem>
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
|
||||
<!-- Team Inboxes -->
|
||||
<Collapsible defaultOpen class="group/collapsible" v-if="userTeams.length">
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger as-child>
|
||||
<SidebarMenuButton asChild>
|
||||
<router-link to="#">
|
||||
<Users />
|
||||
<span>Team inboxes</span>
|
||||
<ChevronRight
|
||||
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
/>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub v-for="team in userTeams" :key="team.id">
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuButton
|
||||
size="sm"
|
||||
:is-active="route.params.teamID == team.id"
|
||||
asChild
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: 'team-inbox', params: { teamID: team.id } }"
|
||||
>
|
||||
{{ team.emoji }}<span>{{ team.name }}</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuSubItem>
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
<!-- Views -->
|
||||
<Collapsible class="group/collapsible" defaultOpen>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger as-child>
|
||||
<SidebarMenuButton asChild>
|
||||
<router-link to="#">
|
||||
<!-- <SlidersHorizontal /> -->
|
||||
<span>Views</span>
|
||||
<div>
|
||||
<Plus
|
||||
size="18"
|
||||
@click.stop="openCreateViewDialog"
|
||||
class="rounded-lg cursor-pointer opacity-0 transition-all duration-200 group-hover:opacity-100 hover:bg-gray-200 hover:shadow-sm text-gray-600 hover:text-gray-800 transform hover:scale-105 active:scale-100 p-1"
|
||||
/>
|
||||
</div>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<!-- Views -->
|
||||
<Collapsible class="group/collapsible" defaultOpen>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger as-child>
|
||||
<SidebarMenuButton asChild>
|
||||
<router-link to="#">
|
||||
<SlidersHorizontal />
|
||||
<span>Views</span>
|
||||
<div>
|
||||
<Plus
|
||||
size="18"
|
||||
@click.stop="openCreateViewDialog"
|
||||
class="rounded-lg cursor-pointer opacity-0 transition-all duration-200 group-hover:opacity-100 hover:bg-gray-200 hover:shadow-sm text-gray-600 hover:text-gray-800 transform hover:scale-105 active:scale-100 p-1"
|
||||
/>
|
||||
</div>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<SidebarMenuAction>
|
||||
<ChevronRight
|
||||
class="transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
v-if="userViews.length"
|
||||
/>
|
||||
</SidebarMenuAction>
|
||||
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub v-for="view in userViews" :key="view.id">
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuButton
|
||||
size="sm"
|
||||
:isActive="route.params.viewID == view.id"
|
||||
asChild
|
||||
>
|
||||
<router-link :to="{ name: 'view-inbox', params: { viewID: view.id } }">
|
||||
<span class="break-all w-24">{{ view.name }}</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
|
||||
<SidebarMenuAction>
|
||||
<ChevronRight
|
||||
class="transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
v-if="userViews.length"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<EllipsisVertical />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem @click="() => editView(view)">
|
||||
<span>Edit</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="() => deleteView(view)">
|
||||
<span>Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuAction>
|
||||
</SidebarMenuSubItem>
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</template>
|
||||
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub v-for="view in userViews" :key="view.id">
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuButton
|
||||
size="sm"
|
||||
:isActive="route.params.viewID == view.id"
|
||||
asChild
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: 'view-inbox', params: { viewID: view.id } }"
|
||||
>
|
||||
<span class="break-all w-24">{{ view.name }}</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
|
||||
<SidebarMenuAction>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<EllipsisVertical />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem @click="() => editView(view)">
|
||||
<span>Edit</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="() => deleteView(view)">
|
||||
<span>Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuAction>
|
||||
</SidebarMenuSubItem>
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</template>
|
||||
</Sidebar>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<SidebarInset>
|
||||
<slot></slot>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Main Content Area -->
|
||||
<SidebarInset>
|
||||
<slot></slot>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
export const reportsNavItems = [
|
||||
{
|
||||
title: 'Overview',
|
||||
href: '/reports/overview',
|
||||
permission: 'reports:manage'
|
||||
}
|
||||
]
|
||||
|
||||
export const adminNavItems = [
|
||||
{
|
||||
title: 'Workspace',
|
||||
children: [
|
||||
{
|
||||
title: 'General',
|
||||
href: '/admin/general',
|
||||
permission: 'general_settings:manage'
|
||||
},
|
||||
{
|
||||
title: 'Business Hours',
|
||||
href: '/admin/business-hours',
|
||||
permission: 'business_hours:manage'
|
||||
},
|
||||
{
|
||||
title: 'SLA',
|
||||
href: '/admin/sla',
|
||||
permission: 'sla:manage'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Conversations',
|
||||
children: [
|
||||
{
|
||||
title: 'Tags',
|
||||
href: '/admin/conversations/tags',
|
||||
permission: 'tags:manage'
|
||||
},
|
||||
{
|
||||
title: 'Macros',
|
||||
href: '/admin/conversations/macros',
|
||||
permission: 'macros:manage'
|
||||
},
|
||||
{
|
||||
title: 'Statuses',
|
||||
href: '/admin/conversations/statuses',
|
||||
permission: 'status:manage'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Inboxes',
|
||||
children: [
|
||||
{
|
||||
title: 'Inboxes',
|
||||
href: '/admin/inboxes',
|
||||
permission: 'inboxes:manage'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Teammates',
|
||||
children: [
|
||||
{
|
||||
title: 'Users',
|
||||
href: '/admin/teams/users',
|
||||
permission: 'users:manage'
|
||||
},
|
||||
{
|
||||
title: 'Teams',
|
||||
href: '/admin/teams/teams',
|
||||
permission: 'teams:manage'
|
||||
},
|
||||
{
|
||||
title: 'Roles',
|
||||
href: '/admin/teams/roles',
|
||||
permission: 'roles:manage'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Automations',
|
||||
children: [
|
||||
{
|
||||
title: 'Automations',
|
||||
href: '/admin/automations',
|
||||
permission: 'automations:manage'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Notifications',
|
||||
children: [
|
||||
{
|
||||
title: 'Email',
|
||||
href: '/admin/notification',
|
||||
permission: 'notification_settings:manage'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Templates',
|
||||
children: [
|
||||
{
|
||||
title: 'Templates',
|
||||
href: '/admin/templates',
|
||||
permission: 'templates:manage'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Security',
|
||||
children: [
|
||||
{
|
||||
title: 'SSO',
|
||||
href: '/admin/oidc',
|
||||
permission: 'oidc:manage'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
export const accountNavItems = [
|
||||
{
|
||||
title: 'Profile',
|
||||
href: '/account/profile',
|
||||
description: 'Update your profile'
|
||||
}
|
||||
]
|
||||
@@ -38,8 +38,8 @@
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem @click="handleSortChange('oldest')">Oldest</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="handleSortChange('newest')">Newest</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="handleSortChange('oldest')">Oldest activity</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="handleSortChange('newest')">Newest activity</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="handleSortChange('started_first')"
|
||||
>Started first</DropdownMenuItem
|
||||
>
|
||||
|
||||
@@ -82,7 +82,7 @@ const convStore = useConversationStore()
|
||||
const showQuotedText = ref(false)
|
||||
|
||||
const getAvatar = computed(() => {
|
||||
return convStore.current?.avatar_url || ''
|
||||
return convStore.current?.contact.avatar_url || ''
|
||||
})
|
||||
|
||||
const sanitizedMessageContent = computed(() => {
|
||||
|
||||
@@ -58,8 +58,8 @@ export const useConversationStore = defineStore('conversation', () => {
|
||||
}
|
||||
|
||||
const sortFieldLabels = {
|
||||
oldest: 'Oldest',
|
||||
newest: 'Newest',
|
||||
oldest: 'Oldest activity',
|
||||
newest: 'Newest activity',
|
||||
started_first: 'Started first',
|
||||
started_last: 'Started last',
|
||||
waiting_longest: 'Waiting longest',
|
||||
|
||||
@@ -3,6 +3,8 @@ import { defineStore } from 'pinia'
|
||||
import { handleHTTPError } from '@/utils/http'
|
||||
import { useEmitter } from '@/composables/useEmitter'
|
||||
import { EMITTER_EVENTS } from '@/constants/emitterEvents'
|
||||
import { adminNavItems, reportsNavItems } from '@/constants/navigation'
|
||||
import { filterNavItems } from '@/utils/nav-permissions'
|
||||
import api from '@/api'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
@@ -37,6 +39,18 @@ export const useUserStore = defineStore('user', () => {
|
||||
return `${firstInitial}${lastInitial}`
|
||||
})
|
||||
|
||||
const can = (permission) => {
|
||||
return user.value.permissions.includes(permission)
|
||||
}
|
||||
|
||||
const hasAdminTabPermissions = computed(() => {
|
||||
return filterNavItems(adminNavItems, can).length > 0
|
||||
})
|
||||
|
||||
const hasReportTabPermissions = computed(() => {
|
||||
return filterNavItems(reportsNavItems, can).length > 0
|
||||
})
|
||||
|
||||
const getCurrentUser = async () => {
|
||||
try {
|
||||
const response = await api.getCurrentUser()
|
||||
@@ -69,10 +83,6 @@ export const useUserStore = defineStore('user', () => {
|
||||
user.value.avatar_url = ''
|
||||
}
|
||||
|
||||
const can = (permission) => {
|
||||
return user.value.permissions.includes(permission)
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
userID,
|
||||
@@ -84,6 +94,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
permissions,
|
||||
getFullName,
|
||||
getInitials,
|
||||
hasAdminTabPermissions,
|
||||
hasReportTabPermissions,
|
||||
getCurrentUser,
|
||||
clearAvatar,
|
||||
setAvatar,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export const filterNavItems = (navItems, can) => {
|
||||
return navItems
|
||||
.map(item => {
|
||||
// Process children first
|
||||
const filteredChildren = item.children
|
||||
? filterNavItems(item.children, can)
|
||||
: undefined
|
||||
// Check item's permission
|
||||
const hasAccess = item.permission ? can(item.permission) : true
|
||||
// Only keep the item if:
|
||||
// 1. Has required permission (or none required)
|
||||
// 2. Has valid children (if parent item)
|
||||
const keep = hasAccess && (!item.children || filteredChildren.length > 0)
|
||||
return keep ? { ...item, children: filteredChildren } : null
|
||||
})
|
||||
.filter(Boolean) // Remove null entries
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<div class="grow">
|
||||
<div class="grow min-w-[500px]">
|
||||
<Conversation v-if="conversationStore.current || conversationStore.conversation.loading" />
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ConversationPlaceholder v-if="!conversationStore.current && !conversationStore.conversation.loading" />
|
||||
<ConversationPlaceholder v-if="route.name === 'inbox'" />
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ module.exports = {
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
sidebar: "white",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
|
||||
Reference in New Issue
Block a user