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,348 @@
1
+ <template>
2
+ <FieldWrapper :field :rule v-model="modelValue">
3
+ <NSelect :placeholder="t(field.key)" :value="selectValue" @update:value="onUpdateSelectValue" :options="options" remote
4
+ clearable :filterable="!!searchIn && searchIn.length > 0" :loading="loading" :reset-menu-on-options-change="false"
5
+ :multiple="!!field.isArray" consistent-menu-width max-tag-count="responsive"
6
+ @update:show="(show) => show && loadOptions()" @scroll="handleScroll" @search="debouncedLoadOptions" v-bind="field.inputProps
7
+ ? typeof field.inputProps === 'function'
8
+ ? field.inputProps(modelValue) ?? {}
9
+ : field.inputProps
10
+ : {}">
11
+ <template #action v-if="table?.allowedMethods?.includes('c')">
12
+ <NFlex justify="center">
13
+ <NButton round strong secondary type="primary" @click="() => openDrawer(field.table as string)">
14
+ <template #icon>
15
+ <Icon name="tabler:plus" />
16
+ </template>
17
+ </NButton>
18
+ </NFlex>
19
+ </template>
20
+ </NSelect>
21
+ </FieldWrapper>
22
+ </template>
23
+
24
+ <script lang="ts" setup>
25
+ import type { pageInfo } from "inibase";
26
+ import { isObject, isValidID } from "inibase/utils";
27
+ import Inison from "inison";
28
+ import type { FormItemRule } from "naive-ui";
29
+ import { debounce } from "~/composables";
30
+
31
+ const { field } = defineProps<{ field: Field }>();
32
+ const modelValue = defineModel<Item | Item[]>();
33
+ const options = ref<tableOption[] | undefined>();
34
+ const loading = ref(false);
35
+ const loadingMore = ref(false);
36
+ const database = useState<Database>("database");
37
+
38
+ // Monotonic token that invalidates stale responses from superseded requests.
39
+ let requestSeq = 0;
40
+ // The resolved query (search + where) the currently loaded options belong to.
41
+ let loadedQuery: string | undefined;
42
+ const table = database.value.tables?.find(({ slug }) => slug === field.table);
43
+
44
+ // Id-only model values (translated / stored table refs) are resolved through
45
+ // the shared cache which batches all ids of the current page into a single
46
+ // request per referenced table. Object values never trigger a fetch.
47
+ const referenced = useReferencedItems(field.table);
48
+
49
+ watch(
50
+ modelValue,
51
+ (value) => {
52
+ if (!value) return;
53
+ const entries = ([] as unknown[])
54
+ .concat(value)
55
+ .filter((entry) => entry !== undefined && entry !== null);
56
+ const objectItems = entries.filter(isObject) as Item[];
57
+ const idEntries = entries
58
+ .filter((entry) => !isObject(entry))
59
+ .map((entry) => String(entry).trim())
60
+ .filter(Boolean);
61
+
62
+ if (idEntries.length) referenced.register(idEntries);
63
+
64
+ // Resolved entries become select options: id-strings via the cache,
65
+ // objects as-is.
66
+ const resolvedOptions = [
67
+ ...idEntries
68
+ .map((id) => referenced.items.value[id])
69
+ .filter((item): item is Item => Boolean(item?.id))
70
+ .map(singleOption),
71
+ ...objectItems.map(singleOption),
72
+ ];
73
+ if (!resolvedOptions.length) return;
74
+ if (!options.value) {
75
+ options.value = resolvedOptions;
76
+ return;
77
+ }
78
+ const known = new Set(options.value.map(({ value }) => value));
79
+ const fresh = resolvedOptions.filter(({ value }) => !known.has(value));
80
+ if (fresh.length) options.value = [...options.value, ...fresh];
81
+ },
82
+ { immediate: true },
83
+ );
84
+
85
+ // Rebuild id options once the batched fetch populates the cache.
86
+ watch(
87
+ () => referenced.items.value,
88
+ () => {
89
+ const value = modelValue.value;
90
+ if (!value) return;
91
+ const idEntries = ([] as unknown[])
92
+ .concat(value)
93
+ .filter((entry) => !isObject(entry))
94
+ .map((entry) => String(entry).trim())
95
+ .filter(Boolean);
96
+ if (!idEntries.length) return;
97
+ const idOptions = idEntries
98
+ .map((id) => referenced.items.value[id])
99
+ .filter((item): item is Item => Boolean(item?.id))
100
+ .map(singleOption);
101
+ if (!idOptions.length) return;
102
+ const known = new Set((options.value ?? []).map(({ value }) => value));
103
+ const fresh = idOptions.filter(({ value }) => !known.has(value));
104
+ if (fresh.length) options.value = [...(options.value ?? []), ...fresh];
105
+ },
106
+ );
107
+
108
+ const selectValue = computed<null | string | string[]>(() => {
109
+ const value = modelValue.value as unknown;
110
+ if (value === undefined || value === null || value === "") return null;
111
+ const toID = (entry: unknown) =>
112
+ (isObject(entry) ? (entry as Item).id : entry) as string;
113
+ if (field.isArray)
114
+ // the search form stores selected ids as a comma joined string
115
+ return (Array.isArray(value) ? value : String(value).split(","))
116
+ .map(toID)
117
+ .filter(Boolean);
118
+ return toID(Array.isArray(value) ? value[0] : value) ?? null;
119
+ });
120
+
121
+ const rule: FormItemRule = {
122
+ trigger: ["blur", "change"],
123
+ type: !field.isArray ? "string" : "array",
124
+ required: field.required,
125
+ min: field.isArray ? field.min : undefined,
126
+ validator: async () => {
127
+ await nextTick();
128
+ return fieldValidator(field, modelValue.value);
129
+ },
130
+ };
131
+
132
+ const config = useRuntimeConfig();
133
+
134
+ type tableOption = {
135
+ label: string;
136
+ value: string;
137
+ raw: Item;
138
+ };
139
+
140
+ function singleOption(option: Item): tableOption {
141
+ return {
142
+ label: renderLabel(table, option),
143
+ value: option.id as string,
144
+ raw: option,
145
+ };
146
+ }
147
+
148
+ async function onUpdateSelectValue(
149
+ _id: string | string[],
150
+ option: tableOption | tableOption[],
151
+ ) {
152
+ modelValue.value = option
153
+ ? Array.isArray(option)
154
+ ? option.map(({ raw }) => raw)
155
+ : option.raw
156
+ : undefined;
157
+ await nextTick();
158
+ if (
159
+ options.value &&
160
+ modelValue.value &&
161
+ modelValue.value.length === options.value.length
162
+ )
163
+ options.value = options.value.filter(({ value }) =>
164
+ Array.isArray(_id) ? _id.includes(value) : _id === value,
165
+ );
166
+ }
167
+
168
+ const searchIn = table?.defaultSearchableColumns
169
+ ? table.defaultSearchableColumns.map((columnID) =>
170
+ getPath(table.schema ?? [], columnID),
171
+ )
172
+ : field.searchIn;
173
+
174
+ const pagination = ref<pageInfo>();
175
+ const where = ref<string>();
176
+
177
+ const debouncedLoadOptions = debounce(async (searchValue) => {
178
+ await loadOptions(searchValue);
179
+ }, 1000);
180
+
181
+ const sessionID = useScopedCookie<string>("sid", database.value?.slug);
182
+
183
+ async function loadOptions(searchValue?: string | number) {
184
+ const seq = ++requestSeq;
185
+ const searchOrObject =
186
+ searchValue &&
187
+ (typeof searchValue !== "string" || searchValue.trim().length) &&
188
+ searchIn
189
+ ? (searchIn.reduce((result, searchKey) => {
190
+ Object.assign(result, {
191
+ [searchKey]: `*%${searchValue}%`,
192
+ });
193
+ return result;
194
+ }, {}) ?? false)
195
+ : false;
196
+
197
+ let _where = "";
198
+ if (field.where) {
199
+ if (searchOrObject)
200
+ _where = Inison.stringify({
201
+ ...((typeof field.where === "string"
202
+ ? Inison.unstringify(
203
+ renderLabel({ ...(table as Table), label: field.where }),
204
+ )
205
+ : field.where) as any),
206
+ or: searchOrObject,
207
+ });
208
+ else
209
+ _where =
210
+ typeof field.where === "string"
211
+ ? renderLabel({ ...(table as Table), label: field.where })
212
+ : renderLabel({
213
+ ...(table as Table),
214
+ label: Inison.stringify(field.where),
215
+ });
216
+ } else if (searchOrObject)
217
+ _where = Inison.stringify({
218
+ or: searchOrObject,
219
+ });
220
+
221
+ // Add direct ID search if the searchValue is a valid ID
222
+ if (searchValue && isValidID(searchValue))
223
+ _where = Inison.stringify({ id: searchValue });
224
+
225
+ // Already loaded for this query (e.g. the dropdown was re-opened) — keep
226
+ // the existing options instead of fetching and rebuilding the whole menu.
227
+ if (loadedQuery === _where && options.value !== undefined) {
228
+ loading.value = false;
229
+ return;
230
+ }
231
+
232
+ loadedQuery = _where;
233
+ where.value = _where || undefined;
234
+ pagination.value = undefined;
235
+ loading.value = true;
236
+ loadingMore.value = false;
237
+
238
+ const request = await $fetch<apiResponse<tableOption[]>>(
239
+ `${config.public.apiBase}${database.value.slug}/${field.table}`,
240
+ {
241
+ params: {
242
+ where: where.value,
243
+ options: Inison.stringify({
244
+ columns: table?.columns,
245
+ }),
246
+ [`${database.value.slug}_sid`]: sessionID.value,
247
+ },
248
+ cache: "no-cache",
249
+ credentials: "include",
250
+ },
251
+ ).finally(() => {
252
+ if (seq === requestSeq) loading.value = false;
253
+ });
254
+
255
+ // Ignore stale responses from superseded requests
256
+ if (seq !== requestSeq) return;
257
+
258
+ pagination.value = request.options;
259
+
260
+ if (modelValue.value) {
261
+ const currentSelectedOptions = (options.value ?? []).filter(({ value }) =>
262
+ selectValue.value?.includes(value),
263
+ );
264
+ options.value = [
265
+ ...currentSelectedOptions,
266
+ ...(request.result
267
+ ?.map(singleOption)
268
+ .filter(({ value }) => !selectValue.value?.includes(value)) ?? []),
269
+ ];
270
+ } else options.value = request.result?.map(singleOption) ?? [];
271
+ }
272
+
273
+ let lastScrollCall = 0;
274
+ function handleScroll(e: Event) {
275
+ const now = Date.now();
276
+ if (now - lastScrollCall < 120) return;
277
+ lastScrollCall = now;
278
+
279
+ const currentTarget = e.currentTarget as HTMLElement;
280
+ if (
281
+ loading.value ||
282
+ loadingMore.value ||
283
+ !pagination.value ||
284
+ !pagination.value.page ||
285
+ !pagination.value.totalPages
286
+ )
287
+ return;
288
+ if (
289
+ currentTarget.scrollTop + currentTarget.clientHeight >=
290
+ currentTarget.scrollHeight - 80 &&
291
+ pagination.value.page < pagination.value.totalPages
292
+ )
293
+ void loadMoreOptions();
294
+ }
295
+
296
+ async function loadMoreOptions() {
297
+ if (
298
+ loading.value ||
299
+ loadingMore.value ||
300
+ !pagination.value ||
301
+ !pagination.value.page ||
302
+ !pagination.value.totalPages ||
303
+ pagination.value.page >= pagination.value.totalPages
304
+ )
305
+ return;
306
+
307
+ loadingMore.value = true;
308
+ const page = pagination.value.page + 1;
309
+ const seq = requestSeq;
310
+
311
+ const request = await $fetch<apiResponse<tableOption[]>>(
312
+ `${config.public.apiBase}${database.value.slug}/${field.table}`,
313
+ {
314
+ params: {
315
+ where: where.value,
316
+ options: Inison.stringify({
317
+ page,
318
+ columns: table?.columns,
319
+ }),
320
+ [`${database.value.slug}_sid`]: sessionID.value,
321
+ },
322
+ cache: "no-cache",
323
+ credentials: "include",
324
+ },
325
+ ).finally(() => {
326
+ if (seq === requestSeq) loadingMore.value = false;
327
+ });
328
+
329
+ // Ignore stale responses from superseded requests
330
+ if (seq !== requestSeq) return;
331
+
332
+ if (request.result) request.result = request.result.map(singleOption);
333
+ pagination.value = request.options;
334
+ if (options.value && request.result) {
335
+ const known = new Set(options.value.map(({ value }) => value));
336
+ options.value.push(
337
+ ...request.result.filter(({ value }) => !known.has(value)),
338
+ );
339
+ }
340
+ }
341
+
342
+ if (
343
+ field.inputProps &&
344
+ typeof field.inputProps === "object" &&
345
+ field.inputProps.show
346
+ )
347
+ loadOptions();
348
+ </script>
@@ -0,0 +1,113 @@
1
+ <template>
2
+ <FieldWrapper ref="dynamicTags" :field :rule>
3
+ <NFlex align="center" :wrap="true">
4
+ <NTag v-for="(value, index) in field.defaultValue" :key="'default-' + index" round>
5
+ {{ t(value) }}
6
+ </NTag>
7
+ <NTag v-for="(value, index) in nonDefaultValues" :key="'user-' + index" round closable
8
+ @close="removeValue(index)">
9
+ {{ value }}
10
+ </NTag>
11
+ <NInput v-if="showInput" ref="inputRef" v-model:value="inputValue" size="small" round autofocus autosize style="min-width: 150px"
12
+ :placeholder="t(field.key)" clearable @keyup.enter="addValue" @keyup.esc="cancel"
13
+ @blur="cancel" v-bind="field.inputProps
14
+ ? typeof field.inputProps === 'function'
15
+ ? field.inputProps(modelValue) ?? {}
16
+ : field.inputProps
17
+ : {}">
18
+ <template #suffix>
19
+ <NTooltip :disabled="!fieldChildrenLabels" :delay="1500">
20
+ <template #trigger>
21
+ <component :is="getField(field).icon" />
22
+ </template>
23
+ <template v-if="fieldChildrenLabels" #default>
24
+ {{ t('thisInputSupports') }}: {{ fieldChildrenLabels }}
25
+ </template>
26
+ </NTooltip>
27
+ </template>
28
+ </NInput>
29
+ <NButton v-if="!showInput" type="primary" size="small" dashed :disabled="disabled" @click="showInput = true">
30
+ <template #icon>
31
+ <Icon name="tabler:plus" />
32
+ </template>
33
+ </NButton>
34
+ </NFlex>
35
+ </FieldWrapper>
36
+ </template>
37
+
38
+ <script lang="ts" setup>
39
+ import type { FormInst, FormItemRule, InputInst } from "naive-ui"
40
+
41
+ import { validateFieldType } from "inibase/utils"
42
+ import { NTag } from "#components"
43
+
44
+ const { field } = defineProps<{ field: Field }>()
45
+
46
+ const modelValue = defineModel<(string | number)[]>()
47
+
48
+ const showInput = ref(false)
49
+ const inputValue = ref()
50
+ const inputRef = ref<InputInst>()
51
+
52
+ const disabled = computed(() => field.max ? (modelValue.value?.length ?? 0) >= field.max : false)
53
+
54
+ const nonDefaultValues = computed(() =>
55
+ modelValue.value?.filter((value) => !field.defaultValue || !field.defaultValue.includes(value)) ?? [],
56
+ )
57
+
58
+ const fieldChildrenLabels = field.children
59
+ ? flatFieldsList()
60
+ .filter(({ key }) =>
61
+ ([] as string[])
62
+ .concat(field.children as string | string[])
63
+ .includes(key),
64
+ )
65
+ .map(({ label }) => label)
66
+ .join(" | ")
67
+ : undefined
68
+
69
+ const rule: FormItemRule = {
70
+ type: "array",
71
+ required: field.required,
72
+ min: field.min,
73
+ max: field.max,
74
+ validator: async () => {
75
+ await nextTick()
76
+ return fieldValidator(field, modelValue.value, validateFieldType)
77
+ },
78
+ }
79
+ const dynamicTags = ref<FormInst>()
80
+
81
+ function addValue() {
82
+ if (inputValue.value) {
83
+ modelValue.value = [...(modelValue.value ?? []), inputValue.value]
84
+ inputValue.value = null
85
+ inputRef.value?.focus()
86
+ }
87
+ }
88
+
89
+ function cancel() {
90
+ showInput.value = false
91
+ inputValue.value = null
92
+ }
93
+
94
+ function removeValue(index: number) {
95
+ const nonDefault = modelValue.value?.filter(
96
+ (value) => !field.defaultValue || !field.defaultValue.includes(value),
97
+ ) ?? []
98
+ const valueToRemove = nonDefault[index]
99
+ modelValue.value = modelValue.value?.filter((v) => v !== valueToRemove) ?? []
100
+ }
101
+
102
+ watch(modelValue, () => {
103
+ try {
104
+ dynamicTags.value?.validate()
105
+ } catch {}
106
+ })
107
+
108
+ watch(showInput, (value) => {
109
+ if (value) {
110
+ nextTick(() => inputRef.value?.focus())
111
+ }
112
+ })
113
+ </script>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <FieldWrapper :field :rule v-model="modelValue">
3
+ <NInput :value="modelValue !== undefined && modelValue !== null ? String(modelValue) : null"
4
+ @update:value="(value) => modelValue = value" :placeholder="t(field.key)" clearable v-bind="field.inputProps
5
+ ? typeof field.inputProps === 'function'
6
+ ? field.inputProps(modelValue) ?? {}
7
+ : field.inputProps
8
+ : {}">
9
+ <template #suffix>
10
+ <component :is="getField(field).icon" />
11
+ </template>
12
+ </NInput>
13
+ </FieldWrapper>
14
+ </template>
15
+
16
+ <script lang="ts" setup>
17
+ import type { FormItemRule } from "naive-ui"
18
+
19
+ const { field } = defineProps<{ field: Field }>()
20
+
21
+ const modelValue = defineModel<string | number>()
22
+
23
+ const rule: FormItemRule = {
24
+ required: field.required,
25
+ trigger: ["blur", "input"],
26
+ validator: async () => {
27
+ await nextTick()
28
+ return fieldValidator(field, modelValue.value)
29
+ },
30
+ }
31
+ </script>
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <FieldWrapper :field :rule v-model="modelValue">
3
+ <NInput v-model:value="modelValue" :placeholder="t(field.key)" clearable show-count type="textarea"
4
+ :rows="field.isTable ? 1 : undefined" :autosize="field.isTable ? false : {
5
+ minRows: 3,
6
+ }" v-bind="field.inputProps
7
+ ? typeof field.inputProps === 'function'
8
+ ? field.inputProps(modelValue) ?? {}
9
+ : field.inputProps
10
+ : {}">
11
+ <template #suffix>
12
+ <component :is="getField(field).icon" />
13
+ </template>
14
+ </NInput>
15
+ </FieldWrapper>
16
+ </template>
17
+
18
+ <script lang="ts" setup>
19
+ import type { FormItemRule } from "naive-ui"
20
+
21
+ const { field } = defineProps<{ field: Field }>()
22
+
23
+ const modelValue = defineModel<string>()
24
+
25
+ const rule: FormItemRule = {
26
+ trigger: ["blur", "input"],
27
+ required: field.required,
28
+ validator: async () => {
29
+ await nextTick()
30
+ return fieldValidator(field, modelValue.value)
31
+ },
32
+ }
33
+ </script>
@@ -0,0 +1,40 @@
1
+ <template>
2
+ <FieldWrapper :field :rule v-model="modelValue">
3
+ <NInput v-model:value="modelValue" :placeholder="t(field.key)" clearable v-bind="field.inputProps
4
+ ? typeof field.inputProps === 'function'
5
+ ? { ...(field.inputProps(modelValue) ?? {}), type: 'url' }
6
+ : { ...field.inputProps, type: 'url' }
7
+ : { type: 'url' }">
8
+ <template #suffix>
9
+ <component :is="getField(field).icon" />
10
+ </template>
11
+ </NInput>
12
+ </FieldWrapper>
13
+ </template>
14
+
15
+ <script lang="ts" setup>
16
+ import { isURL } from "inibase/utils"
17
+ import type { FormItemRule } from "naive-ui"
18
+
19
+ const { field } = defineProps<{ field: Field }>()
20
+
21
+ const modelValue = defineModel<string>()
22
+
23
+ // Accept real URLs (isURL already covers http(s), tel:, mailto:, #anchor) as
24
+ // well as site-relative paths (e.g. "/about") — isURL alone rejects those.
25
+ // A relative path must start with "/" and contain no whitespace/control
26
+ // characters (still allows non-ASCII slugs, e.g. "/منتجات").
27
+ const relativePathPattern = /^\/[^\s\x00-\x1f]*$/
28
+ const isURLOrRelativePath = (input: unknown) =>
29
+ typeof input === "string" &&
30
+ (relativePathPattern.test(input) || isURL(input))
31
+
32
+ const rule: FormItemRule = {
33
+ trigger: ["blur", "input"],
34
+ required: field.required,
35
+ validator: async () => {
36
+ await nextTick()
37
+ return fieldValidator(field, modelValue.value, isURLOrRelativePath)
38
+ },
39
+ }
40
+ </script>
@@ -0,0 +1,128 @@
1
+ <template>
2
+ <NFormItem :id="field.id" :label="resolvedLabel" :rule :path="String(field.id)"
3
+ :style="{ flex: fieldFlex }" v-bind="field.labelProps">
4
+ <slot></slot>
5
+ <template #label>
6
+ <NFlex v-if="field.description" align="center" :size="0">
7
+ <NDropdown
8
+ :disabled="([] as string[]).concat(field.type).every(type => !['table', 'array', 'date'].includes(type))"
9
+ show-arrow placement="top" trigger="hover" :delay="800" :options="dropdownOptions"
10
+ @select="handleSelect">
11
+ {{ resolvedLabel }}
12
+ </NDropdown>
13
+ <NTooltip>
14
+ <template #trigger>
15
+ <NButton circle text size="tiny">
16
+ <template #icon>
17
+ <Icon name="tabler:question-mark" />
18
+ </template>
19
+ </NButton>
20
+ </template>
21
+ {{ t(field.description) }}
22
+ </NTooltip>
23
+ </NFlex>
24
+ <template v-else>
25
+ <NDropdown
26
+ :disabled="([] as string[]).concat(field.type).every(type => !['table', 'array', 'date'].includes(type))"
27
+ show-arrow placement="top" trigger="hover" :delay="1500" :options="dropdownOptions" size="small"
28
+ @select="handleSelect">
29
+ {{ resolvedLabel }}
30
+ </NDropdown>
31
+ </template>
32
+ <slot name="label"></slot>
33
+ </template>
34
+ </NFormItem>
35
+ </template>
36
+
37
+ <script lang="ts" setup>
38
+ import { isObject, isStringified } from "inibase/utils";
39
+ import Inison from "inison";
40
+ import type { DropdownOption, FormItemRule } from "naive-ui";
41
+ import { Icon, NIcon } from "#components";
42
+
43
+ const { field, rule } = defineProps<{ field: Field; rule: FormItemRule }>();
44
+
45
+ const modelValue = defineModel<any>();
46
+ const resolvedLabel = computed(() =>
47
+ field.labelKey ? t(field.labelKey) : (field.label ?? t(field.key)),
48
+ );
49
+
50
+ const { isMobile } = useDevice();
51
+ function numberToPercentage(width: number, wrapperWidth?: string | number) {
52
+ if (wrapperWidth && typeof wrapperWidth !== "string") {
53
+ if (wrapperWidth < 350) return "100";
54
+ if (wrapperWidth < 500) return "50";
55
+ if (wrapperWidth < 650 && width !== 2) return "33.33";
56
+ }
57
+ switch (width) {
58
+ case 2:
59
+ return "50";
60
+ case 3:
61
+ return "33.33";
62
+ case 4:
63
+ return "25";
64
+ case 5:
65
+ return "20";
66
+ case 6:
67
+ return "16.66";
68
+ }
69
+ }
70
+ const Drawers = useState<DrawerRef>("drawers", () => []);
71
+ const fieldFlex = computed(() =>
72
+ field.width && field.width !== 1 && !isMobile
73
+ ? `1 1 calc(${numberToPercentage(field.width, Drawers.value.find(({ show }) => !!show)?.width)}% - 12px)`
74
+ : "1 1 100%",
75
+ );
76
+
77
+ async function handleSelect(value: string) {
78
+ switch (value) {
79
+ case "copy": {
80
+ if (!modelValue.value) return;
81
+ await copyToClipboard(Inison.stringify(modelValue.value));
82
+ window.$message.success(t("copiedSuccessfully"));
83
+ break;
84
+ }
85
+ case "paste": {
86
+ try {
87
+ const itemFromClipboard = await navigator.clipboard.readText();
88
+
89
+ if (!itemFromClipboard) {
90
+ window.$message.error(t("clipboardEmpty"));
91
+ return;
92
+ }
93
+ if (!isStringified(itemFromClipboard)) {
94
+ window.$message.error(t("clipboardItemIsNotCorrect"));
95
+ return;
96
+ }
97
+
98
+ const unstringifiedItem = Inison.unstringify<Item[]>(itemFromClipboard);
99
+ if (
100
+ !unstringifiedItem &&
101
+ !Array.isArray(unstringifiedItem) &&
102
+ !isObject(unstringifiedItem)
103
+ ) {
104
+ window.$message.error(t("clipboardItemIsNotCorrect"));
105
+ return;
106
+ }
107
+
108
+ modelValue.value = unstringifiedItem;
109
+ } catch {
110
+ window.$message.error(t("clipboardItemIsNotCorrect"));
111
+ }
112
+ }
113
+ }
114
+ }
115
+ const dropdownOptions = computed<DropdownOption[]>(() => [
116
+ {
117
+ label: t("copyItem"),
118
+ key: "copy",
119
+ show: !!modelValue.value,
120
+ icon: () => h(NIcon, () => h(Icon, { name: "tabler:copy" })),
121
+ },
122
+ {
123
+ label: t("pasteItem"),
124
+ key: "paste",
125
+ icon: () => h(NIcon, () => h(Icon, { name: "tabler:clipboard" })),
126
+ },
127
+ ]);
128
+ </script>