@finema/finework-layer 0.2.68 → 0.2.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,329 +1,329 @@
1
- <template>
2
- <aside
3
- :class="[
4
- `
5
- flex h-full flex-col overflow-y-auto bg-white
6
- transition-all duration-300
7
- `,
8
- ]"
9
- >
10
- <div
11
- :class="[
12
- 'flex h-[72px] items-center max-sm:pr-5',
13
- {
14
- 'justify-center': isCollapsed,
15
- 'w-[260px] justify-between pl-5': !isCollapsed,
16
- },
17
- ]"
18
- >
19
- <NuxtLink :to="routes.home.to">
20
- <img
21
- v-if="!isCollapsed"
22
- src="/logo-mini.png"
23
- alt="logo_mini_color"
24
- class="h-[27px]"
25
- />
26
- </NuxtLink>
27
-
28
- <Navbar />
29
- </div>
30
- <div
31
- class="bg-primary flex items-center justify-between px-7 py-4 font-bold text-white"
32
- >
33
- {{ isCollapsed ? "" : label }}
34
-
35
- <Icon
36
- v-if="!isCollapsed"
37
- name="fluent:arrow-circle-left-24-regular"
38
- class="size-[24px] cursor-pointer"
39
- @click="$emit('toggle-collapsed')"
40
- />
41
- <Icon
42
- v-else
43
- name="fluent:arrow-circle-right-24-regular"
44
- class="size-[24px] cursor-pointer"
45
- @click="$emit('toggle-collapsed')"
46
- />
47
- </div>
48
-
49
- <div class="flex-1 overflow-y-auto border-r border-gray-200 p-4">
50
- <div
51
- v-if="isGroup"
52
- class="space-y-5"
53
- >
54
- <div
55
- v-for="group in navigationItems"
56
- :key="group.label"
57
- >
58
- <div class="truncate px-2.5 text-xs font-bold text-gray-500">
59
- {{ group.label }}
60
- </div>
61
- <Separator
62
- v-if="group.label"
63
- class="mt-3 mb-1"
64
- />
65
- <NavigationMenu
66
- orientation="vertical"
67
- :items="group.children"
68
- :collapsed="isCollapsed"
69
- :popover="isCollapsed"
70
- :tooltip="isCollapsed"
71
- :ui="{
72
- list: 'space-y-2 ',
73
- label: [
74
- 'text-sm font-bold text-gray-500 py-[12px] px-[10px] rounded-lg',
75
- 'hover:text-primary',
76
- ],
77
- link: [
78
- 'cursor-pointer text-sm font-bold text-gray-500 px-[10px] rounded-lg gap-3',
79
- 'hover:text-primary',
80
- 'data-active:before:bg-[#F9FAFB] data-active:before:rounded-lg data-active:text-primary font-semibold',
81
- ],
82
- linkLabel: '!text-[#344054] font-bold',
83
- linkLeadingIcon:
84
- 'group-data-[state=open]:text-current text-current size-[24px] group-hover:text-primary',
85
- childList: 'border-none ms-0 bg-white mt-2 rounded-lg',
86
- childLink: 'ps-0',
87
- childItem: 'ps-0',
88
- }"
89
- class="w-full justify-center"
90
- />
91
- </div>
92
- </div>
93
- <NavigationMenu
94
- v-else
95
- orientation="vertical"
96
- :items="navigationItems"
97
- :collapsed="isCollapsed"
98
- :popover="isCollapsed"
99
- :tooltip="isCollapsed"
100
- :ui="{
101
- list: 'space-y-2 ',
102
- label: [
103
- 'text-sm font-bold text-gray-500 py-[12px] px-[10px] rounded-lg',
104
- 'hover:text-primary ',
105
- ],
106
- link: [
107
- 'cursor-pointer text-sm font-bold text-gray-500 px-[10px] rounded-lg gap-3',
108
- 'hover:text-primary',
109
- 'data-active:before:bg-[#F9FAFB] data-active:before:rounded-lg data-active:text-primary font-semibold',
110
- ],
111
- linkLabel: '!text-[#344054] font-bold',
112
- linkLeadingIcon:
113
- 'group-data-[state=open]:text-current text-current size-[24px] group-hover:text-primary ',
114
- childList: 'border-none ms-0 bg-white mt-2 rounded-lg',
115
- childLink: 'ps-0',
116
- childItem: 'ps-0',
117
- }"
118
- class="w-full justify-center"
119
- />
120
- </div>
121
- <div
122
- v-if="isMobile"
123
- class="border-t border-gray-100 p-3"
124
- >
125
- <div class="flex items-center justify-between gap-2">
126
- <div class="flex min-w-0 flex-1 items-center gap-3">
127
- <Avatar
128
- class="border-muted size-[32px] flex-shrink-0 border text-lg"
129
- icon="ri:user-line"
130
- :src="auth.me.value?.avatar_url || ''"
131
- />
132
- <div class="flex min-w-0 flex-1 flex-col">
133
- <p class="truncate text-sm font-bold">
134
- {{ auth.me.value?.display_name || auth.me.value?.full_name }}
135
- </p>
136
- <p class="text-muted truncate text-xs">
137
- {{ auth.me.value?.email || "" }}
138
- </p>
139
- </div>
140
- </div>
141
- <DropdownMenu
142
- arrow
143
- size="xl"
144
- :items="userMenuItems"
145
- :ui="{
146
- content: 'w-48',
147
- }"
148
- >
149
- <Button
150
- icon="ph:dots-three-outline-vertical-bold"
151
- variant="ghost"
152
- color="neutral"
153
- size="xs"
154
- />
155
- </DropdownMenu>
156
- </div>
157
- </div>
158
- </aside>
159
- </template>
160
-
161
- <script lang="ts" setup>
162
- import type { NavigationMenuItem } from '@nuxt/ui'
163
- import { computed } from 'vue'
164
- import { useRoute } from 'vue-router'
165
- import type { Permission, UserModule } from '#imports'
166
- import Navbar from '../Apps.vue'
167
-
168
- defineEmits<{
169
- 'toggle-collapsed': []
170
- }>()
171
-
172
- const props = defineProps<{
173
- label: string
174
- isCollapsed: boolean
175
- isMobile?: boolean
176
- items: NavigationMenuItem[]
177
- isGroup?: boolean
178
- }>()
179
-
180
- const route = useRoute()
181
- const auth = useAuth()
182
- const userMenuItems = [
183
- {
184
- label: 'View profile',
185
- icon: 'i-lucide-user',
186
- to: routes.account.profile.to,
187
- },
188
-
189
- {
190
- label: routes.logout.label,
191
- icon: 'i-lucide-log-out',
192
- to: routes.logout.to,
193
- external: true,
194
- },
195
- ]
196
-
197
- const normalizePath = (path: string) => path.replace(/\/$/, '')
198
-
199
- const mappedItems = (items: NavigationMenuItem[]): NavigationMenuItem[] => {
200
- return items.map((item) => {
201
- let isAnyChildActive = false
202
- const mappedChildren = item.children?.map((child) => {
203
- const isChildCurrentlyActive
204
- = normalizePath(route.path) === normalizePath(String(child.to))
205
-
206
- if (isChildCurrentlyActive) {
207
- isAnyChildActive = true
208
- }
209
-
210
- const baseClass
211
- = 'hover:bg-[#F9FAFB] hover:text-gray-700 hover:font-bold py-2 pl-8 data-active:before:bg-[#F9FAFB] data-active:text-primary data-active:font-bold'
212
-
213
- const hasIcon = !!child.icon
214
- const className = hasIcon ? baseClass : `${baseClass} gap-0 data-active:text-gray-500!`
215
- const icon = hasIcon ? child.icon : 'ph:dot-outline-fill'
216
-
217
- return {
218
- active: isChildCurrentlyActive,
219
- class: className,
220
- icon,
221
- ...child,
222
- }
223
- })
224
-
225
- const allPaths = props.items
226
- .flatMap((i) => [i.to, ...(i.children?.map((c) => c.to) || [])])
227
- .filter(Boolean) as string[]
228
-
229
- const hasExactMatch = allPaths.some(
230
- (p) => normalizePath(route.path) === normalizePath(p),
231
- )
232
-
233
- const basePath = item.to ? normalizePath(String(item.to)) : ''
234
- const currentPath = normalizePath(route.path)
235
- const selfIsActive = item.to
236
- ? (
237
- currentPath === basePath
238
- || (
239
- currentPath.startsWith(`${basePath}/`)
240
- && !currentPath.slice(basePath.length + 1).includes('/')
241
- && !hasExactMatch
242
- )
243
- )
244
- : false
245
-
246
- const itemIsActive = selfIsActive || isAnyChildActive
247
-
248
- const itemDefaultOpen = item.children ? isAnyChildActive : false
249
-
250
- return {
251
- ...item,
252
- active: itemIsActive,
253
- class: itemIsActive
254
- ? 'before:bg-[#F9FAFB] before:rounded-lg text-primary'
255
- : '',
256
- defaultOpen: itemDefaultOpen || selfIsActive,
257
- open: itemDefaultOpen || selfIsActive,
258
- children: mappedChildren,
259
- to: mappedChildren ? undefined : item.to,
260
- }
261
- })
262
- }
263
-
264
- // filter items base on auth.hasPermission and nested children , permissions: ['pmo:USER', 'pmo:ADMIN', 'pmo:SUPER']
265
- const filterItems = (items: NavigationMenuItem[]): NavigationMenuItem[] => {
266
- return items
267
- .filter((item) => {
268
- if (item.permissions && Array.isArray(item.permissions)) {
269
- // permissions: ['pmo:USER', 'pmo:ADMIN', 'pmo:SUPER'] => module: 'pmo', permissions: ['USER', 'ADMIN', 'SUPER']
270
- const permissionStrings = item.permissions as string[]
271
-
272
- // Extract module from first permission (all should have same module)
273
- const firstPermission = permissionStrings[0]
274
-
275
- if (!firstPermission) {
276
- return true
277
- }
278
-
279
- const [moduleStr] = firstPermission.split(':')
280
-
281
- // Extract all permission levels
282
- const permissionLevels = permissionStrings.map(
283
- (p) => p.split(':')[1],
284
- ) as Permission[]
285
-
286
- return auth.hasPermission(moduleStr as UserModule, ...permissionLevels)
287
- }
288
-
289
- // Recursively filter children
290
- if (item.children) {
291
- const filteredChildren = filterItems(
292
- item.children as NavigationMenuItem[],
293
- )
294
-
295
- // Only include parent if it has visible children or no permission requirement
296
- return filteredChildren.length > 0
297
- }
298
-
299
- return true
300
- })
301
- .map((item) => {
302
- // Apply filtering to children
303
- if (item.children) {
304
- return {
305
- ...item,
306
- children: filterItems(item.children as NavigationMenuItem[]),
307
- }
308
- }
309
-
310
- return item
311
- })
312
- }
313
-
314
- const navigationItems = computed<NavigationMenuItem[]>(() => {
315
- // First filter items based on permissions
316
- const filteredItems = filterItems(props.items)
317
-
318
- if (props.isGroup) {
319
- return filteredItems.map((group) => {
320
- return {
321
- ...group,
322
- children: mappedItems(group.children as NavigationMenuItem[]),
323
- }
324
- })
325
- }
326
-
327
- return mappedItems(filteredItems)
328
- })
329
- </script>
1
+ <template>
2
+ <aside
3
+ :class="[
4
+ `
5
+ flex h-full flex-col overflow-y-auto bg-white
6
+ transition-all duration-300
7
+ `,
8
+ ]"
9
+ >
10
+ <div
11
+ :class="[
12
+ 'flex h-[72px] items-center max-sm:pr-5',
13
+ {
14
+ 'justify-center': isCollapsed,
15
+ 'w-[260px] justify-between pl-5': !isCollapsed,
16
+ },
17
+ ]"
18
+ >
19
+ <NuxtLink :to="routes.home.to">
20
+ <img
21
+ v-if="!isCollapsed"
22
+ src="/logo-mini.png"
23
+ alt="logo_mini_color"
24
+ class="h-[27px]"
25
+ />
26
+ </NuxtLink>
27
+
28
+ <Navbar />
29
+ </div>
30
+ <div
31
+ class="bg-primary flex items-center justify-between px-7 py-4 font-bold text-white"
32
+ >
33
+ {{ isCollapsed ? "" : label }}
34
+
35
+ <Icon
36
+ v-if="!isCollapsed"
37
+ name="fluent:arrow-circle-left-24-regular"
38
+ class="size-[24px] cursor-pointer"
39
+ @click="$emit('toggle-collapsed')"
40
+ />
41
+ <Icon
42
+ v-else
43
+ name="fluent:arrow-circle-right-24-regular"
44
+ class="size-[24px] cursor-pointer"
45
+ @click="$emit('toggle-collapsed')"
46
+ />
47
+ </div>
48
+
49
+ <div class="flex-1 overflow-y-auto border-r border-gray-200 p-4">
50
+ <div
51
+ v-if="isGroup"
52
+ class="space-y-5"
53
+ >
54
+ <div
55
+ v-for="group in navigationItems"
56
+ :key="group.label"
57
+ >
58
+ <div class="truncate px-2.5 text-xs font-bold text-gray-500">
59
+ {{ group.label }}
60
+ </div>
61
+ <Separator
62
+ v-if="group.label"
63
+ class="mt-3 mb-1"
64
+ />
65
+ <NavigationMenu
66
+ orientation="vertical"
67
+ :items="group.children"
68
+ :collapsed="isCollapsed"
69
+ :popover="isCollapsed"
70
+ :tooltip="isCollapsed"
71
+ :ui="{
72
+ list: 'space-y-2 ',
73
+ label: [
74
+ 'text-sm font-bold text-gray-500 py-[12px] px-[10px] rounded-lg',
75
+ 'hover:text-primary',
76
+ ],
77
+ link: [
78
+ 'cursor-pointer text-sm font-bold text-gray-500 px-[10px] rounded-lg gap-3',
79
+ 'hover:text-primary',
80
+ 'data-active:before:bg-[#F9FAFB] data-active:before:rounded-lg data-active:text-primary font-semibold',
81
+ ],
82
+ linkLabel: '!text-[#344054] font-bold',
83
+ linkLeadingIcon:
84
+ 'group-data-[state=open]:text-current text-current size-[24px] group-hover:text-primary',
85
+ childList: 'border-none ms-0 bg-white mt-2 rounded-lg',
86
+ childLink: 'ps-0',
87
+ childItem: 'ps-0',
88
+ }"
89
+ class="w-full justify-center"
90
+ />
91
+ </div>
92
+ </div>
93
+ <NavigationMenu
94
+ v-else
95
+ orientation="vertical"
96
+ :items="navigationItems"
97
+ :collapsed="isCollapsed"
98
+ :popover="isCollapsed"
99
+ :tooltip="isCollapsed"
100
+ :ui="{
101
+ list: 'space-y-2 ',
102
+ label: [
103
+ 'text-sm font-bold text-gray-500 py-[12px] px-[10px] rounded-lg',
104
+ 'hover:text-primary ',
105
+ ],
106
+ link: [
107
+ 'cursor-pointer text-sm font-bold text-gray-500 px-[10px] rounded-lg gap-3',
108
+ 'hover:text-primary',
109
+ 'data-active:before:bg-[#F9FAFB] data-active:before:rounded-lg data-active:text-primary font-semibold',
110
+ ],
111
+ linkLabel: '!text-[#344054] font-bold',
112
+ linkLeadingIcon:
113
+ 'group-data-[state=open]:text-current text-current size-[24px] group-hover:text-primary ',
114
+ childList: 'border-none ms-0 bg-white mt-2 rounded-lg',
115
+ childLink: 'ps-0',
116
+ childItem: 'ps-0',
117
+ }"
118
+ class="w-full justify-center"
119
+ />
120
+ </div>
121
+ <div
122
+ v-if="isMobile"
123
+ class="border-t border-gray-100 p-3"
124
+ >
125
+ <div class="flex items-center justify-between gap-2">
126
+ <div class="flex min-w-0 flex-1 items-center gap-3">
127
+ <Avatar
128
+ class="border-muted size-[32px] flex-shrink-0 border text-lg"
129
+ icon="ri:user-line"
130
+ :src="auth.me.value?.avatar_url || ''"
131
+ />
132
+ <div class="flex min-w-0 flex-1 flex-col">
133
+ <p class="truncate text-sm font-bold">
134
+ {{ auth.me.value?.display_name || auth.me.value?.full_name }}
135
+ </p>
136
+ <p class="text-muted truncate text-xs">
137
+ {{ auth.me.value?.email || "" }}
138
+ </p>
139
+ </div>
140
+ </div>
141
+ <DropdownMenu
142
+ arrow
143
+ size="xl"
144
+ :items="userMenuItems"
145
+ :ui="{
146
+ content: 'w-48',
147
+ }"
148
+ >
149
+ <Button
150
+ icon="ph:dots-three-outline-vertical-bold"
151
+ variant="ghost"
152
+ color="neutral"
153
+ size="xs"
154
+ />
155
+ </DropdownMenu>
156
+ </div>
157
+ </div>
158
+ </aside>
159
+ </template>
160
+
161
+ <script lang="ts" setup>
162
+ import type { NavigationMenuItem } from '@nuxt/ui'
163
+ import { computed } from 'vue'
164
+ import { useRoute } from 'vue-router'
165
+ import type { Permission, UserModule } from '#imports'
166
+ import Navbar from '../Apps.vue'
167
+
168
+ defineEmits<{
169
+ 'toggle-collapsed': []
170
+ }>()
171
+
172
+ const props = defineProps<{
173
+ label: string
174
+ isCollapsed: boolean
175
+ isMobile?: boolean
176
+ items: NavigationMenuItem[]
177
+ isGroup?: boolean
178
+ }>()
179
+
180
+ const route = useRoute()
181
+ const auth = useAuth()
182
+ const userMenuItems = [
183
+ {
184
+ label: 'View profile',
185
+ icon: 'i-lucide-user',
186
+ to: routes.account.profile.to,
187
+ },
188
+
189
+ {
190
+ label: routes.logout.label,
191
+ icon: 'i-lucide-log-out',
192
+ to: routes.logout.to,
193
+ external: true,
194
+ },
195
+ ]
196
+
197
+ const normalizePath = (path: string) => path.replace(/\/$/, '')
198
+
199
+ const mappedItems = (items: NavigationMenuItem[]): NavigationMenuItem[] => {
200
+ return items.map((item) => {
201
+ let isAnyChildActive = false
202
+ const mappedChildren = item.children?.map((child) => {
203
+ const isChildCurrentlyActive
204
+ = normalizePath(route.path) === normalizePath(String(child.to))
205
+
206
+ if (isChildCurrentlyActive) {
207
+ isAnyChildActive = true
208
+ }
209
+
210
+ const baseClass
211
+ = 'hover:bg-[#F9FAFB] hover:text-gray-700 hover:font-bold py-2 pl-8 data-active:before:bg-[#F9FAFB] data-active:text-primary data-active:font-bold'
212
+
213
+ const hasIcon = !!child.icon
214
+ const className = hasIcon ? baseClass : `${baseClass} gap-0 data-active:text-gray-500!`
215
+ const icon = hasIcon ? child.icon : 'ph:dot-outline-fill'
216
+
217
+ return {
218
+ active: isChildCurrentlyActive,
219
+ class: className,
220
+ icon,
221
+ ...child,
222
+ }
223
+ })
224
+
225
+ const allPaths = props.items
226
+ .flatMap((i) => [i.to, ...(i.children?.map((c) => c.to) || [])])
227
+ .filter(Boolean) as string[]
228
+
229
+ const hasExactMatch = allPaths.some(
230
+ (p) => normalizePath(route.path) === normalizePath(p),
231
+ )
232
+
233
+ const basePath = item.to ? normalizePath(String(item.to)) : ''
234
+ const currentPath = normalizePath(route.path)
235
+ const selfIsActive = item.to
236
+ ? (
237
+ currentPath === basePath
238
+ || (
239
+ currentPath.startsWith(`${basePath}/`)
240
+ && !currentPath.slice(basePath.length + 1).includes('/')
241
+ && !hasExactMatch
242
+ )
243
+ )
244
+ : false
245
+
246
+ const itemIsActive = selfIsActive || isAnyChildActive
247
+
248
+ const itemDefaultOpen = item.children ? isAnyChildActive : false
249
+
250
+ return {
251
+ ...item,
252
+ active: itemIsActive,
253
+ class: itemIsActive
254
+ ? 'before:bg-[#F9FAFB] before:rounded-lg text-primary'
255
+ : '',
256
+ defaultOpen: itemDefaultOpen || selfIsActive,
257
+ open: itemDefaultOpen || selfIsActive,
258
+ children: mappedChildren,
259
+ to: mappedChildren ? undefined : item.to,
260
+ }
261
+ })
262
+ }
263
+
264
+ // filter items base on auth.hasPermission and nested children , permissions: ['pmo:USER', 'pmo:ADMIN', 'pmo:SUPER']
265
+ const filterItems = (items: NavigationMenuItem[]): NavigationMenuItem[] => {
266
+ return items
267
+ .filter((item) => {
268
+ if (item.permissions && Array.isArray(item.permissions)) {
269
+ // permissions: ['pmo:USER', 'pmo:ADMIN', 'pmo:SUPER'] => module: 'pmo', permissions: ['USER', 'ADMIN', 'SUPER']
270
+ const permissionStrings = item.permissions as string[]
271
+
272
+ // Extract module from first permission (all should have same module)
273
+ const firstPermission = permissionStrings[0]
274
+
275
+ if (!firstPermission) {
276
+ return true
277
+ }
278
+
279
+ const [moduleStr] = firstPermission.split(':')
280
+
281
+ // Extract all permission levels
282
+ const permissionLevels = permissionStrings.map(
283
+ (p) => p.split(':')[1],
284
+ ) as Permission[]
285
+
286
+ return auth.hasPermission(moduleStr as UserModule, ...permissionLevels)
287
+ }
288
+
289
+ // Recursively filter children
290
+ if (item.children) {
291
+ const filteredChildren = filterItems(
292
+ item.children as NavigationMenuItem[],
293
+ )
294
+
295
+ // Only include parent if it has visible children or no permission requirement
296
+ return filteredChildren.length > 0
297
+ }
298
+
299
+ return true
300
+ })
301
+ .map((item) => {
302
+ // Apply filtering to children
303
+ if (item.children) {
304
+ return {
305
+ ...item,
306
+ children: filterItems(item.children as NavigationMenuItem[]),
307
+ }
308
+ }
309
+
310
+ return item
311
+ })
312
+ }
313
+
314
+ const navigationItems = computed<NavigationMenuItem[]>(() => {
315
+ // First filter items based on permissions
316
+ const filteredItems = filterItems(props.items)
317
+
318
+ if (props.isGroup) {
319
+ return filteredItems.map((group) => {
320
+ return {
321
+ ...group,
322
+ children: mappedItems(group.children as NavigationMenuItem[]),
323
+ }
324
+ })
325
+ }
326
+
327
+ return mappedItems(filteredItems)
328
+ })
329
+ </script>