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,621 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<NForm :model="modelValue" ref="formValidationRef">
|
|
3
|
+
<slot :data="modelValue" :schema>
|
|
4
|
+
<FieldS v-model="modelValue" v-model:schema="schema" />
|
|
5
|
+
</slot>
|
|
6
|
+
</NForm>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script lang="ts" setup>
|
|
10
|
+
import { flattenSchema, isArrayOfObjects } from "inibase/utils";
|
|
11
|
+
import type { FormInst } from "naive-ui";
|
|
12
|
+
import { debounce } from "~/composables";
|
|
13
|
+
import { normalizeTranslationValue } from "~/composables/translationValue";
|
|
14
|
+
|
|
15
|
+
const props = defineProps<{
|
|
16
|
+
// biome-ignore lint/correctness/noVueDuplicateKeys: false positive with translation POST body's `table` key
|
|
17
|
+
table?: string;
|
|
18
|
+
onBeforeCreate?: (data?: Item) => any;
|
|
19
|
+
onAfterCreate?: (data?: Item) => any;
|
|
20
|
+
onBeforeUpdate?: (data?: Item) => any;
|
|
21
|
+
onAfterUpdate?: (data?: Item) => any;
|
|
22
|
+
onBeforeDelete?: (data?: Item) => any;
|
|
23
|
+
onAfterDelete?: (data?: Item) => any;
|
|
24
|
+
}>();
|
|
25
|
+
|
|
26
|
+
const schema = defineModel<Schema>("schema", { default: () => reactive([]) });
|
|
27
|
+
defineSlots<{
|
|
28
|
+
default(data?: Item, schema?: Schema): any;
|
|
29
|
+
}>();
|
|
30
|
+
|
|
31
|
+
const modelValue = defineModel<Item>({ default: () => reactive({}) });
|
|
32
|
+
|
|
33
|
+
defineExpose<FormRef>({
|
|
34
|
+
create: CREATE,
|
|
35
|
+
update: UPDATE,
|
|
36
|
+
delete: DELETE,
|
|
37
|
+
schema: schema as unknown as Schema,
|
|
38
|
+
data: modelValue.value,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const config = useRuntimeConfig();
|
|
42
|
+
const Loading = useState<Record<string, boolean>>("Loading", () => ({}));
|
|
43
|
+
const route = useRoute();
|
|
44
|
+
const database = useState<Database>("database");
|
|
45
|
+
const table = useState<Table>("table");
|
|
46
|
+
|
|
47
|
+
function countItems(items: Schema): number {
|
|
48
|
+
return items.reduce((count, item) => {
|
|
49
|
+
return (
|
|
50
|
+
count +
|
|
51
|
+
1 +
|
|
52
|
+
(item.children && isArrayOfObjects(item.children)
|
|
53
|
+
? countItems(item.children)
|
|
54
|
+
: 0)
|
|
55
|
+
);
|
|
56
|
+
}, 0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function mergeItems(existing: Schema, updated: Schema): Schema {
|
|
60
|
+
const mergedSchema: Schema = [];
|
|
61
|
+
const customItemsIndex = existing
|
|
62
|
+
.map((item, index) =>
|
|
63
|
+
item.id === undefined ||
|
|
64
|
+
(typeof item.id === "string" && (item.id as string).startsWith("temp-"))
|
|
65
|
+
? index
|
|
66
|
+
: -1,
|
|
67
|
+
)
|
|
68
|
+
.filter((index) => index !== -1);
|
|
69
|
+
|
|
70
|
+
for (let index = 0; index < updated.length; index++) {
|
|
71
|
+
const item = updated[index];
|
|
72
|
+
if (!item) continue;
|
|
73
|
+
|
|
74
|
+
const existingItem = existing.find(
|
|
75
|
+
(_item, index) =>
|
|
76
|
+
!customItemsIndex.includes(index) && _item.id === item.id,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Fields that only exist in the updated schema (e.g. fields the server
|
|
80
|
+
// adds after a role change) have no counterpart to merge with — keep
|
|
81
|
+
// them as-is instead of dropping them from the rendered form.
|
|
82
|
+
if (!existingItem) {
|
|
83
|
+
mergedSchema.push(item);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (
|
|
88
|
+
item.children &&
|
|
89
|
+
isArrayOfObjects(item.children) &&
|
|
90
|
+
existingItem?.children &&
|
|
91
|
+
isArrayOfObjects(existingItem.children)
|
|
92
|
+
)
|
|
93
|
+
item.children = mergeItems(
|
|
94
|
+
existingItem.children as Schema,
|
|
95
|
+
item.children as Schema,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const prioritizedFields = {
|
|
99
|
+
options: existingItem.options ?? item.options,
|
|
100
|
+
type: existingItem.type ?? item.type,
|
|
101
|
+
render: existingItem.render ?? item.render,
|
|
102
|
+
extraButtons: existingItem.extraButtons ?? item.extraButtons,
|
|
103
|
+
extraActions: existingItem.extraActions ?? item.extraActions,
|
|
104
|
+
itemExtraButtons: existingItem.itemExtraButtons ?? item.itemExtraButtons,
|
|
105
|
+
itemExtraActions: existingItem.itemExtraActions ?? item.itemExtraActions,
|
|
106
|
+
onDelete: existingItem.onDelete ?? item.onDelete,
|
|
107
|
+
onCreate: existingItem.onCreate ?? item.onCreate,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
mergedSchema.push({
|
|
111
|
+
...existingItem,
|
|
112
|
+
...item,
|
|
113
|
+
...prioritizedFields,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (let index = 0; index < customItemsIndex.length; index++) {
|
|
118
|
+
const elementIndex = customItemsIndex[index];
|
|
119
|
+
|
|
120
|
+
if (elementIndex === undefined || !existing[elementIndex]) continue;
|
|
121
|
+
|
|
122
|
+
if (elementIndex === 0) mergedSchema.unshift(existing[elementIndex]);
|
|
123
|
+
else if (
|
|
124
|
+
existing[elementIndex - 1] &&
|
|
125
|
+
!updated.some((item) => item.key === existing[elementIndex]?.key)
|
|
126
|
+
) {
|
|
127
|
+
const prevItemIndex = mergedSchema.findIndex(
|
|
128
|
+
(item) => item.id === existing[elementIndex - 1]?.id,
|
|
129
|
+
);
|
|
130
|
+
if (prevItemIndex !== -1)
|
|
131
|
+
mergedSchema.splice(prevItemIndex + 1, 0, existing[elementIndex]);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return mergedSchema;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const Language = useScopedCookie<LanguagesType>(
|
|
139
|
+
"language",
|
|
140
|
+
database.value?.slug,
|
|
141
|
+
);
|
|
142
|
+
const PostSchemaResp = useState<
|
|
143
|
+
Record<string, apiResponse<{ schema: Schema; data: Item }>>
|
|
144
|
+
>("PostSchema", () => ({}));
|
|
145
|
+
const filterDefaultColumns = (field: Field) =>
|
|
146
|
+
!["id", "createdAt", "createdBy", "updatedAt", "updatedBy"].includes(
|
|
147
|
+
field.key,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const sessionID = useScopedCookie<string>("sid", database.value?.slug);
|
|
151
|
+
|
|
152
|
+
function hasFunctionsProperties(items: Schema): boolean {
|
|
153
|
+
return items.some((item) => {
|
|
154
|
+
if (
|
|
155
|
+
item.render !== undefined ||
|
|
156
|
+
item.itemExtraActions !== undefined ||
|
|
157
|
+
item.itemExtraButtons !== undefined ||
|
|
158
|
+
item.extraActions !== undefined ||
|
|
159
|
+
item.extraButtons !== undefined ||
|
|
160
|
+
item.onDelete !== undefined ||
|
|
161
|
+
item.onCreate !== undefined
|
|
162
|
+
)
|
|
163
|
+
return true;
|
|
164
|
+
|
|
165
|
+
if (item.children && isArrayOfObjects(item.children))
|
|
166
|
+
return hasFunctionsProperties(item.children as Schema);
|
|
167
|
+
|
|
168
|
+
return false;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const oldModelValue = ref();
|
|
172
|
+
let schemaFetchSequence = 0;
|
|
173
|
+
// Fetch schema and data dynamically from the correct endpoint
|
|
174
|
+
async function fetchSchemaAndData() {
|
|
175
|
+
const bodyContent = toRaw(modelValue.value);
|
|
176
|
+
const requestSequence = ++schemaFetchSequence;
|
|
177
|
+
|
|
178
|
+
let response: apiResponse<{ schema: Schema; data: Item }>;
|
|
179
|
+
let setSchema = false;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const currentPOSTSchemaResp =
|
|
183
|
+
PostSchemaResp.value[
|
|
184
|
+
props.table ?? table.value?.slug ?? route.params.table
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
if (
|
|
188
|
+
currentPOSTSchemaResp &&
|
|
189
|
+
(Object.keys(bodyContent).length === 0 ||
|
|
190
|
+
JSON.stringify(bodyContent) ===
|
|
191
|
+
JSON.stringify(currentPOSTSchemaResp?.result.data))
|
|
192
|
+
)
|
|
193
|
+
response = currentPOSTSchemaResp as apiResponse<{
|
|
194
|
+
schema: Schema;
|
|
195
|
+
data: Item;
|
|
196
|
+
}>;
|
|
197
|
+
else {
|
|
198
|
+
Loading.value.SCHEMA = true;
|
|
199
|
+
|
|
200
|
+
response = await $fetch<apiResponse<{ schema: Schema; data: Item }>>(
|
|
201
|
+
`${config.public.apiBase}${database.value.slug}/${props.table ?? table.value?.slug ?? route.params.table}/schema`,
|
|
202
|
+
{
|
|
203
|
+
method: bodyContent?.id ? "PUT" : "POST",
|
|
204
|
+
body: bodyContent,
|
|
205
|
+
params: {
|
|
206
|
+
locale: Language.value,
|
|
207
|
+
[`${database.value.slug}_sid`]: sessionID.value,
|
|
208
|
+
},
|
|
209
|
+
credentials: "include",
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
if (Object.keys(bodyContent).length === 0) setSchema = true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// A newer edit already started another request; don't let this
|
|
217
|
+
// stale response overwrite the schema/data the user is now seeing.
|
|
218
|
+
if (requestSequence !== schemaFetchSequence) return;
|
|
219
|
+
|
|
220
|
+
const currentSchema = schema.value.filter(filterDefaultColumns);
|
|
221
|
+
|
|
222
|
+
// Update the schema
|
|
223
|
+
if (response.result?.schema) {
|
|
224
|
+
response.result.schema =
|
|
225
|
+
response.result.schema.filter(filterDefaultColumns);
|
|
226
|
+
if (!currentSchema?.length) {
|
|
227
|
+
if (props.table) {
|
|
228
|
+
const targetTableSchema = database.value.tables
|
|
229
|
+
?.find(
|
|
230
|
+
({ slug }) =>
|
|
231
|
+
slug ===
|
|
232
|
+
(props.table ?? table.value?.slug ?? route.params.table),
|
|
233
|
+
)
|
|
234
|
+
?.schema?.filter(filterDefaultColumns);
|
|
235
|
+
if (targetTableSchema)
|
|
236
|
+
response.result.schema = mergeItems(
|
|
237
|
+
targetTableSchema,
|
|
238
|
+
response.result.schema,
|
|
239
|
+
);
|
|
240
|
+
} else if (table.value?.schema)
|
|
241
|
+
response.result.schema = mergeItems(
|
|
242
|
+
table.value.schema.filter(filterDefaultColumns),
|
|
243
|
+
response.result.schema,
|
|
244
|
+
);
|
|
245
|
+
} else if (
|
|
246
|
+
countItems(currentSchema) !== countItems(response.result.schema) ||
|
|
247
|
+
hasFunctionsProperties(currentSchema)
|
|
248
|
+
)
|
|
249
|
+
response.result.schema = mergeItems(
|
|
250
|
+
currentSchema,
|
|
251
|
+
response.result.schema,
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
if (hasFunctionsProperties(response.result.schema))
|
|
255
|
+
delete PostSchemaResp.value[
|
|
256
|
+
props.table ?? table.value?.slug ?? route.params.table
|
|
257
|
+
];
|
|
258
|
+
else if (setSchema)
|
|
259
|
+
PostSchemaResp.value[
|
|
260
|
+
props.table ?? table.value?.slug ?? route.params.table
|
|
261
|
+
] = response;
|
|
262
|
+
|
|
263
|
+
schema.value = response.result.schema;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Update data only if the API sends changes
|
|
267
|
+
if (response.result?.data) {
|
|
268
|
+
if (modelValue.value)
|
|
269
|
+
Object.assign(modelValue.value, response.result.data);
|
|
270
|
+
else modelValue.value = response.result.data;
|
|
271
|
+
}
|
|
272
|
+
} catch (error) {
|
|
273
|
+
// The /schema endpoint needs the network and can be unreachable when
|
|
274
|
+
// offline (device on Wi-Fi/LAN without internet, captive portal, etc.).
|
|
275
|
+
// Queuing an "error fetching schema" toast for that is pure noise — it
|
|
276
|
+
// would also re-fire on every debounced refetch while the user types.
|
|
277
|
+
//
|
|
278
|
+
// Render the form from the table's default schema (the cached database
|
|
279
|
+
// metadata, the same source the success path merges in) whenever there
|
|
280
|
+
// are no fields yet: an empty create drawer must never be blocked by an
|
|
281
|
+
// unreachable /schema call, and the create button stays clickable so the
|
|
282
|
+
// drawer can complete and close.
|
|
283
|
+
const defaultSchema = props.table
|
|
284
|
+
? database.value.tables?.find(
|
|
285
|
+
({ slug }) =>
|
|
286
|
+
slug === (props.table ?? table.value?.slug ?? route.params.table),
|
|
287
|
+
)?.schema
|
|
288
|
+
: table.value?.schema;
|
|
289
|
+
if (!schema.value.length && defaultSchema?.length)
|
|
290
|
+
schema.value = defaultSchema.filter(filterDefaultColumns);
|
|
291
|
+
|
|
292
|
+
// Network failures and junk (HTML-stub) responses are silent. Only
|
|
293
|
+
// genuine server errors (HTTP status, valid JSON API) get a toast.
|
|
294
|
+
if (!isNetworkError(error) && !isJunkResponse(error)) {
|
|
295
|
+
console.error("Error fetching schema:", error);
|
|
296
|
+
window.$message.error(t("errorFetchingSchema"));
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
} finally {
|
|
300
|
+
oldModelValue.value = JSON.parse(JSON.stringify(modelValue.value));
|
|
301
|
+
Loading.value.SCHEMA = false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Debounced version of fetchSchemaAndData
|
|
306
|
+
const debouncedFetchSchemaAndData = debounce(async () => {
|
|
307
|
+
await fetchSchemaAndData();
|
|
308
|
+
}, 1000);
|
|
309
|
+
|
|
310
|
+
// Debounce the change check itself so a fast typist doesn't pay the cost
|
|
311
|
+
// of deep-diffing the whole form object on every keystroke.
|
|
312
|
+
const debouncedCheckForChanges = debounce((v: Item) => {
|
|
313
|
+
if (JSON.stringify(oldModelValue.value) !== JSON.stringify(v))
|
|
314
|
+
debouncedFetchSchemaAndData();
|
|
315
|
+
}, 300);
|
|
316
|
+
|
|
317
|
+
// Trigger schema fetch on input changes with debounce
|
|
318
|
+
watch(
|
|
319
|
+
modelValue,
|
|
320
|
+
(v) => {
|
|
321
|
+
debouncedCheckForChanges(v);
|
|
322
|
+
},
|
|
323
|
+
{ deep: true, immediate: true },
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
onMounted(async () => {
|
|
327
|
+
// Save on Ctrl+S or Command+S
|
|
328
|
+
document.onkeydown = (e) => {
|
|
329
|
+
if (
|
|
330
|
+
!(
|
|
331
|
+
(e.ctrlKey || e.metaKey) &&
|
|
332
|
+
(e.key.toLowerCase() === "s" || e.key === "س")
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
return;
|
|
336
|
+
e.preventDefault();
|
|
337
|
+
if (modelValue.value?.id) UPDATE();
|
|
338
|
+
else CREATE();
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const formValidationRef = ref<FormInst>();
|
|
343
|
+
|
|
344
|
+
async function UPDATE() {
|
|
345
|
+
if (Loading.value.UPDATE) return;
|
|
346
|
+
|
|
347
|
+
formValidationRef.value?.validate(async (errors) => {
|
|
348
|
+
if (!errors) {
|
|
349
|
+
const bodyContent = toRaw(modelValue.value);
|
|
350
|
+
|
|
351
|
+
if (props.onBeforeUpdate)
|
|
352
|
+
modelValue.value = props.onBeforeUpdate(bodyContent);
|
|
353
|
+
|
|
354
|
+
Loading.value.UPDATE = true;
|
|
355
|
+
const data = await useOfflineFetch()
|
|
356
|
+
.request(
|
|
357
|
+
`${config.public.apiBase}${database.value.slug}/${
|
|
358
|
+
props.table ?? table.value?.slug ?? route.params.table
|
|
359
|
+
}/${bodyContent?.id}`,
|
|
360
|
+
{
|
|
361
|
+
method: "PUT",
|
|
362
|
+
body: bodyContent,
|
|
363
|
+
params: {
|
|
364
|
+
return: false,
|
|
365
|
+
locale: Language.value,
|
|
366
|
+
[`${database.value.slug}_sid`]: sessionID.value,
|
|
367
|
+
},
|
|
368
|
+
credentials: "include",
|
|
369
|
+
offline: {
|
|
370
|
+
database: database.value.slug,
|
|
371
|
+
table:
|
|
372
|
+
props.table ?? table.value?.slug ?? route.params.table ?? "",
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
)
|
|
376
|
+
.catch(() => {
|
|
377
|
+
window.$message.error(t("updateFailed"));
|
|
378
|
+
return null;
|
|
379
|
+
});
|
|
380
|
+
Loading.value.UPDATE = false;
|
|
381
|
+
|
|
382
|
+
if (isOfflineQueuedResult(data)) {
|
|
383
|
+
window.$message.warning(t("queuedOfflineToast"));
|
|
384
|
+
useOfflineSync().refreshCounts();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (
|
|
389
|
+
(typeof (data as any)?.result === "boolean" &&
|
|
390
|
+
(data as any).result === true) ||
|
|
391
|
+
(typeof (data as any)?.result !== "boolean" &&
|
|
392
|
+
(data as any)?.result?.id)
|
|
393
|
+
) {
|
|
394
|
+
window.$message.success((data as any).message);
|
|
395
|
+
if (props.onAfterUpdate) return props.onAfterUpdate(bodyContent);
|
|
396
|
+
} else window.$message.error((data as any).message);
|
|
397
|
+
} else window.$message.error(t("inputsAreInvalid"));
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function DELETE() {
|
|
402
|
+
if (Loading.value.DELETE) return;
|
|
403
|
+
|
|
404
|
+
const bodyContent = toRaw(modelValue.value);
|
|
405
|
+
|
|
406
|
+
if (props.onBeforeDelete) props.onBeforeDelete(bodyContent);
|
|
407
|
+
|
|
408
|
+
Loading.value.DELETE = true;
|
|
409
|
+
const data = await useOfflineFetch()
|
|
410
|
+
.request(
|
|
411
|
+
`${config.public.apiBase}${database.value.slug}/${
|
|
412
|
+
props.table ?? table.value?.slug ?? route.params.table
|
|
413
|
+
}/${bodyContent?.id}`,
|
|
414
|
+
{
|
|
415
|
+
method: "DELETE",
|
|
416
|
+
params: {
|
|
417
|
+
locale: Language.value,
|
|
418
|
+
[`${database.value.slug}_sid`]: sessionID.value,
|
|
419
|
+
},
|
|
420
|
+
credentials: "include",
|
|
421
|
+
offline: {
|
|
422
|
+
database: database.value.slug,
|
|
423
|
+
table: props.table ?? table.value?.slug ?? route.params.table ?? "",
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
)
|
|
427
|
+
.catch(() => {
|
|
428
|
+
window.$message.error(t("deleteFailed"));
|
|
429
|
+
return null;
|
|
430
|
+
});
|
|
431
|
+
Loading.value.DELETE = false;
|
|
432
|
+
|
|
433
|
+
if (isOfflineQueuedResult(data)) {
|
|
434
|
+
window.$message.warning(t("queuedOfflineToast"));
|
|
435
|
+
useOfflineSync().refreshCounts();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if ((data as any)?.result) {
|
|
440
|
+
window.$message.success((data as any).message);
|
|
441
|
+
await refreshNuxtData(
|
|
442
|
+
`${database.value.slug}/${props.table ?? table.value?.slug ?? route.params.table}`,
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
if (props.onAfterDelete) return props.onAfterDelete((data as any).result);
|
|
446
|
+
|
|
447
|
+
await navigateTo(
|
|
448
|
+
`${route.params.database ? `/${route.params.database}` : ""}/admin/tables/${props.table ?? table.value?.slug ?? route.params.table}`,
|
|
449
|
+
);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
window.$message.error((data as any)?.message);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Submit form data
|
|
456
|
+
async function CREATE() {
|
|
457
|
+
if (Loading.value.CREATE) return;
|
|
458
|
+
|
|
459
|
+
formValidationRef.value?.validate(async (errors) => {
|
|
460
|
+
if (!errors) {
|
|
461
|
+
const bodyContent = toRaw(modelValue.value);
|
|
462
|
+
|
|
463
|
+
if (props.onBeforeCreate)
|
|
464
|
+
modelValue.value = props.onBeforeCreate(bodyContent);
|
|
465
|
+
|
|
466
|
+
Loading.value.CREATE = true;
|
|
467
|
+
const data = await useOfflineFetch()
|
|
468
|
+
.request(
|
|
469
|
+
`${config.public.apiBase}${database.value.slug}/${props.table ?? table.value?.slug ?? route.params.table}`,
|
|
470
|
+
{
|
|
471
|
+
method: "POST",
|
|
472
|
+
body: bodyContent,
|
|
473
|
+
params: {
|
|
474
|
+
locale: Language.value,
|
|
475
|
+
[`${database.value.slug}_sid`]: sessionID.value,
|
|
476
|
+
},
|
|
477
|
+
credentials: "include",
|
|
478
|
+
offline: {
|
|
479
|
+
database: database.value.slug,
|
|
480
|
+
table:
|
|
481
|
+
props.table ?? table.value?.slug ?? route.params.table ?? "",
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
)
|
|
485
|
+
.catch(() => {
|
|
486
|
+
window.$message.error(t("createFailed"));
|
|
487
|
+
return null;
|
|
488
|
+
});
|
|
489
|
+
Loading.value.CREATE = false;
|
|
490
|
+
|
|
491
|
+
if (isOfflineQueuedResult(data)) {
|
|
492
|
+
window.$message.warning(t("queuedOfflineToast"));
|
|
493
|
+
useOfflineSync().refreshCounts();
|
|
494
|
+
// Re-read the (cached) table data so the new item shows up in
|
|
495
|
+
// the list — Table/index.vue merges queued creates into view.
|
|
496
|
+
await refreshNuxtData(
|
|
497
|
+
`${database.value.slug}/${props.table ?? table.value?.slug ?? route.params.table}`,
|
|
498
|
+
);
|
|
499
|
+
return navigateTo(
|
|
500
|
+
`${route.params.database ? `/${route.params.database}` : ""}/admin/tables/${props.table ?? table.value?.slug ?? route.params.table}`,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (!(data as any)?.result || !(data as any)?.result.id)
|
|
505
|
+
return window.$message.error((data as any)?.message);
|
|
506
|
+
|
|
507
|
+
// When creating an item in a secondary language, also post the
|
|
508
|
+
// entered values as translations for that language.
|
|
509
|
+
if (Language.value && Language.value !== database.value?.primaryLanguage)
|
|
510
|
+
await postTranslationsForNewItem(bodyContent, String(data.result.id));
|
|
511
|
+
|
|
512
|
+
window.$message.success(data.message);
|
|
513
|
+
await refreshNuxtData(
|
|
514
|
+
`${database.value.slug}/${props.table ?? table.value?.slug ?? route.params.table}`,
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
if (props.onAfterCreate) return props.onAfterCreate(data.result);
|
|
518
|
+
|
|
519
|
+
return navigateTo(
|
|
520
|
+
`${route.params.database ? `/${route.params.database}` : ""}/admin/tables/${props.table ?? table.value?.slug ?? route.params.table}/${data.result.id}/edit`,
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
window.$message.error(t("inputsAreInvalid"));
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ── Post translations when creating an item in a secondary language ──────────
|
|
528
|
+
|
|
529
|
+
const notTranslatableKeys = new Set([
|
|
530
|
+
"id",
|
|
531
|
+
"createdAt",
|
|
532
|
+
"createdBy",
|
|
533
|
+
"updatedAt",
|
|
534
|
+
"updatedBy",
|
|
535
|
+
"password",
|
|
536
|
+
"email",
|
|
537
|
+
"color",
|
|
538
|
+
"icon",
|
|
539
|
+
"link",
|
|
540
|
+
"role",
|
|
541
|
+
]);
|
|
542
|
+
|
|
543
|
+
const extraTranslatableKeys = new Set([
|
|
544
|
+
"url",
|
|
545
|
+
"table",
|
|
546
|
+
"asset",
|
|
547
|
+
"array-table",
|
|
548
|
+
"array-asset",
|
|
549
|
+
]);
|
|
550
|
+
|
|
551
|
+
const translatableTypes = new Set(["string", "text", "textarea", "html"]);
|
|
552
|
+
|
|
553
|
+
function isArrayField(field: Field): boolean {
|
|
554
|
+
const type = Array.isArray(field.type) ? field.type[0] : field.type;
|
|
555
|
+
return (
|
|
556
|
+
field.isArray === true ||
|
|
557
|
+
type === "array" ||
|
|
558
|
+
(type === "multiple" && field.subType !== "select")
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function isTranslatableField(field: Field): boolean {
|
|
563
|
+
const type = Array.isArray(field.type) ? field.type[0] : field.type;
|
|
564
|
+
const subType = field.subType;
|
|
565
|
+
const resolved = (subType ?? type) as string;
|
|
566
|
+
if (notTranslatableKeys.has(resolved)) return false;
|
|
567
|
+
if (["ids", "id"].includes(field.key)) return false;
|
|
568
|
+
if (translatableTypes.has(resolved)) return true;
|
|
569
|
+
if (extraTranslatableKeys.has(resolved)) return true;
|
|
570
|
+
if (isArrayField(field)) return true;
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function normalizeValue(field: Field, value: unknown): string {
|
|
575
|
+
// References are stored as their id(s) and arrays with Inison, matching the
|
|
576
|
+
// translation drawer so both writers produce identical records.
|
|
577
|
+
return normalizeTranslationValue(field, value);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function postTranslationsForNewItem(
|
|
581
|
+
bodyContent: Item,
|
|
582
|
+
newItemId: string,
|
|
583
|
+
) {
|
|
584
|
+
if (!table.value?.schema) return;
|
|
585
|
+
const fields = flattenSchema(table.value.schema, true).filter(
|
|
586
|
+
(field) => !field.key.includes(".") && isTranslatableField(field),
|
|
587
|
+
);
|
|
588
|
+
const ops: Promise<any>[] = [];
|
|
589
|
+
|
|
590
|
+
for (const field of fields) {
|
|
591
|
+
const raw = (bodyContent as any)[field.key];
|
|
592
|
+
const value = normalizeValue(field, raw);
|
|
593
|
+
if (!value) continue;
|
|
594
|
+
ops.push(
|
|
595
|
+
$fetch(`${config.public.apiBase}${database.value.slug}/translations`, {
|
|
596
|
+
method: "POST",
|
|
597
|
+
body: {
|
|
598
|
+
translation: value,
|
|
599
|
+
locale: Language.value,
|
|
600
|
+
table: table.value.id,
|
|
601
|
+
field: field.id,
|
|
602
|
+
item: newItemId,
|
|
603
|
+
},
|
|
604
|
+
params: {
|
|
605
|
+
locale: Language.value,
|
|
606
|
+
[`${database.value.slug}_sid`]: sessionID.value,
|
|
607
|
+
},
|
|
608
|
+
credentials: "include",
|
|
609
|
+
}),
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (ops.length) {
|
|
614
|
+
try {
|
|
615
|
+
await Promise.all(ops);
|
|
616
|
+
} catch (e) {
|
|
617
|
+
console.error("[Form] failed to post translations", e);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
</script>
|