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,348 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<FieldWrapper :field :rule v-model="modelValue">
|
|
3
|
+
<NSelect :placeholder="t(field.key)" :value="selectValue" @update:value="onUpdateSelectValue" :options="options" remote
|
|
4
|
+
clearable :filterable="!!searchIn && searchIn.length > 0" :loading="loading" :reset-menu-on-options-change="false"
|
|
5
|
+
:multiple="!!field.isArray" consistent-menu-width max-tag-count="responsive"
|
|
6
|
+
@update:show="(show) => show && loadOptions()" @scroll="handleScroll" @search="debouncedLoadOptions" v-bind="field.inputProps
|
|
7
|
+
? typeof field.inputProps === 'function'
|
|
8
|
+
? field.inputProps(modelValue) ?? {}
|
|
9
|
+
: field.inputProps
|
|
10
|
+
: {}">
|
|
11
|
+
<template #action v-if="table?.allowedMethods?.includes('c')">
|
|
12
|
+
<NFlex justify="center">
|
|
13
|
+
<NButton round strong secondary type="primary" @click="() => openDrawer(field.table as string)">
|
|
14
|
+
<template #icon>
|
|
15
|
+
<Icon name="tabler:plus" />
|
|
16
|
+
</template>
|
|
17
|
+
</NButton>
|
|
18
|
+
</NFlex>
|
|
19
|
+
</template>
|
|
20
|
+
</NSelect>
|
|
21
|
+
</FieldWrapper>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script lang="ts" setup>
|
|
25
|
+
import type { pageInfo } from "inibase";
|
|
26
|
+
import { isObject, isValidID } from "inibase/utils";
|
|
27
|
+
import Inison from "inison";
|
|
28
|
+
import type { FormItemRule } from "naive-ui";
|
|
29
|
+
import { debounce } from "~/composables";
|
|
30
|
+
|
|
31
|
+
const { field } = defineProps<{ field: Field }>();
|
|
32
|
+
const modelValue = defineModel<Item | Item[]>();
|
|
33
|
+
const options = ref<tableOption[] | undefined>();
|
|
34
|
+
const loading = ref(false);
|
|
35
|
+
const loadingMore = ref(false);
|
|
36
|
+
const database = useState<Database>("database");
|
|
37
|
+
|
|
38
|
+
// Monotonic token that invalidates stale responses from superseded requests.
|
|
39
|
+
let requestSeq = 0;
|
|
40
|
+
// The resolved query (search + where) the currently loaded options belong to.
|
|
41
|
+
let loadedQuery: string | undefined;
|
|
42
|
+
const table = database.value.tables?.find(({ slug }) => slug === field.table);
|
|
43
|
+
|
|
44
|
+
// Id-only model values (translated / stored table refs) are resolved through
|
|
45
|
+
// the shared cache which batches all ids of the current page into a single
|
|
46
|
+
// request per referenced table. Object values never trigger a fetch.
|
|
47
|
+
const referenced = useReferencedItems(field.table);
|
|
48
|
+
|
|
49
|
+
watch(
|
|
50
|
+
modelValue,
|
|
51
|
+
(value) => {
|
|
52
|
+
if (!value) return;
|
|
53
|
+
const entries = ([] as unknown[])
|
|
54
|
+
.concat(value)
|
|
55
|
+
.filter((entry) => entry !== undefined && entry !== null);
|
|
56
|
+
const objectItems = entries.filter(isObject) as Item[];
|
|
57
|
+
const idEntries = entries
|
|
58
|
+
.filter((entry) => !isObject(entry))
|
|
59
|
+
.map((entry) => String(entry).trim())
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
|
|
62
|
+
if (idEntries.length) referenced.register(idEntries);
|
|
63
|
+
|
|
64
|
+
// Resolved entries become select options: id-strings via the cache,
|
|
65
|
+
// objects as-is.
|
|
66
|
+
const resolvedOptions = [
|
|
67
|
+
...idEntries
|
|
68
|
+
.map((id) => referenced.items.value[id])
|
|
69
|
+
.filter((item): item is Item => Boolean(item?.id))
|
|
70
|
+
.map(singleOption),
|
|
71
|
+
...objectItems.map(singleOption),
|
|
72
|
+
];
|
|
73
|
+
if (!resolvedOptions.length) return;
|
|
74
|
+
if (!options.value) {
|
|
75
|
+
options.value = resolvedOptions;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const known = new Set(options.value.map(({ value }) => value));
|
|
79
|
+
const fresh = resolvedOptions.filter(({ value }) => !known.has(value));
|
|
80
|
+
if (fresh.length) options.value = [...options.value, ...fresh];
|
|
81
|
+
},
|
|
82
|
+
{ immediate: true },
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Rebuild id options once the batched fetch populates the cache.
|
|
86
|
+
watch(
|
|
87
|
+
() => referenced.items.value,
|
|
88
|
+
() => {
|
|
89
|
+
const value = modelValue.value;
|
|
90
|
+
if (!value) return;
|
|
91
|
+
const idEntries = ([] as unknown[])
|
|
92
|
+
.concat(value)
|
|
93
|
+
.filter((entry) => !isObject(entry))
|
|
94
|
+
.map((entry) => String(entry).trim())
|
|
95
|
+
.filter(Boolean);
|
|
96
|
+
if (!idEntries.length) return;
|
|
97
|
+
const idOptions = idEntries
|
|
98
|
+
.map((id) => referenced.items.value[id])
|
|
99
|
+
.filter((item): item is Item => Boolean(item?.id))
|
|
100
|
+
.map(singleOption);
|
|
101
|
+
if (!idOptions.length) return;
|
|
102
|
+
const known = new Set((options.value ?? []).map(({ value }) => value));
|
|
103
|
+
const fresh = idOptions.filter(({ value }) => !known.has(value));
|
|
104
|
+
if (fresh.length) options.value = [...(options.value ?? []), ...fresh];
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const selectValue = computed<null | string | string[]>(() => {
|
|
109
|
+
const value = modelValue.value as unknown;
|
|
110
|
+
if (value === undefined || value === null || value === "") return null;
|
|
111
|
+
const toID = (entry: unknown) =>
|
|
112
|
+
(isObject(entry) ? (entry as Item).id : entry) as string;
|
|
113
|
+
if (field.isArray)
|
|
114
|
+
// the search form stores selected ids as a comma joined string
|
|
115
|
+
return (Array.isArray(value) ? value : String(value).split(","))
|
|
116
|
+
.map(toID)
|
|
117
|
+
.filter(Boolean);
|
|
118
|
+
return toID(Array.isArray(value) ? value[0] : value) ?? null;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const rule: FormItemRule = {
|
|
122
|
+
trigger: ["blur", "change"],
|
|
123
|
+
type: !field.isArray ? "string" : "array",
|
|
124
|
+
required: field.required,
|
|
125
|
+
min: field.isArray ? field.min : undefined,
|
|
126
|
+
validator: async () => {
|
|
127
|
+
await nextTick();
|
|
128
|
+
return fieldValidator(field, modelValue.value);
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const config = useRuntimeConfig();
|
|
133
|
+
|
|
134
|
+
type tableOption = {
|
|
135
|
+
label: string;
|
|
136
|
+
value: string;
|
|
137
|
+
raw: Item;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
function singleOption(option: Item): tableOption {
|
|
141
|
+
return {
|
|
142
|
+
label: renderLabel(table, option),
|
|
143
|
+
value: option.id as string,
|
|
144
|
+
raw: option,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function onUpdateSelectValue(
|
|
149
|
+
_id: string | string[],
|
|
150
|
+
option: tableOption | tableOption[],
|
|
151
|
+
) {
|
|
152
|
+
modelValue.value = option
|
|
153
|
+
? Array.isArray(option)
|
|
154
|
+
? option.map(({ raw }) => raw)
|
|
155
|
+
: option.raw
|
|
156
|
+
: undefined;
|
|
157
|
+
await nextTick();
|
|
158
|
+
if (
|
|
159
|
+
options.value &&
|
|
160
|
+
modelValue.value &&
|
|
161
|
+
modelValue.value.length === options.value.length
|
|
162
|
+
)
|
|
163
|
+
options.value = options.value.filter(({ value }) =>
|
|
164
|
+
Array.isArray(_id) ? _id.includes(value) : _id === value,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const searchIn = table?.defaultSearchableColumns
|
|
169
|
+
? table.defaultSearchableColumns.map((columnID) =>
|
|
170
|
+
getPath(table.schema ?? [], columnID),
|
|
171
|
+
)
|
|
172
|
+
: field.searchIn;
|
|
173
|
+
|
|
174
|
+
const pagination = ref<pageInfo>();
|
|
175
|
+
const where = ref<string>();
|
|
176
|
+
|
|
177
|
+
const debouncedLoadOptions = debounce(async (searchValue) => {
|
|
178
|
+
await loadOptions(searchValue);
|
|
179
|
+
}, 1000);
|
|
180
|
+
|
|
181
|
+
const sessionID = useScopedCookie<string>("sid", database.value?.slug);
|
|
182
|
+
|
|
183
|
+
async function loadOptions(searchValue?: string | number) {
|
|
184
|
+
const seq = ++requestSeq;
|
|
185
|
+
const searchOrObject =
|
|
186
|
+
searchValue &&
|
|
187
|
+
(typeof searchValue !== "string" || searchValue.trim().length) &&
|
|
188
|
+
searchIn
|
|
189
|
+
? (searchIn.reduce((result, searchKey) => {
|
|
190
|
+
Object.assign(result, {
|
|
191
|
+
[searchKey]: `*%${searchValue}%`,
|
|
192
|
+
});
|
|
193
|
+
return result;
|
|
194
|
+
}, {}) ?? false)
|
|
195
|
+
: false;
|
|
196
|
+
|
|
197
|
+
let _where = "";
|
|
198
|
+
if (field.where) {
|
|
199
|
+
if (searchOrObject)
|
|
200
|
+
_where = Inison.stringify({
|
|
201
|
+
...((typeof field.where === "string"
|
|
202
|
+
? Inison.unstringify(
|
|
203
|
+
renderLabel({ ...(table as Table), label: field.where }),
|
|
204
|
+
)
|
|
205
|
+
: field.where) as any),
|
|
206
|
+
or: searchOrObject,
|
|
207
|
+
});
|
|
208
|
+
else
|
|
209
|
+
_where =
|
|
210
|
+
typeof field.where === "string"
|
|
211
|
+
? renderLabel({ ...(table as Table), label: field.where })
|
|
212
|
+
: renderLabel({
|
|
213
|
+
...(table as Table),
|
|
214
|
+
label: Inison.stringify(field.where),
|
|
215
|
+
});
|
|
216
|
+
} else if (searchOrObject)
|
|
217
|
+
_where = Inison.stringify({
|
|
218
|
+
or: searchOrObject,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Add direct ID search if the searchValue is a valid ID
|
|
222
|
+
if (searchValue && isValidID(searchValue))
|
|
223
|
+
_where = Inison.stringify({ id: searchValue });
|
|
224
|
+
|
|
225
|
+
// Already loaded for this query (e.g. the dropdown was re-opened) — keep
|
|
226
|
+
// the existing options instead of fetching and rebuilding the whole menu.
|
|
227
|
+
if (loadedQuery === _where && options.value !== undefined) {
|
|
228
|
+
loading.value = false;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
loadedQuery = _where;
|
|
233
|
+
where.value = _where || undefined;
|
|
234
|
+
pagination.value = undefined;
|
|
235
|
+
loading.value = true;
|
|
236
|
+
loadingMore.value = false;
|
|
237
|
+
|
|
238
|
+
const request = await $fetch<apiResponse<tableOption[]>>(
|
|
239
|
+
`${config.public.apiBase}${database.value.slug}/${field.table}`,
|
|
240
|
+
{
|
|
241
|
+
params: {
|
|
242
|
+
where: where.value,
|
|
243
|
+
options: Inison.stringify({
|
|
244
|
+
columns: table?.columns,
|
|
245
|
+
}),
|
|
246
|
+
[`${database.value.slug}_sid`]: sessionID.value,
|
|
247
|
+
},
|
|
248
|
+
cache: "no-cache",
|
|
249
|
+
credentials: "include",
|
|
250
|
+
},
|
|
251
|
+
).finally(() => {
|
|
252
|
+
if (seq === requestSeq) loading.value = false;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Ignore stale responses from superseded requests
|
|
256
|
+
if (seq !== requestSeq) return;
|
|
257
|
+
|
|
258
|
+
pagination.value = request.options;
|
|
259
|
+
|
|
260
|
+
if (modelValue.value) {
|
|
261
|
+
const currentSelectedOptions = (options.value ?? []).filter(({ value }) =>
|
|
262
|
+
selectValue.value?.includes(value),
|
|
263
|
+
);
|
|
264
|
+
options.value = [
|
|
265
|
+
...currentSelectedOptions,
|
|
266
|
+
...(request.result
|
|
267
|
+
?.map(singleOption)
|
|
268
|
+
.filter(({ value }) => !selectValue.value?.includes(value)) ?? []),
|
|
269
|
+
];
|
|
270
|
+
} else options.value = request.result?.map(singleOption) ?? [];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let lastScrollCall = 0;
|
|
274
|
+
function handleScroll(e: Event) {
|
|
275
|
+
const now = Date.now();
|
|
276
|
+
if (now - lastScrollCall < 120) return;
|
|
277
|
+
lastScrollCall = now;
|
|
278
|
+
|
|
279
|
+
const currentTarget = e.currentTarget as HTMLElement;
|
|
280
|
+
if (
|
|
281
|
+
loading.value ||
|
|
282
|
+
loadingMore.value ||
|
|
283
|
+
!pagination.value ||
|
|
284
|
+
!pagination.value.page ||
|
|
285
|
+
!pagination.value.totalPages
|
|
286
|
+
)
|
|
287
|
+
return;
|
|
288
|
+
if (
|
|
289
|
+
currentTarget.scrollTop + currentTarget.clientHeight >=
|
|
290
|
+
currentTarget.scrollHeight - 80 &&
|
|
291
|
+
pagination.value.page < pagination.value.totalPages
|
|
292
|
+
)
|
|
293
|
+
void loadMoreOptions();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function loadMoreOptions() {
|
|
297
|
+
if (
|
|
298
|
+
loading.value ||
|
|
299
|
+
loadingMore.value ||
|
|
300
|
+
!pagination.value ||
|
|
301
|
+
!pagination.value.page ||
|
|
302
|
+
!pagination.value.totalPages ||
|
|
303
|
+
pagination.value.page >= pagination.value.totalPages
|
|
304
|
+
)
|
|
305
|
+
return;
|
|
306
|
+
|
|
307
|
+
loadingMore.value = true;
|
|
308
|
+
const page = pagination.value.page + 1;
|
|
309
|
+
const seq = requestSeq;
|
|
310
|
+
|
|
311
|
+
const request = await $fetch<apiResponse<tableOption[]>>(
|
|
312
|
+
`${config.public.apiBase}${database.value.slug}/${field.table}`,
|
|
313
|
+
{
|
|
314
|
+
params: {
|
|
315
|
+
where: where.value,
|
|
316
|
+
options: Inison.stringify({
|
|
317
|
+
page,
|
|
318
|
+
columns: table?.columns,
|
|
319
|
+
}),
|
|
320
|
+
[`${database.value.slug}_sid`]: sessionID.value,
|
|
321
|
+
},
|
|
322
|
+
cache: "no-cache",
|
|
323
|
+
credentials: "include",
|
|
324
|
+
},
|
|
325
|
+
).finally(() => {
|
|
326
|
+
if (seq === requestSeq) loadingMore.value = false;
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Ignore stale responses from superseded requests
|
|
330
|
+
if (seq !== requestSeq) return;
|
|
331
|
+
|
|
332
|
+
if (request.result) request.result = request.result.map(singleOption);
|
|
333
|
+
pagination.value = request.options;
|
|
334
|
+
if (options.value && request.result) {
|
|
335
|
+
const known = new Set(options.value.map(({ value }) => value));
|
|
336
|
+
options.value.push(
|
|
337
|
+
...request.result.filter(({ value }) => !known.has(value)),
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (
|
|
343
|
+
field.inputProps &&
|
|
344
|
+
typeof field.inputProps === "object" &&
|
|
345
|
+
field.inputProps.show
|
|
346
|
+
)
|
|
347
|
+
loadOptions();
|
|
348
|
+
</script>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<FieldWrapper ref="dynamicTags" :field :rule>
|
|
3
|
+
<NFlex align="center" :wrap="true">
|
|
4
|
+
<NTag v-for="(value, index) in field.defaultValue" :key="'default-' + index" round>
|
|
5
|
+
{{ t(value) }}
|
|
6
|
+
</NTag>
|
|
7
|
+
<NTag v-for="(value, index) in nonDefaultValues" :key="'user-' + index" round closable
|
|
8
|
+
@close="removeValue(index)">
|
|
9
|
+
{{ value }}
|
|
10
|
+
</NTag>
|
|
11
|
+
<NInput v-if="showInput" ref="inputRef" v-model:value="inputValue" size="small" round autofocus autosize style="min-width: 150px"
|
|
12
|
+
:placeholder="t(field.key)" clearable @keyup.enter="addValue" @keyup.esc="cancel"
|
|
13
|
+
@blur="cancel" v-bind="field.inputProps
|
|
14
|
+
? typeof field.inputProps === 'function'
|
|
15
|
+
? field.inputProps(modelValue) ?? {}
|
|
16
|
+
: field.inputProps
|
|
17
|
+
: {}">
|
|
18
|
+
<template #suffix>
|
|
19
|
+
<NTooltip :disabled="!fieldChildrenLabels" :delay="1500">
|
|
20
|
+
<template #trigger>
|
|
21
|
+
<component :is="getField(field).icon" />
|
|
22
|
+
</template>
|
|
23
|
+
<template v-if="fieldChildrenLabels" #default>
|
|
24
|
+
{{ t('thisInputSupports') }}: {{ fieldChildrenLabels }}
|
|
25
|
+
</template>
|
|
26
|
+
</NTooltip>
|
|
27
|
+
</template>
|
|
28
|
+
</NInput>
|
|
29
|
+
<NButton v-if="!showInput" type="primary" size="small" dashed :disabled="disabled" @click="showInput = true">
|
|
30
|
+
<template #icon>
|
|
31
|
+
<Icon name="tabler:plus" />
|
|
32
|
+
</template>
|
|
33
|
+
</NButton>
|
|
34
|
+
</NFlex>
|
|
35
|
+
</FieldWrapper>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<script lang="ts" setup>
|
|
39
|
+
import type { FormInst, FormItemRule, InputInst } from "naive-ui"
|
|
40
|
+
|
|
41
|
+
import { validateFieldType } from "inibase/utils"
|
|
42
|
+
import { NTag } from "#components"
|
|
43
|
+
|
|
44
|
+
const { field } = defineProps<{ field: Field }>()
|
|
45
|
+
|
|
46
|
+
const modelValue = defineModel<(string | number)[]>()
|
|
47
|
+
|
|
48
|
+
const showInput = ref(false)
|
|
49
|
+
const inputValue = ref()
|
|
50
|
+
const inputRef = ref<InputInst>()
|
|
51
|
+
|
|
52
|
+
const disabled = computed(() => field.max ? (modelValue.value?.length ?? 0) >= field.max : false)
|
|
53
|
+
|
|
54
|
+
const nonDefaultValues = computed(() =>
|
|
55
|
+
modelValue.value?.filter((value) => !field.defaultValue || !field.defaultValue.includes(value)) ?? [],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const fieldChildrenLabels = field.children
|
|
59
|
+
? flatFieldsList()
|
|
60
|
+
.filter(({ key }) =>
|
|
61
|
+
([] as string[])
|
|
62
|
+
.concat(field.children as string | string[])
|
|
63
|
+
.includes(key),
|
|
64
|
+
)
|
|
65
|
+
.map(({ label }) => label)
|
|
66
|
+
.join(" | ")
|
|
67
|
+
: undefined
|
|
68
|
+
|
|
69
|
+
const rule: FormItemRule = {
|
|
70
|
+
type: "array",
|
|
71
|
+
required: field.required,
|
|
72
|
+
min: field.min,
|
|
73
|
+
max: field.max,
|
|
74
|
+
validator: async () => {
|
|
75
|
+
await nextTick()
|
|
76
|
+
return fieldValidator(field, modelValue.value, validateFieldType)
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
const dynamicTags = ref<FormInst>()
|
|
80
|
+
|
|
81
|
+
function addValue() {
|
|
82
|
+
if (inputValue.value) {
|
|
83
|
+
modelValue.value = [...(modelValue.value ?? []), inputValue.value]
|
|
84
|
+
inputValue.value = null
|
|
85
|
+
inputRef.value?.focus()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function cancel() {
|
|
90
|
+
showInput.value = false
|
|
91
|
+
inputValue.value = null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function removeValue(index: number) {
|
|
95
|
+
const nonDefault = modelValue.value?.filter(
|
|
96
|
+
(value) => !field.defaultValue || !field.defaultValue.includes(value),
|
|
97
|
+
) ?? []
|
|
98
|
+
const valueToRemove = nonDefault[index]
|
|
99
|
+
modelValue.value = modelValue.value?.filter((v) => v !== valueToRemove) ?? []
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
watch(modelValue, () => {
|
|
103
|
+
try {
|
|
104
|
+
dynamicTags.value?.validate()
|
|
105
|
+
} catch {}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
watch(showInput, (value) => {
|
|
109
|
+
if (value) {
|
|
110
|
+
nextTick(() => inputRef.value?.focus())
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
</script>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<FieldWrapper :field :rule v-model="modelValue">
|
|
3
|
+
<NInput :value="modelValue !== undefined && modelValue !== null ? String(modelValue) : null"
|
|
4
|
+
@update:value="(value) => modelValue = value" :placeholder="t(field.key)" clearable v-bind="field.inputProps
|
|
5
|
+
? typeof field.inputProps === 'function'
|
|
6
|
+
? field.inputProps(modelValue) ?? {}
|
|
7
|
+
: field.inputProps
|
|
8
|
+
: {}">
|
|
9
|
+
<template #suffix>
|
|
10
|
+
<component :is="getField(field).icon" />
|
|
11
|
+
</template>
|
|
12
|
+
</NInput>
|
|
13
|
+
</FieldWrapper>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script lang="ts" setup>
|
|
17
|
+
import type { FormItemRule } from "naive-ui"
|
|
18
|
+
|
|
19
|
+
const { field } = defineProps<{ field: Field }>()
|
|
20
|
+
|
|
21
|
+
const modelValue = defineModel<string | number>()
|
|
22
|
+
|
|
23
|
+
const rule: FormItemRule = {
|
|
24
|
+
required: field.required,
|
|
25
|
+
trigger: ["blur", "input"],
|
|
26
|
+
validator: async () => {
|
|
27
|
+
await nextTick()
|
|
28
|
+
return fieldValidator(field, modelValue.value)
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<FieldWrapper :field :rule v-model="modelValue">
|
|
3
|
+
<NInput v-model:value="modelValue" :placeholder="t(field.key)" clearable show-count type="textarea"
|
|
4
|
+
:rows="field.isTable ? 1 : undefined" :autosize="field.isTable ? false : {
|
|
5
|
+
minRows: 3,
|
|
6
|
+
}" v-bind="field.inputProps
|
|
7
|
+
? typeof field.inputProps === 'function'
|
|
8
|
+
? field.inputProps(modelValue) ?? {}
|
|
9
|
+
: field.inputProps
|
|
10
|
+
: {}">
|
|
11
|
+
<template #suffix>
|
|
12
|
+
<component :is="getField(field).icon" />
|
|
13
|
+
</template>
|
|
14
|
+
</NInput>
|
|
15
|
+
</FieldWrapper>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script lang="ts" setup>
|
|
19
|
+
import type { FormItemRule } from "naive-ui"
|
|
20
|
+
|
|
21
|
+
const { field } = defineProps<{ field: Field }>()
|
|
22
|
+
|
|
23
|
+
const modelValue = defineModel<string>()
|
|
24
|
+
|
|
25
|
+
const rule: FormItemRule = {
|
|
26
|
+
trigger: ["blur", "input"],
|
|
27
|
+
required: field.required,
|
|
28
|
+
validator: async () => {
|
|
29
|
+
await nextTick()
|
|
30
|
+
return fieldValidator(field, modelValue.value)
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
</script>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<FieldWrapper :field :rule v-model="modelValue">
|
|
3
|
+
<NInput v-model:value="modelValue" :placeholder="t(field.key)" clearable v-bind="field.inputProps
|
|
4
|
+
? typeof field.inputProps === 'function'
|
|
5
|
+
? { ...(field.inputProps(modelValue) ?? {}), type: 'url' }
|
|
6
|
+
: { ...field.inputProps, type: 'url' }
|
|
7
|
+
: { type: 'url' }">
|
|
8
|
+
<template #suffix>
|
|
9
|
+
<component :is="getField(field).icon" />
|
|
10
|
+
</template>
|
|
11
|
+
</NInput>
|
|
12
|
+
</FieldWrapper>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script lang="ts" setup>
|
|
16
|
+
import { isURL } from "inibase/utils"
|
|
17
|
+
import type { FormItemRule } from "naive-ui"
|
|
18
|
+
|
|
19
|
+
const { field } = defineProps<{ field: Field }>()
|
|
20
|
+
|
|
21
|
+
const modelValue = defineModel<string>()
|
|
22
|
+
|
|
23
|
+
// Accept real URLs (isURL already covers http(s), tel:, mailto:, #anchor) as
|
|
24
|
+
// well as site-relative paths (e.g. "/about") — isURL alone rejects those.
|
|
25
|
+
// A relative path must start with "/" and contain no whitespace/control
|
|
26
|
+
// characters (still allows non-ASCII slugs, e.g. "/منتجات").
|
|
27
|
+
const relativePathPattern = /^\/[^\s\x00-\x1f]*$/
|
|
28
|
+
const isURLOrRelativePath = (input: unknown) =>
|
|
29
|
+
typeof input === "string" &&
|
|
30
|
+
(relativePathPattern.test(input) || isURL(input))
|
|
31
|
+
|
|
32
|
+
const rule: FormItemRule = {
|
|
33
|
+
trigger: ["blur", "input"],
|
|
34
|
+
required: field.required,
|
|
35
|
+
validator: async () => {
|
|
36
|
+
await nextTick()
|
|
37
|
+
return fieldValidator(field, modelValue.value, isURLOrRelativePath)
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<NFormItem :id="field.id" :label="resolvedLabel" :rule :path="String(field.id)"
|
|
3
|
+
:style="{ flex: fieldFlex }" v-bind="field.labelProps">
|
|
4
|
+
<slot></slot>
|
|
5
|
+
<template #label>
|
|
6
|
+
<NFlex v-if="field.description" align="center" :size="0">
|
|
7
|
+
<NDropdown
|
|
8
|
+
:disabled="([] as string[]).concat(field.type).every(type => !['table', 'array', 'date'].includes(type))"
|
|
9
|
+
show-arrow placement="top" trigger="hover" :delay="800" :options="dropdownOptions"
|
|
10
|
+
@select="handleSelect">
|
|
11
|
+
{{ resolvedLabel }}
|
|
12
|
+
</NDropdown>
|
|
13
|
+
<NTooltip>
|
|
14
|
+
<template #trigger>
|
|
15
|
+
<NButton circle text size="tiny">
|
|
16
|
+
<template #icon>
|
|
17
|
+
<Icon name="tabler:question-mark" />
|
|
18
|
+
</template>
|
|
19
|
+
</NButton>
|
|
20
|
+
</template>
|
|
21
|
+
{{ t(field.description) }}
|
|
22
|
+
</NTooltip>
|
|
23
|
+
</NFlex>
|
|
24
|
+
<template v-else>
|
|
25
|
+
<NDropdown
|
|
26
|
+
:disabled="([] as string[]).concat(field.type).every(type => !['table', 'array', 'date'].includes(type))"
|
|
27
|
+
show-arrow placement="top" trigger="hover" :delay="1500" :options="dropdownOptions" size="small"
|
|
28
|
+
@select="handleSelect">
|
|
29
|
+
{{ resolvedLabel }}
|
|
30
|
+
</NDropdown>
|
|
31
|
+
</template>
|
|
32
|
+
<slot name="label"></slot>
|
|
33
|
+
</template>
|
|
34
|
+
</NFormItem>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script lang="ts" setup>
|
|
38
|
+
import { isObject, isStringified } from "inibase/utils";
|
|
39
|
+
import Inison from "inison";
|
|
40
|
+
import type { DropdownOption, FormItemRule } from "naive-ui";
|
|
41
|
+
import { Icon, NIcon } from "#components";
|
|
42
|
+
|
|
43
|
+
const { field, rule } = defineProps<{ field: Field; rule: FormItemRule }>();
|
|
44
|
+
|
|
45
|
+
const modelValue = defineModel<any>();
|
|
46
|
+
const resolvedLabel = computed(() =>
|
|
47
|
+
field.labelKey ? t(field.labelKey) : (field.label ?? t(field.key)),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const { isMobile } = useDevice();
|
|
51
|
+
function numberToPercentage(width: number, wrapperWidth?: string | number) {
|
|
52
|
+
if (wrapperWidth && typeof wrapperWidth !== "string") {
|
|
53
|
+
if (wrapperWidth < 350) return "100";
|
|
54
|
+
if (wrapperWidth < 500) return "50";
|
|
55
|
+
if (wrapperWidth < 650 && width !== 2) return "33.33";
|
|
56
|
+
}
|
|
57
|
+
switch (width) {
|
|
58
|
+
case 2:
|
|
59
|
+
return "50";
|
|
60
|
+
case 3:
|
|
61
|
+
return "33.33";
|
|
62
|
+
case 4:
|
|
63
|
+
return "25";
|
|
64
|
+
case 5:
|
|
65
|
+
return "20";
|
|
66
|
+
case 6:
|
|
67
|
+
return "16.66";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const Drawers = useState<DrawerRef>("drawers", () => []);
|
|
71
|
+
const fieldFlex = computed(() =>
|
|
72
|
+
field.width && field.width !== 1 && !isMobile
|
|
73
|
+
? `1 1 calc(${numberToPercentage(field.width, Drawers.value.find(({ show }) => !!show)?.width)}% - 12px)`
|
|
74
|
+
: "1 1 100%",
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
async function handleSelect(value: string) {
|
|
78
|
+
switch (value) {
|
|
79
|
+
case "copy": {
|
|
80
|
+
if (!modelValue.value) return;
|
|
81
|
+
await copyToClipboard(Inison.stringify(modelValue.value));
|
|
82
|
+
window.$message.success(t("copiedSuccessfully"));
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case "paste": {
|
|
86
|
+
try {
|
|
87
|
+
const itemFromClipboard = await navigator.clipboard.readText();
|
|
88
|
+
|
|
89
|
+
if (!itemFromClipboard) {
|
|
90
|
+
window.$message.error(t("clipboardEmpty"));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (!isStringified(itemFromClipboard)) {
|
|
94
|
+
window.$message.error(t("clipboardItemIsNotCorrect"));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const unstringifiedItem = Inison.unstringify<Item[]>(itemFromClipboard);
|
|
99
|
+
if (
|
|
100
|
+
!unstringifiedItem &&
|
|
101
|
+
!Array.isArray(unstringifiedItem) &&
|
|
102
|
+
!isObject(unstringifiedItem)
|
|
103
|
+
) {
|
|
104
|
+
window.$message.error(t("clipboardItemIsNotCorrect"));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
modelValue.value = unstringifiedItem;
|
|
109
|
+
} catch {
|
|
110
|
+
window.$message.error(t("clipboardItemIsNotCorrect"));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const dropdownOptions = computed<DropdownOption[]>(() => [
|
|
116
|
+
{
|
|
117
|
+
label: t("copyItem"),
|
|
118
|
+
key: "copy",
|
|
119
|
+
show: !!modelValue.value,
|
|
120
|
+
icon: () => h(NIcon, () => h(Icon, { name: "tabler:copy" })),
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
label: t("pasteItem"),
|
|
124
|
+
key: "paste",
|
|
125
|
+
icon: () => h(NIcon, () => h(Icon, { name: "tabler:clipboard" })),
|
|
126
|
+
},
|
|
127
|
+
]);
|
|
128
|
+
</script>
|