@finema/finework-layer 0.0.13 → 0.0.15

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.15](https://gitlab.finema.co/finema/finework/finework-frontend-layer/compare/0.0.14...0.0.15) (2025-10-14)
4
+
5
+ ### Bug Fixes
6
+
7
+ * **admin:** always show logo in sidebar ([914c216](https://gitlab.finema.co/finema/finework/finework-frontend-layer/commit/914c216503b911574d85d658b7cf76618d58e007))
8
+
9
+ ## [0.0.14](https://gitlab.finema.co/finema/finework/finework-frontend-layer/compare/0.0.13...0.0.14) (2025-10-14)
10
+
11
+ ### Bug Fixes
12
+
13
+ * **ui:** adjust responsive header height and padding ([000e5d7](https://gitlab.finema.co/finema/finework/finework-frontend-layer/commit/000e5d75d2779c5fd94354cf4426f5a9085af733))
14
+ * **ui:** adjust responsive layout of admin header elements ([0c46820](https://gitlab.finema.co/finema/finework/finework-frontend-layer/commit/0c468205b498a1ff6c260eefd32b4e244a671f96))
15
+
3
16
  ## [0.0.13](https://gitlab.finema.co/finema/finework/finework-frontend-layer/compare/0.0.12...0.0.13) (2025-10-14)
4
17
 
5
18
  ### Features
@@ -13,7 +13,6 @@
13
13
  >
14
14
  <NuxtLink :to="routes.home.to">
15
15
  <img
16
- v-if="!isCollapsed"
17
16
  src="/logo-mini.png"
18
17
  alt="logo_mini_color"
19
18
  class="h-[27px]"
@@ -148,6 +147,7 @@
148
147
  import type { NavigationMenuItem } from '@nuxt/ui'
149
148
  import { computed } from 'vue'
150
149
  import { useRoute } from 'vue-router'
150
+ import type { Permission, UserModule } from '#imports'
151
151
  import Navbar from './Apps.vue'
152
152
 
153
153
  defineEmits<{
@@ -218,9 +218,56 @@ const mappedItems = (items: NavigationMenuItem[]): NavigationMenuItem[] => {
218
218
  })
219
219
  }
220
220
 
221
+ // filter items base on auth.hasPermission and nested children , permissions: ['pmo:USER', 'pmo:ADMIN', 'pmo:SUPER']
222
+ const filterItems = (items: NavigationMenuItem[]): NavigationMenuItem[] => {
223
+ return items.filter((item) => {
224
+ if (item.permissions && Array.isArray(item.permissions)) {
225
+ // permissions: ['pmo:USER', 'pmo:ADMIN', 'pmo:SUPER'] => module: 'pmo', permissions: ['USER', 'ADMIN', 'SUPER']
226
+ const permissionStrings = item.permissions as string[]
227
+
228
+ // Extract module from first permission (all should have same module)
229
+ const firstPermission = permissionStrings[0]
230
+
231
+ if (!firstPermission) {
232
+ return true
233
+ }
234
+
235
+ const [moduleStr] = firstPermission.split(':')
236
+
237
+ // Extract all permission levels
238
+ const permissionLevels = permissionStrings.map((p) => p.split(':')[1]) as Permission[]
239
+
240
+ return auth.hasPermission(moduleStr as UserModule, ...permissionLevels)
241
+ }
242
+
243
+ // Recursively filter children
244
+ if (item.children) {
245
+ const filteredChildren = filterItems(item.children as NavigationMenuItem[])
246
+
247
+ // Only include parent if it has visible children or no permission requirement
248
+ return filteredChildren.length > 0
249
+ }
250
+
251
+ return true
252
+ }).map((item) => {
253
+ // Apply filtering to children
254
+ if (item.children) {
255
+ return {
256
+ ...item,
257
+ children: filterItems(item.children as NavigationMenuItem[]),
258
+ }
259
+ }
260
+
261
+ return item
262
+ })
263
+ }
264
+
221
265
  const navigationItems = computed<NavigationMenuItem[]>(() => {
266
+ // First filter items based on permissions
267
+ const filteredItems = filterItems(props.items)
268
+
222
269
  if (props.isGroup) {
223
- return props.items.map((group) => {
270
+ return filteredItems.map((group) => {
224
271
  return {
225
272
  ...group,
226
273
  children: mappedItems(group.children as NavigationMenuItem[]),
@@ -228,6 +275,6 @@ const navigationItems = computed<NavigationMenuItem[]>(() => {
228
275
  })
229
276
  }
230
277
 
231
- return mappedItems(props.items)
278
+ return mappedItems(filteredItems)
232
279
  })
233
280
  </script>
@@ -1,220 +1,220 @@
1
- <template>
2
- <div class="relative flex min-h-screen flex-1">
3
- <div
4
- :class="[`
5
- fixed inset-0 z-30 hidden w-auto
6
- lg:block
7
- `, {
8
- 'max-w-[88px]': isCollapsed,
9
- 'max-w-[260px]': !isCollapsed,
10
- }]"
11
- >
12
- <Sidebar
13
- :label="label"
14
- :is-collapsed="isCollapsed"
15
- :items="sidebarItems"
16
- :is-group="isSidebarGroup"
17
- @toggle-collapsed="isCollapsed = !isCollapsed"
18
- />
19
- </div>
20
- <div
21
- :class="['w-full bg-gray-50', {
22
- 'lg:pl-[88px]': isCollapsed,
23
- 'lg:pl-[260px]': !isCollapsed,
24
- }]"
25
- >
26
- <nav
27
- class="
28
- fixed top-0 left-0 z-20 flex min-h-[72px] w-screen items-center
29
- justify-between gap-4 bg-white px-5
30
- lg:justify-end
31
- "
32
- style="box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.04);"
33
- >
34
- <div class="flex items-center gap-4">
35
- <Slideover
36
- v-model:open="isShowSidebarMobile"
37
- :ui="{
38
- content: 'w-[80%] max-w-[260px] lg:hidden',
39
- overlay: 'lg:hidden',
40
- }"
41
- side="left"
42
- >
43
- <svg
44
- class="
45
- cursor-pointer
46
- lg:hidden
47
- "
48
- width="19"
49
- height="18"
50
- viewBox="0 0 19 18"
51
- fill="none"
52
- xmlns="http://www.w3.org/2000/svg"
53
- transform="scale(-1, 1)"
54
- @click="isShowSidebarMobile = true"
55
- >
56
- <path
57
- d="M10 18L10 16L19 16L19 18L10 18ZM6 2L6 -2.62268e-07L19 -8.30516e-07L19 2L6 2ZM-3.49691e-07 10L-4.37114e-07 8L19 8L19 10L-3.49691e-07 10Z"
58
- fill="#4B5563"
59
- />
60
- </svg>
61
-
62
- <template #content>
63
- <Sidebar
64
- :label="label"
65
- :is-collapsed="false"
66
- :is-mobile="true"
67
- :items="sidebarItems"
68
- :is-group="isSidebarGroup"
69
- @toggle-collapsed="isShowSidebarMobile = false"
70
- />
71
- </template>
72
- </Slideover>
73
- <NuxtLink :to="routes.home.to">
74
- <img
75
- src="/logo-mini.png"
76
- alt="logo_mini_color"
77
- class="
78
- h-[20px]
79
- lg:hidden
80
- "
81
- />
82
- </NuxtLink>
83
- <Apps class="lg:hidden" />
84
- </div>
85
-
86
- <div class="flex items-center justify-center gap-4">
87
- <DropdownMenu
88
- size="xl"
89
- :items="userMenuItems"
90
- :content="{
91
- align: 'end',
92
- side: 'bottom',
93
- }"
94
- :ui="{
95
- content: 'w-48',
96
- }"
97
- >
98
- <div class="relative flex cursor-pointer items-center gap-2">
99
- <div
100
- class="flex flex-col justify-end text-right max-sm:hidden"
101
- >
102
- <p class="font-bold">
103
- {{ auth.me.value?.display_name || auth.me.value?.full_name }}
104
- </p>
105
- <p class="text-muted text-sm">
106
- {{ auth.me.value?.email || '' }}
107
- </p>
108
- </div>
109
- <Avatar
110
- class="border-muted size-[40px] border text-2xl"
111
- icon="ri:user-line"
112
- :src="auth.me.value?.avatar_url || ''"
113
- />
114
- <Icon
115
- name="i-ph:caret-down-light"
116
- class="size-5"
117
- />
118
- </div>
119
- </DropdownMenu>
120
- </div>
121
- </nav>
122
- <div class="w-full bg-gray-50 pt-[72px]">
123
- <main
124
- class="
125
- mx-auto min-h-full w-full flex-1 px-6 py-10 lg:px-8
126
- "
127
- >
128
- <Breadcrumb
129
- v-if="!app.pageMeta.isHideBreadcrumbs && breadcrumbsItems.length > 1"
130
- :items="breadcrumbsItems"
131
- class="mb-6"
132
- :ui="{
133
- item: 'max-w-2/3',
134
- list: 'w-full',
135
- }"
136
- />
137
- <div
138
- v-if="app.pageMeta.title"
139
- class="
140
- mb-4 flex flex-col justify-between gap-1 md:mb-6 md:gap-4
141
- lg:flex-row lg:items-start
142
- "
143
- >
144
- <div class="flex flex-1 flex-col">
145
- <h1
146
- class="
147
- text-3xl font-bold wrap-break-word
148
- lg:max-w-2/3
149
- "
150
- :title="app.pageMeta.title"
151
- >
152
- {{ app.pageMeta.title }}
153
- <span id="page-title-extra" />
154
- </h1>
155
-
156
- <div id="page-subtitle" />
157
- <p
158
- v-if="app.pageMeta.sub_title"
159
- class="text-[#475467]"
160
- >
161
- {{ app.pageMeta.sub_title }}
162
- </p>
163
- </div>
164
- <div id="page-header" />
165
- </div>
166
- <slot />
167
- </main>
168
- </div>
169
- </div>
170
- </div>
171
- </template>
172
-
173
- <script lang="ts" setup>
174
- import type { DropdownMenuItem, NavigationMenuItem } from '@nuxt/ui'
175
- import { computed } from 'vue'
176
- import Sidebar from './Sidebar.vue'
177
- import Apps from './Apps.vue'
178
-
179
- defineProps<{
180
- label: string
181
- sidebarItems: NavigationMenuItem[]
182
- isSidebarGroup?: boolean
183
- }>()
184
-
185
- const app = useApp()
186
- const isShowSidebarMobile = ref(false)
187
- const auth = useAuth()
188
- // Cookie to store user preference for desktop
189
- const isCollapsed = useCookie<boolean>('app.admin.sidebar.isCollapsed', {
190
- default: () => false,
191
- path: '/',
192
- })
193
-
194
- const userMenuItems: DropdownMenuItem[] = [
195
- {
196
- label: 'View profile',
197
- icon: 'i-lucide-user',
198
- to: routes.account.profile.to,
199
- },
200
-
201
- {
202
- label: routes.logout.label,
203
- icon: 'i-lucide-log-out',
204
- to: routes.logout.to,
205
- external: true,
206
- },
207
- ]
208
-
209
- const breadcrumbsItems = computed(() => [
210
- // {
211
- // label: '',
212
- // icon: 'ph:house',
213
- // to: '/',
214
- // },
215
- ...(app.pageMeta.breadcrumbs || []).map((item: any) => ({
216
- ...item,
217
- icon: '',
218
- })),
219
- ])
220
- </script>
1
+ <template>
2
+ <div class="relative flex min-h-screen flex-1">
3
+ <div
4
+ :class="[`
5
+ fixed inset-0 z-30 hidden w-auto
6
+ lg:block
7
+ `, {
8
+ 'max-w-[88px]': isCollapsed,
9
+ 'max-w-[260px]': !isCollapsed,
10
+ }]"
11
+ >
12
+ <Sidebar
13
+ :label="label"
14
+ :is-collapsed="isCollapsed"
15
+ :items="sidebarItems"
16
+ :is-group="isSidebarGroup"
17
+ @toggle-collapsed="isCollapsed = !isCollapsed"
18
+ />
19
+ </div>
20
+ <div
21
+ :class="['w-full bg-gray-50', {
22
+ 'lg:pl-[88px]': isCollapsed,
23
+ 'lg:pl-[260px]': !isCollapsed,
24
+ }]"
25
+ >
26
+ <nav
27
+ class="
28
+ fixed top-0 left-0 z-20 flex min-h-[64px] w-screen items-center justify-between
29
+ gap-4 bg-white px-5 lg:min-h-[72px]
30
+ lg:justify-end
31
+ "
32
+ style="box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.04);"
33
+ >
34
+ <div class="flex items-center gap-4">
35
+ <Slideover
36
+ v-model:open="isShowSidebarMobile"
37
+ :ui="{
38
+ content: 'w-[80%] max-w-[260px] lg:hidden',
39
+ overlay: 'lg:hidden',
40
+ }"
41
+ side="left"
42
+ >
43
+ <svg
44
+ class="
45
+ cursor-pointer
46
+ lg:hidden
47
+ "
48
+ width="19"
49
+ height="18"
50
+ viewBox="0 0 19 18"
51
+ fill="none"
52
+ xmlns="http://www.w3.org/2000/svg"
53
+ transform="scale(-1, 1)"
54
+ @click="isShowSidebarMobile = true"
55
+ >
56
+ <path
57
+ d="M10 18L10 16L19 16L19 18L10 18ZM6 2L6 -2.62268e-07L19 -8.30516e-07L19 2L6 2ZM-3.49691e-07 10L-4.37114e-07 8L19 8L19 10L-3.49691e-07 10Z"
58
+ fill="#4B5563"
59
+ />
60
+ </svg>
61
+
62
+ <template #content>
63
+ <Sidebar
64
+ :label="label"
65
+ :is-collapsed="false"
66
+ :is-mobile="true"
67
+ :items="sidebarItems"
68
+ :is-group="isSidebarGroup"
69
+ @toggle-collapsed="isShowSidebarMobile = false"
70
+ />
71
+ </template>
72
+ </Slideover>
73
+ <NuxtLink :to="routes.home.to">
74
+ <img
75
+ src="/logo-mini.png"
76
+ alt="logo_mini_color"
77
+ class="
78
+ w-[126px] min-w-[126px] lg:hidden
79
+ lg:w-[172px] lg:min-w-[172px]
80
+ "
81
+ />
82
+ </NuxtLink>
83
+ <Apps class="lg:hidden" />
84
+ </div>
85
+
86
+ <div class="flex items-center justify-center gap-4">
87
+ <DropdownMenu
88
+ size="xl"
89
+ :items="userMenuItems"
90
+ :content="{
91
+ align: 'end',
92
+ side: 'bottom',
93
+ }"
94
+ :ui="{
95
+ content: 'w-48',
96
+ }"
97
+ >
98
+ <div class="relative flex cursor-pointer items-center gap-2">
99
+ <div
100
+ class="hidden flex-col justify-end text-right lg:flex"
101
+ >
102
+ <p class="font-bold">
103
+ {{ auth.me.value?.display_name || auth.me.value?.full_name }}
104
+ </p>
105
+ <p class="text-muted text-sm">
106
+ {{ auth.me.value?.email || '' }}
107
+ </p>
108
+ </div>
109
+ <Avatar
110
+ class="border-muted size-[40px] border text-2xl"
111
+ icon="ri:user-line"
112
+ :src="auth.me.value?.avatar_url || ''"
113
+ />
114
+ <Icon
115
+ name="i-ph:caret-down-light"
116
+ class="size-5"
117
+ />
118
+ </div>
119
+ </DropdownMenu>
120
+ </div>
121
+ </nav>
122
+ <div class="w-full bg-gray-50 pt-[64px] lg:pt-[72px]">
123
+ <main
124
+ class="
125
+ mx-auto min-h-full w-full flex-1 px-6 py-10 lg:px-8
126
+ "
127
+ >
128
+ <Breadcrumb
129
+ v-if="!app.pageMeta.isHideBreadcrumbs && breadcrumbsItems.length > 1"
130
+ :items="breadcrumbsItems"
131
+ class="mb-6"
132
+ :ui="{
133
+ item: 'max-w-2/3',
134
+ list: 'w-full',
135
+ }"
136
+ />
137
+ <div
138
+ v-if="app.pageMeta.title"
139
+ class="
140
+ mb-4 flex flex-col justify-between gap-1 md:mb-6 md:gap-4
141
+ lg:flex-row lg:items-start
142
+ "
143
+ >
144
+ <div class="flex flex-1 flex-col">
145
+ <h1
146
+ class="
147
+ text-3xl font-bold wrap-break-word
148
+ lg:max-w-2/3
149
+ "
150
+ :title="app.pageMeta.title"
151
+ >
152
+ {{ app.pageMeta.title }}
153
+ <span id="page-title-extra" />
154
+ </h1>
155
+
156
+ <div id="page-subtitle" />
157
+ <p
158
+ v-if="app.pageMeta.sub_title"
159
+ class="text-[#475467]"
160
+ >
161
+ {{ app.pageMeta.sub_title }}
162
+ </p>
163
+ </div>
164
+ <div id="page-header" />
165
+ </div>
166
+ <slot />
167
+ </main>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </template>
172
+
173
+ <script lang="ts" setup>
174
+ import type { DropdownMenuItem, NavigationMenuItem } from '@nuxt/ui'
175
+ import { computed } from 'vue'
176
+ import Sidebar from './Sidebar.vue'
177
+ import Apps from './Apps.vue'
178
+
179
+ defineProps<{
180
+ label: string
181
+ sidebarItems: NavigationMenuItem[]
182
+ isSidebarGroup?: boolean
183
+ }>()
184
+
185
+ const app = useApp()
186
+ const isShowSidebarMobile = ref(false)
187
+ const auth = useAuth()
188
+ // Cookie to store user preference for desktop
189
+ const isCollapsed = useCookie<boolean>('app.admin.sidebar.isCollapsed', {
190
+ default: () => false,
191
+ path: '/',
192
+ })
193
+
194
+ const userMenuItems: DropdownMenuItem[] = [
195
+ {
196
+ label: 'View profile',
197
+ icon: 'i-lucide-user',
198
+ to: routes.account.profile.to,
199
+ },
200
+
201
+ {
202
+ label: routes.logout.label,
203
+ icon: 'i-lucide-log-out',
204
+ to: routes.logout.to,
205
+ external: true,
206
+ },
207
+ ]
208
+
209
+ const breadcrumbsItems = computed(() => [
210
+ // {
211
+ // label: '',
212
+ // icon: 'ph:house',
213
+ // to: '/',
214
+ // },
215
+ ...(app.pageMeta.breadcrumbs || []).map((item: any) => ({
216
+ ...item,
217
+ icon: '',
218
+ })),
219
+ ])
220
+ </script>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@finema/finework-layer",
3
3
  "type": "module",
4
- "version": "0.0.13",
4
+ "version": "0.0.15",
5
5
  "main": "./nuxt.config.ts",
6
6
  "scripts": {
7
7
  "dev": "nuxi dev .playground -o",