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,68 @@
1
+ <template>
2
+ <LazyFieldText v-if="['string', 'text', 'id', 'ip', 'multiple'].includes(detectedFieldType)" v-model="modelValue"
3
+ :field />
4
+ <LazyFieldSelect v-else-if="detectedFieldType === 'role'" v-model="modelValue" :field="{
5
+ ...field, options: database.roles?.map(({ name, id }) => ({
6
+ label: t(name),
7
+ value: id,
8
+ }))
9
+ }" />
10
+ <LazyFieldIcon v-else-if="detectedFieldType === 'icon'" v-model="modelValue" :field />
11
+ <LazyFieldJSON v-else-if="detectedFieldType === 'json'" v-model="modelValue" :field />
12
+ <LazyFieldTextarea v-else-if="detectedFieldType === 'textarea'" v-model="modelValue" :field />
13
+ <LazyFieldRadio v-else-if="detectedFieldType === 'radio'" v-model="modelValue" :field />
14
+ <LazyFieldCheckbox v-else-if="detectedFieldType === 'checkbox'" v-model="modelValue" :field />
15
+ <LazyFieldAsset v-else-if="field.table === 'assets'" v-model="modelValue" :field />
16
+ <LazyFieldTable v-else-if="detectedFieldType === 'table' || detectedFieldType === 'array-table'"
17
+ v-model="modelValue" :field />
18
+ <LazyFieldColor v-else-if="detectedFieldType === 'color'" v-model="modelValue" :field />
19
+ <LazyFieldUrl v-else-if="detectedFieldType === 'url'" v-model="modelValue" :field />
20
+ <LazyFieldEmail v-else-if="detectedFieldType === 'email'" v-model="modelValue" :field />
21
+ <LazyFieldHtml v-else-if="detectedFieldType === 'html'" v-model="modelValue" :field />
22
+ <LazyFieldNumber v-else-if="detectedFieldType === 'number'" v-model="modelValue" :field />
23
+ <LazyFieldPassword v-else-if="detectedFieldType === 'password'" v-model="modelValue" :field />
24
+ <LazyFieldBoolean v-else-if="detectedFieldType === 'boolean'" v-model="modelValue" :field />
25
+ <LazyFieldDate v-else-if="detectedFieldType === 'date'" v-model="modelValue" :field />
26
+ <LazyFieldSelect v-else-if="['locale', 'select'].includes(detectedFieldType)" v-model="modelValue" :field="detectedFieldType === 'locale' ? {
27
+ ...field, subType: 'select', options: translationLanguages.map((language) => ({
28
+ label: t(`languages.${language}`),
29
+ value: language,
30
+ }))
31
+ } : field" />
32
+ <LazyFieldTags v-else-if="detectedFieldType === 'tags'" v-model="modelValue" :field />
33
+ <LazyFieldMention v-else-if="detectedFieldType === 'mention'" v-model="modelValue" :field />
34
+ <LazyFieldObject v-else-if="detectedFieldType === 'object'" v-model="modelValue" :field />
35
+ <LazyFieldArray v-else-if="detectedFieldType === 'array'" v-model="modelValue" :field />
36
+ <component v-else-if="detectedFieldType === 'custom' && field.render"
37
+ :is="isVNode(field.render) ? field.render : field.render(modelValue)"></component>
38
+ <NEmpty v-else :description="`${t('fieldTypeNotExisting')}: '${String(detectedFieldType)}'`" />
39
+ </template>
40
+
41
+ <script lang="ts" setup>
42
+ import { isVNode } from "vue";
43
+ const field = defineModel<Field>("field", { required: true });
44
+ const { item } = defineProps<{ item?: Item }>();
45
+ const detectedFieldType = computed<DB_FieldType | CMS_FieldType>(() => {
46
+ const fieldType = (field.value.subType ?? field.value.type) as
47
+ | DB_FieldType
48
+ | CMS_FieldType;
49
+ if (Array.isArray(fieldType)) return getField(field.value).key;
50
+ return fieldType;
51
+ });
52
+
53
+ const modelValue = defineModel<any>({ default: () => ref().value });
54
+ const database = useState<Database>("database");
55
+
56
+ watchEffect(() => {
57
+ if (
58
+ field.value.defaultValue !== undefined &&
59
+ (modelValue.value === undefined || modelValue.value === null)
60
+ )
61
+ modelValue.value = field.value.defaultValue;
62
+ if (
63
+ (Array.isArray(field.value.type) && field.value.type.includes("array")) ||
64
+ (typeof field.value.type === "string" && field.value.type === "array")
65
+ )
66
+ field.value.isArray = true;
67
+ });
68
+ </script>
@@ -0,0 +1,73 @@
1
+ <template>
2
+ <template v-if="database?.slug">
3
+ <Transition name="chatbot-panel">
4
+ <div v-if="hasOpened" v-show="isOpen" class="floating-chatbot-panel">
5
+ <NCard :bordered="true" class="chatbot-card" content-style="padding: 0;" :segmented="{ content: true }">
6
+ <template #header>
7
+ <NFlex align="center" :wrap="false">
8
+ <NButton type="primary" circle>
9
+ <Icon name="tabler:robot" />
10
+ </NButton>
11
+ <NText strong>{{ t("chatbot") }}</NText>
12
+ </NFlex>
13
+ </template>
14
+ <template #header-extra>
15
+ <NFlex align="center" :wrap="false" :size="4">
16
+ <NButton quaternary circle size="small" :title="t('newChat')" :aria-label="t('newChat')" @click="resetNonce++">
17
+ <template #icon>
18
+ <NIcon>
19
+ <Icon name="tabler:message-plus" />
20
+ </NIcon>
21
+ </template>
22
+ </NButton>
23
+ <NButton quaternary circle size="small" :title="t('close')" :aria-label="t('close')" @click="isOpen = false">
24
+ <template #icon>
25
+ <NIcon>
26
+ <Icon name="tabler:x" />
27
+ </NIcon>
28
+ </template>
29
+ </NButton>
30
+ </NFlex>
31
+ </template>
32
+ <div class="chatbot-body">
33
+ <LazyChatInterface :key="storageKey" :persist-key="storageKey" :reset-nonce="resetNonce" />
34
+ </div>
35
+ </NCard>
36
+ </div>
37
+ </Transition>
38
+
39
+ <NButton class="floating-chatbot-button" type="primary" circle @click="toggleChatbot">
40
+ <template #icon>
41
+ <NIcon size="24">
42
+ <Icon :name="isOpen ? 'tabler:x' : 'tabler:message-chatbot'" />
43
+ </NIcon>
44
+ </template>
45
+ </NButton>
46
+ </template>
47
+ </template>
48
+
49
+ <script setup lang="ts">
50
+ const database = useState<Database>("database");
51
+ const Language = useScopedCookie<LanguagesType>(
52
+ "language",
53
+ database.value?.slug,
54
+ );
55
+ const isOpen = ref(false);
56
+ const hasOpened = ref(false);
57
+ const resetNonce = ref(0);
58
+
59
+ const storageKey = computed(() =>
60
+ database.value?.slug
61
+ ? `inicontent:floating-chat:${database.value.slug}:${Language.value}`
62
+ : undefined,
63
+ );
64
+
65
+ const toggleChatbot = () => {
66
+ if (isOpen.value) {
67
+ isOpen.value = false;
68
+ return;
69
+ }
70
+ hasOpened.value = true;
71
+ isOpen.value = true;
72
+ };
73
+ </script>
@@ -0,0 +1,151 @@
1
+ <template>
2
+ <div>
3
+ <LazyFormDrawer></LazyFormDrawer>
4
+ <NSpin :show="!!Loading.CREATE || !!Loading.DELETE || !!Loading.UPDATE">
5
+ <NCard :header-style="{ paddingRight: 0, paddingLeft: 0 }" content-style="padding: 0" :bordered="false"
6
+ style="background-color: transparent">
7
+ <template #header>
8
+ <NPerformantEllipsis>{{ t(table.slug) }}: {{ isEdit ? itemLabel : t('newItem') }}
9
+ </NPerformantEllipsis>
10
+ </template>
11
+ <template v-if="formRef?.schema && formRef?.schema.length > 4" #header-extra>
12
+ <NButtonGroup v-if="isEdit">
13
+ <NTooltip :delay="1500">
14
+ <template #trigger>
15
+ <NButton type="info" secondary round>
16
+ <template #icon>
17
+ <NuxtLink
18
+ :to="`${$route.params.database ? `/${$route.params.database}` : ''}/admin/tables/${table.slug}/${$route.params.id}`">
19
+ <NIcon>
20
+ <Icon name="tabler:eye" />
21
+ </NIcon>
22
+ </NuxtLink>
23
+ </template>
24
+ </NButton>
25
+ </template>
26
+ {{ t('view') }}
27
+ </NTooltip>
28
+ <NPopconfirm @positive-click="formRef?.delete">
29
+ <template #trigger>
30
+ <NTooltip :delay="1500">
31
+ <template #trigger>
32
+ <NButton secondary round type="error" :loading="Loading.DELETE">
33
+ <template #icon>
34
+ <NIcon>
35
+ <Icon name="tabler:trash" />
36
+ </NIcon>
37
+ </template>
38
+ </NButton>
39
+ </template>
40
+ {{ t('delete') }}
41
+ </NTooltip>
42
+ </template>
43
+ {{ t("theFollowingActionIsIrreversible") }}
44
+ </NPopconfirm>
45
+
46
+ <NTooltip :delay="1500">
47
+ <template #trigger>
48
+ <NButton secondary round type="primary" @click="formRef?.update"
49
+ :loading="Loading.UPDATE || Loading.SCHEMA">
50
+ <template #icon>
51
+ <NIcon>
52
+ <Icon name="tabler:device-floppy" />
53
+ </NIcon>
54
+ </template>
55
+ </NButton>
56
+ </template>
57
+ {{ t('update') }}
58
+ </NTooltip>
59
+ </NButtonGroup>
60
+ <NTooltip v-else :delay="1500">
61
+ <template #trigger>
62
+ <NButton secondary round type="primary" @click="formRef?.create"
63
+ :loading="Loading.CREATE || Loading.SCHEMA">
64
+ <template #icon>
65
+ <NIcon>
66
+ <Icon name="tabler:send" />
67
+ </NIcon>
68
+ </template>
69
+ </NButton>
70
+ </template>
71
+ {{ t('publish') }}
72
+ </NTooltip>
73
+ </template>
74
+ <NCard style="height: fit-content;min-height: 200px;">
75
+ <template #action>
76
+ <NFlex justify="end">
77
+ <NButtonGroup v-if="isEdit">
78
+ <NButton type="info" secondary round>
79
+ <template #icon>
80
+ <NuxtLink
81
+ :to="`${$route.params.database ? `/${$route.params.database}` : ''}/admin/tables/${table.slug}/${$route.params.id}`">
82
+ <NIcon>
83
+ <Icon name="tabler:eye" />
84
+ </NIcon>
85
+ </NuxtLink>
86
+ </template>
87
+ {{ t('view') }}
88
+ </NButton>
89
+ <NPopconfirm @positive-click="formRef?.delete">
90
+ <template #trigger>
91
+ <NButton secondary round type="error" :loading="Loading.DELETE">
92
+ <template #icon>
93
+ <NIcon>
94
+ <Icon name="tabler:trash" />
95
+ </NIcon>
96
+ </template>
97
+ {{ t('delete') }}
98
+ </NButton>
99
+ </template>
100
+ {{ t("theFollowingActionIsIrreversible") }}
101
+ </NPopconfirm>
102
+
103
+ <NButton secondary round type="primary" @click="formRef?.update"
104
+ :loading="Loading.UPDATE || Loading.SCHEMA">
105
+ <template #icon>
106
+ <NIcon>
107
+ <Icon name="tabler:device-floppy" />
108
+ </NIcon>
109
+ </template>
110
+ {{ t('update') }}
111
+ </NButton>
112
+ </NButtonGroup>
113
+ <NButton v-else secondary round type="primary" @click="formRef?.create"
114
+ :loading="Loading.CREATE || Loading.SCHEMA">
115
+ <template #icon>
116
+ <NIcon>
117
+ <Icon name="tabler:send" />
118
+ </NIcon>
119
+ </template>
120
+ {{ t('publish') }}
121
+ </NButton>
122
+ <slot name="extraActions"></slot>
123
+ </NFlex>
124
+ </template>
125
+ <slot>
126
+ <Form ref="formRef" v-model="modelValue"></Form>
127
+ </slot>
128
+ </NCard>
129
+ </NCard>
130
+ </NSpin>
131
+ </div>
132
+ </template>
133
+
134
+ <script lang="ts" setup>
135
+ const modelValue = defineModel<Item>()
136
+ const isEdit = !!modelValue.value?.id
137
+
138
+ const Loading = useState<Record<string, boolean>>("Loading", () => ({}))
139
+ const table = useState<Table>("table")
140
+ const database = useState<Database>("database")
141
+ const formRef = ref<FormRef>()
142
+
143
+ const itemLabel = computed(() => renderLabel(table.value, modelValue.value))
144
+
145
+ useHead({
146
+ title: `${t(database.value.slug)} | ${t(table.value.slug)} : ${isEdit ? itemLabel.value : t("new")}`,
147
+ link: [
148
+ { rel: "icon", href: database.value?.icon?.publicURL ?? "/favicon.ico" },
149
+ ],
150
+ })
151
+ </script>
@@ -0,0 +1,195 @@
1
+ <template><template v-for="(drawer, index) in Drawers" :key="index">
2
+ <NDrawer :onEsc :show="drawer.show || false" @update:show="(show) => onUpdateShow(index, show)"
3
+ :width="$device.isMobile ? '100%' : drawer.width" @update:width="(width) => {
4
+ if (index === 0) defaultWidth = width
5
+ drawer.width = width
6
+ }" resizable :placement="Language === 'ar' ? 'left' : 'right'" :class="`${drawer.table.replaceAll(' ','_')}-drawer`"
7
+ :id="index === (Drawers.length - 1) ? 'activeDrawer' : undefined" :close-on-esc="false">
8
+ <NDrawerContent closable :native-scrollbar="false">
9
+ <template #header>
10
+ <span v-if="drawer.id">
11
+ {{ t(drawer.mode === 'view' ? 'view' : 'edit') }}
12
+ <NuxtLink
13
+ :to="`${$route.params.database ? `/${$route.params.database}` : ''}/admin/tables/${drawer.table}/${drawer.id}${drawer.mode === 'view' ? '' : '/edit'}`">
14
+ <NText type="primary">
15
+ {{ itemsLabels[index] }}
16
+ <NIcon size="small">
17
+ <Icon :name="drawer.mode === 'view' ? 'tabler:eye' : 'tabler:external-link'" />
18
+ </NIcon>
19
+ </NText>
20
+ </NuxtLink>
21
+ </span>
22
+ <span v-else>
23
+ {{ t('new') }} {{ t(drawer.table) }}
24
+ </span>
25
+ </template>
26
+
27
+ <template #footer>
28
+ <NFlex :style="{ width: '100%' }" :justify="!$device.isMobile ? 'space-between' : 'end'">
29
+ <NButton v-if="!$device.isMobile" round secondary type="info" @click="toggleDrawerWidth(index)">
30
+ <template #icon>
31
+ <NIcon>
32
+ <Icon name="tabler:chevron-right"
33
+ v-if="drawer.width && (typeof drawer.width === 'string' || drawer.width >= screenHalf)" />
34
+ <Icon name="tabler:chevron-left" v-else />
35
+ </NIcon>
36
+ </template>
37
+ </NButton>
38
+
39
+ <NButton v-if="drawer.mode === 'view' && canEditDrawer(drawer)" round secondary type="info"
40
+ @click="switchDrawerToEdit(index)">
41
+ <template #icon>
42
+ <NIcon>
43
+ <Icon name="tabler:pencil" />
44
+ </NIcon>
45
+ </template>
46
+ {{ t('edit') }}
47
+ </NButton>
48
+
49
+ <NButton v-else round secondary type="primary"
50
+ :disabled="!drawer.schema?.length || Loading.UPDATE || Loading.CREATE || Loading.SCHEMA"
51
+ :loading="Loading.UPDATE || Loading.CREATE || Loading.SCHEMA"
52
+ @click="drawer.id ? formRefs[index]?.update() : formRefs[index]?.create()">
53
+ <template #icon>
54
+ <NIcon>
55
+ <Icon name="tabler:device-floppy" />
56
+ </NIcon>
57
+ </template>
58
+ {{ drawer.id ? t('update') : t('create') }}
59
+ </NButton>
60
+ </NFlex>
61
+ </template>
62
+ <div class="drawerSpin" v-if="isDrawerFormMode(drawer) && (!drawer.schema?.length || Loading.UPDATE || Loading.CREATE)">
63
+ <NSpin />
64
+ </div>
65
+
66
+ <slot v-if="isDrawerFormMode(drawer)" @after-create="() => onAfterUpdateCreate(index)" @after-update="() => onAfterUpdateCreate(index)">
67
+ <Form :ref="(el: any) => formRefs[index] = el" v-model="drawer.data" :table="drawer.table"
68
+ @after-create="() => onAfterUpdateCreate(index)"
69
+ @after-update="() => onAfterUpdateCreate(index)" v-model:schema="drawer.schema"></Form>
70
+ </slot>
71
+
72
+ <NSpin v-else :show="!!(drawer.id && Loading[`Drawer_${drawer.table}_${drawer.id}`])">
73
+ <LazyDataS v-if="drawer.data && getDrawerTable(drawer)?.schema"
74
+ :value="drawer.data"
75
+ :schema="(getDrawerTable(drawer)?.schema ?? [])" />
76
+ </NSpin>
77
+ </NDrawerContent>
78
+ </NDrawer>
79
+ </template>
80
+ </template>
81
+
82
+ <script setup lang="ts">
83
+ const database = useState<Database>("database");
84
+ const Language = useScopedCookie<LanguagesType>("language", database.value?.slug);
85
+ const Loading = useState<Record<string, boolean>>("Loading", () => ({}));
86
+
87
+ defineSlots<{
88
+ default({
89
+ onAfterCreate,
90
+ onAfterUpdate,
91
+ }: {
92
+ onAfterCreate: () => any;
93
+ onAfterUpdate: () => any;
94
+ }): {
95
+ onAfterCreate: () => any;
96
+ onAfterUpdate: () => any;
97
+ };
98
+ }>();
99
+
100
+ const Drawers = useState<DrawerRef>("drawers", () => []);
101
+ const defaultWidth = useCookie<number | string>("width");
102
+ const formRefs = ref<FormRef[]>([]);
103
+
104
+ const screenHalf = window.screen.width / 2;
105
+
106
+ function getDrawerTable(drawer?: DrawerRef[number]) {
107
+ if (!drawer?.table) return undefined;
108
+ return database.value.tables?.find(({ slug }) => slug === drawer.table);
109
+ }
110
+
111
+ function isDrawerFormMode(drawer?: DrawerRef[number]) {
112
+ return drawer?.mode !== "view" || !drawer?.id;
113
+ }
114
+
115
+ function canEditDrawer(drawer?: DrawerRef[number]) {
116
+ return !!(drawer?.id && getDrawerTable(drawer)?.allowedMethods?.includes("u"));
117
+ }
118
+
119
+ function switchDrawerToEdit(index: number) {
120
+ const drawer = Drawers.value[index];
121
+ if (!drawer?.id) return;
122
+ drawer.mode = "edit";
123
+ }
124
+
125
+ function onUpdateShow(index: number, show: boolean) {
126
+ if (!Drawers.value[index]) return;
127
+ Drawers.value[index].show = show;
128
+ if (!show) {
129
+ for (let i = 0; i < index; i++) {
130
+ const drawer = Drawers.value[i];
131
+ if (!drawer || typeof drawer.width !== "number") continue;
132
+ const lastIncrement = drawer.nestedWidthIncrements?.pop();
133
+ if (!lastIncrement) continue;
134
+ drawer.width = Math.max(251, drawer.width - lastIncrement);
135
+ }
136
+ setTimeout(() => Drawers.value.splice(index, 1), 100);
137
+ }
138
+ }
139
+
140
+ async function onAfterUpdateCreate(index: number) {
141
+ onUpdateShow(index, false);
142
+ const drawer = Drawers.value[index];
143
+ if (!drawer) return;
144
+ await refreshNuxtData(`${database.value.slug}/${drawer.table}`);
145
+ }
146
+
147
+ const toggleDrawerWidth = (index: number) => {
148
+ const drawer = Drawers.value[index];
149
+ if (!drawer) return;
150
+ if (typeof drawer.width === "string") drawer.width = 251;
151
+ else
152
+ drawer.width = !drawer.width || drawer.width >= screenHalf ? 251 : "100%";
153
+ nextTick(() => {
154
+ if (drawer.width) defaultWidth.value = drawer.width;
155
+ });
156
+ };
157
+ function onEsc() {
158
+ const openDrawerIndex = Drawers.value.findLastIndex((drawer) => drawer.show);
159
+ if (openDrawerIndex !== -1) {
160
+ // The asset preview modal keeps ".assetLightbox" on the DOM while it's open
161
+ // (Asset/Preview.vue). First Esc closes only the preview; the drawer waits
162
+ // until the preview is gone before closing itself.
163
+ const previewModal = document.querySelector(".assetLightbox");
164
+ if (!previewModal) onUpdateShow(openDrawerIndex, false);
165
+ }
166
+ }
167
+ const itemsLabels = ref<string[]>([]);
168
+ watchEffect(() => {
169
+ for (let index = 0; index < Drawers.value.length; index++) {
170
+ const drawer = Drawers.value[index];
171
+ if (!drawer) continue;
172
+ const table = drawer.table
173
+ ? database.value.tables?.find(({ slug }) => slug === drawer.table)
174
+ : undefined;
175
+
176
+ if (table) {
177
+ itemsLabels.value[index] = renderLabel(table, drawer.data);
178
+ continue;
179
+ }
180
+ itemsLabels.value[index] = "--";
181
+ }
182
+ });
183
+
184
+ onBeforeRouteLeave(() => {
185
+ Drawers.value = [];
186
+ });
187
+
188
+ onBeforeRouteUpdate((_to, _from) => {
189
+ const openDrawerIndex = Drawers.value.findLastIndex((drawer) => drawer.show);
190
+ if (openDrawerIndex !== -1) {
191
+ onUpdateShow(openDrawerIndex, false);
192
+ return false; // Prevent navigation until all drawers are closed
193
+ } else return; // Allow navigation without altering the query in the URL
194
+ });
195
+ </script>