@finema/finework-layer 0.2.19 → 0.2.20

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,316 +1,316 @@
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">
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
50
  <div
51
51
  v-if="isGroup"
52
52
  class="space-y-5"
53
- >
53
+ >
54
54
  <div
55
55
  v-for="group in navigationItems"
56
56
  :key="group.label"
57
- >
58
- <div class="truncate px-2.5 text-xs font-bold text-gray-500">
59
- {{ group.label }}
60
- </div>
57
+ >
58
+ <div class="truncate px-2.5 text-xs font-bold text-gray-500">
59
+ {{ group.label }}
60
+ </div>
61
61
  <Separator
62
62
  v-if="group.label"
63
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>
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
121
  <div
122
122
  v-if="isMobile"
123
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
- return {
211
- active: isChildCurrentlyActive,
212
- class:
213
- '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',
214
- icon: '',
215
- ...child,
216
- }
217
- })
218
-
219
- const allPaths = props.items
220
- .flatMap((i) => [i.to, ...(i.children?.map((c) => c.to) || [])])
221
- .filter(Boolean) as string[]
222
-
223
- const hasExactMatch = allPaths.some(
224
- (p) => normalizePath(route.path) === normalizePath(p),
225
- )
226
-
227
- const selfIsActive = item.to
228
- ? normalizePath(route.path) === normalizePath(String(item.to))
229
- || (route.path.startsWith(normalizePath(String(item.to)))
230
- && !hasExactMatch)
231
- : false
232
-
233
- const itemIsActive = selfIsActive || isAnyChildActive
234
-
235
- const itemDefaultOpen = item.children ? isAnyChildActive : false
236
-
237
- return {
238
- ...item,
239
- active: itemIsActive,
240
- class: itemIsActive
241
- ? 'before:bg-[#F9FAFB] before:rounded-lg text-primary'
242
- : '',
243
- defaultOpen: itemDefaultOpen || selfIsActive,
244
- open: itemDefaultOpen || selfIsActive,
245
- children: mappedChildren,
246
- to: mappedChildren ? undefined : item.to,
247
- }
248
- })
249
- }
250
-
251
- // filter items base on auth.hasPermission and nested children , permissions: ['pmo:USER', 'pmo:ADMIN', 'pmo:SUPER']
252
- const filterItems = (items: NavigationMenuItem[]): NavigationMenuItem[] => {
253
- return items
254
- .filter((item) => {
255
- if (item.permissions && Array.isArray(item.permissions)) {
256
- // permissions: ['pmo:USER', 'pmo:ADMIN', 'pmo:SUPER'] => module: 'pmo', permissions: ['USER', 'ADMIN', 'SUPER']
257
- const permissionStrings = item.permissions as string[]
258
-
259
- // Extract module from first permission (all should have same module)
260
- const firstPermission = permissionStrings[0]
261
-
262
- if (!firstPermission) {
263
- return true
264
- }
265
-
266
- const [moduleStr] = firstPermission.split(':')
267
-
268
- // Extract all permission levels
269
- const permissionLevels = permissionStrings.map(
270
- (p) => p.split(':')[1],
271
- ) as Permission[]
272
-
273
- return auth.hasPermission(moduleStr as UserModule, ...permissionLevels)
274
- }
275
-
276
- // Recursively filter children
277
- if (item.children) {
278
- const filteredChildren = filterItems(
279
- item.children as NavigationMenuItem[],
280
- )
281
-
282
- // Only include parent if it has visible children or no permission requirement
283
- return filteredChildren.length > 0
284
- }
285
-
286
- return true
287
- })
288
- .map((item) => {
289
- // Apply filtering to children
290
- if (item.children) {
291
- return {
292
- ...item,
293
- children: filterItems(item.children as NavigationMenuItem[]),
294
- }
295
- }
296
-
297
- return item
298
- })
299
- }
300
-
301
- const navigationItems = computed<NavigationMenuItem[]>(() => {
302
- // First filter items based on permissions
303
- const filteredItems = filterItems(props.items)
304
-
305
- if (props.isGroup) {
306
- return filteredItems.map((group) => {
307
- return {
308
- ...group,
309
- children: mappedItems(group.children as NavigationMenuItem[]),
310
- }
311
- })
312
- }
313
-
314
- return mappedItems(filteredItems)
315
- })
316
- </script>
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
+ return {
211
+ active: isChildCurrentlyActive,
212
+ class:
213
+ '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',
214
+ icon: '',
215
+ ...child,
216
+ }
217
+ })
218
+
219
+ const allPaths = props.items
220
+ .flatMap((i) => [i.to, ...(i.children?.map((c) => c.to) || [])])
221
+ .filter(Boolean) as string[]
222
+
223
+ const hasExactMatch = allPaths.some(
224
+ (p) => normalizePath(route.path) === normalizePath(p),
225
+ )
226
+
227
+ const selfIsActive = item.to
228
+ ? normalizePath(route.path) === normalizePath(String(item.to))
229
+ || (route.path.startsWith(normalizePath(String(item.to)))
230
+ && !hasExactMatch)
231
+ : false
232
+
233
+ const itemIsActive = selfIsActive || isAnyChildActive
234
+
235
+ const itemDefaultOpen = item.children ? isAnyChildActive : false
236
+
237
+ return {
238
+ ...item,
239
+ active: itemIsActive,
240
+ class: itemIsActive
241
+ ? 'before:bg-[#F9FAFB] before:rounded-lg text-primary'
242
+ : '',
243
+ defaultOpen: itemDefaultOpen || selfIsActive,
244
+ open: itemDefaultOpen || selfIsActive,
245
+ children: mappedChildren,
246
+ to: mappedChildren ? undefined : item.to,
247
+ }
248
+ })
249
+ }
250
+
251
+ // filter items base on auth.hasPermission and nested children , permissions: ['pmo:USER', 'pmo:ADMIN', 'pmo:SUPER']
252
+ const filterItems = (items: NavigationMenuItem[]): NavigationMenuItem[] => {
253
+ return items
254
+ .filter((item) => {
255
+ if (item.permissions && Array.isArray(item.permissions)) {
256
+ // permissions: ['pmo:USER', 'pmo:ADMIN', 'pmo:SUPER'] => module: 'pmo', permissions: ['USER', 'ADMIN', 'SUPER']
257
+ const permissionStrings = item.permissions as string[]
258
+
259
+ // Extract module from first permission (all should have same module)
260
+ const firstPermission = permissionStrings[0]
261
+
262
+ if (!firstPermission) {
263
+ return true
264
+ }
265
+
266
+ const [moduleStr] = firstPermission.split(':')
267
+
268
+ // Extract all permission levels
269
+ const permissionLevels = permissionStrings.map(
270
+ (p) => p.split(':')[1],
271
+ ) as Permission[]
272
+
273
+ return auth.hasPermission(moduleStr as UserModule, ...permissionLevels)
274
+ }
275
+
276
+ // Recursively filter children
277
+ if (item.children) {
278
+ const filteredChildren = filterItems(
279
+ item.children as NavigationMenuItem[],
280
+ )
281
+
282
+ // Only include parent if it has visible children or no permission requirement
283
+ return filteredChildren.length > 0
284
+ }
285
+
286
+ return true
287
+ })
288
+ .map((item) => {
289
+ // Apply filtering to children
290
+ if (item.children) {
291
+ return {
292
+ ...item,
293
+ children: filterItems(item.children as NavigationMenuItem[]),
294
+ }
295
+ }
296
+
297
+ return item
298
+ })
299
+ }
300
+
301
+ const navigationItems = computed<NavigationMenuItem[]>(() => {
302
+ // First filter items based on permissions
303
+ const filteredItems = filterItems(props.items)
304
+
305
+ if (props.isGroup) {
306
+ return filteredItems.map((group) => {
307
+ return {
308
+ ...group,
309
+ children: mappedItems(group.children as NavigationMenuItem[]),
310
+ }
311
+ })
312
+ }
313
+
314
+ return mappedItems(filteredItems)
315
+ })
316
+ </script>