inicontent 1.0.0

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.
Files changed (205) hide show
  1. package/.github/pull_request.yml +18 -0
  2. package/CONTRIBUTING.md +67 -0
  3. package/LICENSE +21 -0
  4. package/README.md +142 -0
  5. package/app/app.vue +106 -0
  6. package/app/assets/main.css +974 -0
  7. package/app/components/Api/EndpointCard.vue +169 -0
  8. package/app/components/Api/SchemaDocsTable.vue +208 -0
  9. package/app/components/Asset/Card.vue +1363 -0
  10. package/app/components/Asset/Grid.vue +295 -0
  11. package/app/components/Asset/Icon.vue +38 -0
  12. package/app/components/Asset/Preview.vue +486 -0
  13. package/app/components/Asset/Scanner.vue +700 -0
  14. package/app/components/Asset/Thumb.vue +708 -0
  15. package/app/components/Auth/Reset.vue +99 -0
  16. package/app/components/Auth/index.vue +346 -0
  17. package/app/components/ChatInterface.vue +2326 -0
  18. package/app/components/ChatPagePreview.vue +34 -0
  19. package/app/components/Column/Array.vue +29 -0
  20. package/app/components/Column/Asset.vue +21 -0
  21. package/app/components/Column/Boolean.vue +12 -0
  22. package/app/components/Column/Color.vue +9 -0
  23. package/app/components/Column/Date.vue +57 -0
  24. package/app/components/Column/Edit.vue +215 -0
  25. package/app/components/Column/Email.vue +9 -0
  26. package/app/components/Column/Html.vue +27 -0
  27. package/app/components/Column/Id.vue +20 -0
  28. package/app/components/Column/Locale.vue +32 -0
  29. package/app/components/Column/Object.vue +18 -0
  30. package/app/components/Column/Password.vue +7 -0
  31. package/app/components/Column/Role.vue +15 -0
  32. package/app/components/Column/S.vue +14 -0
  33. package/app/components/Column/Table/Single.vue +49 -0
  34. package/app/components/Column/Table/index.vue +42 -0
  35. package/app/components/Column/Tags.vue +31 -0
  36. package/app/components/Column/Text.vue +9 -0
  37. package/app/components/Column/Url.vue +9 -0
  38. package/app/components/Column/index.vue +41 -0
  39. package/app/components/Dashboard/Editor.vue +221 -0
  40. package/app/components/Dashboard/Grid.vue +270 -0
  41. package/app/components/Dashboard/View.vue +131 -0
  42. package/app/components/Dashboard/Widget/BarChart.vue +73 -0
  43. package/app/components/Dashboard/Widget/Counter.vue +51 -0
  44. package/app/components/Dashboard/Widget/LineChart.vue +76 -0
  45. package/app/components/Dashboard/Widget/PieChart.vue +75 -0
  46. package/app/components/Dashboard/Widget/Table.vue +94 -0
  47. package/app/components/Dashboard/WidgetEditor.vue +274 -0
  48. package/app/components/Data/Array.vue +102 -0
  49. package/app/components/Data/Html.vue +9 -0
  50. package/app/components/Data/Icon.vue +9 -0
  51. package/app/components/Data/Object.vue +10 -0
  52. package/app/components/Data/S.vue +16 -0
  53. package/app/components/Data/Table.vue +53 -0
  54. package/app/components/Data/Value.vue +34 -0
  55. package/app/components/Data/index.vue +14 -0
  56. package/app/components/Field/Array.vue +324 -0
  57. package/app/components/Field/Asset/Actions.vue +97 -0
  58. package/app/components/Field/Asset/index.vue +639 -0
  59. package/app/components/Field/Boolean.vue +26 -0
  60. package/app/components/Field/Checkbox.vue +48 -0
  61. package/app/components/Field/Color.vue +27 -0
  62. package/app/components/Field/Date.vue +30 -0
  63. package/app/components/Field/Email.vue +51 -0
  64. package/app/components/Field/EmailTemplate.vue +420 -0
  65. package/app/components/Field/Html/ColorPicker.vue +127 -0
  66. package/app/components/Field/Html/Editor.vue +544 -0
  67. package/app/components/Field/Html/index.vue +25 -0
  68. package/app/components/Field/Icon.vue +114 -0
  69. package/app/components/Field/JSON.vue +113 -0
  70. package/app/components/Field/Mention.vue +36 -0
  71. package/app/components/Field/Number.vue +27 -0
  72. package/app/components/Field/Object.vue +92 -0
  73. package/app/components/Field/Password.vue +55 -0
  74. package/app/components/Field/Radio.vue +47 -0
  75. package/app/components/Field/S.vue +44 -0
  76. package/app/components/Field/Select.vue +49 -0
  77. package/app/components/Field/Table.vue +348 -0
  78. package/app/components/Field/Tags.vue +113 -0
  79. package/app/components/Field/Text.vue +31 -0
  80. package/app/components/Field/Textarea.vue +33 -0
  81. package/app/components/Field/Url.vue +40 -0
  82. package/app/components/Field/Wrapper.vue +128 -0
  83. package/app/components/Field/index.vue +68 -0
  84. package/app/components/FloatingChatbot.vue +73 -0
  85. package/app/components/Form/Card.vue +151 -0
  86. package/app/components/Form/Drawer.vue +195 -0
  87. package/app/components/Form/index.vue +621 -0
  88. package/app/components/Header.vue +328 -0
  89. package/app/components/Offline/ConflictModal.vue +200 -0
  90. package/app/components/Offline/SyncStatus.vue +249 -0
  91. package/app/components/PlatformFeatureLanding.vue +65 -0
  92. package/app/components/Table/Backups.vue +638 -0
  93. package/app/components/Table/Flows.vue +814 -0
  94. package/app/components/Table/Grid.vue +296 -0
  95. package/app/components/Table/Icon.vue +36 -0
  96. package/app/components/Table/Logs.vue +181 -0
  97. package/app/components/Table/Schedules.vue +722 -0
  98. package/app/components/Table/Search/Button.vue +256 -0
  99. package/app/components/Table/Search/Items.vue +286 -0
  100. package/app/components/Table/Search/index.vue +87 -0
  101. package/app/components/Table/Settings/Schema.vue +801 -0
  102. package/app/components/Table/Settings/index.vue +631 -0
  103. package/app/components/Table/TranslateDrawer.vue +526 -0
  104. package/app/components/Table/Views/Kanban.vue +289 -0
  105. package/app/components/Table/Views/Table.vue +1273 -0
  106. package/app/components/Table/index.vue +890 -0
  107. package/app/composables/Translation/define.ts +41 -0
  108. package/app/composables/Translation/fetch.ts +107 -0
  109. package/app/composables/Translation/languages.ts +1 -0
  110. package/app/composables/Translation/loadCoreTranslations.ts +38 -0
  111. package/app/composables/Translation/translate.ts +250 -0
  112. package/app/composables/databaseCookies.ts +79 -0
  113. package/app/composables/fieldValidator.ts +72 -0
  114. package/app/composables/fieldsList.ts +301 -0
  115. package/app/composables/formatDatabase.ts +101 -0
  116. package/app/composables/index.ts +444 -0
  117. package/app/composables/openDrawer.ts +118 -0
  118. package/app/composables/optimizeFile.ts +287 -0
  119. package/app/composables/renderLabel.ts +88 -0
  120. package/app/composables/schemaClipboard.ts +95 -0
  121. package/app/composables/search.ts +353 -0
  122. package/app/composables/setThemeConfig.ts +48 -0
  123. package/app/composables/translationValue.ts +169 -0
  124. package/app/composables/useAssetPreview.ts +84 -0
  125. package/app/composables/useAssetUploader.ts +56 -0
  126. package/app/composables/useDashboardData.ts +310 -0
  127. package/app/composables/useDocumentThumbnail.ts +351 -0
  128. package/app/composables/useEcharts.ts +33 -0
  129. package/app/composables/useLocalDatabaseConfig.ts +51 -0
  130. package/app/composables/useOcr.ts +300 -0
  131. package/app/composables/useOfflineFetch.ts +194 -0
  132. package/app/composables/useOfflineItem.ts +107 -0
  133. package/app/composables/useOfflineQueue.ts +302 -0
  134. package/app/composables/useOfflineSync.ts +235 -0
  135. package/app/composables/usePasskeyAuth.ts +156 -0
  136. package/app/composables/usePdfCompressor.ts +180 -0
  137. package/app/composables/usePlatformRedirect.ts +72 -0
  138. package/app/composables/useRealtimeSync.ts +233 -0
  139. package/app/composables/useReferencedItems.ts +180 -0
  140. package/app/composables/useScanner.ts +664 -0
  141. package/app/composables/useSubscription.ts +650 -0
  142. package/app/composables/useThumbnailCache.ts +136 -0
  143. package/app/composables/useVideoCompressor.ts +194 -0
  144. package/app/error.vue +112 -0
  145. package/app/layouts/api.vue +188 -0
  146. package/app/layouts/dashboard.vue +91 -0
  147. package/app/layouts/default.vue +228 -0
  148. package/app/layouts/table.vue +471 -0
  149. package/app/locales/ar.ts +996 -0
  150. package/app/locales/en.ts +655 -0
  151. package/app/locales/es.ts +996 -0
  152. package/app/locales/fr.ts +997 -0
  153. package/app/middleware/dashboard.ts +33 -0
  154. package/app/middleware/database.ts +83 -0
  155. package/app/middleware/global.ts +1 -0
  156. package/app/middleware/sid-ingest.global.ts +29 -0
  157. package/app/middleware/table.ts +54 -0
  158. package/app/middleware/user.ts +26 -0
  159. package/app/pages/[[database]]/admin/api/auth/index.vue +306 -0
  160. package/app/pages/[[database]]/admin/api/index.vue +18 -0
  161. package/app/pages/[[database]]/admin/api/tables/[table]/index.vue +497 -0
  162. package/app/pages/[[database]]/admin/api/tables/index.vue +87 -0
  163. package/app/pages/[[database]]/admin/billing/index.vue +16 -0
  164. package/app/pages/[[database]]/admin/dashboards/[id]/edit.vue +66 -0
  165. package/app/pages/[[database]]/admin/dashboards/[id]/index.vue +117 -0
  166. package/app/pages/[[database]]/admin/dashboards/index.vue +20 -0
  167. package/app/pages/[[database]]/admin/index.vue +28 -0
  168. package/app/pages/[[database]]/admin/settings.vue +526 -0
  169. package/app/pages/[[database]]/admin/tables/[table]/[id]/edit.vue +143 -0
  170. package/app/pages/[[database]]/admin/tables/[table]/[id]/index.vue +146 -0
  171. package/app/pages/[[database]]/admin/tables/[table]/flows.vue +10 -0
  172. package/app/pages/[[database]]/admin/tables/[table]/index.vue +10 -0
  173. package/app/pages/[[database]]/admin/tables/[table]/new.vue +12 -0
  174. package/app/pages/[[database]]/admin/tables/[table]/schedules.vue +10 -0
  175. package/app/pages/[[database]]/admin/tables/[table]/settings.vue +30 -0
  176. package/app/pages/[[database]]/admin/tables/assets/[...path].vue +24 -0
  177. package/app/pages/[[database]]/admin/tables/assets/flows.vue +10 -0
  178. package/app/pages/[[database]]/admin/tables/backups/index.vue +10 -0
  179. package/app/pages/[[database]]/admin/tables/blocks/index.vue +16 -0
  180. package/app/pages/[[database]]/admin/tables/index.vue +20 -0
  181. package/app/pages/[[database]]/admin/tables/pages/index.vue +16 -0
  182. package/app/pages/[[database]]/admin/tables/templates/index.vue +16 -0
  183. package/app/pages/[[database]]/api/index.vue +120 -0
  184. package/app/pages/[[database]]/auth/index.vue +12 -0
  185. package/app/pages/[[database]]/auth/reset.vue +10 -0
  186. package/app/pages/[[database]]/index.vue +10 -0
  187. package/app/plugins/offline-warmup.client.ts +53 -0
  188. package/app/plugins/template-globals.ts +9 -0
  189. package/app/spa-loading-template.html +93 -0
  190. package/biome.json +53 -0
  191. package/index.d.ts +380 -0
  192. package/modules/naiveui.ts +41 -0
  193. package/nuxt.config.ts +269 -0
  194. package/package.json +56 -0
  195. package/pnpm-workspace.yaml +5 -0
  196. package/public/apple-touch-icon.png +0 -0
  197. package/public/favicon-16x16.png +0 -0
  198. package/public/favicon-32x32.png +0 -0
  199. package/public/favicon.ico +0 -0
  200. package/public/pwa-192x192.png +0 -0
  201. package/public/pwa-512x512.png +0 -0
  202. package/public/pwa-maskable-192x192.png +0 -0
  203. package/public/pwa-maskable-512x512.png +0 -0
  204. package/public/vendor/opencv.js +48 -0
  205. package/tsconfig.json +9 -0
@@ -0,0 +1,471 @@
1
+ <template>
2
+ <NuxtLayout name="dashboard">
3
+ <NLayout position="absolute" has-sider>
4
+ <NLayoutSider v-if="database?.slug" :collapsed="!isMenuOpen" @update-collapsed="(collapsed) =>
5
+ isMenuOpen = !collapsed" :style="{ zIndex: 1000, borderLeft: Language === 'ar' ? '1px solid var(--n-border-color)' : 'none', borderRight: Language === 'ar' ? 'none' : '1px solid var(--n-border-color)' }" bordered show-trigger="bar" collapse-mode="width"
6
+ :collapsed-width="$device.isMobile ? 0 : 64" width="240" :native-scrollbar="false">
7
+ <NMenu :collapsed="!isMenuOpen" :collapsed-icon-size="22" :collapsed-width="$device.isMobile ? 0 : 64"
8
+ v-model:value="selectedValue" :options="menuOptions" :render-extra="renderMenuExtra" accordion />
9
+ </NLayoutSider>
10
+ <NLayoutContent id="pageContent" :content-style="{
11
+ padding: $device.isMobile
12
+ ? '24px 12px'
13
+ : Language === 'ar'
14
+ ? '0 24px 24px 24px'
15
+ : '0 24px 24px 24px',
16
+ }" :native-scrollbar="false">
17
+ <slot></slot>
18
+ </NLayoutContent>
19
+ </NLayout>
20
+ </NuxtLayout>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ import type { MenuOption } from "naive-ui";
25
+ import { Icon, LazyTableIcon, NButton, NButtonGroup, NIcon, NPopover, NEllipsis, NuxtLink } from "#components";
26
+
27
+ interface TableAction {
28
+ key: string;
29
+ to: string;
30
+ icon: string;
31
+ labelKey: string;
32
+ show: boolean;
33
+ }
34
+
35
+ const database = useState<Database>("database");
36
+ const Language = useScopedCookie<LanguagesType>("language", database.value?.slug);
37
+ const isMenuOpen = useScopedCookie<boolean>("isMenuOpen", database.value?.slug);
38
+
39
+ const config = useRuntimeConfig();
40
+ const route = useRoute();
41
+ const user = useState<User>("user");
42
+ const table = useState<Table>("table");
43
+ const dashboards = ref<Dashboard[]>([]);
44
+ const adminBase = computed(() =>
45
+ route.params.database ? `/${route.params.database}` : "",
46
+ );
47
+ const defaultValue = computed(() => {
48
+ const pathSegments = route.path.split("/").filter(Boolean);
49
+ const lastPathInRoute = decodeURIComponent(pathSegments.at(-1) ?? "");
50
+ if (route.path.includes("/admin/dashboards")) {
51
+ const dashboardIndex = pathSegments.indexOf("dashboards");
52
+ return decodeURIComponent(
53
+ (pathSegments[dashboardIndex + 1] as string | undefined) ?? "dashboards",
54
+ );
55
+ }
56
+ if (table.value?.slug) {
57
+ if (lastPathInRoute && lastPathInRoute !== table.value.slug)
58
+ return `${table.value.slug}-${lastPathInRoute}`;
59
+ return table.value.slug;
60
+ }
61
+ return decodeURI(lastPathInRoute?.toString() ?? "");
62
+ });
63
+ const sessionID = useScopedCookie<string>("sid", database.value?.slug);
64
+ const hasDashboardsTable = computed(
65
+ () =>
66
+ database.value?.tables?.some(
67
+ ({ slug, allowedMethods, show }) =>
68
+ slug === "dashboards" &&
69
+ !!allowedMethods &&
70
+ allowedMethods.includes("r") &&
71
+ show !== false,
72
+ ) ?? false,
73
+ );
74
+
75
+ const fetchDashboards = async () => {
76
+ if (!hasDashboardsTable.value || !database.value?.slug) {
77
+ dashboards.value = [];
78
+ return;
79
+ }
80
+ try {
81
+ const res = await $fetch<apiResponse<Dashboard[]>>(
82
+ `${config.public.apiBase}${database.value.slug}/dashboards`,
83
+ {
84
+ params: {
85
+ locale: Language.value,
86
+ [`${database.value.slug}_sid`]: sessionID.value,
87
+ },
88
+ credentials: "include",
89
+ },
90
+ );
91
+ dashboards.value = res.result ?? [];
92
+ } catch {
93
+ dashboards.value = [];
94
+ }
95
+ };
96
+
97
+ watch(table, () => fetchTranslation(true))
98
+
99
+ watch(
100
+ () => [database.value?.slug, sessionID.value, Language.value],
101
+ () => {
102
+ fetchDashboards();
103
+ },
104
+ { immediate: true },
105
+ );
106
+
107
+ function getTableActions(tbl: Table): TableAction[] {
108
+ const tableUrl = `${adminBase.value}/admin/tables/${tbl.slug}`;
109
+ return [
110
+ {
111
+ labelKey: "showAll",
112
+ to: tableUrl,
113
+ key: tbl.slug,
114
+ show: tbl.slug !== "assets",
115
+ icon: "tabler:eye",
116
+ },
117
+ {
118
+ labelKey: "newItem",
119
+ to: `${tableUrl}/new`,
120
+ key: `${tbl.slug}-new`,
121
+ show: tbl.slug !== "assets" && !!tbl.allowedMethods?.includes("c"),
122
+ icon: "tabler:plus",
123
+ },
124
+ {
125
+ labelKey: "settings",
126
+ to: `${tableUrl}/settings`,
127
+ key: `${tbl.slug}-settings`,
128
+ icon: "tabler:settings",
129
+ show:
130
+ user.value?.role === config.public.idOne &&
131
+ !["sessions", "translations", "assets"].includes(tbl.slug),
132
+ },
133
+ {
134
+ labelKey: "flows",
135
+ to: `${tableUrl}/flows`,
136
+ key: `${tbl.slug}-flows`,
137
+ icon: "tabler:webhook",
138
+ show:
139
+ user.value?.role === config.public.idOne &&
140
+ !["sessions", "translations"].includes(tbl.slug),
141
+ },
142
+ {
143
+ labelKey: "schedules",
144
+ to: `${tableUrl}/schedules`,
145
+ key: `${tbl.slug}-schedules`,
146
+ icon: "tabler:clock-play",
147
+ show:
148
+ user.value?.role === config.public.idOne &&
149
+ !["sessions", "translations", "assets"].includes(tbl.slug) &&
150
+ !!tbl.allowedMethods?.includes("c"),
151
+ },
152
+ ];
153
+ }
154
+
155
+ function renderActionsPopover(
156
+ items: { key: string; to: string; icon: string; title: string; active: boolean }[]
157
+ ) {
158
+ if (!items.length) return undefined;
159
+ return h(
160
+ NPopover,
161
+ {
162
+ placement: Language.value === "ar" ? "left" : "right",
163
+ style: { padding: 0 },
164
+ },
165
+ {
166
+ trigger: () =>
167
+ h(
168
+ NButton,
169
+ {
170
+ quaternary: true,
171
+ size: "small",
172
+ },
173
+ {
174
+ icon: () => h(NIcon, { size: 16 }, () => h(Icon, { name: "tabler:dots" })),
175
+ }
176
+ ),
177
+ default: () =>
178
+ h(
179
+ NButtonGroup,
180
+ { vertical: true },
181
+ items.map((item) =>
182
+ h(NButton,
183
+ {
184
+ style: { justifyContent: "start" },
185
+ secondary: true,
186
+ type: item.active ? "primary" : "default",
187
+ tag: "a",
188
+ href: item.to,
189
+ onClick: (e: Event) => {
190
+ e.stopPropagation();
191
+ e.preventDefault();
192
+ navigateTo(item.to);
193
+ },
194
+ }, {
195
+ default: () => item.title,
196
+ icon: () => h(NIcon, { size: 18 }, () => h(Icon, { name: item.icon }))
197
+ }
198
+ ),
199
+ ),
200
+ ),
201
+ },
202
+ );
203
+ }
204
+
205
+ function renderTableExtra(tbl: Table) {
206
+ const actions = getTableActions(tbl).filter((a) => a.show);
207
+ return renderActionsPopover(
208
+ actions.map((action) => ({
209
+ key: action.key,
210
+ to: action.to,
211
+ icon: action.icon,
212
+ title: t(action.labelKey),
213
+ active: defaultValue.value === action.key,
214
+ }))
215
+ );
216
+ }
217
+
218
+ function renderDashboardsExtra() {
219
+ const dashboardsUrl = `${adminBase.value}/admin/dashboards`;
220
+ return renderActionsPopover(
221
+ [
222
+ {
223
+ key: "dashboards",
224
+ to: dashboardsUrl,
225
+ icon: "tabler:chart-bar",
226
+ title: t("showAll"),
227
+ active: defaultValue.value === "dashboards",
228
+ },
229
+ ...dashboards.value.map((dashboard) => ({
230
+ key: String(dashboard.id),
231
+ to: `${dashboardsUrl}/${dashboard.id}`,
232
+ icon: dashboard.icon ? `tabler:${dashboard.icon}` : "tabler:chart-bar",
233
+ title: dashboard.name?.trim() || t("dashboard"),
234
+ active: defaultValue.value === String(dashboard.id),
235
+ })),
236
+ ]
237
+ );
238
+ }
239
+
240
+ function renderMenuExtra(option: { key?: string | number }) {
241
+ if (!isMenuOpen.value) return undefined;
242
+ const key = String(option.key ?? "");
243
+ if (key === "backups") return undefined;
244
+ if (key === "dashboards") return renderDashboardsExtra();
245
+ const tbl = database.value?.tables?.find((candidate) => candidate.slug === key);
246
+ if (tbl) return renderTableExtra(tbl);
247
+ return undefined;
248
+ }
249
+
250
+ function renderSingleItem(tbl: Table): MenuOption {
251
+ const actions = getTableActions(tbl);
252
+ const itemChildren = actions.map((action) => ({
253
+ label: () =>
254
+ h(
255
+ NuxtLink,
256
+ {
257
+ to: action.to,
258
+ },
259
+ { default: () => t(action.labelKey) },
260
+ ),
261
+ key: action.key,
262
+ show: action.show,
263
+ icon: () => h(NIcon, () => h(Icon, { name: action.icon })),
264
+ }));
265
+ return {
266
+ label: () =>
267
+ h(
268
+ NuxtLink,
269
+ {
270
+ to: `${adminBase.value}/admin/tables/${tbl.slug}`,
271
+ },
272
+ { default: () => h(NEllipsis, { tooltip: false, style: { maxWidth: '130px' } }, () => t(tbl.slug)) },
273
+ ),
274
+ key: isMenuOpen.value ? tbl.slug : `${tbl.slug}Group`,
275
+ icon: () => h(LazyTableIcon, { table: tbl }),
276
+ children: isMenuOpen.value
277
+ ? undefined
278
+ : [
279
+ {
280
+ type: "group",
281
+ label: t(tbl.slug),
282
+ key: tbl.slug,
283
+ children: itemChildren.length ? itemChildren : undefined,
284
+ },
285
+ ],
286
+ };
287
+ }
288
+
289
+ function renderDashboardItem(): MenuOption {
290
+ const dashboardsUrl = `${adminBase.value}/admin/dashboards`;
291
+ return {
292
+ label: () =>
293
+ h(
294
+ NuxtLink,
295
+ {
296
+ to: dashboardsUrl,
297
+ },
298
+ { default: () => t("dashboards") },
299
+ ),
300
+ key: isMenuOpen.value ? "dashboards" : "dashboardsGroup",
301
+ icon: () =>
302
+ h(NIcon, () => h(Icon, { name: "tabler:layout-dashboard" })),
303
+ children: isMenuOpen.value
304
+ ? undefined
305
+ : [
306
+ {
307
+ type: "group",
308
+ label: t("dashboards"),
309
+ key: "dashboards-menu",
310
+ children: [
311
+ {
312
+ label: () =>
313
+ h(
314
+ NuxtLink,
315
+ {
316
+ to: dashboardsUrl,
317
+ },
318
+ { default: () => t("showAll") },
319
+ ),
320
+ key: "dashboards",
321
+ icon: () =>
322
+ h(NIcon, () => h(Icon, { name: "tabler:chart-bar" })),
323
+ },
324
+ ...dashboards.value.map((dashboard) => ({
325
+ label: () =>
326
+ h(
327
+ NuxtLink,
328
+ {
329
+ to: `${dashboardsUrl}/${dashboard.id}`,
330
+ },
331
+ {
332
+ default: () => dashboard.name?.trim() || t("dashboard"),
333
+ },
334
+ ),
335
+ key: String(dashboard.id),
336
+ icon: () =>
337
+ h(NIcon, () =>
338
+ h(Icon, {
339
+ name: dashboard.icon
340
+ ? `tabler:${dashboard.icon}`
341
+ : "tabler:chart-bar",
342
+ }),
343
+ ),
344
+ })),
345
+ ],
346
+ },
347
+ ],
348
+ };
349
+ }
350
+
351
+ function renderBackupsItem(): MenuOption {
352
+ const backupsUrl = `${adminBase.value}/admin/tables/backups`;
353
+ return {
354
+ label: () =>
355
+ h(
356
+ NuxtLink,
357
+ {
358
+ to: backupsUrl,
359
+ },
360
+ { default: () => t("backups") },
361
+ ),
362
+ key: isMenuOpen.value ? "backups" : "backupsGroup",
363
+ icon: () => h(NIcon, () => h(Icon, { name: "tabler:database-export" })),
364
+ children: isMenuOpen.value
365
+ ? undefined
366
+ : [
367
+ {
368
+ type: "group",
369
+ label: t("backups"),
370
+ key: "backups-menu",
371
+ children: undefined,
372
+ },
373
+ ],
374
+ };
375
+ }
376
+
377
+ const SECONDARY_SLUGS = ["users", "sessions", "assets", "translations", "pages", "blocks", "templates"];
378
+ const EXCLUDED_SLUGS = [...SECONDARY_SLUGS, "dashboards", "backups"];
379
+
380
+ function isTableVisible(tbl: Table) {
381
+ return !!tbl.allowedMethods && tbl.allowedMethods.trim() !== "" && tbl.show !== false;
382
+ }
383
+
384
+ const primaryTables = computed(() => {
385
+ if (!database.value?.tables) return [] as Table[];
386
+ return database.value.tables.filter(
387
+ (tbl) => !EXCLUDED_SLUGS.includes(tbl.slug) && isTableVisible(tbl),
388
+ );
389
+ });
390
+
391
+ const secondaryTables = computed(() => {
392
+ if (!database.value?.tables) return [] as Table[];
393
+ return database.value.tables.filter(
394
+ (tbl) => SECONDARY_SLUGS.includes(tbl.slug) && isTableVisible(tbl),
395
+ );
396
+ });
397
+
398
+ const menuOptions = computed(() => {
399
+ const primaryTableOptions = primaryTables.value.map(renderSingleItem) ?? [];
400
+
401
+ const secondaryTableOptions = secondaryTables.value.map(renderSingleItem) ?? [];
402
+
403
+ const options: MenuOption[] = [...primaryTableOptions];
404
+
405
+ if (hasDashboardsTable.value) {
406
+ if (primaryTableOptions.length)
407
+ options.push({ key: "divider-1", type: "divider" });
408
+ options.push(renderDashboardItem());
409
+ }
410
+
411
+ if (secondaryTableOptions.length) {
412
+ options.push({ key: "divider-2", type: "divider" });
413
+ }
414
+
415
+ options.push(...secondaryTableOptions);
416
+
417
+ // Backups is a database-level admin surface, always available to the owner
418
+ // even on legacy databases that lack the table in their meta row.
419
+ if (user.value?.role === config.public.idOne) {
420
+ options.push({ key: "divider-3", type: "divider" });
421
+ options.push(renderBackupsItem());
422
+ }
423
+
424
+ return options;
425
+ });
426
+
427
+ // The menu selection is derived from the current route. It is a computed that
428
+ // re-normalizes against the live menu structure, so it stays correct when the
429
+ // sidebar is toggled between expanded and collapsed mode (the menu keys differ
430
+ // between modes: leaves like `products` vs. groups like `productsGroup`).
431
+ // Child pages (e.g. /admin/tables/:slug/new) have no item of their own when
432
+ // the sidebar is expanded, so they highlight the parent table item instead.
433
+ function optionContainsKey(options: MenuOption[], key: string): boolean {
434
+ return options.some((option) => {
435
+ if (String(option.key ?? "") === key) return true;
436
+ const children = option.children;
437
+ return (
438
+ Array.isArray(children) &&
439
+ children.some((child) => {
440
+ if (String(child.key ?? "") === key) return true;
441
+ const grandchildren = child.children;
442
+ return (
443
+ Array.isArray(grandchildren) &&
444
+ grandchildren.some((grand) => String(grand.key ?? "") === key)
445
+ );
446
+ })
447
+ );
448
+ });
449
+ }
450
+
451
+ function normalizeMenuValue(value: string): string {
452
+ if (optionContainsKey(menuOptions.value, value)) return value;
453
+ // Prefer the longest matching slug so e.g. "user-roles-*" isn't mistaken
454
+ // for a child page of "users".
455
+ const tbl = database.value?.tables
456
+ ?.filter((candidate) => value.startsWith(`${candidate.slug}-`))
457
+ .sort((a, b) => b.slug.length - a.slug.length)[0];
458
+ if (tbl) return tbl.slug;
459
+ // Specific dashboard pages have no item of their own when expanded.
460
+ if (route.path.includes("/admin/dashboards")) return "dashboards";
461
+ return value;
462
+ }
463
+
464
+ // Read-only from the component's perspective: NMenu's transient value changes
465
+ // (e.g. opening a collapsed submenu sets the parent's key) are discarded, and
466
+ // the highlight always reflects the real current page + menu mode.
467
+ const selectedValue = computed({
468
+ get: () => normalizeMenuValue(defaultValue.value),
469
+ set: () => {},
470
+ });
471
+ </script>