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.
- package/.github/pull_request.yml +18 -0
- package/CONTRIBUTING.md +67 -0
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/app/app.vue +106 -0
- package/app/assets/main.css +974 -0
- package/app/components/Api/EndpointCard.vue +169 -0
- package/app/components/Api/SchemaDocsTable.vue +208 -0
- package/app/components/Asset/Card.vue +1363 -0
- package/app/components/Asset/Grid.vue +295 -0
- package/app/components/Asset/Icon.vue +38 -0
- package/app/components/Asset/Preview.vue +486 -0
- package/app/components/Asset/Scanner.vue +700 -0
- package/app/components/Asset/Thumb.vue +708 -0
- package/app/components/Auth/Reset.vue +99 -0
- package/app/components/Auth/index.vue +346 -0
- package/app/components/ChatInterface.vue +2326 -0
- package/app/components/ChatPagePreview.vue +34 -0
- package/app/components/Column/Array.vue +29 -0
- package/app/components/Column/Asset.vue +21 -0
- package/app/components/Column/Boolean.vue +12 -0
- package/app/components/Column/Color.vue +9 -0
- package/app/components/Column/Date.vue +57 -0
- package/app/components/Column/Edit.vue +215 -0
- package/app/components/Column/Email.vue +9 -0
- package/app/components/Column/Html.vue +27 -0
- package/app/components/Column/Id.vue +20 -0
- package/app/components/Column/Locale.vue +32 -0
- package/app/components/Column/Object.vue +18 -0
- package/app/components/Column/Password.vue +7 -0
- package/app/components/Column/Role.vue +15 -0
- package/app/components/Column/S.vue +14 -0
- package/app/components/Column/Table/Single.vue +49 -0
- package/app/components/Column/Table/index.vue +42 -0
- package/app/components/Column/Tags.vue +31 -0
- package/app/components/Column/Text.vue +9 -0
- package/app/components/Column/Url.vue +9 -0
- package/app/components/Column/index.vue +41 -0
- package/app/components/Dashboard/Editor.vue +221 -0
- package/app/components/Dashboard/Grid.vue +270 -0
- package/app/components/Dashboard/View.vue +131 -0
- package/app/components/Dashboard/Widget/BarChart.vue +73 -0
- package/app/components/Dashboard/Widget/Counter.vue +51 -0
- package/app/components/Dashboard/Widget/LineChart.vue +76 -0
- package/app/components/Dashboard/Widget/PieChart.vue +75 -0
- package/app/components/Dashboard/Widget/Table.vue +94 -0
- package/app/components/Dashboard/WidgetEditor.vue +274 -0
- package/app/components/Data/Array.vue +102 -0
- package/app/components/Data/Html.vue +9 -0
- package/app/components/Data/Icon.vue +9 -0
- package/app/components/Data/Object.vue +10 -0
- package/app/components/Data/S.vue +16 -0
- package/app/components/Data/Table.vue +53 -0
- package/app/components/Data/Value.vue +34 -0
- package/app/components/Data/index.vue +14 -0
- package/app/components/Field/Array.vue +324 -0
- package/app/components/Field/Asset/Actions.vue +97 -0
- package/app/components/Field/Asset/index.vue +639 -0
- package/app/components/Field/Boolean.vue +26 -0
- package/app/components/Field/Checkbox.vue +48 -0
- package/app/components/Field/Color.vue +27 -0
- package/app/components/Field/Date.vue +30 -0
- package/app/components/Field/Email.vue +51 -0
- package/app/components/Field/EmailTemplate.vue +420 -0
- package/app/components/Field/Html/ColorPicker.vue +127 -0
- package/app/components/Field/Html/Editor.vue +544 -0
- package/app/components/Field/Html/index.vue +25 -0
- package/app/components/Field/Icon.vue +114 -0
- package/app/components/Field/JSON.vue +113 -0
- package/app/components/Field/Mention.vue +36 -0
- package/app/components/Field/Number.vue +27 -0
- package/app/components/Field/Object.vue +92 -0
- package/app/components/Field/Password.vue +55 -0
- package/app/components/Field/Radio.vue +47 -0
- package/app/components/Field/S.vue +44 -0
- package/app/components/Field/Select.vue +49 -0
- package/app/components/Field/Table.vue +348 -0
- package/app/components/Field/Tags.vue +113 -0
- package/app/components/Field/Text.vue +31 -0
- package/app/components/Field/Textarea.vue +33 -0
- package/app/components/Field/Url.vue +40 -0
- package/app/components/Field/Wrapper.vue +128 -0
- package/app/components/Field/index.vue +68 -0
- package/app/components/FloatingChatbot.vue +73 -0
- package/app/components/Form/Card.vue +151 -0
- package/app/components/Form/Drawer.vue +195 -0
- package/app/components/Form/index.vue +621 -0
- package/app/components/Header.vue +328 -0
- package/app/components/Offline/ConflictModal.vue +200 -0
- package/app/components/Offline/SyncStatus.vue +249 -0
- package/app/components/PlatformFeatureLanding.vue +65 -0
- package/app/components/Table/Backups.vue +638 -0
- package/app/components/Table/Flows.vue +814 -0
- package/app/components/Table/Grid.vue +296 -0
- package/app/components/Table/Icon.vue +36 -0
- package/app/components/Table/Logs.vue +181 -0
- package/app/components/Table/Schedules.vue +722 -0
- package/app/components/Table/Search/Button.vue +256 -0
- package/app/components/Table/Search/Items.vue +286 -0
- package/app/components/Table/Search/index.vue +87 -0
- package/app/components/Table/Settings/Schema.vue +801 -0
- package/app/components/Table/Settings/index.vue +631 -0
- package/app/components/Table/TranslateDrawer.vue +526 -0
- package/app/components/Table/Views/Kanban.vue +289 -0
- package/app/components/Table/Views/Table.vue +1273 -0
- package/app/components/Table/index.vue +890 -0
- package/app/composables/Translation/define.ts +41 -0
- package/app/composables/Translation/fetch.ts +107 -0
- package/app/composables/Translation/languages.ts +1 -0
- package/app/composables/Translation/loadCoreTranslations.ts +38 -0
- package/app/composables/Translation/translate.ts +250 -0
- package/app/composables/databaseCookies.ts +79 -0
- package/app/composables/fieldValidator.ts +72 -0
- package/app/composables/fieldsList.ts +301 -0
- package/app/composables/formatDatabase.ts +101 -0
- package/app/composables/index.ts +444 -0
- package/app/composables/openDrawer.ts +118 -0
- package/app/composables/optimizeFile.ts +287 -0
- package/app/composables/renderLabel.ts +88 -0
- package/app/composables/schemaClipboard.ts +95 -0
- package/app/composables/search.ts +353 -0
- package/app/composables/setThemeConfig.ts +48 -0
- package/app/composables/translationValue.ts +169 -0
- package/app/composables/useAssetPreview.ts +84 -0
- package/app/composables/useAssetUploader.ts +56 -0
- package/app/composables/useDashboardData.ts +310 -0
- package/app/composables/useDocumentThumbnail.ts +351 -0
- package/app/composables/useEcharts.ts +33 -0
- package/app/composables/useLocalDatabaseConfig.ts +51 -0
- package/app/composables/useOcr.ts +300 -0
- package/app/composables/useOfflineFetch.ts +194 -0
- package/app/composables/useOfflineItem.ts +107 -0
- package/app/composables/useOfflineQueue.ts +302 -0
- package/app/composables/useOfflineSync.ts +235 -0
- package/app/composables/usePasskeyAuth.ts +156 -0
- package/app/composables/usePdfCompressor.ts +180 -0
- package/app/composables/usePlatformRedirect.ts +72 -0
- package/app/composables/useRealtimeSync.ts +233 -0
- package/app/composables/useReferencedItems.ts +180 -0
- package/app/composables/useScanner.ts +664 -0
- package/app/composables/useSubscription.ts +650 -0
- package/app/composables/useThumbnailCache.ts +136 -0
- package/app/composables/useVideoCompressor.ts +194 -0
- package/app/error.vue +112 -0
- package/app/layouts/api.vue +188 -0
- package/app/layouts/dashboard.vue +91 -0
- package/app/layouts/default.vue +228 -0
- package/app/layouts/table.vue +471 -0
- package/app/locales/ar.ts +996 -0
- package/app/locales/en.ts +655 -0
- package/app/locales/es.ts +996 -0
- package/app/locales/fr.ts +997 -0
- package/app/middleware/dashboard.ts +33 -0
- package/app/middleware/database.ts +83 -0
- package/app/middleware/global.ts +1 -0
- package/app/middleware/sid-ingest.global.ts +29 -0
- package/app/middleware/table.ts +54 -0
- package/app/middleware/user.ts +26 -0
- package/app/pages/[[database]]/admin/api/auth/index.vue +306 -0
- package/app/pages/[[database]]/admin/api/index.vue +18 -0
- package/app/pages/[[database]]/admin/api/tables/[table]/index.vue +497 -0
- package/app/pages/[[database]]/admin/api/tables/index.vue +87 -0
- package/app/pages/[[database]]/admin/billing/index.vue +16 -0
- package/app/pages/[[database]]/admin/dashboards/[id]/edit.vue +66 -0
- package/app/pages/[[database]]/admin/dashboards/[id]/index.vue +117 -0
- package/app/pages/[[database]]/admin/dashboards/index.vue +20 -0
- package/app/pages/[[database]]/admin/index.vue +28 -0
- package/app/pages/[[database]]/admin/settings.vue +526 -0
- package/app/pages/[[database]]/admin/tables/[table]/[id]/edit.vue +143 -0
- package/app/pages/[[database]]/admin/tables/[table]/[id]/index.vue +146 -0
- package/app/pages/[[database]]/admin/tables/[table]/flows.vue +10 -0
- package/app/pages/[[database]]/admin/tables/[table]/index.vue +10 -0
- package/app/pages/[[database]]/admin/tables/[table]/new.vue +12 -0
- package/app/pages/[[database]]/admin/tables/[table]/schedules.vue +10 -0
- package/app/pages/[[database]]/admin/tables/[table]/settings.vue +30 -0
- package/app/pages/[[database]]/admin/tables/assets/[...path].vue +24 -0
- package/app/pages/[[database]]/admin/tables/assets/flows.vue +10 -0
- package/app/pages/[[database]]/admin/tables/backups/index.vue +10 -0
- package/app/pages/[[database]]/admin/tables/blocks/index.vue +16 -0
- package/app/pages/[[database]]/admin/tables/index.vue +20 -0
- package/app/pages/[[database]]/admin/tables/pages/index.vue +16 -0
- package/app/pages/[[database]]/admin/tables/templates/index.vue +16 -0
- package/app/pages/[[database]]/api/index.vue +120 -0
- package/app/pages/[[database]]/auth/index.vue +12 -0
- package/app/pages/[[database]]/auth/reset.vue +10 -0
- package/app/pages/[[database]]/index.vue +10 -0
- package/app/plugins/offline-warmup.client.ts +53 -0
- package/app/plugins/template-globals.ts +9 -0
- package/app/spa-loading-template.html +93 -0
- package/biome.json +53 -0
- package/index.d.ts +380 -0
- package/modules/naiveui.ts +41 -0
- package/nuxt.config.ts +269 -0
- package/package.json +56 -0
- package/pnpm-workspace.yaml +5 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/pwa-192x192.png +0 -0
- package/public/pwa-512x512.png +0 -0
- package/public/pwa-maskable-192x192.png +0 -0
- package/public/pwa-maskable-512x512.png +0 -0
- package/public/vendor/opencv.js +48 -0
- 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
|
+
}
|