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,353 @@
1
+ import { FormatObjectCriteriaValue, isArrayOfObjects } from "inibase/utils";
2
+
3
+ function _generateSearchInOption(
4
+ schema: Schema,
5
+ { id, key, type, children }: Field,
6
+ useIDasValue?: boolean,
7
+ path?: string,
8
+ ): any {
9
+ if ((type === "object" || type === "array") && isArrayOfObjects(children)) {
10
+ const _children = (children as Schema).filter(({ type }) =>
11
+ Array.isArray(type)
12
+ ? type.some((t) => !["table", "array", "object"].includes(t))
13
+ : !["table", "array", "object"].includes(type),
14
+ );
15
+ if (!_children.length) return undefined;
16
+ return {
17
+ label: t(key),
18
+ value: useIDasValue ? id : (path ?? "") + key,
19
+ children: _children.map((field) =>
20
+ _generateSearchInOption(
21
+ schema,
22
+ field,
23
+ useIDasValue,
24
+ `${(path ?? "") + key}.`,
25
+ ),
26
+ ),
27
+ };
28
+ }
29
+
30
+ return {
31
+ label: t(key),
32
+ value: useIDasValue ? id : (path ?? "") + key,
33
+ };
34
+ }
35
+
36
+ export function generateSearchInOptions(
37
+ schema?: Schema,
38
+ excludedKeys?: string[],
39
+ useIDasValue?: boolean,
40
+ ) {
41
+ if (!schema) return [];
42
+ const RETURN = schema
43
+ .map((field) => _generateSearchInOption(schema, field, useIDasValue))
44
+ .filter(Boolean)
45
+ .flat(Number.POSITIVE_INFINITY);
46
+ if (excludedKeys)
47
+ return RETURN.filter(({ value }) => !excludedKeys.includes(value));
48
+ return RETURN;
49
+ }
50
+
51
+ // ==================== ENCODE/DECODE LOGIC ====================
52
+
53
+ type RelativeUnit =
54
+ | "year"
55
+ | "month"
56
+ | "week"
57
+ | "day"
58
+ | "hour"
59
+ | "minute"
60
+ | "second";
61
+
62
+ const relativeUnitMap: Record<string, RelativeUnit> = {
63
+ year: "year",
64
+ years: "year",
65
+ yr: "year",
66
+ yrs: "year",
67
+ month: "month",
68
+ months: "month",
69
+ mo: "month",
70
+ mos: "month",
71
+ week: "week",
72
+ weeks: "week",
73
+ wk: "week",
74
+ wks: "week",
75
+ day: "day",
76
+ days: "day",
77
+ d: "day",
78
+ hour: "hour",
79
+ hours: "hour",
80
+ hr: "hour",
81
+ hrs: "hour",
82
+ minute: "minute",
83
+ minutes: "minute",
84
+ min: "minute",
85
+ mins: "minute",
86
+ second: "second",
87
+ seconds: "second",
88
+ sec: "second",
89
+ secs: "second",
90
+ };
91
+
92
+ function resolveRelativeDate(rawValue: unknown): number | undefined {
93
+ if (rawValue === null || rawValue === undefined) return undefined;
94
+ const text = String(rawValue).trim();
95
+ if (!text) return undefined;
96
+ const lowered = text.toLowerCase();
97
+ if (lowered === "now") return Date.now();
98
+ if (lowered === "yesterday") return shiftDate(new Date(), -1, "day");
99
+ if (lowered === "tomorrow") return shiftDate(new Date(), 1, "day");
100
+ if (lowered === "last week") return shiftDate(new Date(), -1, "week");
101
+ if (lowered === "last month") return shiftDate(new Date(), -1, "month");
102
+ if (lowered === "last year") return shiftDate(new Date(), -1, "year");
103
+ if (lowered === "next week") return shiftDate(new Date(), 1, "week");
104
+ if (lowered === "next month") return shiftDate(new Date(), 1, "month");
105
+ if (lowered === "next year") return shiftDate(new Date(), 1, "year");
106
+
107
+ let expression = lowered.replace(/\s+/g, " ").trim();
108
+ let implicitFuture = false;
109
+ if (expression.startsWith("in ")) {
110
+ expression = expression.slice(3).trim();
111
+ implicitFuture = true;
112
+ }
113
+
114
+ const match = expression.match(
115
+ /^([+-]?\d+)\s*([a-z]+)(?:\s+(ago|from now|later|after|before|ahead))?$/,
116
+ );
117
+ if (match) {
118
+ const amountText = match[1];
119
+ const unitToken = match[2];
120
+ if (!amountText || !unitToken) return undefined;
121
+ let amount = Number.parseInt(amountText, 10);
122
+ if (!Number.isFinite(amount)) return undefined;
123
+ const directionToken =
124
+ match[3] ?? (implicitFuture ? "from now" : undefined);
125
+ if (directionToken === "ago") amount = -Math.abs(amount);
126
+ else if (directionToken) amount = Math.abs(amount);
127
+ const mappedUnit = relativeUnitMap[unitToken];
128
+ if (!mappedUnit) return undefined;
129
+ return shiftDate(new Date(), amount, mappedUnit);
130
+ }
131
+
132
+ const fallback = Date.parse(text);
133
+ return Number.isNaN(fallback) ? undefined : fallback;
134
+ }
135
+
136
+ function shiftDate(baseDate: Date, amount: number, unit: RelativeUnit) {
137
+ const date = new Date(baseDate.getTime());
138
+ switch (unit) {
139
+ case "year":
140
+ date.setFullYear(date.getFullYear() + amount);
141
+ break;
142
+ case "month":
143
+ date.setMonth(date.getMonth() + amount);
144
+ break;
145
+ case "week":
146
+ date.setDate(date.getDate() + amount * 7);
147
+ break;
148
+ case "day":
149
+ date.setDate(date.getDate() + amount);
150
+ break;
151
+ case "hour":
152
+ date.setHours(date.getHours() + amount);
153
+ break;
154
+ case "minute":
155
+ date.setMinutes(date.getMinutes() + amount);
156
+ break;
157
+ case "second":
158
+ date.setSeconds(date.getSeconds() + amount);
159
+ break;
160
+ }
161
+ return date.getTime();
162
+ }
163
+
164
+ function fieldHasType(field: unknown, expectedType: FieldType) {
165
+ if (!field || typeof field !== "object") return false;
166
+ const fieldType = (field as { type?: FieldType | FieldType[] }).type;
167
+ if (!fieldType) return false;
168
+ if (Array.isArray(fieldType)) return fieldType.includes(expectedType);
169
+ return fieldType === expectedType;
170
+ }
171
+
172
+ function resolveRelativeNumber(rawValue: unknown): number | undefined {
173
+ if (rawValue === null || rawValue === undefined) return undefined;
174
+ if (typeof rawValue === "number")
175
+ return Number.isFinite(rawValue) ? rawValue : undefined;
176
+
177
+ if (typeof rawValue === "string") {
178
+ const text = rawValue.trim();
179
+ if (!text) return undefined;
180
+ const normalized = text.replaceAll(",", "");
181
+ const parsed = Number(normalized);
182
+ return Number.isFinite(parsed) ? parsed : undefined;
183
+ }
184
+
185
+ return undefined;
186
+ }
187
+
188
+ function isRecord(value: unknown): value is Record<string, unknown> {
189
+ return typeof value === "object" && value !== null && !Array.isArray(value);
190
+ }
191
+
192
+ function appendCriterionValue(
193
+ conditionTarget: Record<string, unknown>,
194
+ condition: "and" | "or",
195
+ fieldKey: string,
196
+ criterionValue: string,
197
+ ) {
198
+ const existingValue = conditionTarget[fieldKey];
199
+ if (existingValue === undefined) {
200
+ conditionTarget[fieldKey] = criterionValue;
201
+ return;
202
+ }
203
+
204
+ if (isRecord(existingValue) && isRecord(existingValue[condition])) {
205
+ const logicalGroup = existingValue[condition] as Record<string, unknown>;
206
+ logicalGroup[Object.keys(logicalGroup).length.toString()] = criterionValue;
207
+ return;
208
+ }
209
+
210
+ conditionTarget[fieldKey] = {
211
+ [condition]: {
212
+ "0": String(existingValue),
213
+ "1": criterionValue,
214
+ },
215
+ };
216
+ }
217
+
218
+ function prepareCriterionForMode(
219
+ operator: string | undefined,
220
+ rawValue: any,
221
+ field: Field,
222
+ mode: "display" | "fetch",
223
+ ) {
224
+ const normalizedOperator =
225
+ typeof operator === "string" && operator ? operator : "=";
226
+ if (mode === "fetch" && normalizedOperator.startsWith("r")) {
227
+ const absoluteValue = fieldHasType(field, "number")
228
+ ? resolveRelativeNumber(rawValue)
229
+ : resolveRelativeDate(rawValue);
230
+ if (absoluteValue === undefined)
231
+ return { operator: normalizedOperator, value: undefined };
232
+ const strippedOperator =
233
+ normalizedOperator.length > 1 ? normalizedOperator.slice(1) : "=";
234
+ return { operator: strippedOperator, value: absoluteValue };
235
+ }
236
+ return { operator: normalizedOperator, value: rawValue };
237
+ }
238
+
239
+ export function generateSearchString(
240
+ searchArray: any,
241
+ mode: "display" | "fetch" = "display",
242
+ ) {
243
+ const RETURN: Record<string, Record<string, unknown>> = {};
244
+ if (!searchArray) return undefined;
245
+ for (const condition in searchArray) {
246
+ const items = searchArray[condition];
247
+ if (!["and", "or"].includes(condition)) continue;
248
+ if (!Array.isArray(items)) continue;
249
+ for (const item of items) {
250
+ if (!Array.isArray(item)) continue;
251
+ const fieldKey = item[0];
252
+ if (!fieldKey) continue;
253
+ const field = item[3] as Field | undefined;
254
+ if (!RETURN[condition]) RETURN[condition] = {};
255
+ const { operator, value } = prepareCriterionForMode(
256
+ typeof item[1] === "string" ? item[1] : "=",
257
+ item[2],
258
+ field,
259
+ mode,
260
+ );
261
+ if (value === undefined) continue;
262
+ const finalValue = value === undefined ? "" : value;
263
+ appendCriterionValue(
264
+ RETURN[condition],
265
+ condition as "and" | "or",
266
+ fieldKey,
267
+ `${operator === "=" ? "" : operator}${finalValue}`,
268
+ );
269
+ }
270
+ }
271
+ function isDeepEmpty(obj: unknown): boolean {
272
+ if (typeof obj !== "object" || obj === null) return false;
273
+ const keys = Object.keys(obj);
274
+ if (keys.length === 0) return true;
275
+ return keys.every((k) => isDeepEmpty((obj as Record<string, unknown>)[k]));
276
+ }
277
+ return !isDeepEmpty(RETURN) ? RETURN : undefined;
278
+ }
279
+
280
+ function decodeStoredCriterion(value: unknown): [string, unknown] {
281
+ const RELATIVE_OPERATORS = ["r>=", "r<=", "r!=", "r>", "r<", "r="] as const;
282
+
283
+ if (typeof value === "string" && value.startsWith("r")) {
284
+ const matched = RELATIVE_OPERATORS.find((operator) =>
285
+ value.startsWith(operator),
286
+ );
287
+ if (matched)
288
+ return [matched, value.slice(matched.length)] as [string, unknown];
289
+ }
290
+ if (typeof value === "string") return FormatObjectCriteriaValue(value);
291
+ return ["=", value];
292
+ }
293
+
294
+ function extractGroupedCriteriaValues(
295
+ value: unknown,
296
+ condition: "and" | "or",
297
+ ): unknown[] | null {
298
+ if (!value) return null;
299
+
300
+ // Support payloads like: { and: { 0: ">10", 1: "<20" } }
301
+ if (isRecord(value)) {
302
+ const fromCondition = value[condition];
303
+ if (Array.isArray(fromCondition)) return fromCondition;
304
+ if (isRecord(fromCondition)) return Object.values(fromCondition);
305
+
306
+ // Support payloads like: { 0: ">10", 1: "<20" }
307
+ const objectValues = Object.values(value);
308
+ if (
309
+ objectValues.length &&
310
+ objectValues.every((entry) => typeof entry === "string")
311
+ )
312
+ return objectValues;
313
+ }
314
+
315
+ // Support payloads like: [">10", "<20"]
316
+ if (Array.isArray(value)) return value;
317
+
318
+ return null;
319
+ }
320
+
321
+ export function generateSearchArray(searchQuery: any): searchType {
322
+ const RETURN: searchType = {};
323
+ for (const [condition, items] of Object.entries(searchQuery)) {
324
+ if (!RETURN[condition as "and" | "or"])
325
+ RETURN[condition as "and" | "or"] = [];
326
+ for (const [key, value] of Object.entries(items)) {
327
+ if (["and", "or"].includes(key))
328
+ RETURN[condition as "and" | "or"]?.push({
329
+ [key]: generateSearchArray(value),
330
+ });
331
+ else {
332
+ const groupedValues = extractGroupedCriteriaValues(
333
+ value,
334
+ condition as "and" | "or",
335
+ );
336
+ if (groupedValues?.length) {
337
+ for (const nestedValue of groupedValues)
338
+ RETURN[condition as "and" | "or"]?.push([
339
+ key,
340
+ ...decodeStoredCriterion(nestedValue),
341
+ ]);
342
+ continue;
343
+ }
344
+
345
+ RETURN[condition as "and" | "or"]?.push([
346
+ key,
347
+ ...decodeStoredCriterion(value),
348
+ ]);
349
+ }
350
+ }
351
+ }
352
+ return RETURN;
353
+ }
@@ -0,0 +1,48 @@
1
+ import { hexToRGB } from "~/composables"
2
+
3
+ function adjustColor(hex: string, factor: number) {
4
+ // Convert hex to RGB
5
+ let [r, g, b] = hexToRGB(hex)
6
+
7
+ // Adjust RGB values
8
+ r = Math.min(255, Math.max(0, Math.round(r * factor)))
9
+ g = Math.min(255, Math.max(0, Math.round(g * factor)))
10
+ b = Math.min(255, Math.max(0, Math.round(b * factor)))
11
+
12
+ // Convert back to hex
13
+ return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`
14
+ }
15
+
16
+ export default function () {
17
+ const database = useState<Database>("database")
18
+ const Theme = useCookie<"light" | "dark">("theme", { sameSite: true })
19
+ const Language = useScopedCookie<LanguagesType>("language", database.value?.slug)
20
+ const ThemeConfig = useState<ThemeConfig>("ThemeConfig")
21
+
22
+ const mainColor =
23
+ (Theme.value === "light" || !database.value.primaryDarkColor
24
+ ? database.value.primaryColor
25
+ : database.value.primaryDarkColor) ?? "#FF9800"
26
+ ThemeConfig.value = {
27
+ primaryColor: mainColor,
28
+ primaryColorHover: adjustColor(mainColor, 1.1),
29
+ primaryColorPressed: adjustColor(mainColor, 0.9),
30
+ primaryColorSuppl: adjustColor(mainColor, 0.8),
31
+ }
32
+
33
+ const supportedLanguages = [
34
+ database.value?.primaryLanguage,
35
+ ...(database.value?.secondaryLanguages ?? []),
36
+ ].filter(Boolean) as LanguagesType[]
37
+
38
+ // Keep user's language choice. Only fall back when no language exists,
39
+ // or when current language is not available for this database.
40
+ if (!Language.value) {
41
+ Language.value = database.value?.primaryLanguage ?? "en"
42
+ } else if (
43
+ supportedLanguages.length > 0
44
+ && !supportedLanguages.includes(Language.value)
45
+ ) {
46
+ Language.value = database.value?.primaryLanguage ?? "en"
47
+ }
48
+ }
@@ -0,0 +1,169 @@
1
+ import { isObject } from "inibase/utils";
2
+ import Inison from "inison";
3
+
4
+ // Matches the translatable field rules used by the translation drawer
5
+ // (app/components/Table/TranslateDrawer.vue).
6
+ const NOT_TRANSLATABLE = new Set([
7
+ "password",
8
+ "email",
9
+ "color",
10
+ "icon",
11
+ "link",
12
+ "role",
13
+ ]);
14
+ const TRANSLATABLE_TYPES = new Set(["string", "text", "textarea", "html"]);
15
+ const EXTRA_TRANSLATABLE = new Set([
16
+ "url",
17
+ "table",
18
+ "asset",
19
+ "array-table",
20
+ "array-asset",
21
+ ]);
22
+
23
+ /** Resolve the effective type of a field (subType wins, first type for unions). */
24
+ export function fieldTypeOf(field: Field): string {
25
+ const type = Array.isArray(field.type) ? field.type[0] : field.type;
26
+ return (field.subType ?? type) as string;
27
+ }
28
+
29
+ /** True when the field value is an array (repeater, tags, multi-select, array refs…). */
30
+ export function isArrayLikeField(field: Field): boolean {
31
+ const type = Array.isArray(field.type) ? field.type[0] : field.type;
32
+ const resolved = fieldTypeOf(field);
33
+ return (
34
+ field.isArray === true ||
35
+ type === "array" ||
36
+ resolved === "array-table" ||
37
+ resolved === "array-asset" ||
38
+ (type === "multiple" && field.subType !== "select")
39
+ );
40
+ }
41
+
42
+ /** True for table/asset references (single or array). */
43
+ export function isReferenceField(field: Field): boolean {
44
+ const resolved = fieldTypeOf(field);
45
+ return (
46
+ ["table", "asset", "array-table", "array-asset"].includes(resolved) ||
47
+ field.table === "assets"
48
+ );
49
+ }
50
+
51
+ /** True for array reference fields (array refs / asset folders). */
52
+ export function isReferenceArrayField(field: Field): boolean {
53
+ return isArrayLikeField(field) && !!field.table;
54
+ }
55
+
56
+ /** Whether a field should be translated at all (mirrors the drawer rules). */
57
+ export function isTranslatableField(field: Field): boolean {
58
+ const resolved = fieldTypeOf(field);
59
+ if (NOT_TRANSLATABLE.has(resolved)) return false;
60
+ if (["ids", "id"].includes(field.key)) return false;
61
+ if (TRANSLATABLE_TYPES.has(resolved)) return true;
62
+ if (EXTRA_TRANSLATABLE.has(resolved)) return true;
63
+ return isArrayLikeField(field);
64
+ }
65
+
66
+ /** True when the value should be treated as "no translation". */
67
+ export function isEmptyTranslationValue(value: unknown): boolean {
68
+ if (value === undefined || value === null) return true;
69
+ if (typeof value === "string") return value.trim().length === 0;
70
+ if (Array.isArray(value)) return value.length === 0;
71
+ return false;
72
+ }
73
+
74
+ /** Extract the id from a reference value (object → id, otherwise the scalar). */
75
+ export function extractReferenceId(value: unknown): string {
76
+ if (value === undefined || value === null) return "";
77
+ if (isObject(value) && !Array.isArray(value)) {
78
+ const id = (value as Item).id;
79
+ return id === undefined || id === null ? "" : String(id);
80
+ }
81
+ return String(value);
82
+ }
83
+
84
+ /**
85
+ * Serialize a draft value into the string stored on the translations table:
86
+ * - table/asset references are stored as their id / array of ids
87
+ * - arrays (and objects) are stored with Inison
88
+ * - scalars are stored as-is
89
+ */
90
+ export function normalizeTranslationValue(
91
+ field: Field,
92
+ value: unknown,
93
+ ): string {
94
+ if (isEmptyTranslationValue(value)) return "";
95
+
96
+ // Reference arrays (array refs / asset folders) → Inison list of ids.
97
+ if (isReferenceArrayField(field)) {
98
+ const ids = (Array.isArray(value) ? value : [value])
99
+ .map((entry) => extractReferenceId(entry))
100
+ .filter((id) => id.length > 0);
101
+ return Inison.stringify(ids);
102
+ }
103
+
104
+ // Single table/asset reference → its id.
105
+ if (isReferenceField(field)) {
106
+ return extractReferenceId(value);
107
+ }
108
+
109
+ if (Array.isArray(value)) return Inison.stringify(value);
110
+ if (isObject(value)) return Inison.stringify(value);
111
+ return String(value);
112
+ }
113
+
114
+ function tryParseStored(value: string): unknown {
115
+ const trimmed = value.trim();
116
+ if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
117
+ try {
118
+ return JSON.parse(trimmed);
119
+ } catch {
120
+ // fall through to Inison
121
+ }
122
+ try {
123
+ return Inison.unstringify(trimmed);
124
+ } catch {
125
+ return trimmed;
126
+ }
127
+ }
128
+ return trimmed;
129
+ }
130
+
131
+ /** Reduce a stored reference value (plain id, legacy object/array, …) to its id string. */
132
+ function resolveReferenceId(value: string): string {
133
+ const trimmed = value.trim();
134
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
135
+ const parsed = tryParseStored(trimmed);
136
+ if (Array.isArray(parsed))
137
+ return parsed.length ? extractReferenceId(parsed[0]) : "";
138
+ return extractReferenceId(parsed);
139
+ }
140
+ return extractReferenceId(trimmed);
141
+ }
142
+
143
+ /**
144
+ * Convert a stored translation string back into the typed value expected by the
145
+ * field component (id string(s), array, or scalar string).
146
+ */
147
+ export function parseTranslationValue(
148
+ field: Field | undefined,
149
+ stored: string | undefined | null,
150
+ ): unknown {
151
+ if (stored === undefined || stored === null || stored === "") {
152
+ return field && isArrayLikeField(field) ? [] : "";
153
+ }
154
+ const value = stored.trim();
155
+
156
+ // Array-like fields (array refs, tags, multi-select, repeaters, …)
157
+ if (field && isArrayLikeField(field)) {
158
+ const parsed = tryParseStored(value);
159
+ return Array.isArray(parsed) ? parsed : [parsed];
160
+ }
161
+
162
+ // Single reference field: the drawer stores plain ids; legacy records can
163
+ // hold a serialized object — reduce either form to the id string.
164
+ if (field && isReferenceField(field)) {
165
+ return resolveReferenceId(value);
166
+ }
167
+
168
+ return stored;
169
+ }
@@ -0,0 +1,84 @@
1
+ interface UseAssetPreviewOptions {
2
+ assets?: Asset[];
3
+ index?: number;
4
+ }
5
+
6
+ function filterPreviewAssets(assets: Asset[]): Asset[] {
7
+ return assets.filter((asset) => asset.type !== "dir" && !!asset.publicURL);
8
+ }
9
+
10
+ export function useAssetPreview() {
11
+ const Loading = useState<Record<string, boolean>>("Loading", () => ({}));
12
+ const currentPreviewAsset = useState<Asset | undefined>("currentPreviewAsset");
13
+ const previewAssetList = useState<Asset[]>("previewAssetList", () => []);
14
+ const previewAssetIndex = useState<number>("previewAssetIndex", () => 0);
15
+ const previewImageRotation = useState<number>("previewImageRotation", () => 0);
16
+
17
+ function openPreview(asset: Asset, options?: UseAssetPreviewOptions) {
18
+ const assets = options?.assets?.length
19
+ ? filterPreviewAssets(options.assets)
20
+ : [asset];
21
+ if (!assets.length) return;
22
+
23
+ const requestedIndex =
24
+ typeof options?.index === "number"
25
+ ? options.index
26
+ : assets.findIndex((value) => value.id === asset.id);
27
+ const resolvedIndex = requestedIndex > -1 ? requestedIndex : 0;
28
+
29
+ previewAssetList.value = assets;
30
+ previewAssetIndex.value = resolvedIndex;
31
+ currentPreviewAsset.value = assets[resolvedIndex];
32
+ previewImageRotation.value = 0;
33
+ Loading.value.previewModal = false;
34
+ }
35
+
36
+ function closePreview() {
37
+ currentPreviewAsset.value = undefined;
38
+ Loading.value.previewModal = false;
39
+ previewImageRotation.value = 0;
40
+ }
41
+
42
+ function showPrevPreviewAsset() {
43
+ if (!previewAssetList.value.length) return;
44
+ previewAssetIndex.value =
45
+ (previewAssetIndex.value - 1 + previewAssetList.value.length) %
46
+ previewAssetList.value.length;
47
+ currentPreviewAsset.value = previewAssetList.value[previewAssetIndex.value];
48
+ previewImageRotation.value = 0;
49
+ }
50
+
51
+ function showNextPreviewAsset() {
52
+ if (!previewAssetList.value.length) return;
53
+ previewAssetIndex.value =
54
+ (previewAssetIndex.value + 1) % previewAssetList.value.length;
55
+ currentPreviewAsset.value = previewAssetList.value[previewAssetIndex.value];
56
+ previewImageRotation.value = 0;
57
+ }
58
+
59
+ function rotatePreviewImageLeft() {
60
+ previewImageRotation.value -= 90;
61
+ }
62
+
63
+ function rotatePreviewImageRight() {
64
+ previewImageRotation.value += 90;
65
+ }
66
+
67
+ function resetPreviewImageRotation() {
68
+ previewImageRotation.value = 0;
69
+ }
70
+
71
+ return {
72
+ currentPreviewAsset,
73
+ previewAssetList,
74
+ previewAssetIndex,
75
+ previewImageRotation,
76
+ openPreview,
77
+ closePreview,
78
+ showPrevPreviewAsset,
79
+ showNextPreviewAsset,
80
+ rotatePreviewImageLeft,
81
+ rotatePreviewImageRight,
82
+ resetPreviewImageRotation,
83
+ };
84
+ }
@@ -0,0 +1,56 @@
1
+ import type { UploadCustomRequestOptions } from "naive-ui"
2
+
3
+ interface UploadAssetWithProgressOptions {
4
+ url: string
5
+ method: "POST" | "PUT"
6
+ headers?: Record<string, string>
7
+ file?: File | null
8
+ onProgress?: UploadCustomRequestOptions["onProgress"]
9
+ }
10
+
11
+ export function useAssetUploader() {
12
+ async function uploadAssetWithProgress({
13
+ url,
14
+ method,
15
+ headers,
16
+ file,
17
+ onProgress,
18
+ }: UploadAssetWithProgressOptions) {
19
+ if (!file) return
20
+ if (typeof XMLHttpRequest === "undefined") {
21
+ await $fetch(url, {
22
+ method,
23
+ headers,
24
+ body: file,
25
+ })
26
+ onProgress?.({ percent: 100 })
27
+ return
28
+ }
29
+
30
+ await new Promise<void>((resolve, reject) => {
31
+ const xhr = new XMLHttpRequest()
32
+ xhr.open(method, url)
33
+ for (const [key, value] of Object.entries(headers ?? {})) {
34
+ if (value) xhr.setRequestHeader(key, value)
35
+ }
36
+ xhr.upload.onprogress = (event) => {
37
+ if (!event.lengthComputable) return
38
+ const percent = 10 + (event.loaded / event.total) * 90
39
+ onProgress?.({ percent })
40
+ }
41
+ xhr.onload = () => {
42
+ if (xhr.status >= 200 && xhr.status < 300) {
43
+ onProgress?.({ percent: 100 })
44
+ resolve()
45
+ } else reject(new Error(`Upload failed with status ${xhr.status}`))
46
+ }
47
+ xhr.onerror = () => reject(new Error("Upload failed"))
48
+ xhr.onabort = () => reject(new Error("Upload aborted"))
49
+ xhr.send(file)
50
+ })
51
+ }
52
+
53
+ return {
54
+ uploadAssetWithProgress,
55
+ }
56
+ }