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,1273 @@
1
+ <template>
2
+ <NDataTable :bordered="false" :scroll-x="tableWidth" :scroll-y="tableViewportHeight" resizable id="DataTable" remote ref="dataTableRef" :columns
3
+ :data="data?.result ?? []" :loading="!!Loading?.data" :pagination="dataTablePagination" :row-key="(row) => row.id"
4
+ v-model:checked-row-keys="checkedRowKeys" @update:sorter="handleSorterChange" :getCsvCell :getCsvHeader
5
+ :rowProps @scroll="handleScroll" :size="tablesConfig[table.slug]?.size" />
6
+ <NDropdown show-arrow size="small" placement="right" trigger="manual" :x :y :options="dropdownOptions"
7
+ :show="showDropdown" :onClickoutside @select="handleSelect" />
8
+ </template>
9
+ <script setup lang="ts">
10
+ import { isArrayOfObjects, isObject } from "inibase/utils";
11
+ import Inison from "inison";
12
+ import type {
13
+ DataTableColumns,
14
+ DataTableGetCsvCell,
15
+ DataTableGetCsvHeader,
16
+ DataTableInst,
17
+ DropdownOption,
18
+ } from "naive-ui";
19
+ import type { VNodeChild } from "vue";
20
+
21
+ import {
22
+ Icon,
23
+ LazyColumn,
24
+ LazyColumnEdit,
25
+ NButton,
26
+ NFlex,
27
+ NIcon,
28
+ NPerformantEllipsis,
29
+ NPopover,
30
+ } from "#components";
31
+
32
+ const dataTableRef = ref<DataTableInst>();
33
+ defineExpose({ dataTableRef });
34
+
35
+ const props = defineProps<{
36
+ slots: any;
37
+ }>();
38
+
39
+ const columns = defineModel<DataTableColumns>("columns");
40
+ const searchString = defineModel<string>("searchString");
41
+ const data = defineModel<apiResponse<Item[]>>("data");
42
+
43
+ const Loading = useState<Record<string, boolean>>("Loading", () => ({
44
+ data: false,
45
+ }));
46
+ const database = useState<Database>("database");
47
+ const table = useState<Table>("table");
48
+ const route = useRoute();
49
+ const router = useRouter();
50
+ const config = useRuntimeConfig();
51
+ const { isMobile } = useDevice();
52
+ const Language = useScopedCookie<LanguagesType>(
53
+ "language",
54
+ database.value?.slug,
55
+ );
56
+ const sessionID = useScopedCookie<string>("sid", database.value?.slug);
57
+ const user = useState<User>("user");
58
+ const tablesConfig = computed({
59
+ get: () => user.value?.config?.tables ?? {},
60
+ set: (v) => {
61
+ user.value.config = { ...(user.value.config ?? {}), tables: v };
62
+ $fetch(
63
+ `${config.public.apiBase}${database.value.slug}/users/${user.value?.id}`,
64
+ {
65
+ method: "PUT",
66
+ body: user.value,
67
+ params: {
68
+ return: false,
69
+ locale: Language.value,
70
+ [`${database.value.slug}_sid`]: sessionID.value,
71
+ },
72
+ credentials: "include",
73
+ },
74
+ ).catch((err) => window.$message.error(err.message));
75
+ },
76
+ });
77
+
78
+ const renderItemButtons = inject("renderItemButtons") as (item: Item) => VNode;
79
+
80
+ const deleteItem = inject("deleteItem") as (
81
+ ids?: string | string[],
82
+ ) => Promise<apiResponse<Item[]>>;
83
+
84
+ const isSlotSet = inject("isSlotSet") as (slotName: string) => boolean;
85
+
86
+ function isSlotEmpty(slotName: string): boolean {
87
+ // Check if all nodes are comments or have undefined children
88
+ return ((props.slots[slotName] as any)({}) as VNode[])?.every(
89
+ (vnode) => vnode.type === Comment || vnode.children === undefined,
90
+ );
91
+ }
92
+
93
+ watch(
94
+ searchString,
95
+ (value) => {
96
+ if (!value || value === undefined) {
97
+ // Reset to page 1 when search is cleared
98
+ pagination.onUpdatePage(1);
99
+ }
100
+ },
101
+ { deep: true },
102
+ );
103
+
104
+ const getTableDimensions = () => {
105
+ const size = tablesConfig.value[table.value?.slug as string]?.size;
106
+ if (size === "small") {
107
+ return { columnHeaderHeight: 39, rowHeight: 46, paginationHeight: 28 };
108
+ }
109
+ if (size === "large") {
110
+ return { columnHeaderHeight: 49, rowHeight: 59, paginationHeight: 28 };
111
+ }
112
+ return { columnHeaderHeight: 47, rowHeight: 59, paginationHeight: 28 };
113
+ };
114
+
115
+ const getTableViewportHeight = () => {
116
+ if (typeof window === "undefined") return 600;
117
+
118
+ const tableCard = document.getElementById("tableCard");
119
+ const cardHeader = tableCard?.querySelector(
120
+ ".n-card-header",
121
+ ) as HTMLElement | null;
122
+ const headerExtraHeight = cardHeader?.offsetHeight ?? 0;
123
+ const reservedTopHeight = 65 + headerExtraHeight + 24;
124
+ const { columnHeaderHeight, paginationHeight } = getTableDimensions();
125
+ const availableHeight =
126
+ window.innerHeight -
127
+ reservedTopHeight -
128
+ columnHeaderHeight -
129
+ paginationHeight -
130
+ 20;
131
+
132
+ return Math.max(availableHeight, 260);
133
+ };
134
+
135
+ const tableViewportHeight = computed(() => getTableViewportHeight());
136
+
137
+ const getResponsivePageSize = () => {
138
+ if (typeof window === "undefined") return 15;
139
+
140
+ const { rowHeight } = getTableDimensions();
141
+ const tableCard = document.getElementById("tableCard");
142
+ const cardHeader = tableCard?.querySelector(
143
+ ".n-card-header",
144
+ ) as HTMLElement | null;
145
+ const headerExtraHeight = cardHeader?.offsetHeight ?? 0;
146
+ const reservedTopHeight = 65 + headerExtraHeight + 24;
147
+ const { columnHeaderHeight, paginationHeight } = getTableDimensions();
148
+ const availableHeight =
149
+ window.innerHeight -
150
+ reservedTopHeight -
151
+ columnHeaderHeight -
152
+ paginationHeight -
153
+ 20;
154
+ const calculatedPageSize = Math.floor(availableHeight / rowHeight);
155
+
156
+ return Math.min(120, Math.max(8, calculatedPageSize));
157
+ };
158
+
159
+ const getResponsivePageSizes = () => {
160
+ const defaultSize = getResponsivePageSize();
161
+ return Array.from(
162
+ new Set([8, 10, 15, defaultSize, defaultSize + 10, 30, 50, 100, 500]),
163
+ )
164
+ .sort((a, b) => a - b)
165
+ .slice(0, 8);
166
+ };
167
+
168
+ const syncPaginationPageSize = () => {
169
+ if (route.query.perPage) return;
170
+ const nextPageSize = getResponsivePageSize();
171
+ if (pagination.pageSize !== nextPageSize) {
172
+ pagination.pageSize = nextPageSize;
173
+ }
174
+ };
175
+
176
+ const pagination = reactive({
177
+ page: route.query.page ? Number(route.query.page) : 1,
178
+ pageCount: 1,
179
+ pageSize: route.query.perPage
180
+ ? Number(route.query.perPage)
181
+ : getResponsivePageSize(),
182
+ itemCount: 0,
183
+ async onUpdatePage(currentPage: number) {
184
+ pagination.page = currentPage;
185
+ let { page, ...Query }: any = route.query;
186
+ Query = {
187
+ ...Query,
188
+ page: currentPage !== 1 ? currentPage : undefined,
189
+ };
190
+ router.push({ query: Query });
191
+ await refreshNuxtData(
192
+ `${database.value.slug}/${table.value?.slug as string}`,
193
+ );
194
+ },
195
+ async onUpdatePageSize(pageSize: number) {
196
+ const OLD_pageSize = toRaw(pagination.pageSize);
197
+ pagination.pageSize = pageSize;
198
+ let { perPage, page, ...Query }: any = route.query;
199
+ pagination.page = Math.round(
200
+ OLD_pageSize < pageSize
201
+ ? page / (pageSize / OLD_pageSize)
202
+ : page * (pageSize / OLD_pageSize),
203
+ );
204
+ if (Number.isNaN(pagination.page)) pagination.page = 1;
205
+ Query = {
206
+ ...Query,
207
+ perPage: pageSize,
208
+ page: pagination.page === 1 ? undefined : pagination.page,
209
+ };
210
+ router.push({
211
+ query: Query,
212
+ });
213
+ await refreshNuxtData(
214
+ `${database.value.slug}/${table.value?.slug as string}`,
215
+ );
216
+ },
217
+ });
218
+
219
+ watch(
220
+ () => tablesConfig.value[table.value?.slug as string]?.size,
221
+ () => {
222
+ if (!route.query.perPage) syncPaginationPageSize();
223
+ },
224
+ );
225
+
226
+ onMounted(() => {
227
+ syncPaginationPageSize();
228
+ window.addEventListener("resize", syncPaginationPageSize);
229
+ });
230
+
231
+ onBeforeUnmount(() => {
232
+ window.removeEventListener("resize", syncPaginationPageSize);
233
+ });
234
+
235
+ const sort = ref<Record<string, "asc" | "desc">>({});
236
+
237
+ const queryOptions = computed(() =>
238
+ Inison.stringify({
239
+ page: pagination.page,
240
+ perPage: pagination.pageSize,
241
+ columns: table.value?.columns,
242
+ sort: Object.keys(sort.value).length ? sort.value : "",
243
+ }),
244
+ );
245
+
246
+ const { data: _data } = await useLazyFetch<apiResponse<Item[]>>(
247
+ `${config.public.apiBase}${database.value.slug}/${table.value?.slug as string}`,
248
+ {
249
+ key: `${database.value.slug}/${table.value?.slug as string}`,
250
+ query: {
251
+ options: queryOptions,
252
+ where: searchString,
253
+ locale: Language,
254
+ [`${database.value.slug}_sid`]: sessionID.value,
255
+ },
256
+ onRequest() {
257
+ Loading.value.data = true;
258
+ },
259
+ onResponse({ response: { _data } }) {
260
+ Loading.value.data = false;
261
+ pagination.pageCount = _data?.options.totalPages ?? 0;
262
+ pagination.itemCount = _data?.options.total ?? 0;
263
+ // TODO
264
+ // if (![202, 404].includes(code)) window.$message.error(message)
265
+ },
266
+ credentials: "include",
267
+ },
268
+ );
269
+
270
+ watch(_data, (v) => {
271
+ data.value = v || undefined;
272
+ });
273
+
274
+ const dataTablePagination = computed(() => ({
275
+ disabled: !_data.value?.options.total,
276
+ simple: isMobile,
277
+ showSizePicker: !!_data.value?.options?.total,
278
+ pageSizes: getResponsivePageSizes(),
279
+ prefix: ({ itemCount }: { itemCount?: number }) => itemCount,
280
+ ...pagination,
281
+ }));
282
+
283
+ const currentItem = ref<Item>();
284
+ const showDropdown = ref(false);
285
+ const x = ref(0);
286
+ const y = ref(0);
287
+ const rowTouchTimeout = ref<ReturnType<typeof setTimeout> | null>(null);
288
+
289
+ async function handleSelect(value: string) {
290
+ showDropdown.value = false;
291
+ if (!currentItem.value) return;
292
+ switch (value) {
293
+ case "copy": {
294
+ await copyToClipboard(Inison.stringify(currentItem.value));
295
+ window.$message.success(t("copiedSuccessfully"));
296
+ currentItem.value = undefined;
297
+ }
298
+ }
299
+ }
300
+ function onClickoutside(e: MouseEvent) {
301
+ const isRightClick = e.button === 2;
302
+ if (!isRightClick) showDropdown.value = false;
303
+ }
304
+ const dropdownOptions: DropdownOption[] = [
305
+ {
306
+ label: t("copyItem"),
307
+ key: "copy",
308
+ icon: () => h(NIcon, () => h(Icon, { name: "tabler:copy" })),
309
+ },
310
+ ];
311
+
312
+ function clearRowTouchTimeout() {
313
+ if (rowTouchTimeout.value) {
314
+ clearTimeout(rowTouchTimeout.value);
315
+ rowTouchTimeout.value = null;
316
+ }
317
+ }
318
+
319
+ function shouldIgnoreRowContextMenu(target: HTMLElement | null) {
320
+ if (!target) return true;
321
+
322
+ return !!(
323
+ target.closest("a[href]") ||
324
+ target.closest("img") ||
325
+ target.closest("button") ||
326
+ target.closest('[role="button"]') ||
327
+ target.closest("input, textarea, select") ||
328
+ target.closest('[contenteditable="true"]') ||
329
+ target.closest(".editable") ||
330
+ target.closest(".n-ellipsis") ||
331
+ target.closest(".n-tag") ||
332
+ target.closest(".n-text")
333
+ );
334
+ }
335
+
336
+ async function openRowDropdown(
337
+ row: Item,
338
+ {
339
+ target,
340
+ clientX,
341
+ clientY,
342
+ checkSelection = false,
343
+ }: {
344
+ target: HTMLElement | null;
345
+ clientX: number;
346
+ clientY: number;
347
+ checkSelection?: boolean;
348
+ },
349
+ ) {
350
+ if (shouldIgnoreRowContextMenu(target)) {
351
+ showDropdown.value = false;
352
+ return;
353
+ }
354
+
355
+ if (checkSelection) {
356
+ const selection = window.getSelection();
357
+ if (
358
+ selection &&
359
+ selection.toString().trim() !== "" &&
360
+ selection.rangeCount
361
+ ) {
362
+ const range = selection.getRangeAt(0);
363
+ const rect = range.getBoundingClientRect();
364
+ if (
365
+ clientX >= rect.left &&
366
+ clientX <= rect.right &&
367
+ clientY >= rect.top &&
368
+ clientY <= rect.bottom
369
+ ) {
370
+ showDropdown.value = false;
371
+ return;
372
+ }
373
+ }
374
+ }
375
+
376
+ showDropdown.value = false;
377
+ await nextTick();
378
+ currentItem.value = (({ id, createdAt, updatedAt, ...rest }) => rest)(row);
379
+ showDropdown.value = true;
380
+ x.value = clientX + 8;
381
+ y.value = clientY + 6;
382
+ }
383
+
384
+ function rowProps(row: Item) {
385
+ // Rows merged in from the offline queue (see getPendingCreates) don't exist
386
+ // on the server yet — dim them so it's clear they're pending local changes.
387
+ const pendingRow = !!(row as any).__pending;
388
+ return {
389
+ class: pendingRow ? "pending-row" : undefined,
390
+ style: pendingRow ? { opacity: "0.6", fontStyle: "italic" } : undefined,
391
+ onContextmenu: async (e: MouseEvent) => {
392
+ e.preventDefault();
393
+ clearRowTouchTimeout();
394
+ await openRowDropdown(row, {
395
+ target: e.target as HTMLElement,
396
+ clientX: e.clientX,
397
+ clientY: e.clientY,
398
+ checkSelection: true,
399
+ });
400
+ },
401
+ onTouchstart: (e: TouchEvent) => {
402
+ const touch = e.touches[0];
403
+ const target = e.target as HTMLElement | null;
404
+ if (!touch || shouldIgnoreRowContextMenu(target)) return;
405
+
406
+ clearRowTouchTimeout();
407
+ rowTouchTimeout.value = window.setTimeout(() => {
408
+ openRowDropdown(row, {
409
+ target,
410
+ clientX: touch.clientX,
411
+ clientY: touch.clientY,
412
+ });
413
+ }, 450);
414
+ },
415
+ onTouchend: clearRowTouchTimeout,
416
+ onTouchcancel: clearRowTouchTimeout,
417
+ onTouchmove: clearRowTouchTimeout,
418
+ };
419
+ }
420
+
421
+ onBeforeUnmount(clearRowTouchTimeout);
422
+
423
+ function getVNodeTextContent(vnode: VNodeChild): string {
424
+ if (vnode === null || vnode === undefined || vnode === false) return "";
425
+ // primitive text
426
+ if (typeof vnode === "string" || typeof vnode === "number")
427
+ return String(vnode);
428
+
429
+ // arrays of VNodes
430
+ if (Array.isArray(vnode))
431
+ return vnode.map((n) => getVNodeTextContent(n)).join("");
432
+
433
+ // VNode shape (element/component)
434
+ const vn: any = vnode as any;
435
+
436
+ // direct children string (common for elements)
437
+ if (typeof vn.children === "string") return vn.children;
438
+
439
+ // children as array or single VNode
440
+ if (Array.isArray(vn.children))
441
+ return vn.children.map((c: any) => getVNodeTextContent(c)).join("");
442
+
443
+ // children as function (scoped slot) -> invoke safely
444
+ if (typeof vn.children === "function") {
445
+ try {
446
+ const res = vn.children();
447
+ return getVNodeTextContent(res);
448
+ } catch {
449
+ return "";
450
+ }
451
+ }
452
+
453
+ // children as slots object { default: fn, ... }
454
+ if (vn.children && typeof vn.children === "object") {
455
+ let out = "";
456
+ for (const key of Object.keys(vn.children)) {
457
+ const slot = (vn.children as Record<string, any>)[key];
458
+ if (typeof slot === "function") {
459
+ try {
460
+ out += getVNodeTextContent(slot());
461
+ } catch {
462
+ /* ignore */
463
+ }
464
+ } else {
465
+ out += getVNodeTextContent(slot);
466
+ }
467
+ }
468
+ if (out) return out;
469
+ }
470
+
471
+ // if this vnode is a mounted component, prefer its rendered subTree
472
+ if (vn.component?.subTree) return getVNodeTextContent(vn.component.subTree);
473
+
474
+ // fallback empty
475
+ return "";
476
+ }
477
+ const getCsvCell: DataTableGetCsvCell = (value, row, column) => {
478
+ if (["boolean", "string", "number"].includes(typeof value))
479
+ return String(value);
480
+
481
+ if (column.type === "custom" && column.render)
482
+ return getVNodeTextContent((column as any).render(row));
483
+
484
+ if (!value) return null;
485
+
486
+ if (isObject(value) && Object.hasOwn(value, "id")) {
487
+ const columnField = table.value?.schema?.find(
488
+ ({ key }) => key === column.key,
489
+ );
490
+ if (!columnField) return value.id;
491
+
492
+ const columnTable = database.value.tables?.find(
493
+ ({ slug }) => slug === columnField.table,
494
+ );
495
+ if (!columnTable) return value.id;
496
+
497
+ return renderLabel(columnTable, value);
498
+ }
499
+ if (
500
+ Array.isArray(value) &&
501
+ isArrayOfObjects(value) &&
502
+ value.every((_v) => Object.hasOwn(_v, "id"))
503
+ ) {
504
+ const columnTable = database.value.tables?.find(
505
+ ({ slug }) => slug === column.key,
506
+ );
507
+ if (!columnTable) value = value.map((_v) => _v.id);
508
+ else value = value.map((_v) => renderLabel(columnTable, _v));
509
+ }
510
+ return `"${Inison.stringify(value)}"`;
511
+ };
512
+
513
+ const getCsvHeader: DataTableGetCsvHeader = (col) => {
514
+ if (col.key === "actions") return "";
515
+ return (col.key as string) || "Unknown";
516
+ };
517
+
518
+ async function handleSorterChange({
519
+ columnKey,
520
+ order,
521
+ }: {
522
+ columnKey: string;
523
+ order: string;
524
+ }) {
525
+ if (!order) delete sort.value[columnKey];
526
+ else sort.value[columnKey] = order.slice(0, -3) as "asc" | "desc";
527
+
528
+ // Anchor sorting to the currently selected page size so it doesn't fall
529
+ // back to the responsive default.
530
+ if (pagination.pageSize !== getResponsivePageSize()) {
531
+ let { page, ...Query }: any = route.query;
532
+ pagination.page = 1;
533
+ Query = {
534
+ ...Query,
535
+ perPage: pagination.pageSize,
536
+ };
537
+ await router.push({ query: Query });
538
+ }
539
+ }
540
+
541
+ const checkedRowKeys = ref<string[]>([]);
542
+
543
+ function handleScroll() {
544
+ clearRowTouchTimeout();
545
+ showDropdown.value = false; // Hide dropdown on scroll
546
+ }
547
+
548
+ const tableWidth = ref<number>(0);
549
+ let measureTextWidthSpan: HTMLSpanElement | null = null;
550
+ const visualWidthCache = new WeakMap<Item[], Map<string, number[]>>();
551
+ const visualWidthVersion = ref(0);
552
+ const textWidthCache = new Map<string, number>();
553
+ let refreshVisualWidthsTask: Promise<void> | null = null;
554
+ let tableWidthResizeObserver: ResizeObserver | null = null;
555
+ let tableObserverStartTimer: ReturnType<typeof setTimeout> | null = null;
556
+ let isWidthsUnmounted = false;
557
+
558
+ function invalidateVisualWidthCache() {
559
+ visualWidthVersion.value += 1;
560
+ const rows = _data.value?.result;
561
+ if (rows) visualWidthCache.delete(rows);
562
+ }
563
+
564
+ function updateTableWidthFromColumns() {
565
+ tableWidth.value =
566
+ columns.value?.reduce(
567
+ (accumulator: number, { width }) =>
568
+ accumulator + ((width as number | undefined) ?? 0),
569
+ 40,
570
+ ) ?? 40;
571
+ }
572
+
573
+ async function refreshVisualWidths() {
574
+ if (typeof window === "undefined") {
575
+ updateTableWidthFromColumns();
576
+ return;
577
+ }
578
+
579
+ if (refreshVisualWidthsTask) return refreshVisualWidthsTask;
580
+
581
+ refreshVisualWidthsTask = (async () => {
582
+ invalidateVisualWidthCache();
583
+
584
+ // Bail out as soon as the component is unmounted. Continuing the
585
+ // async measurement loop after teardown mutates reactive `columns`
586
+ // and races NDataTable's own DOM patch (leaving Vue with a null
587
+ // parent). Detached-table safety is handled inside
588
+ // getVisualColumnWidths(), which bails on a table that is no longer
589
+ // connected.
590
+ if (isWidthsUnmounted) return;
591
+
592
+ // First load can render table internals lazily; retry for a few frames.
593
+ for (let attempt = 0; attempt < 8; attempt += 1) {
594
+ await nextTick();
595
+ await new Promise<void>((resolve) =>
596
+ requestAnimationFrame(() => resolve()),
597
+ );
598
+ if (isWidthsUnmounted) return;
599
+ const measured = applyVisualColumnWidths();
600
+ if (measured) break;
601
+ }
602
+
603
+ await nextTick();
604
+ if (isWidthsUnmounted) return;
605
+ updateTableWidthFromColumns();
606
+ })().finally(() => {
607
+ refreshVisualWidthsTask = null;
608
+ });
609
+
610
+ return refreshVisualWidthsTask;
611
+ }
612
+
613
+ async function ensureVisualWidthsReady(maxAttempts = 8) {
614
+ if (typeof window === "undefined") {
615
+ updateTableWidthFromColumns();
616
+ return;
617
+ }
618
+
619
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
620
+ await refreshVisualWidths();
621
+
622
+ if (isWidthsUnmounted) return;
623
+
624
+ if (!_data.value?.result?.length || !columns.value?.length) return;
625
+
626
+ if (getVisualColumnWidths().length) {
627
+ updateTableWidthFromColumns();
628
+ return;
629
+ }
630
+
631
+ await new Promise<void>((resolve) => setTimeout(resolve, 80));
632
+ }
633
+ }
634
+
635
+ function isTableElementConnected(): boolean {
636
+ if (typeof document === "undefined") return false;
637
+ const tableElement = document.getElementById("DataTable");
638
+ return !!tableElement?.isConnected;
639
+ }
640
+
641
+ function stopTableWidthObservers() {
642
+ if (tableObserverStartTimer) {
643
+ clearTimeout(tableObserverStartTimer);
644
+ tableObserverStartTimer = null;
645
+ }
646
+
647
+ tableWidthResizeObserver?.disconnect();
648
+ tableWidthResizeObserver = null;
649
+ }
650
+
651
+ function startTableWidthObservers() {
652
+ if (typeof window === "undefined") return;
653
+
654
+ stopTableWidthObservers();
655
+
656
+ const tableElement = document.getElementById("DataTable");
657
+ if (!tableElement) {
658
+ tableObserverStartTimer = setTimeout(() => {
659
+ startTableWidthObservers();
660
+ }, 120);
661
+ return;
662
+ }
663
+
664
+ tableWidthResizeObserver = new ResizeObserver(() => {
665
+ if (isWidthsUnmounted || !isTableElementConnected()) return;
666
+ void ensureVisualWidthsReady(3);
667
+ });
668
+ tableWidthResizeObserver.observe(tableElement);
669
+ }
670
+
671
+ function measureTextWidth(
672
+ text: string,
673
+ opts?: { min?: number; startWith?: number },
674
+ ): number {
675
+ const str = String(text ?? "");
676
+ const startWith = opts?.startWith || 0;
677
+ const min = opts?.min;
678
+ const cacheKey = `${str}:${startWith}:${min ?? ""}`;
679
+ const cachedWidth = textWidthCache.get(cacheKey);
680
+ if (cachedWidth !== undefined) return cachedWidth;
681
+
682
+ if (typeof document === "undefined") {
683
+ const fallbackWidth = Math.ceil((str || " ").length * 8) + startWith;
684
+ const result = min ? Math.max(fallbackWidth, min) : fallbackWidth;
685
+ if (textWidthCache.size > 1000) textWidthCache.clear();
686
+ textWidthCache.set(cacheKey, result);
687
+ return result;
688
+ }
689
+
690
+ if (!measureTextWidthSpan) {
691
+ measureTextWidthSpan = document.createElement("span");
692
+ const s = measureTextWidthSpan.style;
693
+ s.position = "absolute";
694
+ s.visibility = "hidden";
695
+ s.whiteSpace = "pre";
696
+ s.pointerEvents = "none";
697
+ s.left = "-9999px";
698
+ s.top = "-9999px";
699
+ }
700
+
701
+ if (!measureTextWidthSpan.parentElement)
702
+ document.body.appendChild(measureTextWidthSpan);
703
+
704
+ measureTextWidthSpan.textContent = str || " ";
705
+ const width = Math.ceil(measureTextWidthSpan.getBoundingClientRect().width);
706
+ measureTextWidthSpan.textContent = "";
707
+
708
+ const measuredWidth = width + startWith;
709
+ const result = min ? Math.max(measuredWidth, min) : measuredWidth;
710
+ if (textWidthCache.size > 1000) textWidthCache.clear();
711
+ textWidthCache.set(cacheKey, result);
712
+ return result;
713
+ }
714
+
715
+ function getVisualColumnWidths(): number[] {
716
+ const resultRows = _data.value?.result;
717
+ if (typeof document === "undefined" || !resultRows?.length) return [];
718
+
719
+ const tableElement = document.getElementById("DataTable");
720
+ if (!tableElement || !tableElement.isConnected) return [];
721
+
722
+ const sizeKey = tablesConfig.value[table.value.slug]?.size ?? "default";
723
+ const columnSignature = (columns.value ?? [])
724
+ .map((column) => {
725
+ if ("key" in column) {
726
+ return `${String(column.type ?? "")}:${String(column.key ?? "")}`;
727
+ }
728
+ return String(column.type ?? "");
729
+ })
730
+ .join("|");
731
+ const rowSignature = resultRows
732
+ .map((row, index) => String(row?.id ?? `row-${index}`))
733
+ .join("|");
734
+ const cacheKey = `${sizeKey}:${pagination.page}:${pagination.pageSize}:${resultRows.length}:${rowSignature}:${columnSignature}:${visualWidthVersion.value}`;
735
+ const cachedColumns = visualWidthCache.get(resultRows)?.get(cacheKey);
736
+ if (cachedColumns) return cachedColumns;
737
+
738
+ const headerCells = Array.from(
739
+ tableElement.querySelectorAll<HTMLElement>(".n-data-table-th"),
740
+ );
741
+ if (!headerCells.length) return [];
742
+
743
+ const widths = headerCells.map((headerCell, index) => {
744
+ const column = columns.value?.[index] as { minWidth?: number } | undefined;
745
+ const minWidth = typeof column?.minWidth === "number" ? column.minWidth : 0;
746
+ const headerTitle =
747
+ headerCell
748
+ .querySelector<HTMLElement>(".n-data-table-th__title")
749
+ ?.textContent?.trim() ?? "";
750
+ const measuredHeaderWidth = headerTitle
751
+ ? measureTextWidth(headerTitle, { startWith: 56, min: minWidth })
752
+ : minWidth;
753
+ return Math.ceil(Math.max(minWidth, measuredHeaderWidth));
754
+ });
755
+ const rows = tableElement.querySelectorAll<HTMLElement>(".n-data-table-tr");
756
+
757
+ function getCellVisualWidth(cell: HTMLElement): number {
758
+ const buttonElements = Array.from(
759
+ cell.querySelectorAll<HTMLElement>(".n-button"),
760
+ ).filter((button) => button.getClientRects().length > 0);
761
+ if (buttonElements.length) {
762
+ const cellRect = cell.getBoundingClientRect();
763
+ let minLeft = Number.POSITIVE_INFINITY;
764
+ let maxRight = 0;
765
+ let intrinsicButtonsWidth = 0;
766
+
767
+ buttonElements.forEach((button, index) => {
768
+ const buttonRect = button.getBoundingClientRect();
769
+ const left = buttonRect.left - cellRect.left;
770
+ const right = buttonRect.right - cellRect.left;
771
+ minLeft = Math.min(minLeft, left);
772
+ maxRight = Math.max(maxRight, right);
773
+
774
+ const buttonContent =
775
+ button.querySelector<HTMLElement>(".n-button__content");
776
+ const buttonLabel = buttonContent?.textContent?.trim() ?? "";
777
+ const labelWidth = buttonLabel ? measureTextWidth(buttonLabel) : 0;
778
+ const iconPadding = button.querySelector(".n-icon") ? 52 : 32;
779
+ const intrinsicWidth = Math.max(
780
+ button.scrollWidth,
781
+ buttonContent?.scrollWidth ?? 0,
782
+ labelWidth + iconPadding,
783
+ Math.ceil(buttonRect.width),
784
+ );
785
+ intrinsicButtonsWidth += intrinsicWidth;
786
+ if (index < buttonElements.length - 1) intrinsicButtonsWidth += 6;
787
+ });
788
+
789
+ const visualSpan =
790
+ Number.isFinite(minLeft) && maxRight > minLeft ? maxRight - minLeft : 0;
791
+ return Math.ceil(Math.max(visualSpan, intrinsicButtonsWidth) + 24);
792
+ }
793
+
794
+ const tagElements = cell.querySelectorAll<HTMLElement>(".n-tag");
795
+ if (tagElements.length) {
796
+ let tagWidth = 0;
797
+ tagElements.forEach((tag, index) => {
798
+ tagWidth += tag.getBoundingClientRect().width;
799
+ if (index < tagElements.length - 1) tagWidth += 6;
800
+ });
801
+ return Math.ceil(tagWidth + 24);
802
+ }
803
+
804
+ const textLikeElements = Array.from(
805
+ cell.querySelectorAll<HTMLElement>(
806
+ ".n-base-selection-label, .n-radio__label, .n-checkbox__label, .n-ellipsis, .n-text",
807
+ ),
808
+ ).filter((element) => element.getClientRects().length > 0);
809
+ if (textLikeElements.length) {
810
+ const contentWidth = textLikeElements.reduce((maxWidth, element) => {
811
+ const label = element.textContent?.trim() ?? "";
812
+ const measuredTextWidth = label ? measureTextWidth(label) : 0;
813
+ return Math.max(
814
+ maxWidth,
815
+ element.scrollWidth,
816
+ Math.ceil(element.getBoundingClientRect().width),
817
+ measuredTextWidth,
818
+ );
819
+ }, 0);
820
+ return Math.ceil(contentWidth + 32);
821
+ }
822
+
823
+ const plainText = cell.textContent?.trim() ?? "";
824
+ if (plainText) {
825
+ return Math.ceil(measureTextWidth(plainText, { startWith: 32 }));
826
+ }
827
+
828
+ const childWidths = Array.from(cell.children).map((child) =>
829
+ Math.ceil((child as HTMLElement).getBoundingClientRect().width),
830
+ );
831
+ if (childWidths.length) {
832
+ return Math.max(...childWidths) + 24;
833
+ }
834
+
835
+ return 0;
836
+ }
837
+
838
+ for (const row of rows) {
839
+ if (!row.querySelector(".n-data-table-td")) continue;
840
+ const cells = row.children;
841
+ const limit = Math.min(cells.length, widths.length);
842
+ for (let index = 0; index < limit; index += 1) {
843
+ const cell = cells.item(index) as HTMLElement | null;
844
+ if (!cell) continue;
845
+ widths[index] = Math.max(widths[index] ?? 0, getCellVisualWidth(cell));
846
+ }
847
+ }
848
+
849
+ let cache = visualWidthCache.get(resultRows);
850
+ if (!cache) {
851
+ cache = new Map();
852
+ visualWidthCache.set(resultRows, cache);
853
+ }
854
+ cache.set(cacheKey, widths);
855
+
856
+ return widths;
857
+ }
858
+
859
+ function applyVisualColumnWidths(): boolean {
860
+ if (!columns.value?.length) return false;
861
+ const measuredWidths = getVisualColumnWidths();
862
+ if (!measuredWidths.length) return false;
863
+
864
+ // Only reassign columns when at least one width actually changed.
865
+ // Unconditionally replacing `columns` (even with an identical array) makes
866
+ // NDataTable rebuild its columns/body every measurement pass. When that
867
+ // happens while the table is also re-rendering changed data on a page
868
+ // change, two DOM patches race and Vue can hit a null parent.
869
+ let changed = false;
870
+ const nextColumns = columns.value.map((column, index) => {
871
+ if (
872
+ column.type === "selection" ||
873
+ ("key" in column && column.key === "actions")
874
+ )
875
+ return column;
876
+
877
+ const measured = measuredWidths[index];
878
+ if (!measured) return column;
879
+
880
+ const minWidth =
881
+ typeof column.minWidth === "number" ? column.minWidth : 150;
882
+ const targetWidth = Math.max(measured, minWidth);
883
+
884
+ if (column.width === targetWidth) return column;
885
+
886
+ changed = true;
887
+ return {
888
+ ...column,
889
+ width: targetWidth,
890
+ };
891
+ }) as DataTableColumns;
892
+
893
+ if (changed) columns.value = nextColumns;
894
+
895
+ return true;
896
+ }
897
+
898
+ function dedupeColumnsByKey(cols: DataTableColumns): DataTableColumns {
899
+ const seenKeys = new Set<string | number>();
900
+ return cols.filter((col) => {
901
+ if (!("key" in col)) return true;
902
+ const key = col.key;
903
+ if (typeof key !== "string" && typeof key !== "number") return true;
904
+ if (seenKeys.has(key)) return false;
905
+ seenKeys.add(key);
906
+ return true;
907
+ }) as DataTableColumns;
908
+ }
909
+
910
+ function getContentAwareMinWidth(field: Field, headerMinWidth: number): number {
911
+ if (!field.table || !_data.value?.result?.length) return headerMinWidth;
912
+
913
+ const relatedTable = database.value.tables?.find(
914
+ ({ slug }) => slug === field.table,
915
+ );
916
+ if (!relatedTable) return headerMinWidth;
917
+
918
+ let longestLabel = "";
919
+ let hasMultipleRelations = false;
920
+
921
+ for (const row of _data.value.result) {
922
+ const raw = row[field.key];
923
+ if (raw === undefined || raw === null) continue;
924
+
925
+ const relations = ([] as Item[]).concat(raw as Item | Item[]);
926
+ if (relations.length > 1) hasMultipleRelations = true;
927
+
928
+ for (const relation of relations) {
929
+ if (!relation || typeof relation !== "object") continue;
930
+ const label = renderLabel(relatedTable, relation);
931
+ if (label.length > longestLabel.length) longestLabel = label;
932
+ }
933
+ }
934
+
935
+ if (!longestLabel) return headerMinWidth;
936
+
937
+ const labelWidth = measureTextWidth(longestLabel, {
938
+ // Icon + rounded button paddings + spacing.
939
+ startWith: 96,
940
+ min: headerMinWidth,
941
+ });
942
+
943
+ // In grouped mode we show "first +N", so reserve room for the count button.
944
+ return hasMultipleRelations ? labelWidth + 56 : labelWidth;
945
+ }
946
+
947
+ async function setColumns(skipVisualWidths = false) {
948
+ const cols = [
949
+ ...(table.value?.allowedMethods !== "r"
950
+ ? [
951
+ {
952
+ type: "selection",
953
+ fixed: "left",
954
+ options: [
955
+ {
956
+ label: t("delete"),
957
+ key: "delete",
958
+ disabled: checkedRowKeys.value.length === 0,
959
+ icon: () => h(NIcon, () => h(Icon, { name: "tabler:trash" })),
960
+ children: [
961
+ {
962
+ label: () =>
963
+ h(
964
+ "span",
965
+ {
966
+ onClick: async () => {
967
+ await deleteItem(checkedRowKeys.value);
968
+ checkedRowKeys.value = [];
969
+ },
970
+ },
971
+ t("deleteSelectedItems"),
972
+ ),
973
+ key: "delete",
974
+ icon: () =>
975
+ h(
976
+ NIcon,
977
+ {
978
+ onClick: async () => {
979
+ await deleteItem(checkedRowKeys.value);
980
+ checkedRowKeys.value = [];
981
+ },
982
+ },
983
+ () => h(Icon, { name: "tabler:table-alias" }),
984
+ ),
985
+ },
986
+ ...(checkedRowKeys.value.length ===
987
+ _data.value?.result?.length
988
+ ? [
989
+ {
990
+ label: () =>
991
+ h(
992
+ "span",
993
+ {
994
+ onClick: async () => {
995
+ await deleteItem();
996
+ checkedRowKeys.value = [];
997
+ },
998
+ },
999
+ t("clearTable"),
1000
+ ),
1001
+ key: "clear",
1002
+ icon: () =>
1003
+ h(
1004
+ NIcon,
1005
+ {
1006
+ onClick: async () => {
1007
+ await deleteItem();
1008
+ checkedRowKeys.value = [];
1009
+ },
1010
+ },
1011
+ () => h(Icon, { name: "tabler:table-minus" }),
1012
+ ),
1013
+ },
1014
+ ]
1015
+ : []),
1016
+ ],
1017
+ },
1018
+ {
1019
+ label: t("columns"),
1020
+ key: "columns",
1021
+ icon: () =>
1022
+ h(NIcon, () => h(Icon, { name: "tabler:columns-3" })),
1023
+ children: table.value?.schema?.map(({ id, key }) => ({
1024
+ label: t(key),
1025
+ key: id,
1026
+ icon: () =>
1027
+ h(
1028
+ NIcon,
1029
+ {
1030
+ onClick() {
1031
+ if (
1032
+ tablesConfig?.value[
1033
+ table.value?.slug as string
1034
+ ]?.columns?.includes(id as number)
1035
+ )
1036
+ // @ts-expect-error
1037
+ tablesConfig.value[
1038
+ table.value?.slug as string
1039
+ ].columns = tablesConfig.value[
1040
+ table.value?.slug as string
1041
+ ]?.columns?.filter(
1042
+ (itemID: number) => itemID !== id,
1043
+ );
1044
+ else {
1045
+ const clonedTablesConfig = structuredClone(
1046
+ toRaw(tablesConfig.value),
1047
+ );
1048
+ if (
1049
+ !clonedTablesConfig[table.value?.slug as string]
1050
+ )
1051
+ clonedTablesConfig[table.value?.slug as string] =
1052
+ { columns: [] };
1053
+
1054
+ // @ts-expect-error
1055
+ clonedTablesConfig[
1056
+ table.value?.slug as string
1057
+ ].columns?.push(id as number);
1058
+
1059
+ tablesConfig.value = clonedTablesConfig;
1060
+ }
1061
+ },
1062
+ },
1063
+ () =>
1064
+ h(Icon, {
1065
+ name:
1066
+ id !== undefined &&
1067
+ tablesConfig.value[
1068
+ table.value?.slug as string
1069
+ ]?.columns?.includes(id)
1070
+ ? "tabler:eye-off"
1071
+ : "tabler:eye",
1072
+ }),
1073
+ ),
1074
+ })),
1075
+ },
1076
+ ],
1077
+ },
1078
+ ]
1079
+ : []),
1080
+ ...((table.value?.defaultTableColumns && table.value?.schema
1081
+ ? [
1082
+ ...(tablesConfig.value[table.value?.slug as string]?.columns
1083
+ ? table.value.schema.filter(
1084
+ ({ id }) =>
1085
+ id !== undefined &&
1086
+ !tablesConfig.value[
1087
+ table.value?.slug as string
1088
+ ]?.columns?.includes(id) &&
1089
+ !table.value?.defaultTableColumns?.includes(id),
1090
+ )
1091
+ : []),
1092
+ ...table.value.defaultTableColumns
1093
+ .map(
1094
+ (id) =>
1095
+ table.value?.schema?.find((field) => field.id === id) as Field,
1096
+ )
1097
+ .filter(Boolean),
1098
+ ]
1099
+ : table.value?.schema
1100
+ )
1101
+ ?.filter(
1102
+ ({ id }) =>
1103
+ id !== undefined &&
1104
+ !tablesConfig.value[table.value?.slug]?.columns?.includes(id),
1105
+ )
1106
+ .map((field) => {
1107
+ const headerMinWidth = measureTextWidth(t(field.key), {
1108
+ startWith: 70,
1109
+ min: 150,
1110
+ });
1111
+ const minWidth = getContentAwareMinWidth(field, headerMinWidth);
1112
+ return {
1113
+ title: () =>
1114
+ h(NFlex, { wrap: false, align: "center", justify: "start" }, () => [
1115
+ getField(field).icon(),
1116
+ h(NPerformantEllipsis, () => t(field.key)),
1117
+ ]),
1118
+ minWidth,
1119
+ width: minWidth,
1120
+ key: field.key,
1121
+ sorter: !!_data.value?.result,
1122
+ ellipsis:
1123
+ field.table !== undefined
1124
+ ? false
1125
+ : {
1126
+ tooltip: true,
1127
+ },
1128
+ resizable: !field.children || !isArrayOfObjects(field.children),
1129
+ sortOrder: sort.value[field.key]
1130
+ ? `${sort.value[field.key]}end`
1131
+ : undefined,
1132
+ render: (row: Item) =>
1133
+ field.render
1134
+ ? field.render(row)
1135
+ : table.value?.allowedMethods?.includes("u") &&
1136
+ ![
1137
+ "id",
1138
+ "createdAt",
1139
+ "createdBy",
1140
+ "updatedAt",
1141
+ "updatedBy",
1142
+ ].includes(field.key) &&
1143
+ Language.value === database.value?.primaryLanguage
1144
+ ? h(LazyColumnEdit, {
1145
+ editKey: `${row.id ?? "row"}-${field.key}`,
1146
+ itemLabel: renderLabel(table.value, row),
1147
+ loading:
1148
+ !!row.id && Loading.value[`${row.id}-${field.key}`],
1149
+ modelValue: row[field.key],
1150
+ "onUpdate:modelValue": async (value: any) => {
1151
+ if (!row.id) return;
1152
+ Loading.value[`${row.id}-${field.key}`] = true;
1153
+ row[field.key] = value;
1154
+ await refreshVisualWidths();
1155
+ const __data = await $fetch<apiResponse<Item | boolean>>(
1156
+ `${config.public.apiBase}${database.value.slug}/${
1157
+ table.value?.slug
1158
+ }/${row.id}`,
1159
+ {
1160
+ method: "PUT",
1161
+ body: row,
1162
+ params: {
1163
+ return: false,
1164
+ locale: Language.value,
1165
+ [`${database.value.slug}_sid`]: sessionID.value,
1166
+ },
1167
+ credentials: "include",
1168
+ },
1169
+ );
1170
+ if (
1171
+ (typeof __data?.result === "boolean" &&
1172
+ __data.result !== true) ||
1173
+ (typeof __data.result !== "boolean" &&
1174
+ !__data.result?.id)
1175
+ )
1176
+ window.$message.error(__data.message);
1177
+ Loading.value[`${row.id}-${field.key}`] = false;
1178
+ },
1179
+ field,
1180
+ })
1181
+ : h(LazyColumn, {
1182
+ value: row[field.key],
1183
+ itemLabel: renderLabel(table.value, row),
1184
+ field,
1185
+ }),
1186
+ };
1187
+ }) ?? []),
1188
+ ...(isSlotSet("itemActions") && isSlotEmpty("itemActions")
1189
+ ? []
1190
+ : [
1191
+ {
1192
+ title: t("actions"),
1193
+ align: "center",
1194
+ width:
1195
+ isMobile || tablesConfig.value[table.value.slug]?.size === "small"
1196
+ ? 70
1197
+ : 150 +
1198
+ (isSlotSet("itemExtraButtons") &&
1199
+ !isSlotEmpty("itemExtraButtons")
1200
+ ? (props.slots.itemExtraButtons as any)().length * 20
1201
+ : 0),
1202
+ key: "actions",
1203
+ fixed: "right",
1204
+ render: (row: any) =>
1205
+ isSlotSet("itemActions") && !isSlotEmpty("itemActions")
1206
+ ? props.slots.itemActions(row)
1207
+ : [
1208
+ isSlotSet("itemExtraActions") &&
1209
+ !isSlotEmpty("itemExtraActions")
1210
+ ? props.slots.itemExtraActions(row)
1211
+ : undefined,
1212
+ isMobile ||
1213
+ tablesConfig.value[table.value.slug]?.size === "small"
1214
+ ? h(
1215
+ NPopover,
1216
+ {
1217
+ scrollable: true,
1218
+ style: "max-height: 240px;border-radius:34px",
1219
+ contentStyle: "padding: 0",
1220
+ trigger: "click",
1221
+ },
1222
+ {
1223
+ trigger: () =>
1224
+ h(
1225
+ NButton,
1226
+ {
1227
+ size: "small",
1228
+ circle: true,
1229
+ secondary: true,
1230
+ type: "primary",
1231
+ },
1232
+ {
1233
+ icon: () =>
1234
+ h(NIcon, () =>
1235
+ h(Icon, { name: "tabler:dots" }),
1236
+ ),
1237
+ },
1238
+ ),
1239
+ default: () => renderItemButtons(row),
1240
+ },
1241
+ )
1242
+ : renderItemButtons(row),
1243
+ ],
1244
+ },
1245
+ ]),
1246
+ ] as DataTableColumns;
1247
+
1248
+ const uniqueCols = dedupeColumnsByKey(cols);
1249
+
1250
+ if (uniqueCols.length > 2 || table.value?.defaultTableColumns)
1251
+ columns.value = uniqueCols;
1252
+
1253
+ if (!skipVisualWidths) await ensureVisualWidthsReady();
1254
+ }
1255
+ watch([Language, _data, tablesConfig, table], () => setColumns(), {
1256
+ deep: true,
1257
+ immediate: true,
1258
+ flush: "post",
1259
+ });
1260
+ // Selection doesn't affect column widths, so rebuild the columns (to
1261
+ // refresh the delete-selection dropdown state) without paying for the
1262
+ // DOM remeasure pass that only matters when content actually changed.
1263
+ watch(checkedRowKeys, () => setColumns(true), { flush: "post" });
1264
+
1265
+ onMounted(() => {
1266
+ void ensureVisualWidthsReady();
1267
+ });
1268
+
1269
+ onBeforeUnmount(() => {
1270
+ isWidthsUnmounted = true;
1271
+ stopTableWidthObservers();
1272
+ });
1273
+ </script>