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,256 @@
1
+ <template>
2
+ <NPopover style="max-height: 240px;" :style="`width: ${isMobile ? '100vw' : '500px'}`" placement="bottom-end"
3
+ scrollable v-model:show="isSearchPopoverVisible" :x="isMobile ? 0 : undefined" :y="isMobile ? 180 : undefined"
4
+ :show-arrow="!isMobile" trigger="click">
5
+ <template #trigger>
6
+ <NTooltip :delay="1500">
7
+ <template #trigger>
8
+ <NButton secondary round :size="props.size" :tertiary="isSearchPopoverVisible"
9
+ :type="isSearchPopoverVisible ? 'primary' : undefined">
10
+ <template #icon>
11
+ <NIcon>
12
+ <Icon v-if="isSearchPopoverVisible" name="tabler:x" />
13
+ <Icon v-else name="tabler:search" />
14
+ </NIcon>
15
+ </template>
16
+ </NButton>
17
+ </template>
18
+ {{ t("search") }}
19
+ </NTooltip>
20
+ </template>
21
+ <template #footer>
22
+ <NFlex justify="space-between">
23
+ <NTooltip :delay="500" placement="bottom">
24
+ <template #trigger>
25
+ <NPopover trigger="click" scrollable style="max-height: 300px;">
26
+ <template #trigger>
27
+ <NButton round type="success" size="small" secondary>
28
+ <template #icon>
29
+ <NIcon>
30
+ <Icon name="tabler:bookmark" />
31
+ </NIcon>
32
+ </template>
33
+ </NButton>
34
+ </template>
35
+ <template #footer>
36
+ <NInputGroup style="width: 100%;">
37
+ <NInput v-model:value="newFilterName" :placeholder="t('filterName')" size="small"
38
+ round
39
+ @keyup.enter="() => { saveFavoriteFilter(newFilterName); newFilterName = '' }" />
40
+ <NButton round type="success" secondary size="small" :loading="false"
41
+ :disabled="!newFilterName.trim()"
42
+ @click="() => { saveFavoriteFilter(newFilterName); newFilterName = '' }">
43
+ <template #icon>
44
+ <NIcon>
45
+ <Icon name="tabler:plus" />
46
+ </NIcon>
47
+ </template>
48
+ </NButton>
49
+ </NInputGroup>
50
+ </template>
51
+ <NEmpty v-if="favoriteFilters.length === 0" size="small"
52
+ :description="t('noFavoriteFilters')" />
53
+ <NFlex v-else vertical size="small" style="padding: 0 8px;">
54
+ <NFlex justify="space-between" v-for="filter in favoriteFilters" :key="filter.id" block
55
+ quaternary @click="loadFavoriteFilter(filter)" style="cursor: pointer;">
56
+ <template #default>
57
+ <NFlex justify="space-between" style="width: 100%;">
58
+ <span>{{ filter.name }}</span>
59
+ <NButton circle size="tiny" type="error" text
60
+ @click.stop="deleteFavoriteFilter(filter.id)">
61
+ <template #icon>
62
+ <NIcon>
63
+ <Icon name="tabler:trash" />
64
+ </NIcon>
65
+ </template>
66
+ </NButton>
67
+ </NFlex>
68
+ </template>
69
+ </NFlex>
70
+ </NFlex>
71
+ </NPopover>
72
+ </template>
73
+ {{ t("favoriteFilters") }}
74
+ </NTooltip>
75
+
76
+ <NButtonGroup>
77
+ <NButton round type="error" secondary size="small" :loading="Loading.data"
78
+ :disabled="props.disabled" @click="handleReset">
79
+ <template #icon>
80
+ <NIcon>
81
+ <Icon name="tabler:x" />
82
+ </NIcon>
83
+ </template>
84
+ {{ t("reset") }}
85
+ </NButton>
86
+ <NButton round type="primary" secondary size="small" :loading="Loading.data"
87
+ :disabled="props.disabled" @click="handleSearch">
88
+ <template #icon>
89
+ <NIcon>
90
+ <Icon name="tabler:search" />
91
+ </NIcon>
92
+ </template>
93
+ {{ t("search") }}
94
+ </NButton>
95
+ </NButtonGroup>
96
+ </NFlex>
97
+ </template>
98
+ <LazyTableSearch v-model="localSearchArray" :callback="handleSearch" v-model:schema="schema" />
99
+ </NPopover>
100
+ </template>
101
+
102
+ <script setup lang="ts">
103
+ import Inison from "inison";
104
+ import {
105
+ Icon,
106
+ NButton,
107
+ NButtonGroup,
108
+ NIcon,
109
+ NPopover,
110
+ NTooltip,
111
+ NFlex,
112
+ LazyTableSearch,
113
+ NEmpty,
114
+ NInput,
115
+ NInputGroup,
116
+ } from "#components";
117
+ import { deepClone } from "~/composables";
118
+ import { generateSearchString } from "~/composables/search";
119
+
120
+ const props = withDefaults(
121
+ defineProps<{
122
+ size?: "small" | "medium" | "large";
123
+ disabled?: boolean;
124
+ }>(),
125
+ {
126
+ disabled: false,
127
+ },
128
+ );
129
+
130
+ const schema = defineModel<Schema>("schema");
131
+
132
+ const searchString = defineModel<string | undefined>("string");
133
+ const searchArray = defineModel<searchType>("array", {
134
+ default: () => reactive({ and: [[null, "=", null]] }),
135
+ });
136
+
137
+ const config = useRuntimeConfig();
138
+ const { isMobile } = useDevice();
139
+ const user = useState<User>("user");
140
+ const database = useState<Database>("database");
141
+ const table = useState<Table>("table");
142
+ const sessionID = useState<string>("sessionID");
143
+ const Loading = useState<Record<string, boolean>>("Loading", () => ({}));
144
+
145
+ const isSearchPopoverVisible = ref(false);
146
+ const newFilterName = ref("");
147
+
148
+ // Local working copy of search array
149
+ const localSearchArray = ref<searchType>(deepClone(searchArray.value) ?? { and: [[null, "=", null]] });
150
+
151
+ // Sync local copy when external searchArray changes (without triggering search)
152
+ watch(
153
+ () => searchArray.value,
154
+ (newVal) => {
155
+ localSearchArray.value = deepClone(newVal) as searchType;
156
+ },
157
+ );
158
+
159
+ // Convert searchArray back to string for emit
160
+ function emitSearchQuery(locSearchArray: searchType) {
161
+ const searchInput = generateSearchString(locSearchArray, "display");
162
+ searchArray.value = locSearchArray;
163
+ searchString.value = searchInput ? Inison.stringify(searchInput as any) : undefined;
164
+ }
165
+
166
+ // Handle search button click
167
+ function handleSearch() {
168
+ emitSearchQuery(localSearchArray.value);
169
+ }
170
+
171
+ // Handle reset button click
172
+ function handleReset() {
173
+ const resetArray = { and: [[null, "=", null]] } as searchType;
174
+ localSearchArray.value = resetArray;
175
+ emitSearchQuery(resetArray);
176
+ }
177
+
178
+ // Get favorite filters for current table
179
+ const favoriteFilters = computed({
180
+ get: () => {
181
+ const filters = user.value?.config?.filters ?? {};
182
+ return (filters as Record<string, any>)[table.value?.slug ?? ""] ?? [];
183
+ },
184
+ set: (v) => {
185
+ updateFavoriteFilters(v);
186
+ },
187
+ });
188
+
189
+ // Update favorite filters in user config
190
+ async function updateFavoriteFilters(filters: any[]) {
191
+ if (!user.value || !table.value || !database.value) return;
192
+
193
+ try {
194
+ const allFilters = { ...(user.value?.config?.filters ?? {}) };
195
+ allFilters[table.value.slug] = filters;
196
+ const updatedUser = {
197
+ ...user.value,
198
+ config: { ...(user.value.config ?? {}), filters: allFilters },
199
+ };
200
+
201
+ await $fetch(
202
+ `${config.public.apiBase}${database.value.slug}/users/${user.value.id}`,
203
+ {
204
+ method: "PUT",
205
+ body: updatedUser,
206
+ params: { [`${database.value.slug}_sid`]: sessionID.value },
207
+ credentials: "include",
208
+ },
209
+ );
210
+ } catch (err: any) {
211
+ window.$message.error(err.message);
212
+ }
213
+ }
214
+
215
+ // ==================== FAVORITE FILTERS LOGIC ====================
216
+
217
+ async function saveFavoriteFilter(filterName: string) {
218
+ if (!filterName.trim()) {
219
+ window.$message.error(t("filterNameCannotBeEmpty"));
220
+ return;
221
+ }
222
+
223
+ if (favoriteFilters.value.some((f: any) => f.name === filterName)) {
224
+ window.$message.error(t("filterNameAlreadyExists"));
225
+ return;
226
+ }
227
+
228
+ const newFilter = {
229
+ id: Date.now().toString(),
230
+ name: filterName,
231
+ searchArray: deepClone(localSearchArray.value),
232
+ createdAt: new Date().toISOString(),
233
+ };
234
+
235
+ const updatedFilters = [...favoriteFilters.value, newFilter];
236
+ favoriteFilters.value = updatedFilters;
237
+ window.$message.success(t("filterSavedSuccessfully"));
238
+ }
239
+
240
+ function loadFavoriteFilter(filter: any) {
241
+ const loadedArray = (deepClone(filter.searchArray) as searchType) ?? {
242
+ and: [[null, "=", null]],
243
+ };
244
+ localSearchArray.value = loadedArray;
245
+ emitSearchQuery(loadedArray);
246
+ window.$message.success(`${t("filterLoaded")}: ${filter.name}`);
247
+ }
248
+
249
+ function deleteFavoriteFilter(filterId: string) {
250
+ const updatedFilters = favoriteFilters.value.filter(
251
+ (f: any) => f.id !== filterId,
252
+ );
253
+ favoriteFilters.value = updatedFilters;
254
+ window.$message.success(t("filterDeletedSuccessfully"));
255
+ }
256
+ </script>
@@ -0,0 +1,286 @@
1
+ <template>
2
+ <NFlex item-style="width: 100%">
3
+ <template v-for="(item, index) in formatedItems">
4
+ <NInputGroup v-if="Array.isArray(item)" class="searchGroupInput">
5
+ <NCascader size="small" :consistent-menu-width="false" filterable :value="item[0]"
6
+ @update:value="(v) => item[0] = v" :options="generateSearchInOptions(schema)"
7
+ :style="`width:${item[3] ? 33.33 : 100}%`" check-strategy="child" />
8
+ <template v-if="item[3]">
9
+ <NCascader size="small" filterable check-strategy="child" :value="item[1]"
10
+ @update:value="(v) => item[1] = v" :options="getAvailableComparisonOperator(item[3])"
11
+ style="width:33.33%" />
12
+ <template v-if="isRelativeOperator(item[1])">
13
+ <NSelect size="small" style="width:33.33%" :placeholder="t('relativePlaceholder')"
14
+ :value="(item[2] as string | null) ?? null"
15
+ :options="relativeSelectOptions[index] ?? getRelativeSelectOptions('', item[2] as string | null, item[3])"
16
+ filterable remote clearable :on-search="(pattern) => handleRelativeSearch(index, pattern)"
17
+ tag @update:value="(v) => updateRelativeValue(item, v as string | null)"
18
+ @keydown.enter.prevent="() => callback && callback()" />
19
+ </template>
20
+ <Field v-else :model-value="item[2]" @update:modelValue="(v) => updateFieldValue(item, v)"
21
+ :field="getFieldFromItem(item)" />
22
+ </template>
23
+ <NButton :disabled="formatedItems?.length === 1" @click="modelValue?.splice(index, 1)" circle
24
+ size="small">
25
+ <template #icon>
26
+ <NIcon>
27
+ <Icon name="tabler:minus" />
28
+ </NIcon>
29
+ </template>
30
+ </NButton>
31
+ </NInputGroup>
32
+ <LazyTableSearch v-else v-model="(modelValue[index] as searchType)" v-model:schema="schema" :callback />
33
+ </template>
34
+ </NFlex>
35
+ </template>
36
+
37
+ <script lang="ts" setup>
38
+ import {
39
+ getField as getFieldFromSchema,
40
+ isArrayOfObjects,
41
+ isNumber,
42
+ isObject,
43
+ } from "inibase/utils"
44
+ import { Icon } from "#components"
45
+
46
+ const { callback } = defineProps<{
47
+ callback?: CallableFunction
48
+ }>()
49
+
50
+ const schema = defineModel<Table['schema']>("schema")
51
+
52
+ const modelValue = defineModel<searchTypeValue>({
53
+ default: [[null, "=", null]],
54
+ })
55
+
56
+ type OperatorOption = {
57
+ label?: string
58
+ value?: string
59
+ children?: OperatorOption[]
60
+ type?: string
61
+ key?: string
62
+ }
63
+
64
+ const relativeBaseSuggestionValues = [
65
+ "now",
66
+ "yesterday",
67
+ "tomorrow",
68
+ "last week",
69
+ "last month",
70
+ "last year",
71
+ "next week",
72
+ "next month",
73
+ "next year",
74
+ ]
75
+
76
+ const relativeBaseAutocompleteOptions = relativeBaseSuggestionValues.map(
77
+ (value) => ({
78
+ label: t(value),
79
+ value,
80
+ }),
81
+ )
82
+
83
+ const formatedItems = computed(() =>
84
+ modelValue.value?.map((item) => {
85
+ if (Array.isArray(item) && item[0]) {
86
+ item[3] = getFieldFromSchema(item[0], schema.value as any)
87
+ item[2] = normalizeSearchItemValue(item)
88
+ }
89
+ return item
90
+ }),
91
+ )
92
+
93
+ function normalizeSearchItemValue(item: searchTypeValueItem) {
94
+ const value = item[2]
95
+ const field = item[3] as Field | undefined
96
+ if (!field || isRelativeOperator(item[1])) return value
97
+
98
+ if (
99
+ checkFieldType(field.type, ["date", "number"]) &&
100
+ typeof value === "string" &&
101
+ isNumber(value)
102
+ )
103
+ return Number(value)
104
+
105
+ return value
106
+ }
107
+
108
+ function getSearchSubType(field: Field) {
109
+ const resolvedType = field.subType ?? field.type
110
+ if (["radio", "checkbox"].includes(resolvedType as string)) return "select"
111
+ // long text fields are searched through a single line input
112
+ if (resolvedType === "textarea") return undefined
113
+ return field.subType
114
+ }
115
+
116
+ function getFieldFromItem(item: searchTypeValueItem) {
117
+ return {
118
+ ...item[3],
119
+ subType: getSearchSubType(item[3]),
120
+ required: false,
121
+ labelProps: {
122
+ showLabel: false,
123
+ style: { width: "33.33%" },
124
+ showFeedback: false,
125
+ },
126
+ inputProps: {
127
+ size: "small",
128
+ onKeydown: ({ key }: KeyboardEvent) => {
129
+ if (key === "Enter" && callback) callback()
130
+ },
131
+ },
132
+ }
133
+ }
134
+
135
+ function getAvailableComparisonOperator(field: Field) {
136
+ const baseOptions = comparisonOperatorOptions() as OperatorOption[]
137
+ const selectableOptions = baseOptions.filter((option) => {
138
+ const optionValue = option.value
139
+ if (!optionValue) return false
140
+
141
+ if (Array.isArray(field.type))
142
+ return [
143
+ "=",
144
+ "!=",
145
+ ...(field.type.some((type: string) =>
146
+ ["string", "email", "url"].includes(type),
147
+ )
148
+ ? ["*", "!*"]
149
+ : []),
150
+ ...(field.type.some((type: string) => ["number", "date"].includes(type))
151
+ ? [">", ">=", "<", "<="]
152
+ : []),
153
+ ].includes(optionValue)
154
+
155
+ if (checkFieldType(field.type, ["number", "date"]))
156
+ return ["=", "!=", ">", ">=", "<", "<="].includes(optionValue)
157
+ if (
158
+ checkFieldType(field.type, "array") ||
159
+ checkFieldType(field.type, "table")
160
+ )
161
+ return ![">", ">=", "<", "<="].includes(optionValue)
162
+ return ![">", ">=", "<", "<=", "[]", "![]"].includes(optionValue)
163
+ })
164
+
165
+ if (checkFieldType(field.type, ["date", "number"])) {
166
+ const relativeCandidates = selectableOptions.filter(
167
+ (option) =>
168
+ option.value &&
169
+ ["=", "!=", ">", ">=", "<", "<="].includes(option.value),
170
+ )
171
+ if (relativeCandidates.length)
172
+ return [
173
+ {
174
+ value: "relative",
175
+ label: t("relativeGroup"),
176
+ children: relativeCandidates.map((option) => ({
177
+ label: option.label ?? option.value ?? "",
178
+ value: `r${option.value as string}`,
179
+ })),
180
+ },
181
+ ...selectableOptions,
182
+ ]
183
+ }
184
+
185
+ return selectableOptions
186
+ }
187
+
188
+ function isRelativeOperator(value: string | undefined) {
189
+ return typeof value === "string" && value.startsWith("r")
190
+ }
191
+
192
+ function getRelativeAutocompleteOptions(value: unknown, field?: Field) {
193
+ if (field?.type && checkFieldType(field.type, "number")) {
194
+ const input = typeof value === "string" ? value.trim() : ""
195
+ if (!input)
196
+ return ["0", "1", "-1", "10", "-10"].map((v) => ({ label: v, value: v }))
197
+
198
+ const numericPattern = /^[+-]?\d+(?:\.\d+)?$/
199
+ if (numericPattern.test(input))
200
+ return [{ label: input, value: input }]
201
+
202
+ return []
203
+ }
204
+
205
+ const input = typeof value === "string" ? value.trim() : ""
206
+ const numericPattern = /^[+-]?\d+$/
207
+ if (numericPattern.test(input)) {
208
+ const parsed = Number.parseInt(input, 10)
209
+ if (!Number.isFinite(parsed)) return relativeBaseAutocompleteOptions
210
+ const absolute = Math.abs(parsed)
211
+ if (absolute === 0) return relativeBaseAutocompleteOptions
212
+ const units = [
213
+ { singular: "day", plural: "days" },
214
+ { singular: "week", plural: "weeks" },
215
+ { singular: "month", plural: "months" },
216
+ { singular: "year", plural: "years" },
217
+ ]
218
+ const numberText = absolute.toString()
219
+ const options: { label: string; value: string }[] = []
220
+
221
+ for (const { singular, plural } of units) {
222
+ const unitText = t(absolute === 1 ? singular : plural)
223
+ const baseLabel = `${numberText} ${unitText}`
224
+ options.push({ label: `${baseLabel} ${t('ago')}`, value: `${baseLabel} ago` })
225
+ }
226
+ for (const { singular, plural } of units) {
227
+ const unitText = t(absolute === 1 ? singular : plural)
228
+ const baseLabel = `${numberText} ${unitText}`
229
+ options.push({ label: `${t('in')} ${baseLabel}`, value: `in ${baseLabel}` })
230
+ }
231
+ return options
232
+ }
233
+ return relativeBaseAutocompleteOptions
234
+ }
235
+
236
+ // NSelect support (remote filter): state and helpers
237
+ const relativeSelectOptions = ref<Record<number, { label: string; value: string }[]>>({})
238
+
239
+ function getRelativeSelectOptions(
240
+ pattern: unknown,
241
+ currentValue: string | null,
242
+ field?: Field,
243
+ ) {
244
+ // Reuse existing generator, ensure current value is present
245
+ const options = getRelativeAutocompleteOptions(pattern, field)
246
+ const list = Array.isArray(options) ? [...options] : []
247
+ if (currentValue && !list.some((o) => o.value === currentValue)) {
248
+ list.unshift({ label: currentValue, value: currentValue })
249
+ }
250
+ return list
251
+ }
252
+
253
+ function handleRelativeSearch(index: number, pattern: string) {
254
+ const currentItem = modelValue.value?.[index]
255
+ const currentValue = Array.isArray(currentItem) ? (currentItem[2] as string | null) : null
256
+ const currentField = Array.isArray(currentItem) ? (currentItem[3] as Field | undefined) : undefined
257
+ relativeSelectOptions.value[index] = getRelativeSelectOptions(pattern, currentValue, currentField)
258
+ }
259
+
260
+ function updateRelativeValue(
261
+ item: searchTypeValueItem,
262
+ value: string | null,
263
+ ) {
264
+ const sanitized = value?.trim()
265
+ item[2] = sanitized ? sanitized : undefined
266
+ }
267
+
268
+ function updateFieldValue(item: searchTypeValueItem, value: any) {
269
+ if (value !== undefined) {
270
+ if (isObject(value) && Object.hasOwn(value, "id"))
271
+ value = value.id
272
+ else if (
273
+ isArrayOfObjects(value) &&
274
+ value.every((_v: any) => Object.hasOwn(_v, "id"))
275
+ )
276
+ value = value.map((_value: any) => _value.id)
277
+ item[2] = Array.isArray(value) ? value.join(",") : value
278
+ } else item[2] = undefined
279
+ }
280
+ </script>
281
+
282
+ <style scoped>
283
+ .searchGroupInput * {
284
+ border-radius: 0 !important;
285
+ }
286
+ </style>
@@ -0,0 +1,87 @@
1
+ <template>
2
+ <NCollapse :triggerAreas="['main', 'arrow']" accordion default-expanded-names="0">
3
+ <NCollapseItem v-for="(_items, condition, index) in modelValue" :key="condition" :name="index.toString()"
4
+ :title="t(`${condition}Group`)">
5
+ <template #header-extra>
6
+ <NButtonGroup>
7
+ <NDropdown :options="conditionDropdownOptions" style="max-height: 200px;" scrollable
8
+ @select="(value: 'and' | 'or') => modelValue[condition]?.push({ [value]: [[null, '=', null]] })">
9
+ <NButton round type="primary" secondary @click="modelValue[condition]?.push([null, '=', null])"
10
+ circle size="small">
11
+ <template #icon>
12
+ <NIcon>
13
+ <Icon name="tabler:plus" />
14
+ </NIcon>
15
+ </template>
16
+ </NButton>
17
+ </NDropdown>
18
+
19
+ <NTooltip :delay="1500">
20
+ <template #trigger>
21
+ <NButton round type="info" secondary @click="() => toggleCondition(condition)" circle
22
+ size="small">
23
+ <template #icon>
24
+ <NIcon>
25
+ <Icon name="tabler:switch-horizontal" />
26
+ </NIcon>
27
+ </template>
28
+ </NButton>
29
+ </template>
30
+ {{ t(
31
+ `convert_to_${condition === "and" ? "or" : "and"}_group`,
32
+ ) }}
33
+ </NTooltip>
34
+ <NButton round type="error" secondary @click="() => removeCondition(condition)" circle size="small">
35
+ <template #icon>
36
+ <NIcon>
37
+ <Icon name="tabler:trash" />
38
+ </NIcon>
39
+ </template>
40
+ </NButton>
41
+ </NButtonGroup>
42
+ </template>
43
+ <TableSearchItems v-model="modelValue[condition]" v-model:schema="schema" :callback />
44
+ </NCollapseItem>
45
+ </NCollapse>
46
+ </template>
47
+
48
+ <script lang="ts" setup>
49
+ import { Icon, NIcon } from "#components"
50
+
51
+ const { callback } = defineProps<{ callback?: CallableFunction }>()
52
+
53
+ const table = useState<Table>("table")
54
+
55
+ const schema = defineModel<Table['schema']>("schema")
56
+ if (!schema.value)
57
+ schema.value = table.value?.schema
58
+
59
+ const modelValue = defineModel<searchType>({
60
+ default: { and: [[null, "=", null]] },
61
+ })
62
+
63
+ const conditionDropdownOptions = [
64
+ {
65
+ key: "and",
66
+ label: t("andGroup"),
67
+ icon: () => h(NIcon, () => h(Icon, { name: "tabler:arrow-merge" })),
68
+ },
69
+ {
70
+ key: "or",
71
+ label: t("orGroup"),
72
+ icon: () => h(NIcon, () => h(Icon, { name: "tabler:arrow-fork" })),
73
+ },
74
+ ]
75
+
76
+ function toggleCondition(oldCondition: "and" | "or") {
77
+ modelValue.value[oldCondition === "and" ? "or" : "and"] =
78
+ modelValue.value[oldCondition]
79
+ delete modelValue.value[oldCondition]
80
+ }
81
+
82
+ function removeCondition(condition: "and" | "or") {
83
+ if (Object.keys(modelValue.value).length === 1)
84
+ modelValue.value[condition] = [[null, "=", null]]
85
+ else delete modelValue.value[condition]
86
+ }
87
+ </script>