adminforth 1.3.54-next.1 → 1.3.54-next.11
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/basePlugin.ts +3 -1
- package/dataConnectors/clickhouse.ts +3 -3
- package/dist/basePlugin.js +2 -0
- package/dist/dataConnectors/clickhouse.js +2 -2
- package/dist/index.js +15 -0
- package/index.ts +17 -0
- package/package.json +1 -1
- package/spa/package-lock.json +29 -0
- package/spa/package.json +2 -0
- package/spa/src/App.vue +1 -1
- package/spa/src/components/ResourceListTable.vue +11 -5
- package/spa/src/renderers/CompactUUID.vue +48 -0
- package/spa/src/renderers/CountryFlag.vue +69 -0
- package/spa/src/utils.ts +11 -0
- package/spa/src/views/ListView.vue +78 -5
- package/types/AdminForthConfig.ts +1 -0
package/basePlugin.ts
CHANGED
|
@@ -16,7 +16,7 @@ export default class AdminForthPlugin implements IAdminForthPlugin {
|
|
|
16
16
|
customFolderPath: string;
|
|
17
17
|
pluginOptions: any;
|
|
18
18
|
resourceConfig: AdminForthResource;
|
|
19
|
-
|
|
19
|
+
className: string;
|
|
20
20
|
activationOrder: number = 0;
|
|
21
21
|
|
|
22
22
|
constructor(pluginOptions: any, metaUrl: string) {
|
|
@@ -24,6 +24,8 @@ export default class AdminForthPlugin implements IAdminForthPlugin {
|
|
|
24
24
|
this.pluginDir = currentFileDir(metaUrl);
|
|
25
25
|
this.customFolderPath = path.join(this.pluginDir, this.customFolderName);
|
|
26
26
|
this.pluginOptions = pluginOptions;
|
|
27
|
+
console.log(`🪲 🪲 🪲 🪲 🪲 🪲 AdminForthPlugin.constructor`, this.constructor.name);
|
|
28
|
+
this.className = this.constructor.name;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
setupEndpoints(server: any) {
|
|
@@ -265,14 +265,14 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
|
|
|
265
265
|
const tableName = resource.table;
|
|
266
266
|
const where = this.whereClause(resource, filters);
|
|
267
267
|
const d = this.whereParams(filters);
|
|
268
|
-
|
|
268
|
+
|
|
269
269
|
const countQ = await this.client.query({
|
|
270
|
-
query: `SELECT COUNT(*) FROM ${tableName} ${where}`,
|
|
270
|
+
query: `SELECT COUNT(*) as count FROM ${tableName} ${where}`,
|
|
271
271
|
format: 'JSONEachRow',
|
|
272
272
|
query_params: d,
|
|
273
273
|
});
|
|
274
274
|
const countResp = await countQ.json()
|
|
275
|
-
return countResp[0]['
|
|
275
|
+
return +countResp[0]['count'];
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
async getMinMaxForColumnsWithOriginalTypes({ resource, columns }: { resource: AdminForthResource, columns: AdminForthResourceColumn[] }): Promise<{ [key: string]: { min: any, max: any } }> {
|
package/dist/basePlugin.js
CHANGED
|
@@ -11,6 +11,8 @@ export default class AdminForthPlugin {
|
|
|
11
11
|
this.pluginDir = currentFileDir(metaUrl);
|
|
12
12
|
this.customFolderPath = path.join(this.pluginDir, this.customFolderName);
|
|
13
13
|
this.pluginOptions = pluginOptions;
|
|
14
|
+
console.log(`🪲 🪲 🪲 🪲 🪲 🪲 AdminForthPlugin.constructor`, this.constructor.name);
|
|
15
|
+
this.className = this.constructor.name;
|
|
14
16
|
}
|
|
15
17
|
setupEndpoints(server) {
|
|
16
18
|
}
|
|
@@ -247,12 +247,12 @@ class ClickhouseConnector extends AdminForthBaseConnector {
|
|
|
247
247
|
const where = this.whereClause(resource, filters);
|
|
248
248
|
const d = this.whereParams(filters);
|
|
249
249
|
const countQ = yield this.client.query({
|
|
250
|
-
query: `SELECT COUNT(*) FROM ${tableName} ${where}`,
|
|
250
|
+
query: `SELECT COUNT(*) as count FROM ${tableName} ${where}`,
|
|
251
251
|
format: 'JSONEachRow',
|
|
252
252
|
query_params: d,
|
|
253
253
|
});
|
|
254
254
|
const countResp = yield countQ.json();
|
|
255
|
-
return countResp[0]['
|
|
255
|
+
return +countResp[0]['count'];
|
|
256
256
|
});
|
|
257
257
|
}
|
|
258
258
|
getMinMaxForColumnsWithOriginalTypes(_a) {
|
package/dist/index.js
CHANGED
|
@@ -72,6 +72,21 @@ class AdminForth {
|
|
|
72
72
|
this.activatedPlugins.push(pluginInstance);
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
|
+
getPluginsByClassName(className) {
|
|
76
|
+
const plugins = this.activatedPlugins.filter((plugin) => plugin.className === className);
|
|
77
|
+
return plugins;
|
|
78
|
+
}
|
|
79
|
+
getPluginByClassName(className) {
|
|
80
|
+
const plugins = this.getPluginsByClassName(className);
|
|
81
|
+
if (plugins.length > 1) {
|
|
82
|
+
throw new Error(`Multiple plugins with className ${className} found. Use getPluginsByClassName instead`);
|
|
83
|
+
}
|
|
84
|
+
if (plugins.length === 0) {
|
|
85
|
+
const similar = suggestIfTypo(this.activatedPlugins.map((p) => p.className), className);
|
|
86
|
+
throw new Error(`Plugin with className ${className} not found. ${similar ? `Did you mean ${similar}?` : ''}`);
|
|
87
|
+
}
|
|
88
|
+
return plugins[0];
|
|
89
|
+
}
|
|
75
90
|
discoverDatabases() {
|
|
76
91
|
return __awaiter(this, void 0, void 0, function* () {
|
|
77
92
|
this.statuses.dbDiscover = 'running';
|
package/index.ts
CHANGED
|
@@ -126,6 +126,23 @@ class AdminForth implements IAdminForth {
|
|
|
126
126
|
);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
getPluginsByClassName<T>(className: string): T[] {
|
|
130
|
+
const plugins = this.activatedPlugins.filter((plugin) => plugin.className === className);
|
|
131
|
+
return plugins as T[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getPluginByClassName<T>(className: string): T {
|
|
135
|
+
const plugins = this.getPluginsByClassName(className);
|
|
136
|
+
if (plugins.length > 1) {
|
|
137
|
+
throw new Error(`Multiple plugins with className ${className} found. Use getPluginsByClassName instead`);
|
|
138
|
+
}
|
|
139
|
+
if (plugins.length === 0) {
|
|
140
|
+
const similar = suggestIfTypo(this.activatedPlugins.map((p) => p.className), className);
|
|
141
|
+
throw new Error(`Plugin with className ${className} not found. ${similar ? `Did you mean ${similar}?` : ''}`);
|
|
142
|
+
}
|
|
143
|
+
return plugins[0] as T;
|
|
144
|
+
}
|
|
145
|
+
|
|
129
146
|
async discoverDatabases() {
|
|
130
147
|
this.statuses.dbDiscover = 'running';
|
|
131
148
|
this.connectorClasses = {
|
package/package.json
CHANGED
package/spa/package-lock.json
CHANGED
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
"autoprefixer": "^10.4.19",
|
|
35
35
|
"eslint": "^8.57.0",
|
|
36
36
|
"eslint-plugin-vue": "^9.23.0",
|
|
37
|
+
"flag-icons": "^7.2.3",
|
|
38
|
+
"i18n-iso-countries": "^7.12.0",
|
|
37
39
|
"npm-run-all2": "^6.1.2",
|
|
38
40
|
"postcss": "^8.4.38",
|
|
39
41
|
"sass": "^1.77.2",
|
|
@@ -1906,6 +1908,13 @@
|
|
|
1906
1908
|
"node": ">=0.10.0"
|
|
1907
1909
|
}
|
|
1908
1910
|
},
|
|
1911
|
+
"node_modules/diacritics": {
|
|
1912
|
+
"version": "1.3.0",
|
|
1913
|
+
"resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
|
|
1914
|
+
"integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==",
|
|
1915
|
+
"dev": true,
|
|
1916
|
+
"license": "MIT"
|
|
1917
|
+
},
|
|
1909
1918
|
"node_modules/didyoumean": {
|
|
1910
1919
|
"version": "1.2.2",
|
|
1911
1920
|
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
|
@@ -2371,6 +2380,13 @@
|
|
|
2371
2380
|
"url": "https://github.com/sponsors/sindresorhus"
|
|
2372
2381
|
}
|
|
2373
2382
|
},
|
|
2383
|
+
"node_modules/flag-icons": {
|
|
2384
|
+
"version": "7.2.3",
|
|
2385
|
+
"resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.2.3.tgz",
|
|
2386
|
+
"integrity": "sha512-X2gUdteNuqdNqob2KKTJTS+ZCvyWeLCtDz9Ty8uJP17Y4o82Y+U/Vd4JNrdwTAjagYsRznOn9DZ+E/Q52qbmqg==",
|
|
2387
|
+
"dev": true,
|
|
2388
|
+
"license": "MIT"
|
|
2389
|
+
},
|
|
2374
2390
|
"node_modules/flat-cache": {
|
|
2375
2391
|
"version": "3.2.0",
|
|
2376
2392
|
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
|
|
@@ -2622,6 +2638,19 @@
|
|
|
2622
2638
|
"entities": "^4.4.0"
|
|
2623
2639
|
}
|
|
2624
2640
|
},
|
|
2641
|
+
"node_modules/i18n-iso-countries": {
|
|
2642
|
+
"version": "7.12.0",
|
|
2643
|
+
"resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.12.0.tgz",
|
|
2644
|
+
"integrity": "sha512-NDFf5j/raA5JrcPT/NcHP3RUMH7TkdkxQKAKdvDlgb+MS296WJzzqvV0Y5uwavSm7A6oYvBeSV0AxoHdDiHIiw==",
|
|
2645
|
+
"dev": true,
|
|
2646
|
+
"license": "MIT",
|
|
2647
|
+
"dependencies": {
|
|
2648
|
+
"diacritics": "1.3.0"
|
|
2649
|
+
},
|
|
2650
|
+
"engines": {
|
|
2651
|
+
"node": ">= 12"
|
|
2652
|
+
}
|
|
2653
|
+
},
|
|
2625
2654
|
"node_modules/ignore": {
|
|
2626
2655
|
"version": "5.3.1",
|
|
2627
2656
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
|
package/spa/package.json
CHANGED
package/spa/src/App.vue
CHANGED
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
</aside>
|
|
174
174
|
|
|
175
175
|
|
|
176
|
-
<div class="sm:ml-64" v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady">
|
|
176
|
+
<div class="sm:ml-64 max-w-[100vw]" v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady">
|
|
177
177
|
<div class="p-0 dark:border-gray-700 mt-14">
|
|
178
178
|
<RouterView/>
|
|
179
179
|
</div>
|
|
@@ -250,7 +250,7 @@
|
|
|
250
250
|
<script setup>
|
|
251
251
|
|
|
252
252
|
|
|
253
|
-
import { computed, ref, watch } from 'vue';
|
|
253
|
+
import { computed, onMounted, ref, watch } from 'vue';
|
|
254
254
|
import { callAdminForthApi } from '@/utils';
|
|
255
255
|
|
|
256
256
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
@@ -260,19 +260,20 @@ import { showSuccesTost, showErrorTost } from '@/composables/useFrontendApi';
|
|
|
260
260
|
import SkeleteLoader from '@/components/SkeleteLoader.vue';
|
|
261
261
|
|
|
262
262
|
import {
|
|
263
|
-
IconInboxOutline,
|
|
263
|
+
IconInboxOutline,
|
|
264
264
|
} from '@iconify-prerendered/vue-flowbite';
|
|
265
265
|
|
|
266
266
|
import {
|
|
267
|
-
IconEyeSolid,
|
|
268
|
-
IconPenSolid,
|
|
269
|
-
IconTrashBinSolid
|
|
267
|
+
IconEyeSolid,
|
|
268
|
+
IconPenSolid,
|
|
269
|
+
IconTrashBinSolid
|
|
270
270
|
} from '@iconify-prerendered/vue-flowbite';
|
|
271
271
|
import router from '@/router';
|
|
272
272
|
|
|
273
273
|
const coreStore = useCoreStore();
|
|
274
274
|
|
|
275
275
|
const props = defineProps([
|
|
276
|
+
'page',
|
|
276
277
|
'resource',
|
|
277
278
|
'rows',
|
|
278
279
|
'totalRows',
|
|
@@ -296,6 +297,7 @@ const page = ref(1);
|
|
|
296
297
|
const sort = ref([]);
|
|
297
298
|
|
|
298
299
|
|
|
300
|
+
|
|
299
301
|
watch(() => page.value, (newPage) => {
|
|
300
302
|
emits('update:page', newPage);
|
|
301
303
|
});
|
|
@@ -319,6 +321,10 @@ watch(() => props.sort, (newSort) => {
|
|
|
319
321
|
sort.value = newSort;
|
|
320
322
|
});
|
|
321
323
|
|
|
324
|
+
watch(() => props.page, (newPage) => {
|
|
325
|
+
page.value = newPage;
|
|
326
|
+
});
|
|
327
|
+
|
|
322
328
|
function addToCheckedValues(id) {
|
|
323
329
|
console.log('checking', checkboxesInternal.value, 'id', id)
|
|
324
330
|
if (checkboxesInternal.value.includes(id)) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span class="flex items-center"
|
|
3
|
+
:data-tooltip-target="`tooltip-${id}`"
|
|
4
|
+
data-tooltip-placement="top"
|
|
5
|
+
>
|
|
6
|
+
{{ visualValue }} <IconFileCopyAltSolid @click.stop="copyToCB" class="w-5 h-5 text-lightPrimary dark:text-darkPrimary"/>
|
|
7
|
+
|
|
8
|
+
<div :id="`tooltip-${id}`" role="tooltip"
|
|
9
|
+
class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
|
|
10
|
+
{{ props.record[props.column.name] }}
|
|
11
|
+
<div class="tooltip-arrow" data-popper-arrow></div>
|
|
12
|
+
</div>
|
|
13
|
+
</span>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup>
|
|
17
|
+
import { computed, ref, onMounted } from 'vue';
|
|
18
|
+
import { IconFileCopyAltSolid } from '@iconify-prerendered/vue-flowbite';
|
|
19
|
+
import { initFlowbite } from 'flowbite';
|
|
20
|
+
|
|
21
|
+
const visualValue = computed(() => {
|
|
22
|
+
// if lenght is more then 8, show only first 4 and last 4 characters, ... in the middle
|
|
23
|
+
const val = props.record[props.column.name];
|
|
24
|
+
if (val && val.length > 8) {
|
|
25
|
+
return `${val.substr(0, 4)}...${val.substr(val.length - 4)}`;
|
|
26
|
+
}
|
|
27
|
+
return val;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const props = defineProps(['column', 'record', 'meta']);
|
|
31
|
+
|
|
32
|
+
const id = ref();
|
|
33
|
+
|
|
34
|
+
function copyToCB() {
|
|
35
|
+
navigator.clipboard.writeText(props.record[props.column.name]);
|
|
36
|
+
window.adminforth.alert({
|
|
37
|
+
message: 'ID copied to clipboard',
|
|
38
|
+
variant: 'success',
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onMounted(async () => {
|
|
43
|
+
id.value = Math.random().toString(36).substring(7);
|
|
44
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
45
|
+
initFlowbite();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
</script>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span class="flex items-center">
|
|
3
|
+
<span
|
|
4
|
+
:class="{[`fi-${countryIsoLow}`]: true, 'flag-icon': countryName}"
|
|
5
|
+
:data-tooltip-target="`tooltip-${id}`"
|
|
6
|
+
></span>
|
|
7
|
+
|
|
8
|
+
<span v-if="meta.showCountryName" class="ms-2">{{ countryName }}</span>
|
|
9
|
+
|
|
10
|
+
<div
|
|
11
|
+
v-if="!meta.showCountryName && countryName"
|
|
12
|
+
:id="`tooltip-${id}`" role="tooltip"
|
|
13
|
+
class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700"
|
|
14
|
+
>
|
|
15
|
+
{{ countryName }}
|
|
16
|
+
<div class="tooltip-arrow" data-popper-arrow></div>
|
|
17
|
+
</div>
|
|
18
|
+
</span>
|
|
19
|
+
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script setup>
|
|
23
|
+
|
|
24
|
+
import { computed, ref, onMounted } from 'vue';
|
|
25
|
+
import { initFlowbite } from 'flowbite';
|
|
26
|
+
import 'flag-icons/css/flag-icons.min.css';
|
|
27
|
+
import isoCountries from 'i18n-iso-countries';
|
|
28
|
+
import enLocal from 'i18n-iso-countries/langs/en.json';
|
|
29
|
+
|
|
30
|
+
isoCountries.registerLocale(enLocal);
|
|
31
|
+
|
|
32
|
+
const props = defineProps(['column', 'record', 'meta', 'resource', 'adminUser']);
|
|
33
|
+
|
|
34
|
+
const id = ref();
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
onMounted(async () => {
|
|
38
|
+
id.value = Math.random().toString(36).substring(7);
|
|
39
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
40
|
+
initFlowbite();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const countryIsoLow = computed(() => {
|
|
44
|
+
return props.record[props.column.name]?.toLowerCase();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const countryName = computed(() => {
|
|
48
|
+
if (!countryIsoLow.value) {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
return isoCountries.getName(countryIsoLow.value, 'en');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<style scoped lang="scss">
|
|
57
|
+
|
|
58
|
+
.flag-icon {
|
|
59
|
+
width: 2rem;
|
|
60
|
+
height: 1.5rem;
|
|
61
|
+
flex-shrink: 0;
|
|
62
|
+
|
|
63
|
+
// border radius for background
|
|
64
|
+
border-radius: 2px;
|
|
65
|
+
// add some silkiness to the flag
|
|
66
|
+
box-shadow: inset -1px -1px 0.5px 0px rgba(0 0 0 / 0.2), inset 1px 1px 0.5px 0px rgba(255 255 255 / 0.2);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
</style>
|
package/spa/src/utils.ts
CHANGED
|
@@ -146,4 +146,15 @@ export function applyRegexValidation(value: any, validation: ValidationObject[]
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function currentQuery() {
|
|
152
|
+
return router.currentRoute.value.query;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function setQuery(query: any) {
|
|
156
|
+
const currentQuery = { ...router.currentRoute.value.query, ...query };
|
|
157
|
+
router.replace({
|
|
158
|
+
query: currentQuery,
|
|
159
|
+
});
|
|
149
160
|
}
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
@click="()=>{checkboxes = []}"
|
|
22
22
|
v-if="checkboxes.length"
|
|
23
23
|
data-tooltip-target="tooltip-remove-all"
|
|
24
|
-
data-tooltip-placement="bottom"
|
|
25
24
|
class="flex gap-1 items-center py-1 px-3 me-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-darkListTable dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
|
|
26
25
|
>
|
|
27
26
|
<IconBanOutline class="w-5 h-5 "/>
|
|
@@ -89,6 +88,7 @@
|
|
|
89
88
|
<ResourceListTable
|
|
90
89
|
:resource="coreStore.resource"
|
|
91
90
|
:rows="rows"
|
|
91
|
+
:page="page"
|
|
92
92
|
@update:page="page = $event"
|
|
93
93
|
@update:sort="sort = $event"
|
|
94
94
|
@update:checkboxes="checkboxes = $event"
|
|
@@ -115,7 +115,7 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
|
115
115
|
import ResourceListTable from '@/components/ResourceListTable.vue';
|
|
116
116
|
import { useCoreStore } from '@/stores/core';
|
|
117
117
|
import { useFiltersStore } from '@/stores/filters';
|
|
118
|
-
import { callAdminForthApi, getIcon } from '@/utils';
|
|
118
|
+
import { callAdminForthApi, currentQuery, getIcon, setQuery } from '@/utils';
|
|
119
119
|
import { computed, onMounted, ref, watch } from 'vue';
|
|
120
120
|
import { useRoute } from 'vue-router';
|
|
121
121
|
import { showErrorTost } from '@/composables/useFrontendApi'
|
|
@@ -227,6 +227,18 @@ async function startBulkAction(actionId) {
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
|
|
230
|
+
class SortQuerySerializer {
|
|
231
|
+
static serialize(sort) {
|
|
232
|
+
return sort.map(s => `${s.field}__${s.direction}`).join(',');
|
|
233
|
+
}
|
|
234
|
+
static deserialize(str) {
|
|
235
|
+
return str.split(',').map(s => {
|
|
236
|
+
const [field, direction] = s.split('__');
|
|
237
|
+
return { field, direction };
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
230
242
|
|
|
231
243
|
async function init() {
|
|
232
244
|
|
|
@@ -236,9 +248,26 @@ async function init() {
|
|
|
236
248
|
|
|
237
249
|
initFlowbite();
|
|
238
250
|
|
|
239
|
-
// !!! clear filters should be in same tick with sort assignment so that watch can catch it
|
|
240
|
-
|
|
241
|
-
|
|
251
|
+
// !!! clear filters should be in same tick with sort assignment so that watch can catch it as one change
|
|
252
|
+
|
|
253
|
+
// try to init filters from query params
|
|
254
|
+
const filters = Object.keys(route.query).filter(k => k.startsWith('filter__')).map(k => {
|
|
255
|
+
const [_, field, operator] = k.split('__');
|
|
256
|
+
return {
|
|
257
|
+
field,
|
|
258
|
+
operator,
|
|
259
|
+
value: JSON.parse(decodeURIComponent(route.query[k]))
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
if (filters.length) {
|
|
263
|
+
filtersStore.setFilters(filters);
|
|
264
|
+
} else {
|
|
265
|
+
filtersStore.clearFilters();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (route.query.sort) {
|
|
269
|
+
sort.value = SortQuerySerializer.deserialize(route.query.sort);
|
|
270
|
+
} else if (coreStore.resource.options?.defaultSort) {
|
|
242
271
|
sort.value = [{
|
|
243
272
|
field: coreStore.resource.options.defaultSort.columnName,
|
|
244
273
|
direction: coreStore.resource.options.defaultSort.direction
|
|
@@ -246,6 +275,11 @@ async function init() {
|
|
|
246
275
|
} else {
|
|
247
276
|
sort.value = [];
|
|
248
277
|
}
|
|
278
|
+
// page init should be also in same tick
|
|
279
|
+
if (route.query.page) {
|
|
280
|
+
page.value = parseInt(route.query.page);
|
|
281
|
+
}
|
|
282
|
+
|
|
249
283
|
// await getList(); - Not needed here, watch will trigger it
|
|
250
284
|
columnsMinMax.value = await callAdminForthApi({
|
|
251
285
|
path: '/get_min_max_for_columns',
|
|
@@ -257,6 +291,8 @@ async function init() {
|
|
|
257
291
|
}
|
|
258
292
|
|
|
259
293
|
watch([page, sort, () => filtersStore.filters], async () => {
|
|
294
|
+
console.log('🔄️ page/sort/filter change fired, page:', page.value);
|
|
295
|
+
|
|
260
296
|
await getList();
|
|
261
297
|
}, { deep: true });
|
|
262
298
|
|
|
@@ -264,16 +300,53 @@ window.adminforth.list.refresh = async () => {
|
|
|
264
300
|
await getList();
|
|
265
301
|
}
|
|
266
302
|
|
|
303
|
+
let initInProcess = false;
|
|
304
|
+
|
|
267
305
|
watch(() => filtersStore.filters, async (to, from) => {
|
|
306
|
+
if (initInProcess) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
console.log('🔄️ filters changed', JSON.stringify(to))
|
|
268
310
|
page.value = 1;
|
|
269
311
|
checkboxes.value = [];
|
|
312
|
+
// update query param for each filter as filter_<column_name>=value
|
|
313
|
+
const query = {};
|
|
314
|
+
const currentQ = currentQuery();
|
|
315
|
+
filtersStore.filters.forEach(f => {
|
|
316
|
+
if (f.value) {
|
|
317
|
+
query[`filter__${f.field}__${f.operator}`] = encodeURIComponent(JSON.stringify(f.value));
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
// set every key in currentQ which starts with filter_ to undefined if it is not in query
|
|
321
|
+
Object.keys(currentQ).forEach(k => {
|
|
322
|
+
if (k.startsWith('filter_') && !query[k]) {
|
|
323
|
+
query[k] = undefined;
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
setQuery(query);
|
|
270
327
|
}, {deep: true});
|
|
271
328
|
|
|
272
329
|
onMounted(async () => {
|
|
330
|
+
initInProcess = true;
|
|
273
331
|
await init();
|
|
274
332
|
initThreeDotsDropdown();
|
|
333
|
+
initInProcess = false;
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
watch([page], async () => {
|
|
337
|
+
setQuery({ page: page.value });
|
|
275
338
|
});
|
|
276
339
|
|
|
277
340
|
|
|
278
341
|
|
|
342
|
+
|
|
343
|
+
watch([sort], async () => {
|
|
344
|
+
if (!sort.value.length) {
|
|
345
|
+
setQuery({ sort: undefined });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
setQuery({ sort: SortQuerySerializer.serialize(sort.value) });
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
|
|
279
352
|
</script>
|
|
@@ -325,6 +325,7 @@ export interface IAdminForthPlugin {
|
|
|
325
325
|
customFolderPath: string;
|
|
326
326
|
pluginOptions: any;
|
|
327
327
|
resourceConfig: AdminForthResource;
|
|
328
|
+
className: string;
|
|
328
329
|
|
|
329
330
|
/**
|
|
330
331
|
* Before activating all plugins are sorted by this number and then activated in order.
|