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,801 @@
1
+ <template>
2
+ <NCollapse style="margin-top: 15px;" :class="{ 'reorder-enabled': reorderEnabled }" accordion
3
+ :trigger-areas="['main', 'arrow']" v-model:expanded-names="expandedNames">
4
+ <NCollapseItem v-if="!skipDefaultFields" name="0" :id="`element-0`" disabled :title="t('id')" class="element" :style="slicedSchema.length > 0 ? 'padding-bottom: 16px;border-bottom: 1px solid var(--n-divider-color);margin-bottom: 16px;' : ''">
5
+ <template #header-extra>
6
+ <NButton round strong secondary size="small" type="primary" disabled>
7
+ <template #icon>
8
+ <Icon name="tabler:id" />
9
+ </template>
10
+ <template v-if="!$device.isMobile" #default>
11
+ {{ t('id') }}
12
+ </template>
13
+ </NButton>
14
+ </template>
15
+ </NCollapseItem>
16
+ <VueDraggable v-model="slicedSchema" item-key="id" ghost-class="ghost" :disabled="!reorderEnabled">
17
+ <NCollapseItem v-for="(element, index) in slicedSchema" :key="fieldTypeSignature(element)" :name="element.id" :id="`element-${element.id}`" class="element"
18
+ :class="{ 'field-selected': selectedFieldIds.has(element.id as string | number)}"
19
+ :disabled="isDisabled(element.key)"
20
+ :title="getDisplayKey(element) ? (isDisabled(element.key) ? t(element.key as string) : getDisplayKey(element)) : '--'"
21
+ @click.capture="(e: MouseEvent) => onFieldClick(e, element)">
22
+ <template #header-extra>
23
+ <NFlex>
24
+ <NButtonGroup>
25
+ <NDropdown
26
+ v-if="['array', 'object'].includes(element.type as string) && isArrayOfObjects(element.children)"
27
+ :options="fieldTypeOptions" style="max-height: 200px;" scrollable
28
+ @select="(type) => pushToChildrenSchema(type, index)">
29
+ <NButton :disabled="!element.key" secondary round size="small"
30
+ @click="pushToChildrenSchema('string', index)">
31
+ <template #icon>
32
+ <NIcon>
33
+ <Icon name="tabler:plus" />
34
+ </NIcon>
35
+ </template>
36
+ </NButton>
37
+ </NDropdown>
38
+ <NDropdown :disabled="isDisabled(element.key)" :options="fieldTypeOptions"
39
+ style="max-height: 200px" trigger="click" scrollable
40
+ @select="(type) => applyFieldType(element, type)">
41
+ <NButton round strong secondary size="small" type="primary"
42
+ :disabled="isDisabled(element.key)">
43
+ <template #icon>
44
+ <component :is="getFieldCached(element).icon" />
45
+ </template>
46
+ <template v-if="!$device.isMobile" #default>
47
+ {{ getFieldCached(element).label }}
48
+ </template>
49
+ </NButton>
50
+ </NDropdown>
51
+ </NButtonGroup>
52
+
53
+ <NButtonGroup v-if="!isDisabled(element.key) && !$device.isMobile">
54
+ <NTooltip :delay="1500">
55
+ <template #trigger>
56
+ <NButton round secondary size="small"
57
+ :type="element.required ? 'error' : 'tertiary'"
58
+ @click="element.required = !element.required">
59
+ <template #icon>
60
+ <NIcon>
61
+ <Icon name="tabler:asterisk" />
62
+ </NIcon>
63
+ </template>
64
+ </NButton>
65
+ </template>
66
+ {{ t('required') }}
67
+ </NTooltip>
68
+ <NTooltip :delay="1500">
69
+ <template #trigger>
70
+ <NPopselect v-model:value="element.width" :options="widthOptions" size="small">
71
+ <NButton round strong secondary size="small" type="info">
72
+ <template #icon>
73
+ <NIcon>
74
+ <Icon name="tabler:arrow-autofit-width" />
75
+ </NIcon>
76
+ </template>
77
+ 1/{{ element.width ?? 1 }}
78
+ </NButton>
79
+ </NPopselect>
80
+ </template>
81
+ {{ t('width') }}
82
+ </NTooltip>
83
+ <NButton round secondary size="small" type="error" @click="slicedSchema.splice(index, 1)">
84
+ <template #icon>
85
+ <NIcon>
86
+ <Icon name="tabler:trash" />
87
+ </NIcon>
88
+ </template>
89
+ </NButton>
90
+ </NButtonGroup>
91
+ <NDropdown v-else-if="!isDisabled(element.key)" trigger="click"
92
+ :options="fieldActionOptions(element)"
93
+ @select="(action) => onFieldAction(action as string, element, index)">
94
+ <NButton round secondary size="small">
95
+ <template #icon>
96
+ <NIcon>
97
+ <Icon name="tabler:dots-vertical" />
98
+ </NIcon>
99
+ </template>
100
+ </NButton>
101
+ </NDropdown>
102
+ </NFlex>
103
+ </template>
104
+
105
+ <NFormItem :label="t('fieldName')" style="margin-bottom:20px">
106
+ <template #feedback>
107
+ {{ `#${getPath(table.schema ?? [], element.id, true) ?? '--'}` }} ({{ element.id }})
108
+ </template>
109
+ <NInput :value="getDisplayKey(element)" @update:value="(v) => onKeyInput(element, v)" />
110
+ </NFormItem>
111
+
112
+ <NFormItem
113
+ v-if="element.table === 'assets' || !element.children || !isArrayOfObjects(element.children)"
114
+ :label="t('fieldDescription')">
115
+ <NInput v-model:value="element.description" />
116
+ </NFormItem>
117
+
118
+ <template v-if="element.table === 'assets'">
119
+ <NFormItem :label="t('allowedFiles')">
120
+ <NSelect multiple :render-label="selectRenderLabelWithIcon" :options="fileTypeSelectOptions"
121
+ v-model:value="element.accept" />
122
+ </NFormItem>
123
+ <NFormItem :label="t('urlSuffix')">
124
+ <NInput v-model:value="element.suffix" />
125
+ <template #feedback>
126
+ {{ t('ie') }}: <strong>/@2/customFolder</strong>
127
+ </template>
128
+ </NFormItem>
129
+ <NFormItem :label="t('optimizeAssets')" label-placement="left">
130
+ <NSwitch v-model:value="element.optimize" :default-value="true" style="margin-top: 4px;" />
131
+ <template #label>
132
+ {{ t('optimizeAssets') }}
133
+ <NTooltip>
134
+ <template #trigger>
135
+ <NIcon style="margin-left: 5px; vertical-align: middle;">
136
+ <Icon name="tabler:info-circle" />
137
+ </NIcon>
138
+ </template>
139
+ {{ t('optimizeAssetsDescription') }}
140
+ </NTooltip>
141
+ </template>
142
+ </NFormItem>
143
+ </template>
144
+ <template v-else-if="element.subType && ['select', 'radio', 'checkbox'].includes(element.subType)">
145
+ <NFormItem :label="t('options')" class="formItemFlex">
146
+ <template v-if="isArrayOfArrays(element.options)">
147
+ <NDataTable :columns="labelsColoringColumns(element)" :data="element.options" />
148
+ <NButton type="primary" secondary style="width:100%"
149
+ @click="(element.options as [string, string][]).push(['', ''])">
150
+ <template #icon>
151
+ <NIcon>
152
+ <Icon name="tabler:plus" />
153
+ </NIcon>
154
+ </template>
155
+ {{ t('add') }}
156
+ </NButton>
157
+ </template>
158
+ <NSelect v-else
159
+ :value="element.options ? (element.options.every(option => typeof option !== 'object') ? element.options : element.options.map(({ value }: any) => value)) : []"
160
+ @update:value="(value: string[]) => handleOptionsUpdate(element, value)" filterable
161
+ multiple tag :show-arrow="false" :show="false" />
162
+ </NFormItem>
163
+ <NFormItem :label="t('labelsColoring')" label-placement="left">
164
+ <NSwitch :value="isArrayOfArrays(element.options)"
165
+ @update:value="(value) => toggleLabelsColoring(element, value)" />
166
+ </NFormItem>
167
+ <NFormItem v-if="element.subType === 'select'" :label="t('allowCustomValues')"
168
+ label-placement="left">
169
+ <NSwitch v-model:value="element.custom" />
170
+ </NFormItem>
171
+ </template>
172
+ <template v-else-if="!Array.isArray(element.type) && element.type === 'object'">
173
+ <NFormItem :label="t('expandByDefault')" label-placement="left">
174
+ <NSwitch v-model:value="element.expand" />
175
+ </NFormItem>
176
+ </template>
177
+ <template v-else-if="Array.isArray(element.type) && !element.subType">
178
+ <NFormItem :label="t('valuesType')">
179
+ <NSelect v-model:value="element.type" filterable multiple :min="1"
180
+ :render-label="selectRenderLabelWithIcon" :options="valuesTypeSelectOptions" />
181
+ </NFormItem>
182
+ </template>
183
+ <template v-else-if="element.subType === 'tags'">
184
+ <NFormItem :label="t('valuesType')">
185
+ <NSelect v-model:value="(element.children as any)" filterable multiple
186
+ :render-label="selectRenderLabelWithIcon" :options="valuesTypeSelectOptions" />
187
+ </NFormItem>
188
+ </template>
189
+ <template
190
+ v-else-if="!Array.isArray(element.type) && ((element.type === 'array' && element.children === 'table') || element.type === 'table')">
191
+ <NFormItem :label="t('tableName')">
192
+ <NSelect filterable v-model:value="element.table" :options="tableSelectOptions" />
193
+ </NFormItem>
194
+ <NFormItem :label="t('extendWhere')">
195
+ <NInput v-model:value="(element.where as string)" />
196
+ <template #feedback>
197
+ {{ t('useInison') }} / {{ t('ie') }}:
198
+ {<strong>subCategory</strong>:<strong>null</strong>}
199
+ </template>
200
+ </NFormItem>
201
+ </template>
202
+
203
+ <template v-if="!Array.isArray(element.type) && element.type === 'array'">
204
+ <NGrid :x-gap="12" :y-gap="12" cols="1 500:2">
205
+ <NGridItem>
206
+ <NFormItem :label="t('minimumItems')">
207
+ <NInputNumber :value="element.min"
208
+ @update:value="(value) => { if (value) element.min = value; else delete element.min }" />
209
+ </NFormItem>
210
+ </NGridItem>
211
+ <NGridItem>
212
+ <NFormItem :label="t('maximumItems')">
213
+ <NInputNumber :value="element.max"
214
+ @update:value="(value) => { if (value) element.max = value; else delete element.max }" />
215
+ </NFormItem>
216
+ </NGridItem>
217
+ </NGrid>
218
+ </template>
219
+
220
+ <NFormItem :label="t('unique')" label-placement="left"
221
+ v-if="!['array', 'object', 'tags'].includes((element.subType ?? element.type) as string)">
222
+ <NSwitch :value="element.unique ? true : false"
223
+ @update:value="(value) => element.unique = value" :checked-value="true"
224
+ :unchecked-value="false" />
225
+ </NFormItem>
226
+ <NFormItem v-if="element.unique" :label="t('uniqueGroup')">
227
+ <NSelect :value="typeof element.unique === 'boolean' ? undefined : element.unique"
228
+ @update:value="(value) => element.unique = value" :options="uniqueGroupOptions" tag
229
+ filterable clearable />
230
+ </NFormItem>
231
+
232
+ <NFormItem v-if="!element.table && (!element.children || !isArrayOfObjects(element.children))"
233
+ :label="t('regex')">
234
+ <NInput v-model:value="element.regex" />
235
+ </NFormItem>
236
+
237
+ <LazyTableSettingsSchema
238
+ v-if="!Array.isArray(element.type) && ['array', 'object'].includes(element.type) && isArrayOfObjects(element.children)"
239
+ v-model="element.children" v-model:expanded-names="expandedChildNames"
240
+ :reorder-enabled="reorderEnabled" skip-default-fields />
241
+ </NCollapseItem>
242
+ </VueDraggable>
243
+
244
+ <NCollapseItem v-if="!skipDefaultFields" name="-1" :id="`element--1`" disabled :title="t('createdAt')">
245
+ <template #header-extra>
246
+ <NButton round strong secondary size="small" type="primary" disabled>
247
+ <template #icon>
248
+ <Icon name="tabler:calendar" />
249
+ </template>
250
+ <template v-if="!$device.isMobile" #default>
251
+ {{ t('createdAt') }}
252
+ </template>
253
+ </NButton>
254
+ </template>
255
+ </NCollapseItem>
256
+
257
+ <NCollapseItem v-if="!skipDefaultFields" name="-2" :id="`element--2`" disabled :title="t('updatedAt')">
258
+ <template #header-extra>
259
+ <NButton round strong secondary size="small" type="primary" disabled>
260
+ <template #icon>
261
+ <Icon name="tabler:calendar" />
262
+ </template>
263
+ <template v-if="!$device.isMobile" #default>
264
+ {{ t('updatedAt') }}
265
+ </template>
266
+ </NButton>
267
+ </template>
268
+ </NCollapseItem>
269
+ </NCollapse>
270
+ </template>
271
+
272
+ <script lang="ts" setup>
273
+ import { isArrayOfArrays, isArrayOfObjects } from "inibase/utils";
274
+ import type { DataTableColumns, SelectOption } from "naive-ui";
275
+ import { VueDraggable } from "vue-draggable-plus";
276
+ import { Icon, NButton, NColorPicker, NFlex, NIcon, NInput } from "#components";
277
+
278
+ const widthOptions = [
279
+ {
280
+ label: "1/1",
281
+ value: 1,
282
+ },
283
+ {
284
+ label: "1/2",
285
+ value: 2,
286
+ },
287
+ {
288
+ label: "1/3",
289
+ value: 3,
290
+ },
291
+ {
292
+ label: "1/4",
293
+ value: 4,
294
+ },
295
+ {
296
+ label: "1/5",
297
+ value: 5,
298
+ },
299
+ {
300
+ label: "1/6",
301
+ value: 6,
302
+ },
303
+ ];
304
+
305
+ // Mobile: collapse the required/width/delete button group into a single dropdown
306
+ function fieldActionOptions(element: Field) {
307
+ return [
308
+ {
309
+ label: element.required ? `${t("required")} ✓` : t("required"),
310
+ key: "required",
311
+ },
312
+ {
313
+ label: t("width"),
314
+ key: "width",
315
+ children: widthOptions.map((option) => ({
316
+ label: option.value === (element.width ?? 1) ? `${option.label} ✓` : option.label,
317
+ key: `width-${option.value}`,
318
+ })),
319
+ },
320
+ {
321
+ label: t("delete"),
322
+ key: "delete",
323
+ },
324
+ ];
325
+ }
326
+
327
+ function onFieldAction(action: string, element: Field, index: number) {
328
+ if (action === "required") element.required = !element.required;
329
+ else if (action === "delete") schema.value.splice(index, 1);
330
+ else if (action.startsWith("width-"))
331
+ element.width = Number(action.slice("width-".length));
332
+ }
333
+
334
+ // ── Copy / cut / paste fields ─────────────────────────────────────────────────
335
+
336
+ const interactiveSelector = [
337
+ "button",
338
+ "input",
339
+ "textarea",
340
+ "select",
341
+ "a[href]",
342
+ '[contenteditable="true"]',
343
+ "[role='button']",
344
+ ".n-input",
345
+ ".n-select",
346
+ ".n-input-number",
347
+ ".n-cascader",
348
+ ".n-switch",
349
+ ".n-checkbox",
350
+ ".n-radio",
351
+ ".n-color-picker",
352
+ ".n-data-table",
353
+ ".n-popover",
354
+ ".n-dropdown",
355
+ ].join(",");
356
+
357
+ function shouldIgnoreFieldTarget(target: HTMLElement | null) {
358
+ if (!target) return true;
359
+ return Boolean(target.closest(interactiveSelector));
360
+ }
361
+
362
+ // Fields currently selected via ctrl/meta click
363
+ const selectedFieldIds = ref<Set<string | number>>(new Set());
364
+
365
+ function onFieldClick(e: MouseEvent, element: Field) {
366
+ if (e.ctrlKey || e.metaKey) {
367
+ if (shouldIgnoreFieldTarget(e.target as HTMLElement)) return;
368
+ e.preventDefault();
369
+ e.stopPropagation();
370
+ toggleFieldSelection(element.id as string | number);
371
+ return;
372
+ }
373
+ if (selectedFieldIds.value.size) selectedFieldIds.value = new Set();
374
+ }
375
+
376
+ function toggleFieldSelection(id: string | number) {
377
+ const next = new Set(selectedFieldIds.value);
378
+ if (next.has(id)) next.delete(id);
379
+ else next.add(id);
380
+ selectedFieldIds.value = next;
381
+ }
382
+
383
+ const selectedFields = computed<Field[]>(() =>
384
+ schema.value.filter(({ id }) => selectedFieldIds.value.has(id as string | number)),
385
+ );
386
+
387
+ // Add a global keydown handler so that ctrl/cmd+c and ctrl/cmd+x copy/cut the
388
+ // currently selected fields. We bail out when focus is inside an editable or
389
+ // interactive control so the browser's native copy/cut keeps working there.
390
+ function onSchemaKeydown(e: KeyboardEvent) {
391
+ if (
392
+ !(e.ctrlKey || e.metaKey) ||
393
+ !["c", "C", "x", "X"].includes(e.key) ||
394
+ !selectedFieldIds.value.size
395
+ )
396
+ return;
397
+
398
+ const target = e.target as HTMLElement | null;
399
+ if (target?.closest(interactiveSelector)) return;
400
+
401
+ e.preventDefault();
402
+ void copySelectedFields(e.key.toLowerCase() === "x");
403
+ }
404
+
405
+ onMounted(() => document.addEventListener("keydown", onSchemaKeydown));
406
+ onBeforeUnmount(() =>
407
+ document.removeEventListener("keydown", onSchemaKeydown),
408
+ );
409
+
410
+ async function copySelectedFields(cut = false) {
411
+ const fields = selectedFields.value;
412
+ const copyable = fields.filter((element) => !isDisabled(element.key));
413
+ if (!copyable.length) {
414
+ window.$message.warning(t("noFieldsToCopy"));
415
+ if (cut) selectedFieldIds.value = new Set();
416
+ return;
417
+ }
418
+ await copySchemaFields(copyable);
419
+ if (cut) {
420
+ schema.value = schema.value.filter(
421
+ ({ id }) => !selectedFieldIds.value.has(id as string | number),
422
+ );
423
+ window.$message.success(t("cutSuccessfully"));
424
+ } else {
425
+ window.$message.success(t("copiedSuccessfully"));
426
+ }
427
+ // Clear the selection outline after copy/cut
428
+ selectedFieldIds.value = new Set();
429
+ }
430
+
431
+ function isDisabled(key?: string) {
432
+ if (!key) return false;
433
+ return disabledKeysSet.value.has(key);
434
+ }
435
+
436
+ // Cache for disabled keys based on current table slug
437
+ const disabledKeysSet = computed<Set<string>>(() => {
438
+ const base: string[] = ["id", "createdAt", "updatedAt"];
439
+ switch (table.value.slug) {
440
+ case "users":
441
+ base.push("username", "email", "password", "role", "createdBy", "config");
442
+ break;
443
+ case "pages":
444
+ base.push("slug", "content", "seo");
445
+ break;
446
+ case "blocks":
447
+ base.push("name", "config", "hideOn");
448
+ break;
449
+ default:
450
+ break;
451
+ }
452
+ return new Set(base);
453
+ });
454
+ const expandedNames = defineModel<(string | number)[]>("expandedNames");
455
+ const expandedChildNames = ref<(string | number)[]>();
456
+ const { reorderEnabled = false, skipDefaultFields } = defineProps<{ reorderEnabled?: boolean, skipDefaultFields?: boolean }>();
457
+ async function pushToChildrenSchema(type: string, index: number) {
458
+ if (!schema.value[index]) return;
459
+ if (!schema.value[index].children)
460
+ schema.value[index].children = [] as Schema;
461
+ (schema.value[index].children as Schema).push({
462
+ id: `temp-${randomID()}`,
463
+ key: null,
464
+ required: false,
465
+ ...handleSelectedSchemaType(type),
466
+ } as any);
467
+
468
+ if (!schema.value[index].id) return;
469
+
470
+ expandedNames.value = [schema.value[index].id];
471
+ const newElementId = (
472
+ (schema.value[index].children as Schema).at(-1) as Field
473
+ ).id;
474
+
475
+ if (!newElementId) return;
476
+
477
+ expandedChildNames.value = [newElementId];
478
+
479
+ setTimeout(
480
+ () => document.getElementById(`element-${newElementId}`)?.scrollIntoView(),
481
+ 300,
482
+ );
483
+ }
484
+ const schema = defineModel<Schema>({
485
+ default: () => reactive([]),
486
+ });
487
+ const slicedSchema = ref(skipDefaultFields || schema.value?.[0]?.id !== 0 ? schema.value : schema.value.slice(1, -2));
488
+ watch(() => slicedSchema.value, (newVal) => {
489
+ if (skipDefaultFields || schema.value?.[0]?.id !== 0) {
490
+ schema.value = newVal
491
+ return;
492
+ }
493
+
494
+ schema.value = [schema.value[0] as Field, ...newVal, schema.value.at(-2)!, schema.value.at(-1)!];
495
+ }, { deep: true });
496
+ watch(() => schema.value, (newVal) => {
497
+ if (skipDefaultFields || newVal?.[0]?.id !== 0) {
498
+ if (newVal.length === slicedSchema.value.length)
499
+ slicedSchema.value = newVal;
500
+ return;
501
+ }
502
+
503
+ if(newVal.length - 3 !== slicedSchema.value.length)
504
+ slicedSchema.value = newVal.slice(1, -2);
505
+ }, { deep: true });
506
+ const database = useState<Database>("database");
507
+ const table = useState<Table>("table");
508
+
509
+ function changeFieldType(
510
+ { id, key, required, children, width }: any,
511
+ newType: string,
512
+ ): any {
513
+ switch (newType) {
514
+ case "object":
515
+ case "array":
516
+ return {
517
+ id,
518
+ key,
519
+ type: newType,
520
+ required,
521
+ children: Array.isArray(children) ? children : [],
522
+ };
523
+ case "multiple":
524
+ return {
525
+ id,
526
+ key,
527
+ type: ["string","number"],
528
+ width,
529
+ required,
530
+ };
531
+ default:
532
+ return {
533
+ id,
534
+ key,
535
+ ...(handleSelectedSchemaType(newType) as any),
536
+ width,
537
+ required,
538
+ };
539
+ }
540
+ }
541
+
542
+ // Mutate the field object in place (instead of replacing the array element) so type-specific
543
+ // leftovers (table/options/subType/etc) are always cleared, even when the object is shared by reference.
544
+ function applyFieldType(element: Field, newType: string) {
545
+ const changes = changeFieldType(element, newType);
546
+ for (const key of Object.keys(element))
547
+ if (!(key in changes)) delete (element as any)[key];
548
+ Object.assign(element, changes);
549
+ }
550
+
551
+ // Forces NCollapseItem to remount its body whenever a field's type-relevant shape changes.
552
+ function fieldTypeSignature(element: Field) {
553
+ const typeKey = Array.isArray(element.type) ? element.type.join(",") : element.type;
554
+ return `${element.id}-${typeKey}-${element.subType ?? ""}-${element.table ?? ""}`;
555
+ }
556
+
557
+ function renderIcon(iconName: string) {
558
+ return () => h(NIcon, () => h(Icon, { name: iconName }));
559
+ }
560
+
561
+ const fileTypeSelectOptions = computed(() => [
562
+ {
563
+ label: t("fileType.image"),
564
+ value: "image",
565
+ icon: renderIcon("tabler:photo"),
566
+ },
567
+ {
568
+ label: t("fileType.video"),
569
+ value: "video",
570
+ icon: renderIcon("tabler:video"),
571
+ },
572
+ {
573
+ label: t("fileType.audio"),
574
+ value: "audio",
575
+ icon: renderIcon("tabler:music"),
576
+ },
577
+ {
578
+ label: t("fileType.document"),
579
+ value: "document",
580
+ icon: renderIcon("tabler:file-description"),
581
+ },
582
+ {
583
+ label: t("fileType.archive"),
584
+ value: "archive",
585
+ icon: renderIcon("tabler:file-zip"),
586
+ },
587
+ ]);
588
+ function selectRenderLabelWithIcon(
589
+ option: SelectOption & { icon: CallableFunction },
590
+ ) {
591
+ return h(NFlex, { align: "center" }, () => [
592
+ option.icon(),
593
+ option.label as string,
594
+ ]);
595
+ }
596
+
597
+ const valuesTypeSelectOptions = computed(() =>
598
+ flatFieldsList()
599
+ .filter(({ key }) =>
600
+ ["string", "number", "password", "email", "url"].includes(key),
601
+ )
602
+ .map((field) => ({
603
+ label: field.label,
604
+ value: field.key,
605
+ icon: field.icon,
606
+ })),
607
+ );
608
+
609
+ // Cache field type options to avoid recomputing heavy dropdown option lists on each re-render
610
+ const fieldTypeOptions = computed(() => fieldsList());
611
+
612
+ const tableSelectOptions = computed(() =>
613
+ database.value.tables?.map(({ slug }) => ({
614
+ label: t(slug),
615
+ value: slug,
616
+ })),
617
+ );
618
+
619
+ const uniqueGroupOptions = computed(() => {
620
+ // Extract all unique group names from the schema
621
+ const groups = schema.value
622
+ .map((field) => (typeof field.unique === "string" ? field.unique : null))
623
+ .filter((group) => group !== null);
624
+ return [...new Set(groups)].map((group) => ({
625
+ label: group,
626
+ value: group,
627
+ }));
628
+ });
629
+
630
+ function toggleLabelsColoring(schemaItem: Field, value: boolean) {
631
+ if (!schemaItem.options || schemaItem.options.length === 0)
632
+ schemaItem.options = [""];
633
+ if (value) {
634
+ // Convert array of strings to array of arrays of two strings
635
+ if (
636
+ Array.isArray(schemaItem.options) &&
637
+ schemaItem.options.every((option) => typeof option === "string")
638
+ ) {
639
+ schemaItem.options = schemaItem.options.map((option) => [option, ""]);
640
+ }
641
+ } else {
642
+ // Convert array of arrays of two strings back to array of strings
643
+ if (
644
+ Array.isArray(schemaItem.options) &&
645
+ schemaItem.options.every(
646
+ (option) => Array.isArray(option) && option.length === 2,
647
+ )
648
+ ) {
649
+ schemaItem.options = schemaItem.options
650
+ .map((option) => option[0])
651
+ .filter((option) => option !== "");
652
+ }
653
+ }
654
+ }
655
+
656
+ function labelsColoringColumns(schemaItem: Field): DataTableColumns<any> {
657
+ return [
658
+ {
659
+ title: t("optionLabel"),
660
+ key: "label",
661
+ render(row: [string | number, string], index: number) {
662
+ return h(NInput, {
663
+ value: row[0].toString(),
664
+ onUpdateValue(v) {
665
+ if (!schemaItem.options?.[index]) return;
666
+ (schemaItem.options[index] as [string | number, string])[0] = v;
667
+ },
668
+ });
669
+ },
670
+ },
671
+ {
672
+ title: t("optionColor"),
673
+ key: "color",
674
+ render(row: [string | number, string], index: number) {
675
+ return h(NColorPicker, {
676
+ modes: ["hex"],
677
+ showAlpha: false,
678
+ value: row[1].toString(),
679
+ onUpdateValue(v) {
680
+ if (!schemaItem.options?.[index]) return;
681
+ (schemaItem.options[index] as [string | number, string])[1] = v;
682
+ },
683
+ });
684
+ },
685
+ },
686
+ {
687
+ title: t("actions"),
688
+ key: "actions",
689
+ align: "center",
690
+ render(row: [string | number, string], index: number) {
691
+ return h(
692
+ NButton,
693
+ {
694
+ type: "error",
695
+ size: "small",
696
+ circle: true,
697
+ secondary: true,
698
+ onClick() {
699
+ if (!schemaItem.options || schemaItem.options.length === 1) {
700
+ schemaItem.options = [["", ""]];
701
+ return;
702
+ }
703
+ (schemaItem.options as [string | number, string][]).splice(
704
+ index,
705
+ 1,
706
+ );
707
+ },
708
+ },
709
+ { icon: () => h(NIcon, () => h(Icon, { name: "tabler:trash" })) },
710
+ );
711
+ },
712
+ },
713
+ ];
714
+ }
715
+
716
+ // Normalize select options: support comma-separated values entered at once
717
+ function normalizeOptionsInput(values: string[]): string[] {
718
+ const out: string[] = [];
719
+ for (const raw of values) {
720
+ // Split on comma (English, Arabic), semicolon, pipe, trim whitespace around items
721
+ const parts = String(raw)
722
+ .split(/[,،;|]/)
723
+ .map((s) => s.trim())
724
+ .filter((s) => s.length > 0);
725
+ out.push(...parts);
726
+ }
727
+ // Deduplicate while preserving order
728
+ const seen = new Set<string>();
729
+ return out.filter((v) => {
730
+ if (seen.has(v)) return false;
731
+ seen.add(v);
732
+ return true;
733
+ });
734
+ }
735
+
736
+ function handleOptionsUpdate(element: Field, value: string[]) {
737
+ const normalized = normalizeOptionsInput(value);
738
+ element.options = normalized;
739
+ }
740
+
741
+ // Debounced key input handling to avoid committing reactive changes on every keystroke
742
+ const keyBuffer = ref<Record<string, string>>({});
743
+ const keyCommitTimers = new Map<string, ReturnType<typeof setTimeout>>();
744
+ function getDisplayKey(element: Field) {
745
+ const id = String(element.id ?? "");
746
+ const buffered = keyBuffer.value[id];
747
+ return buffered !== undefined ? buffered : (element.key ?? "");
748
+ }
749
+
750
+ // Memoize getField(element) by a stable signature of field-shape properties
751
+ const getFieldCache = new Map<string, ReturnType<typeof getField>>();
752
+ function getFieldCached(element: Field) {
753
+ const typeKey = Array.isArray(element.type)
754
+ ? element.type.join(",")
755
+ : element.type;
756
+ const childrenKey = Array.isArray(element.children)
757
+ ? "schema"
758
+ : (element.children ?? "");
759
+ const key = [
760
+ typeKey,
761
+ element.subType ?? "",
762
+ element.date ?? "",
763
+ element.table ?? "",
764
+ childrenKey,
765
+ ].join("|");
766
+ const cached = getFieldCache.get(key);
767
+ if (cached) return cached;
768
+ const v = getField(element);
769
+ getFieldCache.set(key, v);
770
+ return v;
771
+ }
772
+ function onKeyInput(element: Field, v: string) {
773
+ const id = String(element.id ?? "");
774
+ keyBuffer.value[id] = v;
775
+ const existing = keyCommitTimers.get(id);
776
+ if (existing) clearTimeout(existing);
777
+ const timeout = setTimeout(() => {
778
+ element.key = v.trim();
779
+ keyCommitTimers.delete(id);
780
+ }, 200);
781
+ keyCommitTimers.set(id, timeout);
782
+ }
783
+ </script>
784
+
785
+ <style scoped>
786
+ .reorder-enabled .n-collapse-item:not(.n-collapse-item--disabled) :deep(.n-collapse-item__header-main) {
787
+ cursor: move !important;
788
+ }
789
+
790
+ .field-selected {
791
+ border-radius: 12px;
792
+ outline: 2px solid rgb(var(--primaryColor));
793
+ background-color: rgb(var(--primaryColor), 0.08);
794
+ outline-offset: 2px;
795
+ }
796
+
797
+ .formItemFlex :deep(.n-form-item-blank) {
798
+ flex-direction: column;
799
+ gap: 12px
800
+ }
801
+ </style>