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,287 @@
1
+ /**
2
+ * Composable for optimizing files (images and PDFs) before upload
3
+ * Images are converted to WebP or AVIF format for better compression
4
+ * Falls back to original if optimized version is larger
5
+ */
6
+
7
+ interface OptimizationResult {
8
+ file: File
9
+ optimized: boolean
10
+ originalSize: number
11
+ compressedSize?: number
12
+ compressionRatio?: number
13
+ outputFormat?: string
14
+ }
15
+
16
+ export const useOptimizeFile = () => {
17
+ /**
18
+ * Compress image to WebP or AVIF format
19
+ * @param file - The image file to compress
20
+ * @param quality - Compression quality (0-1), defaults to 0.85
21
+ * @param maxWidth - Maximum width in pixels, defaults to 3840 (4K)
22
+ * @param maxHeight - Maximum height in pixels, defaults to 3840 (4K)
23
+ * @returns Promise with compressed blob and metadata, or null if conversion not possible
24
+ */
25
+ const compressImage = async (
26
+ file: File,
27
+ quality = 0.85,
28
+ maxWidth = 3840,
29
+ maxHeight = 3840,
30
+ ): Promise<{ blob: Blob; width: number; height: number; format: string } | null> => {
31
+ return new Promise((resolve) => {
32
+ const reader = new FileReader()
33
+
34
+ reader.onload = (event) => {
35
+ const img = new Image()
36
+
37
+ img.onload = () => {
38
+ // Calculate new dimensions while maintaining aspect ratio
39
+ let { width, height } = img
40
+ const aspectRatio = width / height
41
+ let newWidth = width
42
+ let newHeight = height
43
+
44
+ // Scale down if dimensions exceed max
45
+ if (width > maxWidth) {
46
+ newWidth = maxWidth
47
+ newHeight = maxWidth / aspectRatio
48
+ }
49
+ if (newHeight > maxHeight) {
50
+ newHeight = maxHeight
51
+ newWidth = maxHeight * aspectRatio
52
+ }
53
+
54
+ // Create canvas and compress
55
+ const canvas = document.createElement('canvas')
56
+ canvas.width = Math.round(newWidth)
57
+ canvas.height = Math.round(newHeight)
58
+
59
+ const ctx = canvas.getContext('2d')
60
+ if (!ctx) {
61
+ resolve(null)
62
+ return
63
+ }
64
+
65
+ // Enable image smoothing for better quality
66
+ ctx.imageSmoothingEnabled = true
67
+ ctx.imageSmoothingQuality = 'high'
68
+
69
+ // Draw scaled image
70
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
71
+
72
+ // Try AVIF first (best compression)
73
+ // AVIF: ~50-60% size reduction vs JPEG
74
+ // WebP: ~25-35% size reduction vs JPEG
75
+ canvas.toBlob(
76
+ (avifBlob) => {
77
+ if (avifBlob) {
78
+ // AVIF succeeded, now try WebP to compare
79
+ canvas.toBlob(
80
+ (webpBlob) => {
81
+ if (webpBlob && webpBlob.size < avifBlob.size) {
82
+ // WebP is smaller than AVIF
83
+ resolve({
84
+ blob: webpBlob,
85
+ width: canvas.width,
86
+ height: canvas.height,
87
+ format: 'image/webp',
88
+ })
89
+ } else {
90
+ // AVIF is smaller or WebP failed
91
+ resolve({
92
+ blob: avifBlob,
93
+ width: canvas.width,
94
+ height: canvas.height,
95
+ format: 'image/avif',
96
+ })
97
+ }
98
+ },
99
+ 'image/webp',
100
+ quality,
101
+ )
102
+ } else {
103
+ // AVIF not supported, try WebP
104
+ canvas.toBlob(
105
+ (webpBlob) => {
106
+ if (webpBlob) {
107
+ resolve({
108
+ blob: webpBlob,
109
+ width: canvas.width,
110
+ height: canvas.height,
111
+ format: 'image/webp',
112
+ })
113
+ } else {
114
+ // WebP not supported, use JPEG
115
+ canvas.toBlob(
116
+ (jpegBlob) => {
117
+ resolve(
118
+ jpegBlob
119
+ ? {
120
+ blob: jpegBlob,
121
+ width: canvas.width,
122
+ height: canvas.height,
123
+ format: 'image/jpeg',
124
+ }
125
+ : null,
126
+ )
127
+ },
128
+ 'image/jpeg',
129
+ quality,
130
+ )
131
+ }
132
+ },
133
+ 'image/webp',
134
+ quality,
135
+ )
136
+ }
137
+ },
138
+ 'image/avif',
139
+ quality,
140
+ )
141
+ }
142
+
143
+ img.onerror = () => {
144
+ resolve(null)
145
+ }
146
+
147
+ img.src = event.target?.result as string
148
+ }
149
+
150
+ reader.onerror = () => {
151
+ resolve(null)
152
+ }
153
+
154
+ reader.readAsDataURL(file)
155
+ })
156
+ }
157
+
158
+ /**
159
+ * Determine if file should be optimized
160
+ * Optimizes images > 300KB and PDFs > 500KB
161
+ */
162
+ const shouldOptimize = (file: File): boolean => {
163
+ const mimeType = file.type
164
+ const isImage = mimeType.startsWith('image/')
165
+ const isPDF = mimeType === 'application/pdf'
166
+
167
+ if (isImage) {
168
+ // Optimize images larger than 300KB
169
+ return file.size > 300 * 1024
170
+ }
171
+ if (isPDF) {
172
+ // Optimize PDFs larger than 500KB
173
+ return file.size > 500 * 1024
174
+ }
175
+
176
+ return false
177
+ }
178
+
179
+ /**
180
+ * Optimize a single file
181
+ * For images: convert to WebP/AVIF for better compression
182
+ * Returns original file if optimization doesn't reduce size
183
+ */
184
+ const optimizeFile = async (
185
+ file: File,
186
+ imageQuality = 0.85,
187
+ ): Promise<OptimizationResult> => {
188
+ const isImage = file.type.startsWith('image/')
189
+
190
+ // Only optimize supported types
191
+ if (!isImage) {
192
+ return {
193
+ file,
194
+ optimized: false,
195
+ originalSize: file.size,
196
+ }
197
+ }
198
+
199
+ // Skip optimization if file is too small
200
+ if (!shouldOptimize(file)) {
201
+ return {
202
+ file,
203
+ optimized: false,
204
+ originalSize: file.size,
205
+ }
206
+ }
207
+
208
+ try {
209
+ // Compress image to WebP/AVIF
210
+ const result = await compressImage(
211
+ file,
212
+ imageQuality,
213
+ 3840,
214
+ 3840,
215
+ )
216
+
217
+ if (!result) {
218
+ // Compression failed, use original
219
+ return {
220
+ file,
221
+ optimized: false,
222
+ originalSize: file.size,
223
+ }
224
+ }
225
+
226
+ const { blob, width, height, format } = result
227
+
228
+ // Only use compressed version if it's actually smaller or equal
229
+ if (blob.size <= file.size) {
230
+ const extension = format === 'image/avif' ? 'avif' : format === 'image/webp' ? 'webp' : 'jpg'
231
+ const newFileName = file.name.replace(/\.[^.]+$/, `.${extension}`)
232
+
233
+ const compressedFile = new File([blob], newFileName, {
234
+ type: format,
235
+ lastModified: file.lastModified,
236
+ })
237
+
238
+ const compressionRatio = 1 - blob.size / file.size
239
+ const formatName = format === 'image/avif' ? 'AVIF' : format === 'image/webp' ? 'WebP' : 'JPEG'
240
+
241
+
242
+ return {
243
+ file: compressedFile,
244
+ optimized: true,
245
+ originalSize: file.size,
246
+ compressedSize: blob.size,
247
+ compressionRatio,
248
+ outputFormat: formatName,
249
+ }
250
+ }
251
+
252
+ // Compression resulted in larger file, use original
253
+ return {
254
+ file,
255
+ optimized: false,
256
+ originalSize: file.size,
257
+ }
258
+ } catch (error) {
259
+ console.warn(`File optimization failed, using original: ${error instanceof Error ? error.message : 'Unknown error'}`)
260
+ }
261
+
262
+ return {
263
+ file,
264
+ optimized: false,
265
+ originalSize: file.size,
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Batch optimize multiple files
271
+ */
272
+ const optimizeFiles = async (
273
+ files: File[],
274
+ imageQuality = 0.85,
275
+ ): Promise<OptimizationResult[]> => {
276
+ return Promise.all(
277
+ files.map((file) => optimizeFile(file, imageQuality)),
278
+ )
279
+ }
280
+
281
+ return {
282
+ compressImage,
283
+ shouldOptimize,
284
+ optimizeFile,
285
+ optimizeFiles,
286
+ }
287
+ }
@@ -0,0 +1,88 @@
1
+ import { flattenSchema } from "inibase/utils";
2
+
3
+ export default function renderLabel(
4
+ table?: Table,
5
+ item?: Item,
6
+ defaulValue = "--",
7
+ ): string {
8
+ if (!table || !table.schema) return `${item?.id ?? defaulValue}`;
9
+
10
+ // Tables created without an explicit Item Label template (e.g. quick
11
+ // "new table" with just a slug) have `label: ""`. Falling back to the raw
12
+ // masked id makes referenced items unreadable and — since translated
13
+ // references can point to a different item per language — the cell value
14
+ // appears to change "randomly" when the UI language switches. Show the
15
+ // item's first text field instead.
16
+ if (!table.label) return fallbackLabel(table, item, defaulValue);
17
+
18
+ if (table.customLabel) return table.customLabel(item);
19
+
20
+ const flattenTableSchema = flattenSchema(table.schema as any);
21
+ return table.label
22
+ .replace(
23
+ /@([a-zA-Z0-9_]+)\.(\d+)|@(\d+)/g,
24
+ (match, slug, fieldId, capturedNumber) => {
25
+ if (slug && fieldId !== undefined) {
26
+ if (slug === "user") {
27
+ slug = "users";
28
+ item = useState<User>("user").value;
29
+ }
30
+
31
+ const database = useState<Database>("database");
32
+ const refTable = database.value.tables?.find(
33
+ ({ slug: s }) => s === slug,
34
+ );
35
+ if (!refTable || !refTable.schema) return defaulValue;
36
+ const flattenRefSchema = flattenSchema(refTable.schema as any);
37
+ const field = flattenRefSchema.find(
38
+ ({ id }) => id === Number(fieldId),
39
+ );
40
+ if (!field || !item || !item[field.key]) return defaulValue;
41
+ if (field.table) {
42
+ const fieldTable = database.value.tables?.find(
43
+ ({ slug }) => slug === field.table,
44
+ );
45
+ return renderLabel(fieldTable, item[field.key]);
46
+ }
47
+ return item[field.key];
48
+ }
49
+ if (capturedNumber) {
50
+ const field = flattenTableSchema.find(
51
+ ({ id }) => id === Number(capturedNumber),
52
+ );
53
+ if (!field || !item || !item[field.key]) return defaulValue;
54
+ if (field.table) {
55
+ const database = useState<Database>("database");
56
+ const fieldTable = database.value.tables?.find(
57
+ ({ slug }) => slug === field.table,
58
+ );
59
+ return renderLabel(fieldTable, item[field.key]);
60
+ }
61
+ return item[field.key];
62
+ }
63
+ return defaulValue;
64
+ },
65
+ )
66
+ .replaceAll("\\\\", "\\");
67
+ }
68
+
69
+ function fallbackLabel(table: Table, item?: Item, defaulValue = "--"): string {
70
+ if (!item) return `${defaulValue}`;
71
+ const flattened = flattenSchema(table.schema as any);
72
+ const candidates = flattened
73
+ .filter((field) => field.id !== undefined && Number(field.id) > 0)
74
+ .sort((a, b) => Number(a.id) - Number(b.id));
75
+ for (const field of candidates) {
76
+ const types = Array.isArray(field.type) ? field.type : [field.type];
77
+ if (
78
+ !types.some(
79
+ (type) => type === "string" || type === "text" || type === "number",
80
+ )
81
+ )
82
+ continue;
83
+ const value = (item as Record<string, unknown>)[field.key];
84
+ if (value !== undefined && value !== null && String(value).trim() !== "")
85
+ return String(value);
86
+ }
87
+ return `${item?.id ?? defaulValue}`;
88
+ }
@@ -0,0 +1,95 @@
1
+ import { isArrayOfObjects } from "inibase/utils";
2
+ import Inison from "inison";
3
+ import { copyToClipboard, randomID } from "~/composables";
4
+
5
+ /**
6
+ * Shared clipboard for the table-schema editor.
7
+ *
8
+ * Schema fields are stored on the *system* clipboard (not a module ref) so they
9
+ * can be copied in one table/browser tab and pasted into another. The clipboard
10
+ * value is tagged so it won't collide with normal data item copy/paste.
11
+ */
12
+ const CLIPBOARD_TAG = "INISON_SCHEMA_FIELDS";
13
+
14
+ // Returns a plain, VNode-free copy of a field that is safe to serialize.
15
+ function copyableField(field: Field): Record<string, unknown> {
16
+ const { render, onCreate, onDelete, children, ...rest } = field;
17
+ const cleaned = rest as Record<string, unknown>;
18
+ if (children === undefined) return cleaned;
19
+ if (isArrayOfObjects(children))
20
+ return {
21
+ ...cleaned,
22
+ children: children.map((child) => copyableField(child)),
23
+ };
24
+ return { ...cleaned, children };
25
+ }
26
+
27
+ /**
28
+ * Writes the given fields to the system clipboard so they can be pasted into
29
+ * another schema editor (including a different table or browser tab).
30
+ */
31
+ export async function copySchemaFields(fields: Field[]) {
32
+ const payload = Inison.stringify(fields.map((field) => copyableField(field)));
33
+ await copyToClipboard(`${CLIPBOARD_TAG}:${payload}`);
34
+ }
35
+
36
+ // Deep-clones a copied field and regenerates id (and recursive children ids) so
37
+ // pasting does not collide with existing fields.
38
+ function cloneFieldWithNewIds(field: Field): Field {
39
+ const clone = { ...field };
40
+ clone.id = `temp-${randomID()}`;
41
+ if (isArrayOfObjects(clone.children)) {
42
+ clone.children = clone.children.map((child) =>
43
+ cloneFieldWithNewIds(child as Field),
44
+ );
45
+ }
46
+ return clone;
47
+ }
48
+
49
+ function isFieldLike(value: unknown): boolean {
50
+ return (
51
+ !!value &&
52
+ typeof value === "object" &&
53
+ !Array.isArray(value) &&
54
+ "type" in (value as object) &&
55
+ "key" in (value as object)
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Reads the system clipboard and returns the copied schema fields, or an empty
61
+ * array when the clipboard doesn't contain schema fields.
62
+ */
63
+ export async function readSchemaFieldsFromClipboard(): Promise<Field[]> {
64
+ try {
65
+ const raw = await navigator.clipboard.readText();
66
+ if (!raw?.startsWith(CLIPBOARD_TAG)) return [];
67
+ const payload = raw.slice(CLIPBOARD_TAG.length + 1);
68
+ if (!payload) return [];
69
+ const data = Inison.unstringify<unknown>(payload);
70
+ if (!Array.isArray(data)) return [];
71
+ return data.filter(
72
+ (item): item is Field =>
73
+ isFieldLike(item) &&
74
+ !["id", "createdAt", "updatedAt"].includes(String((item as Field).key)),
75
+ );
76
+ } catch {
77
+ return [];
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Reads the clipboard and inserts the copied fields into `schema` right before
83
+ * `targetIndex`. Returns the newly inserted fields (with fresh temp ids).
84
+ */
85
+ export async function pasteSchemaFields(
86
+ schema: Schema,
87
+ targetIndex: number,
88
+ ): Promise<Field[]> {
89
+ const fields = await readSchemaFieldsFromClipboard();
90
+ const inserted = fields.map((field) => cloneFieldWithNewIds(field));
91
+ if (!inserted.length) return [];
92
+ const index = Math.max(0, Math.min(targetIndex, schema.length));
93
+ schema.splice(index, 0, ...inserted);
94
+ return inserted;
95
+ }