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,256 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<NPopover style="max-height: 240px;" :style="`width: ${isMobile ? '100vw' : '500px'}`" placement="bottom-end"
|
|
3
|
+
scrollable v-model:show="isSearchPopoverVisible" :x="isMobile ? 0 : undefined" :y="isMobile ? 180 : undefined"
|
|
4
|
+
:show-arrow="!isMobile" trigger="click">
|
|
5
|
+
<template #trigger>
|
|
6
|
+
<NTooltip :delay="1500">
|
|
7
|
+
<template #trigger>
|
|
8
|
+
<NButton secondary round :size="props.size" :tertiary="isSearchPopoverVisible"
|
|
9
|
+
:type="isSearchPopoverVisible ? 'primary' : undefined">
|
|
10
|
+
<template #icon>
|
|
11
|
+
<NIcon>
|
|
12
|
+
<Icon v-if="isSearchPopoverVisible" name="tabler:x" />
|
|
13
|
+
<Icon v-else name="tabler:search" />
|
|
14
|
+
</NIcon>
|
|
15
|
+
</template>
|
|
16
|
+
</NButton>
|
|
17
|
+
</template>
|
|
18
|
+
{{ t("search") }}
|
|
19
|
+
</NTooltip>
|
|
20
|
+
</template>
|
|
21
|
+
<template #footer>
|
|
22
|
+
<NFlex justify="space-between">
|
|
23
|
+
<NTooltip :delay="500" placement="bottom">
|
|
24
|
+
<template #trigger>
|
|
25
|
+
<NPopover trigger="click" scrollable style="max-height: 300px;">
|
|
26
|
+
<template #trigger>
|
|
27
|
+
<NButton round type="success" size="small" secondary>
|
|
28
|
+
<template #icon>
|
|
29
|
+
<NIcon>
|
|
30
|
+
<Icon name="tabler:bookmark" />
|
|
31
|
+
</NIcon>
|
|
32
|
+
</template>
|
|
33
|
+
</NButton>
|
|
34
|
+
</template>
|
|
35
|
+
<template #footer>
|
|
36
|
+
<NInputGroup style="width: 100%;">
|
|
37
|
+
<NInput v-model:value="newFilterName" :placeholder="t('filterName')" size="small"
|
|
38
|
+
round
|
|
39
|
+
@keyup.enter="() => { saveFavoriteFilter(newFilterName); newFilterName = '' }" />
|
|
40
|
+
<NButton round type="success" secondary size="small" :loading="false"
|
|
41
|
+
:disabled="!newFilterName.trim()"
|
|
42
|
+
@click="() => { saveFavoriteFilter(newFilterName); newFilterName = '' }">
|
|
43
|
+
<template #icon>
|
|
44
|
+
<NIcon>
|
|
45
|
+
<Icon name="tabler:plus" />
|
|
46
|
+
</NIcon>
|
|
47
|
+
</template>
|
|
48
|
+
</NButton>
|
|
49
|
+
</NInputGroup>
|
|
50
|
+
</template>
|
|
51
|
+
<NEmpty v-if="favoriteFilters.length === 0" size="small"
|
|
52
|
+
:description="t('noFavoriteFilters')" />
|
|
53
|
+
<NFlex v-else vertical size="small" style="padding: 0 8px;">
|
|
54
|
+
<NFlex justify="space-between" v-for="filter in favoriteFilters" :key="filter.id" block
|
|
55
|
+
quaternary @click="loadFavoriteFilter(filter)" style="cursor: pointer;">
|
|
56
|
+
<template #default>
|
|
57
|
+
<NFlex justify="space-between" style="width: 100%;">
|
|
58
|
+
<span>{{ filter.name }}</span>
|
|
59
|
+
<NButton circle size="tiny" type="error" text
|
|
60
|
+
@click.stop="deleteFavoriteFilter(filter.id)">
|
|
61
|
+
<template #icon>
|
|
62
|
+
<NIcon>
|
|
63
|
+
<Icon name="tabler:trash" />
|
|
64
|
+
</NIcon>
|
|
65
|
+
</template>
|
|
66
|
+
</NButton>
|
|
67
|
+
</NFlex>
|
|
68
|
+
</template>
|
|
69
|
+
</NFlex>
|
|
70
|
+
</NFlex>
|
|
71
|
+
</NPopover>
|
|
72
|
+
</template>
|
|
73
|
+
{{ t("favoriteFilters") }}
|
|
74
|
+
</NTooltip>
|
|
75
|
+
|
|
76
|
+
<NButtonGroup>
|
|
77
|
+
<NButton round type="error" secondary size="small" :loading="Loading.data"
|
|
78
|
+
:disabled="props.disabled" @click="handleReset">
|
|
79
|
+
<template #icon>
|
|
80
|
+
<NIcon>
|
|
81
|
+
<Icon name="tabler:x" />
|
|
82
|
+
</NIcon>
|
|
83
|
+
</template>
|
|
84
|
+
{{ t("reset") }}
|
|
85
|
+
</NButton>
|
|
86
|
+
<NButton round type="primary" secondary size="small" :loading="Loading.data"
|
|
87
|
+
:disabled="props.disabled" @click="handleSearch">
|
|
88
|
+
<template #icon>
|
|
89
|
+
<NIcon>
|
|
90
|
+
<Icon name="tabler:search" />
|
|
91
|
+
</NIcon>
|
|
92
|
+
</template>
|
|
93
|
+
{{ t("search") }}
|
|
94
|
+
</NButton>
|
|
95
|
+
</NButtonGroup>
|
|
96
|
+
</NFlex>
|
|
97
|
+
</template>
|
|
98
|
+
<LazyTableSearch v-model="localSearchArray" :callback="handleSearch" v-model:schema="schema" />
|
|
99
|
+
</NPopover>
|
|
100
|
+
</template>
|
|
101
|
+
|
|
102
|
+
<script setup lang="ts">
|
|
103
|
+
import Inison from "inison";
|
|
104
|
+
import {
|
|
105
|
+
Icon,
|
|
106
|
+
NButton,
|
|
107
|
+
NButtonGroup,
|
|
108
|
+
NIcon,
|
|
109
|
+
NPopover,
|
|
110
|
+
NTooltip,
|
|
111
|
+
NFlex,
|
|
112
|
+
LazyTableSearch,
|
|
113
|
+
NEmpty,
|
|
114
|
+
NInput,
|
|
115
|
+
NInputGroup,
|
|
116
|
+
} from "#components";
|
|
117
|
+
import { deepClone } from "~/composables";
|
|
118
|
+
import { generateSearchString } from "~/composables/search";
|
|
119
|
+
|
|
120
|
+
const props = withDefaults(
|
|
121
|
+
defineProps<{
|
|
122
|
+
size?: "small" | "medium" | "large";
|
|
123
|
+
disabled?: boolean;
|
|
124
|
+
}>(),
|
|
125
|
+
{
|
|
126
|
+
disabled: false,
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const schema = defineModel<Schema>("schema");
|
|
131
|
+
|
|
132
|
+
const searchString = defineModel<string | undefined>("string");
|
|
133
|
+
const searchArray = defineModel<searchType>("array", {
|
|
134
|
+
default: () => reactive({ and: [[null, "=", null]] }),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const config = useRuntimeConfig();
|
|
138
|
+
const { isMobile } = useDevice();
|
|
139
|
+
const user = useState<User>("user");
|
|
140
|
+
const database = useState<Database>("database");
|
|
141
|
+
const table = useState<Table>("table");
|
|
142
|
+
const sessionID = useState<string>("sessionID");
|
|
143
|
+
const Loading = useState<Record<string, boolean>>("Loading", () => ({}));
|
|
144
|
+
|
|
145
|
+
const isSearchPopoverVisible = ref(false);
|
|
146
|
+
const newFilterName = ref("");
|
|
147
|
+
|
|
148
|
+
// Local working copy of search array
|
|
149
|
+
const localSearchArray = ref<searchType>(deepClone(searchArray.value) ?? { and: [[null, "=", null]] });
|
|
150
|
+
|
|
151
|
+
// Sync local copy when external searchArray changes (without triggering search)
|
|
152
|
+
watch(
|
|
153
|
+
() => searchArray.value,
|
|
154
|
+
(newVal) => {
|
|
155
|
+
localSearchArray.value = deepClone(newVal) as searchType;
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Convert searchArray back to string for emit
|
|
160
|
+
function emitSearchQuery(locSearchArray: searchType) {
|
|
161
|
+
const searchInput = generateSearchString(locSearchArray, "display");
|
|
162
|
+
searchArray.value = locSearchArray;
|
|
163
|
+
searchString.value = searchInput ? Inison.stringify(searchInput as any) : undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Handle search button click
|
|
167
|
+
function handleSearch() {
|
|
168
|
+
emitSearchQuery(localSearchArray.value);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Handle reset button click
|
|
172
|
+
function handleReset() {
|
|
173
|
+
const resetArray = { and: [[null, "=", null]] } as searchType;
|
|
174
|
+
localSearchArray.value = resetArray;
|
|
175
|
+
emitSearchQuery(resetArray);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Get favorite filters for current table
|
|
179
|
+
const favoriteFilters = computed({
|
|
180
|
+
get: () => {
|
|
181
|
+
const filters = user.value?.config?.filters ?? {};
|
|
182
|
+
return (filters as Record<string, any>)[table.value?.slug ?? ""] ?? [];
|
|
183
|
+
},
|
|
184
|
+
set: (v) => {
|
|
185
|
+
updateFavoriteFilters(v);
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Update favorite filters in user config
|
|
190
|
+
async function updateFavoriteFilters(filters: any[]) {
|
|
191
|
+
if (!user.value || !table.value || !database.value) return;
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const allFilters = { ...(user.value?.config?.filters ?? {}) };
|
|
195
|
+
allFilters[table.value.slug] = filters;
|
|
196
|
+
const updatedUser = {
|
|
197
|
+
...user.value,
|
|
198
|
+
config: { ...(user.value.config ?? {}), filters: allFilters },
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
await $fetch(
|
|
202
|
+
`${config.public.apiBase}${database.value.slug}/users/${user.value.id}`,
|
|
203
|
+
{
|
|
204
|
+
method: "PUT",
|
|
205
|
+
body: updatedUser,
|
|
206
|
+
params: { [`${database.value.slug}_sid`]: sessionID.value },
|
|
207
|
+
credentials: "include",
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
} catch (err: any) {
|
|
211
|
+
window.$message.error(err.message);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ==================== FAVORITE FILTERS LOGIC ====================
|
|
216
|
+
|
|
217
|
+
async function saveFavoriteFilter(filterName: string) {
|
|
218
|
+
if (!filterName.trim()) {
|
|
219
|
+
window.$message.error(t("filterNameCannotBeEmpty"));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (favoriteFilters.value.some((f: any) => f.name === filterName)) {
|
|
224
|
+
window.$message.error(t("filterNameAlreadyExists"));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const newFilter = {
|
|
229
|
+
id: Date.now().toString(),
|
|
230
|
+
name: filterName,
|
|
231
|
+
searchArray: deepClone(localSearchArray.value),
|
|
232
|
+
createdAt: new Date().toISOString(),
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const updatedFilters = [...favoriteFilters.value, newFilter];
|
|
236
|
+
favoriteFilters.value = updatedFilters;
|
|
237
|
+
window.$message.success(t("filterSavedSuccessfully"));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function loadFavoriteFilter(filter: any) {
|
|
241
|
+
const loadedArray = (deepClone(filter.searchArray) as searchType) ?? {
|
|
242
|
+
and: [[null, "=", null]],
|
|
243
|
+
};
|
|
244
|
+
localSearchArray.value = loadedArray;
|
|
245
|
+
emitSearchQuery(loadedArray);
|
|
246
|
+
window.$message.success(`${t("filterLoaded")}: ${filter.name}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function deleteFavoriteFilter(filterId: string) {
|
|
250
|
+
const updatedFilters = favoriteFilters.value.filter(
|
|
251
|
+
(f: any) => f.id !== filterId,
|
|
252
|
+
);
|
|
253
|
+
favoriteFilters.value = updatedFilters;
|
|
254
|
+
window.$message.success(t("filterDeletedSuccessfully"));
|
|
255
|
+
}
|
|
256
|
+
</script>
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<NFlex item-style="width: 100%">
|
|
3
|
+
<template v-for="(item, index) in formatedItems">
|
|
4
|
+
<NInputGroup v-if="Array.isArray(item)" class="searchGroupInput">
|
|
5
|
+
<NCascader size="small" :consistent-menu-width="false" filterable :value="item[0]"
|
|
6
|
+
@update:value="(v) => item[0] = v" :options="generateSearchInOptions(schema)"
|
|
7
|
+
:style="`width:${item[3] ? 33.33 : 100}%`" check-strategy="child" />
|
|
8
|
+
<template v-if="item[3]">
|
|
9
|
+
<NCascader size="small" filterable check-strategy="child" :value="item[1]"
|
|
10
|
+
@update:value="(v) => item[1] = v" :options="getAvailableComparisonOperator(item[3])"
|
|
11
|
+
style="width:33.33%" />
|
|
12
|
+
<template v-if="isRelativeOperator(item[1])">
|
|
13
|
+
<NSelect size="small" style="width:33.33%" :placeholder="t('relativePlaceholder')"
|
|
14
|
+
:value="(item[2] as string | null) ?? null"
|
|
15
|
+
:options="relativeSelectOptions[index] ?? getRelativeSelectOptions('', item[2] as string | null, item[3])"
|
|
16
|
+
filterable remote clearable :on-search="(pattern) => handleRelativeSearch(index, pattern)"
|
|
17
|
+
tag @update:value="(v) => updateRelativeValue(item, v as string | null)"
|
|
18
|
+
@keydown.enter.prevent="() => callback && callback()" />
|
|
19
|
+
</template>
|
|
20
|
+
<Field v-else :model-value="item[2]" @update:modelValue="(v) => updateFieldValue(item, v)"
|
|
21
|
+
:field="getFieldFromItem(item)" />
|
|
22
|
+
</template>
|
|
23
|
+
<NButton :disabled="formatedItems?.length === 1" @click="modelValue?.splice(index, 1)" circle
|
|
24
|
+
size="small">
|
|
25
|
+
<template #icon>
|
|
26
|
+
<NIcon>
|
|
27
|
+
<Icon name="tabler:minus" />
|
|
28
|
+
</NIcon>
|
|
29
|
+
</template>
|
|
30
|
+
</NButton>
|
|
31
|
+
</NInputGroup>
|
|
32
|
+
<LazyTableSearch v-else v-model="(modelValue[index] as searchType)" v-model:schema="schema" :callback />
|
|
33
|
+
</template>
|
|
34
|
+
</NFlex>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script lang="ts" setup>
|
|
38
|
+
import {
|
|
39
|
+
getField as getFieldFromSchema,
|
|
40
|
+
isArrayOfObjects,
|
|
41
|
+
isNumber,
|
|
42
|
+
isObject,
|
|
43
|
+
} from "inibase/utils"
|
|
44
|
+
import { Icon } from "#components"
|
|
45
|
+
|
|
46
|
+
const { callback } = defineProps<{
|
|
47
|
+
callback?: CallableFunction
|
|
48
|
+
}>()
|
|
49
|
+
|
|
50
|
+
const schema = defineModel<Table['schema']>("schema")
|
|
51
|
+
|
|
52
|
+
const modelValue = defineModel<searchTypeValue>({
|
|
53
|
+
default: [[null, "=", null]],
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
type OperatorOption = {
|
|
57
|
+
label?: string
|
|
58
|
+
value?: string
|
|
59
|
+
children?: OperatorOption[]
|
|
60
|
+
type?: string
|
|
61
|
+
key?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const relativeBaseSuggestionValues = [
|
|
65
|
+
"now",
|
|
66
|
+
"yesterday",
|
|
67
|
+
"tomorrow",
|
|
68
|
+
"last week",
|
|
69
|
+
"last month",
|
|
70
|
+
"last year",
|
|
71
|
+
"next week",
|
|
72
|
+
"next month",
|
|
73
|
+
"next year",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
const relativeBaseAutocompleteOptions = relativeBaseSuggestionValues.map(
|
|
77
|
+
(value) => ({
|
|
78
|
+
label: t(value),
|
|
79
|
+
value,
|
|
80
|
+
}),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const formatedItems = computed(() =>
|
|
84
|
+
modelValue.value?.map((item) => {
|
|
85
|
+
if (Array.isArray(item) && item[0]) {
|
|
86
|
+
item[3] = getFieldFromSchema(item[0], schema.value as any)
|
|
87
|
+
item[2] = normalizeSearchItemValue(item)
|
|
88
|
+
}
|
|
89
|
+
return item
|
|
90
|
+
}),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
function normalizeSearchItemValue(item: searchTypeValueItem) {
|
|
94
|
+
const value = item[2]
|
|
95
|
+
const field = item[3] as Field | undefined
|
|
96
|
+
if (!field || isRelativeOperator(item[1])) return value
|
|
97
|
+
|
|
98
|
+
if (
|
|
99
|
+
checkFieldType(field.type, ["date", "number"]) &&
|
|
100
|
+
typeof value === "string" &&
|
|
101
|
+
isNumber(value)
|
|
102
|
+
)
|
|
103
|
+
return Number(value)
|
|
104
|
+
|
|
105
|
+
return value
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getSearchSubType(field: Field) {
|
|
109
|
+
const resolvedType = field.subType ?? field.type
|
|
110
|
+
if (["radio", "checkbox"].includes(resolvedType as string)) return "select"
|
|
111
|
+
// long text fields are searched through a single line input
|
|
112
|
+
if (resolvedType === "textarea") return undefined
|
|
113
|
+
return field.subType
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getFieldFromItem(item: searchTypeValueItem) {
|
|
117
|
+
return {
|
|
118
|
+
...item[3],
|
|
119
|
+
subType: getSearchSubType(item[3]),
|
|
120
|
+
required: false,
|
|
121
|
+
labelProps: {
|
|
122
|
+
showLabel: false,
|
|
123
|
+
style: { width: "33.33%" },
|
|
124
|
+
showFeedback: false,
|
|
125
|
+
},
|
|
126
|
+
inputProps: {
|
|
127
|
+
size: "small",
|
|
128
|
+
onKeydown: ({ key }: KeyboardEvent) => {
|
|
129
|
+
if (key === "Enter" && callback) callback()
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getAvailableComparisonOperator(field: Field) {
|
|
136
|
+
const baseOptions = comparisonOperatorOptions() as OperatorOption[]
|
|
137
|
+
const selectableOptions = baseOptions.filter((option) => {
|
|
138
|
+
const optionValue = option.value
|
|
139
|
+
if (!optionValue) return false
|
|
140
|
+
|
|
141
|
+
if (Array.isArray(field.type))
|
|
142
|
+
return [
|
|
143
|
+
"=",
|
|
144
|
+
"!=",
|
|
145
|
+
...(field.type.some((type: string) =>
|
|
146
|
+
["string", "email", "url"].includes(type),
|
|
147
|
+
)
|
|
148
|
+
? ["*", "!*"]
|
|
149
|
+
: []),
|
|
150
|
+
...(field.type.some((type: string) => ["number", "date"].includes(type))
|
|
151
|
+
? [">", ">=", "<", "<="]
|
|
152
|
+
: []),
|
|
153
|
+
].includes(optionValue)
|
|
154
|
+
|
|
155
|
+
if (checkFieldType(field.type, ["number", "date"]))
|
|
156
|
+
return ["=", "!=", ">", ">=", "<", "<="].includes(optionValue)
|
|
157
|
+
if (
|
|
158
|
+
checkFieldType(field.type, "array") ||
|
|
159
|
+
checkFieldType(field.type, "table")
|
|
160
|
+
)
|
|
161
|
+
return ![">", ">=", "<", "<="].includes(optionValue)
|
|
162
|
+
return ![">", ">=", "<", "<=", "[]", "![]"].includes(optionValue)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
if (checkFieldType(field.type, ["date", "number"])) {
|
|
166
|
+
const relativeCandidates = selectableOptions.filter(
|
|
167
|
+
(option) =>
|
|
168
|
+
option.value &&
|
|
169
|
+
["=", "!=", ">", ">=", "<", "<="].includes(option.value),
|
|
170
|
+
)
|
|
171
|
+
if (relativeCandidates.length)
|
|
172
|
+
return [
|
|
173
|
+
{
|
|
174
|
+
value: "relative",
|
|
175
|
+
label: t("relativeGroup"),
|
|
176
|
+
children: relativeCandidates.map((option) => ({
|
|
177
|
+
label: option.label ?? option.value ?? "",
|
|
178
|
+
value: `r${option.value as string}`,
|
|
179
|
+
})),
|
|
180
|
+
},
|
|
181
|
+
...selectableOptions,
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return selectableOptions
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isRelativeOperator(value: string | undefined) {
|
|
189
|
+
return typeof value === "string" && value.startsWith("r")
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function getRelativeAutocompleteOptions(value: unknown, field?: Field) {
|
|
193
|
+
if (field?.type && checkFieldType(field.type, "number")) {
|
|
194
|
+
const input = typeof value === "string" ? value.trim() : ""
|
|
195
|
+
if (!input)
|
|
196
|
+
return ["0", "1", "-1", "10", "-10"].map((v) => ({ label: v, value: v }))
|
|
197
|
+
|
|
198
|
+
const numericPattern = /^[+-]?\d+(?:\.\d+)?$/
|
|
199
|
+
if (numericPattern.test(input))
|
|
200
|
+
return [{ label: input, value: input }]
|
|
201
|
+
|
|
202
|
+
return []
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const input = typeof value === "string" ? value.trim() : ""
|
|
206
|
+
const numericPattern = /^[+-]?\d+$/
|
|
207
|
+
if (numericPattern.test(input)) {
|
|
208
|
+
const parsed = Number.parseInt(input, 10)
|
|
209
|
+
if (!Number.isFinite(parsed)) return relativeBaseAutocompleteOptions
|
|
210
|
+
const absolute = Math.abs(parsed)
|
|
211
|
+
if (absolute === 0) return relativeBaseAutocompleteOptions
|
|
212
|
+
const units = [
|
|
213
|
+
{ singular: "day", plural: "days" },
|
|
214
|
+
{ singular: "week", plural: "weeks" },
|
|
215
|
+
{ singular: "month", plural: "months" },
|
|
216
|
+
{ singular: "year", plural: "years" },
|
|
217
|
+
]
|
|
218
|
+
const numberText = absolute.toString()
|
|
219
|
+
const options: { label: string; value: string }[] = []
|
|
220
|
+
|
|
221
|
+
for (const { singular, plural } of units) {
|
|
222
|
+
const unitText = t(absolute === 1 ? singular : plural)
|
|
223
|
+
const baseLabel = `${numberText} ${unitText}`
|
|
224
|
+
options.push({ label: `${baseLabel} ${t('ago')}`, value: `${baseLabel} ago` })
|
|
225
|
+
}
|
|
226
|
+
for (const { singular, plural } of units) {
|
|
227
|
+
const unitText = t(absolute === 1 ? singular : plural)
|
|
228
|
+
const baseLabel = `${numberText} ${unitText}`
|
|
229
|
+
options.push({ label: `${t('in')} ${baseLabel}`, value: `in ${baseLabel}` })
|
|
230
|
+
}
|
|
231
|
+
return options
|
|
232
|
+
}
|
|
233
|
+
return relativeBaseAutocompleteOptions
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// NSelect support (remote filter): state and helpers
|
|
237
|
+
const relativeSelectOptions = ref<Record<number, { label: string; value: string }[]>>({})
|
|
238
|
+
|
|
239
|
+
function getRelativeSelectOptions(
|
|
240
|
+
pattern: unknown,
|
|
241
|
+
currentValue: string | null,
|
|
242
|
+
field?: Field,
|
|
243
|
+
) {
|
|
244
|
+
// Reuse existing generator, ensure current value is present
|
|
245
|
+
const options = getRelativeAutocompleteOptions(pattern, field)
|
|
246
|
+
const list = Array.isArray(options) ? [...options] : []
|
|
247
|
+
if (currentValue && !list.some((o) => o.value === currentValue)) {
|
|
248
|
+
list.unshift({ label: currentValue, value: currentValue })
|
|
249
|
+
}
|
|
250
|
+
return list
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function handleRelativeSearch(index: number, pattern: string) {
|
|
254
|
+
const currentItem = modelValue.value?.[index]
|
|
255
|
+
const currentValue = Array.isArray(currentItem) ? (currentItem[2] as string | null) : null
|
|
256
|
+
const currentField = Array.isArray(currentItem) ? (currentItem[3] as Field | undefined) : undefined
|
|
257
|
+
relativeSelectOptions.value[index] = getRelativeSelectOptions(pattern, currentValue, currentField)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function updateRelativeValue(
|
|
261
|
+
item: searchTypeValueItem,
|
|
262
|
+
value: string | null,
|
|
263
|
+
) {
|
|
264
|
+
const sanitized = value?.trim()
|
|
265
|
+
item[2] = sanitized ? sanitized : undefined
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function updateFieldValue(item: searchTypeValueItem, value: any) {
|
|
269
|
+
if (value !== undefined) {
|
|
270
|
+
if (isObject(value) && Object.hasOwn(value, "id"))
|
|
271
|
+
value = value.id
|
|
272
|
+
else if (
|
|
273
|
+
isArrayOfObjects(value) &&
|
|
274
|
+
value.every((_v: any) => Object.hasOwn(_v, "id"))
|
|
275
|
+
)
|
|
276
|
+
value = value.map((_value: any) => _value.id)
|
|
277
|
+
item[2] = Array.isArray(value) ? value.join(",") : value
|
|
278
|
+
} else item[2] = undefined
|
|
279
|
+
}
|
|
280
|
+
</script>
|
|
281
|
+
|
|
282
|
+
<style scoped>
|
|
283
|
+
.searchGroupInput * {
|
|
284
|
+
border-radius: 0 !important;
|
|
285
|
+
}
|
|
286
|
+
</style>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<NCollapse :triggerAreas="['main', 'arrow']" accordion default-expanded-names="0">
|
|
3
|
+
<NCollapseItem v-for="(_items, condition, index) in modelValue" :key="condition" :name="index.toString()"
|
|
4
|
+
:title="t(`${condition}Group`)">
|
|
5
|
+
<template #header-extra>
|
|
6
|
+
<NButtonGroup>
|
|
7
|
+
<NDropdown :options="conditionDropdownOptions" style="max-height: 200px;" scrollable
|
|
8
|
+
@select="(value: 'and' | 'or') => modelValue[condition]?.push({ [value]: [[null, '=', null]] })">
|
|
9
|
+
<NButton round type="primary" secondary @click="modelValue[condition]?.push([null, '=', null])"
|
|
10
|
+
circle size="small">
|
|
11
|
+
<template #icon>
|
|
12
|
+
<NIcon>
|
|
13
|
+
<Icon name="tabler:plus" />
|
|
14
|
+
</NIcon>
|
|
15
|
+
</template>
|
|
16
|
+
</NButton>
|
|
17
|
+
</NDropdown>
|
|
18
|
+
|
|
19
|
+
<NTooltip :delay="1500">
|
|
20
|
+
<template #trigger>
|
|
21
|
+
<NButton round type="info" secondary @click="() => toggleCondition(condition)" circle
|
|
22
|
+
size="small">
|
|
23
|
+
<template #icon>
|
|
24
|
+
<NIcon>
|
|
25
|
+
<Icon name="tabler:switch-horizontal" />
|
|
26
|
+
</NIcon>
|
|
27
|
+
</template>
|
|
28
|
+
</NButton>
|
|
29
|
+
</template>
|
|
30
|
+
{{ t(
|
|
31
|
+
`convert_to_${condition === "and" ? "or" : "and"}_group`,
|
|
32
|
+
) }}
|
|
33
|
+
</NTooltip>
|
|
34
|
+
<NButton round type="error" secondary @click="() => removeCondition(condition)" circle size="small">
|
|
35
|
+
<template #icon>
|
|
36
|
+
<NIcon>
|
|
37
|
+
<Icon name="tabler:trash" />
|
|
38
|
+
</NIcon>
|
|
39
|
+
</template>
|
|
40
|
+
</NButton>
|
|
41
|
+
</NButtonGroup>
|
|
42
|
+
</template>
|
|
43
|
+
<TableSearchItems v-model="modelValue[condition]" v-model:schema="schema" :callback />
|
|
44
|
+
</NCollapseItem>
|
|
45
|
+
</NCollapse>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<script lang="ts" setup>
|
|
49
|
+
import { Icon, NIcon } from "#components"
|
|
50
|
+
|
|
51
|
+
const { callback } = defineProps<{ callback?: CallableFunction }>()
|
|
52
|
+
|
|
53
|
+
const table = useState<Table>("table")
|
|
54
|
+
|
|
55
|
+
const schema = defineModel<Table['schema']>("schema")
|
|
56
|
+
if (!schema.value)
|
|
57
|
+
schema.value = table.value?.schema
|
|
58
|
+
|
|
59
|
+
const modelValue = defineModel<searchType>({
|
|
60
|
+
default: { and: [[null, "=", null]] },
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const conditionDropdownOptions = [
|
|
64
|
+
{
|
|
65
|
+
key: "and",
|
|
66
|
+
label: t("andGroup"),
|
|
67
|
+
icon: () => h(NIcon, () => h(Icon, { name: "tabler:arrow-merge" })),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
key: "or",
|
|
71
|
+
label: t("orGroup"),
|
|
72
|
+
icon: () => h(NIcon, () => h(Icon, { name: "tabler:arrow-fork" })),
|
|
73
|
+
},
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
function toggleCondition(oldCondition: "and" | "or") {
|
|
77
|
+
modelValue.value[oldCondition === "and" ? "or" : "and"] =
|
|
78
|
+
modelValue.value[oldCondition]
|
|
79
|
+
delete modelValue.value[oldCondition]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function removeCondition(condition: "and" | "or") {
|
|
83
|
+
if (Object.keys(modelValue.value).length === 1)
|
|
84
|
+
modelValue.value[condition] = [[null, "=", null]]
|
|
85
|
+
else delete modelValue.value[condition]
|
|
86
|
+
}
|
|
87
|
+
</script>
|