adminforth 1.3.23 → 1.3.25
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/dist/modules/configValidator.js +17 -14
- package/dist/modules/operationalResource.js +9 -2
- package/modules/configValidator.ts +27 -14
- package/modules/operationalResource.ts +11 -3
- package/package.json +1 -1
- package/spa/src/App.vue +1 -1
- package/spa/src/components/ResourceListTable.vue +2 -4
- package/spa/src/components/ThreeDotsMenu.vue +43 -0
- package/spa/src/components/Toast.vue +2 -1
- package/spa/src/composables/useStores.ts +12 -13
- package/spa/src/router/index.ts +1 -1
- package/spa/src/stores/core.ts +5 -1
- package/spa/src/stores/filters.ts +9 -2
- package/spa/src/utils.ts +15 -2
- package/spa/src/views/CreateView.vue +7 -1
- package/spa/src/views/EditView.vue +9 -2
- package/spa/src/views/ListView.vue +20 -10
- package/spa/src/views/LoginView.vue +1 -1
- package/spa/src/views/ShowView.vue +9 -10
- package/types/AdminForthConfig.ts +1 -0
- package/types/FrontendAPI.ts +47 -30
|
@@ -28,7 +28,7 @@ export default class ConfigValidator {
|
|
|
28
28
|
}
|
|
29
29
|
return [];
|
|
30
30
|
}
|
|
31
|
-
validateComponent(component, errors
|
|
31
|
+
validateComponent(component, errors) {
|
|
32
32
|
if (!component) {
|
|
33
33
|
return component;
|
|
34
34
|
}
|
|
@@ -39,6 +39,12 @@ export default class ConfigValidator {
|
|
|
39
39
|
else {
|
|
40
40
|
obj = component;
|
|
41
41
|
}
|
|
42
|
+
let ignoreExistsCheck = false;
|
|
43
|
+
if (this.adminforth.codeInjector.allComponentNames.hasOwnProperty(component.file)) {
|
|
44
|
+
// not obvious, but if we are in this if, it means that this is plugin component
|
|
45
|
+
// if component is plugin component, we don't need to check if it exists in users folder
|
|
46
|
+
ignoreExistsCheck = true;
|
|
47
|
+
}
|
|
42
48
|
if (!ignoreExistsCheck) {
|
|
43
49
|
errors.push(...this.checkCustomFileExists(obj.file));
|
|
44
50
|
}
|
|
@@ -98,10 +104,7 @@ export default class ConfigValidator {
|
|
|
98
104
|
}
|
|
99
105
|
if (this.config.customization.customPages) {
|
|
100
106
|
this.config.customization.customPages.forEach((page, i) => {
|
|
101
|
-
|
|
102
|
-
if (this.adminforth.codeInjector.allComponentNames.hasOwnProperty(page.component)) {
|
|
103
|
-
const validatedPage = this.validateComponent(page.component, errors, true);
|
|
104
|
-
}
|
|
107
|
+
this.validateComponent(page.component, errors);
|
|
105
108
|
});
|
|
106
109
|
}
|
|
107
110
|
else {
|
|
@@ -298,9 +301,14 @@ export default class ConfigValidator {
|
|
|
298
301
|
});
|
|
299
302
|
res.options.bulkActions = bulkActions;
|
|
300
303
|
// if pageInjection is a string, make array with one element. Also check file exists
|
|
301
|
-
const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom'];
|
|
304
|
+
const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems'];
|
|
305
|
+
const possiblePages = ['list', 'show', 'create', 'edit'];
|
|
302
306
|
if (res.options.pageInjections) {
|
|
303
307
|
Object.entries(res.options.pageInjections).map(([key, value]) => {
|
|
308
|
+
if (!possiblePages.includes(key)) {
|
|
309
|
+
const similar = suggestIfTypo(possiblePages, key);
|
|
310
|
+
errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${key}", allowed keys are ${possiblePages.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
|
|
311
|
+
}
|
|
304
312
|
Object.entries(value).map(([injection, target]) => {
|
|
305
313
|
if (possibleInjections.includes(injection)) {
|
|
306
314
|
if (!Array.isArray(res.options.pageInjections[key][injection])) {
|
|
@@ -312,7 +320,8 @@ export default class ConfigValidator {
|
|
|
312
320
|
});
|
|
313
321
|
}
|
|
314
322
|
else {
|
|
315
|
-
|
|
323
|
+
const similar = suggestIfTypo(possibleInjections, injection);
|
|
324
|
+
errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')} ${similar ? `Did you mean "${similar}"?` : ''}`);
|
|
316
325
|
}
|
|
317
326
|
});
|
|
318
327
|
});
|
|
@@ -417,13 +426,7 @@ export default class ConfigValidator {
|
|
|
417
426
|
for (const column of resource.columns) {
|
|
418
427
|
if (column.components) {
|
|
419
428
|
for (const [key, comp] of Object.entries(column.components)) {
|
|
420
|
-
|
|
421
|
-
if (this.adminforth.codeInjector.allComponentNames[comp.file]) {
|
|
422
|
-
// not obvious, but if we are in this if, it means that this is plugin component
|
|
423
|
-
// and there is no sense to check if it exists in users folder
|
|
424
|
-
ignoreExistsCheck = true;
|
|
425
|
-
}
|
|
426
|
-
column.components[key] = this.validateComponent(comp, errors, ignoreExistsCheck);
|
|
429
|
+
column.components[key] = this.validateComponent(comp, errors);
|
|
427
430
|
}
|
|
428
431
|
}
|
|
429
432
|
}
|
|
@@ -32,8 +32,15 @@ export default class OperationalResource {
|
|
|
32
32
|
})).data[0] || null;
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
-
list(
|
|
36
|
-
return __awaiter(this,
|
|
35
|
+
list(filter_1) {
|
|
36
|
+
return __awaiter(this, arguments, void 0, function* (filter, limit = null, offset = null, sort = []) {
|
|
37
|
+
// check if type of limit and offset is number
|
|
38
|
+
if (limit !== null && typeof limit !== 'number') {
|
|
39
|
+
throw new Error('Limit must be a number');
|
|
40
|
+
}
|
|
41
|
+
if (offset !== null && typeof offset !== 'number') {
|
|
42
|
+
throw new Error('Offset must be a number');
|
|
43
|
+
}
|
|
37
44
|
let appliedLimit = limit;
|
|
38
45
|
if (limit === null) {
|
|
39
46
|
appliedLimit = 1000000000;
|
|
@@ -32,7 +32,8 @@ export default class ConfigValidator implements IConfigValidator {
|
|
|
32
32
|
return [];
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
validateComponent(component: AdminForthComponentDeclaration, errors: Array<string
|
|
35
|
+
validateComponent(component: AdminForthComponentDeclaration, errors: Array<string>): AdminForthComponentDeclaration {
|
|
36
|
+
|
|
36
37
|
if (!component) {
|
|
37
38
|
return component;
|
|
38
39
|
}
|
|
@@ -42,6 +43,18 @@ export default class ConfigValidator implements IConfigValidator {
|
|
|
42
43
|
} else {
|
|
43
44
|
obj = component;
|
|
44
45
|
}
|
|
46
|
+
|
|
47
|
+
let ignoreExistsCheck = false;
|
|
48
|
+
if (
|
|
49
|
+
this.adminforth.codeInjector.allComponentNames.hasOwnProperty(
|
|
50
|
+
(component as AdminForthComponentDeclarationFull).file)
|
|
51
|
+
) {
|
|
52
|
+
// not obvious, but if we are in this if, it means that this is plugin component
|
|
53
|
+
// if component is plugin component, we don't need to check if it exists in users folder
|
|
54
|
+
ignoreExistsCheck = true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
45
58
|
if (!ignoreExistsCheck) {
|
|
46
59
|
errors.push(...this.checkCustomFileExists(obj.file));
|
|
47
60
|
}
|
|
@@ -109,10 +122,7 @@ export default class ConfigValidator implements IConfigValidator {
|
|
|
109
122
|
|
|
110
123
|
if (this.config.customization.customPages) {
|
|
111
124
|
this.config.customization.customPages.forEach((page, i) => {
|
|
112
|
-
|
|
113
|
-
if (this.adminforth.codeInjector.allComponentNames.hasOwnProperty(page.component as PropertyKey)) {
|
|
114
|
-
const validatedPage = this.validateComponent(page.component, errors, true);
|
|
115
|
-
}
|
|
125
|
+
this.validateComponent(page.component, errors);
|
|
116
126
|
});
|
|
117
127
|
} else {
|
|
118
128
|
this.config.customization.customPages = [];
|
|
@@ -341,9 +351,16 @@ export default class ConfigValidator implements IConfigValidator {
|
|
|
341
351
|
res.options.bulkActions = bulkActions;
|
|
342
352
|
|
|
343
353
|
// if pageInjection is a string, make array with one element. Also check file exists
|
|
344
|
-
const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom'];
|
|
354
|
+
const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems'];
|
|
355
|
+
const possiblePages = ['list', 'show', 'create', 'edit'];
|
|
356
|
+
|
|
345
357
|
if (res.options.pageInjections) {
|
|
346
358
|
Object.entries(res.options.pageInjections).map(([key, value]) => {
|
|
359
|
+
if (!possiblePages.includes(key)) {
|
|
360
|
+
const similar = suggestIfTypo(possiblePages, key);
|
|
361
|
+
errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${key}", allowed keys are ${possiblePages.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
347
364
|
Object.entries(value).map(([injection, target]) => {
|
|
348
365
|
if (possibleInjections.includes(injection)) {
|
|
349
366
|
if (!Array.isArray(res.options.pageInjections[key][injection])) {
|
|
@@ -354,7 +371,8 @@ export default class ConfigValidator implements IConfigValidator {
|
|
|
354
371
|
res.options.pageInjections[key][injection][i] = this.validateComponent(target, errors);
|
|
355
372
|
});
|
|
356
373
|
} else {
|
|
357
|
-
|
|
374
|
+
const similar = suggestIfTypo(possibleInjections, injection);
|
|
375
|
+
errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')} ${similar ? `Did you mean "${similar}"?` : ''}`);
|
|
358
376
|
}
|
|
359
377
|
});
|
|
360
378
|
|
|
@@ -477,13 +495,8 @@ export default class ConfigValidator implements IConfigValidator {
|
|
|
477
495
|
if (column.components) {
|
|
478
496
|
|
|
479
497
|
for (const [key, comp] of Object.entries(column.components as Record<string, AdminForthComponentDeclarationFull>)) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
// not obvious, but if we are in this if, it means that this is plugin component
|
|
483
|
-
// and there is no sense to check if it exists in users folder
|
|
484
|
-
ignoreExistsCheck = true;
|
|
485
|
-
}
|
|
486
|
-
column.components[key] = this.validateComponent(comp, errors, ignoreExistsCheck);
|
|
498
|
+
|
|
499
|
+
column.components[key] = this.validateComponent(comp, errors);
|
|
487
500
|
}
|
|
488
501
|
}
|
|
489
502
|
}
|
|
@@ -36,10 +36,18 @@ export default class OperationalResource implements IOperationalResource {
|
|
|
36
36
|
|
|
37
37
|
async list(
|
|
38
38
|
filter: IAdminForthFilter | IAdminForthFilter[],
|
|
39
|
-
limit: number | null,
|
|
40
|
-
offset: number | null,
|
|
41
|
-
sort: IAdminForthSort | IAdminForthSort[]
|
|
39
|
+
limit: number | null = null,
|
|
40
|
+
offset: number | null = null,
|
|
41
|
+
sort: IAdminForthSort | IAdminForthSort[] = []
|
|
42
42
|
): Promise<any[]> {
|
|
43
|
+
// check if type of limit and offset is number
|
|
44
|
+
if (limit !== null && typeof limit !== 'number') {
|
|
45
|
+
throw new Error('Limit must be a number');
|
|
46
|
+
}
|
|
47
|
+
if (offset !== null && typeof offset !== 'number') {
|
|
48
|
+
throw new Error('Offset must be a number');
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
let appliedLimit = limit;
|
|
44
52
|
if (limit === null) {
|
|
45
53
|
appliedLimit = 1000000000;
|
package/package.json
CHANGED
package/spa/src/App.vue
CHANGED
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
</div>
|
|
166
166
|
</div>
|
|
167
167
|
<AcceptModal />
|
|
168
|
-
<div v-if="toastStore.toasts.length>0" class="fixed bottom-5 right-5 flex gap-1 flex-col-reverse">
|
|
168
|
+
<div v-if="toastStore.toasts.length>0" class="fixed bottom-5 right-5 flex gap-1 flex-col-reverse z-50">
|
|
169
169
|
<transition-group
|
|
170
170
|
name="fade"
|
|
171
171
|
tag="div"
|
|
@@ -230,10 +230,8 @@
|
|
|
230
230
|
<script setup>
|
|
231
231
|
|
|
232
232
|
|
|
233
|
-
import {
|
|
234
|
-
import {
|
|
235
|
-
import { useRoute } from 'vue-router';
|
|
236
|
-
import { callAdminForthApi, getIcon } from '@/utils';
|
|
233
|
+
import { computed, ref, watch } from 'vue';
|
|
234
|
+
import { callAdminForthApi } from '@/utils';
|
|
237
235
|
|
|
238
236
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
239
237
|
import { getCustomComponent } from '@/utils';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<template >
|
|
2
|
+
<template v-if="threeDotsDropdownItems?.length">
|
|
3
|
+
<button
|
|
4
|
+
data-dropdown-toggle="listThreeDotsDropdown"
|
|
5
|
+
class="flex items-center py-2 px-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-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
|
|
6
|
+
>
|
|
7
|
+
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 4 15">
|
|
8
|
+
<path d="M3.5 1.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm0 6.041a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm0 5.959a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z"/>
|
|
9
|
+
</svg>
|
|
10
|
+
</button>
|
|
11
|
+
|
|
12
|
+
<!-- Dropdown menu -->
|
|
13
|
+
<div
|
|
14
|
+
id="listThreeDotsDropdown"
|
|
15
|
+
class="z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700 dark:divide-gray-600">
|
|
16
|
+
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownMenuIconButton">
|
|
17
|
+
<li v-for="item in threeDotsDropdownItems" :key="`dropdown-item-${item.label}`">
|
|
18
|
+
<a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
|
|
19
|
+
<component :is="getCustomComponent(item)"
|
|
20
|
+
:meta="item.meta"
|
|
21
|
+
:resource="coreStore.resource"
|
|
22
|
+
:adminUser="coreStore.adminUser"
|
|
23
|
+
/>
|
|
24
|
+
</a>
|
|
25
|
+
</li>
|
|
26
|
+
</ul>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
<script setup lang="ts">
|
|
33
|
+
|
|
34
|
+
import { getCustomComponent } from '@/utils';
|
|
35
|
+
import { useCoreStore } from '@/stores/core'
|
|
36
|
+
|
|
37
|
+
const coreStore = useCoreStore()
|
|
38
|
+
|
|
39
|
+
const props = defineProps<{
|
|
40
|
+
threeDotsDropdownItems: any[]
|
|
41
|
+
}>()
|
|
42
|
+
|
|
43
|
+
</script>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
<div id="toast-default" class="flex items-center w-full p-4 text-gray-500 rounded-lg shadow dark:text-gray-400 dark:bg-gray-800"
|
|
4
|
+
<div id="toast-default" class="flex items-center w-full p-4 text-gray-500 rounded-lg shadow dark:text-gray-400 dark:bg-gray-800"
|
|
5
|
+
role="alert"
|
|
5
6
|
:class="
|
|
6
7
|
{
|
|
7
8
|
'danger': 'bg-red-100',
|
|
@@ -24,17 +24,11 @@ import type { AdminForthResourceColumn } from '@/types/AdminForthConfig';
|
|
|
24
24
|
|
|
25
25
|
declare global {
|
|
26
26
|
interface Window {
|
|
27
|
-
adminforth:
|
|
28
|
-
confirm: (params: ConfirmParams) => Promise<void>;
|
|
29
|
-
alert: (params: AlertParams) => void;
|
|
30
|
-
setListFilter: (filter: any) => void;
|
|
31
|
-
updateListFilter: (filter: any) => void;
|
|
32
|
-
clearListFilters: () => void;
|
|
33
|
-
};
|
|
27
|
+
adminforth: FrontendAPIInterface;
|
|
34
28
|
}
|
|
35
29
|
}
|
|
36
30
|
|
|
37
|
-
export class FrontendAPI
|
|
31
|
+
export class FrontendAPI {
|
|
38
32
|
private toastStore:any
|
|
39
33
|
private modalStore:any
|
|
40
34
|
private filtersStore:any
|
|
@@ -45,14 +39,19 @@ export class FrontendAPI implements FrontendAPIInterface {
|
|
|
45
39
|
}
|
|
46
40
|
this.toastStore = useToastStore();
|
|
47
41
|
this.modalStore = useModalStore();
|
|
48
|
-
|
|
42
|
+
|
|
49
43
|
window.adminforth = {
|
|
50
44
|
confirm: this.confirm.bind(this),
|
|
51
45
|
alert: this.alert.bind(this),
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
|
|
47
|
+
list: {
|
|
48
|
+
refresh: () => {/* will be redefined in list*/},
|
|
49
|
+
closeThreeDotsDropdown: () => {/* will be redefined in list*/},
|
|
50
|
+
setFilter: () => this.setListFilter.bind(this),
|
|
51
|
+
updateFilter: () => this.updateListFilter.bind(this),
|
|
52
|
+
clearFilters: () => this.clearListFilters.bind(this),
|
|
53
|
+
}
|
|
54
|
+
};
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
confirm(params: ConfirmParams): Promise<void> {
|
package/spa/src/router/index.ts
CHANGED
package/spa/src/stores/core.ts
CHANGED
|
@@ -47,6 +47,10 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
47
47
|
if (!resource.value) {
|
|
48
48
|
throw new Error('Columns not fetched yet');
|
|
49
49
|
}
|
|
50
|
+
const col = resource.value.columns.find((col: AdminForthResourceColumn) => col.primaryKey);
|
|
51
|
+
if (!col) {
|
|
52
|
+
throw new Error(`Primary key not found in resource ${resourceId}`);
|
|
53
|
+
}
|
|
50
54
|
|
|
51
55
|
const respData = await callAdminForthApi({
|
|
52
56
|
path: '/get_resource_data',
|
|
@@ -56,7 +60,7 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
56
60
|
resourceId: resourceId,
|
|
57
61
|
filters: [
|
|
58
62
|
{
|
|
59
|
-
field:
|
|
63
|
+
field: col.name,
|
|
60
64
|
operator: 'eq',
|
|
61
65
|
value: primaryKey
|
|
62
66
|
}
|
|
@@ -3,7 +3,14 @@ import { defineStore } from 'pinia';
|
|
|
3
3
|
|
|
4
4
|
export const useFiltersStore = defineStore('filters', () => {
|
|
5
5
|
const filters: Ref<any[]> = ref([]);
|
|
6
|
-
|
|
6
|
+
const sort: Ref<any> = ref({});
|
|
7
|
+
|
|
8
|
+
const setSort = (s: any) => {
|
|
9
|
+
sort.value = s;
|
|
10
|
+
}
|
|
11
|
+
const getSort = () => {
|
|
12
|
+
return sort.value;
|
|
13
|
+
}
|
|
7
14
|
const setFilter = (filter: any) => {
|
|
8
15
|
filters.value.push(filter);
|
|
9
16
|
}
|
|
@@ -16,5 +23,5 @@ export const useFiltersStore = defineStore('filters', () => {
|
|
|
16
23
|
const clearFilters = () => {
|
|
17
24
|
filters.value = [];
|
|
18
25
|
}
|
|
19
|
-
return {setFilter, getFilters, clearFilters, filters, setFilters}
|
|
26
|
+
return {setFilter, getFilters, clearFilters, filters, setFilters, setSort, getSort}
|
|
20
27
|
})
|
package/spa/src/utils.ts
CHANGED
|
@@ -2,9 +2,10 @@ import { onMounted, ref, resolveComponent } from 'vue';
|
|
|
2
2
|
import type { CoreConfig } from './spa_types/core';
|
|
3
3
|
|
|
4
4
|
import router from "./router";
|
|
5
|
-
import { useRouter } from 'vue-router';
|
|
6
5
|
import { useCoreStore } from './stores/core';
|
|
7
6
|
import { useUserStore } from './stores/user';
|
|
7
|
+
import { Dropdown } from 'flowbite';
|
|
8
|
+
|
|
8
9
|
|
|
9
10
|
export async function callApi({path, method, body=undefined}: {
|
|
10
11
|
path: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
|
@@ -100,4 +101,16 @@ export function checkAcessByAllowedActions(allowedActions:any, action:any ) {
|
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
|
|
104
|
+
export function initThreeDotsDropdown() {
|
|
105
|
+
const threeDotsDropdown: HTMLElement | null = document.querySelector('#listThreeDotsDropdown');
|
|
106
|
+
if (threeDotsDropdown) {
|
|
107
|
+
// this resource has three dots dropdown
|
|
108
|
+
const dd = new Dropdown(
|
|
109
|
+
threeDotsDropdown,
|
|
110
|
+
document.querySelector('[data-dropdown-toggle="listThreeDotsDropdown"]') as HTMLElement,
|
|
111
|
+
);
|
|
112
|
+
window.adminforth.list.closeThreeDotsDropdown = () => {
|
|
113
|
+
dd.hide();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
Save
|
|
30
30
|
</button>
|
|
31
31
|
|
|
32
|
+
<ThreeDotsMenu
|
|
33
|
+
:threeDotsDropdownItems="coreStore.resourceOptions?.pageInjections?.create?.threeDotsDropdownItems"
|
|
34
|
+
></ThreeDotsMenu>
|
|
35
|
+
|
|
32
36
|
</BreadcrumbsWithButtons>
|
|
33
37
|
|
|
34
38
|
<component
|
|
@@ -73,12 +77,13 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
|
73
77
|
import ResourceForm from '@/components/ResourceForm.vue';
|
|
74
78
|
import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
|
|
75
79
|
import { useCoreStore } from '@/stores/core';
|
|
76
|
-
import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions } from '@/utils';
|
|
80
|
+
import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown } from '@/utils';
|
|
77
81
|
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
|
|
78
82
|
import { onMounted, ref, watch } from 'vue';
|
|
79
83
|
import { useRoute, useRouter } from 'vue-router';
|
|
80
84
|
import { computed } from 'vue';
|
|
81
85
|
import { showErrorTost } from '@/composables/useFrontendApi';
|
|
86
|
+
import ThreeDotsMenu from '@/components/ThreeDotsMenu.vue';
|
|
82
87
|
|
|
83
88
|
|
|
84
89
|
const isValid = ref(false);
|
|
@@ -116,6 +121,7 @@ onMounted(async () => {
|
|
|
116
121
|
});
|
|
117
122
|
loading.value = false;
|
|
118
123
|
checkAcessByAllowedActions(coreStore.resourceOptions.allowedActions,'create');
|
|
124
|
+
initThreeDotsDropdown();
|
|
119
125
|
});
|
|
120
126
|
|
|
121
127
|
async function saveRecord() {
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
Save
|
|
26
26
|
</button>
|
|
27
27
|
|
|
28
|
+
<ThreeDotsMenu
|
|
29
|
+
:threeDotsDropdownItems="coreStore.resourceOptions?.pageInjections?.edit?.threeDotsDropdownItems"
|
|
30
|
+
></ThreeDotsMenu>
|
|
31
|
+
|
|
28
32
|
</BreadcrumbsWithButtons>
|
|
29
33
|
|
|
30
34
|
<component
|
|
@@ -69,11 +73,13 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
|
69
73
|
import ResourceForm from '@/components/ResourceForm.vue';
|
|
70
74
|
import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
|
|
71
75
|
import { useCoreStore } from '@/stores/core';
|
|
72
|
-
import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions } from '@/utils';
|
|
76
|
+
import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown } from '@/utils';
|
|
73
77
|
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
|
|
74
78
|
import { computed, onMounted, ref } from 'vue';
|
|
75
79
|
import { useRoute, useRouter } from 'vue-router';
|
|
76
80
|
import { showErrorTost } from '@/composables/useFrontendApi';
|
|
81
|
+
import ThreeDotsMenu from '@/components/ThreeDotsMenu.vue';
|
|
82
|
+
|
|
77
83
|
|
|
78
84
|
const coreStore = useCoreStore();
|
|
79
85
|
|
|
@@ -107,12 +113,13 @@ const editableRecord = computed(() => {
|
|
|
107
113
|
})
|
|
108
114
|
|
|
109
115
|
onMounted(async () => {
|
|
110
|
-
|
|
111
116
|
loading.value = true;
|
|
112
117
|
|
|
113
118
|
await coreStore.fetchResourceFull({
|
|
114
119
|
resourceId: route.params.resourceId
|
|
115
120
|
});
|
|
121
|
+
initThreeDotsDropdown();
|
|
122
|
+
|
|
116
123
|
await coreStore.fetchRecord({
|
|
117
124
|
resourceId: route.params.resourceId,
|
|
118
125
|
primaryKey: route.params.primaryKey,
|
|
@@ -72,6 +72,10 @@
|
|
|
72
72
|
{{ filtersStore.filters.length }}
|
|
73
73
|
</span>
|
|
74
74
|
</button>
|
|
75
|
+
|
|
76
|
+
<ThreeDotsMenu
|
|
77
|
+
:threeDotsDropdownItems="coreStore.resourceOptions?.pageInjections?.list?.threeDotsDropdownItems"
|
|
78
|
+
></ThreeDotsMenu>
|
|
75
79
|
</BreadcrumbsWithButtons>
|
|
76
80
|
|
|
77
81
|
<component
|
|
@@ -112,11 +116,12 @@ import ResourceListTable from '@/components/ResourceListTable.vue';
|
|
|
112
116
|
import { useCoreStore } from '@/stores/core';
|
|
113
117
|
import { useFiltersStore } from '@/stores/filters';
|
|
114
118
|
import { callAdminForthApi, getIcon } from '@/utils';
|
|
115
|
-
import { initFlowbite } from 'flowbite';
|
|
116
119
|
import { computed, onMounted, ref, watch } from 'vue';
|
|
117
120
|
import { useRoute } from 'vue-router';
|
|
118
121
|
import { showErrorTost } from '@/composables/useFrontendApi'
|
|
119
|
-
import { getCustomComponent } from '@/utils';
|
|
122
|
+
import { getCustomComponent, initThreeDotsDropdown } from '@/utils';
|
|
123
|
+
import { initFlowbite } from 'flowbite';
|
|
124
|
+
import ThreeDotsMenu from '@/components/ThreeDotsMenu.vue';
|
|
120
125
|
|
|
121
126
|
|
|
122
127
|
import {
|
|
@@ -138,6 +143,11 @@ const page = ref(1);
|
|
|
138
143
|
const columnsMinMax = ref({});
|
|
139
144
|
const sort = ref([]);
|
|
140
145
|
|
|
146
|
+
watch(() => sort, async (to, from) => {
|
|
147
|
+
// in store sort might be needed for plugins
|
|
148
|
+
filtersStore.setSort(sort.value);
|
|
149
|
+
}, {deep: true});
|
|
150
|
+
|
|
141
151
|
const rows = ref(null);
|
|
142
152
|
const totalRows = ref(0);
|
|
143
153
|
const checkboxes = ref([]);
|
|
@@ -148,9 +158,6 @@ const DEFAULT_PAGE_SIZE = 10;
|
|
|
148
158
|
const pageSize = computed(() => coreStore.resource?.options?.listPageSize || DEFAULT_PAGE_SIZE);
|
|
149
159
|
|
|
150
160
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
161
|
async function getList() {
|
|
155
162
|
rows.value = null;
|
|
156
163
|
const data = await callAdminForthApi({
|
|
@@ -224,6 +231,8 @@ async function init() {
|
|
|
224
231
|
resourceId: route.params.resourceId
|
|
225
232
|
});
|
|
226
233
|
|
|
234
|
+
initFlowbite();
|
|
235
|
+
|
|
227
236
|
// !!! clear filters should be in same tick with sort assignment so that watch can catch it
|
|
228
237
|
filtersStore.clearFilters();
|
|
229
238
|
if (coreStore.resource.options?.defaultSort) {
|
|
@@ -234,8 +243,7 @@ async function init() {
|
|
|
234
243
|
} else {
|
|
235
244
|
sort.value = [];
|
|
236
245
|
}
|
|
237
|
-
|
|
238
|
-
// await getList();
|
|
246
|
+
// await getList(); - Not needed here, watch will trigger it
|
|
239
247
|
columnsMinMax.value = await callAdminForthApi({
|
|
240
248
|
path: '/get_min_max_for_columns',
|
|
241
249
|
method: 'POST',
|
|
@@ -246,19 +254,21 @@ async function init() {
|
|
|
246
254
|
}
|
|
247
255
|
|
|
248
256
|
watch([page, sort, () => filtersStore.filters], async () => {
|
|
249
|
-
console.log('↘️watch fired getList');
|
|
250
257
|
await getList();
|
|
251
258
|
}, { deep: true });
|
|
252
259
|
|
|
260
|
+
window.adminforth.list.refresh = async () => {
|
|
261
|
+
await getList();
|
|
262
|
+
}
|
|
263
|
+
|
|
253
264
|
watch(() => filtersStore.filters, async (to, from) => {
|
|
254
265
|
page.value = 1;
|
|
255
266
|
checkboxes.value = [];
|
|
256
267
|
}, {deep: true});
|
|
257
268
|
|
|
258
269
|
onMounted(async () => {
|
|
259
|
-
console.log('🧱onMounted fired');
|
|
260
|
-
initFlowbite();
|
|
261
270
|
await init();
|
|
271
|
+
initThreeDotsDropdown();
|
|
262
272
|
});
|
|
263
273
|
|
|
264
274
|
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
<IconTrashBinSolid class="w-4 h-4" />
|
|
23
23
|
Delete
|
|
24
24
|
</button>
|
|
25
|
+
|
|
26
|
+
<ThreeDotsMenu
|
|
27
|
+
:threeDotsDropdownItems="coreStore.resourceOptions?.pageInjections?.show?.threeDotsDropdownItems"
|
|
28
|
+
></ThreeDotsMenu>
|
|
25
29
|
</BreadcrumbsWithButtons>
|
|
26
30
|
|
|
27
31
|
<component
|
|
@@ -119,31 +123,27 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
|
119
123
|
|
|
120
124
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
121
125
|
import { useCoreStore } from '@/stores/core';
|
|
122
|
-
import {
|
|
123
|
-
import { getCustomComponent, checkAcessByAllowedActions } from '@/utils';
|
|
126
|
+
import { getCustomComponent, checkAcessByAllowedActions, initThreeDotsDropdown } from '@/utils';
|
|
124
127
|
import { IconPenSolid, IconTrashBinSolid } from '@iconify-prerendered/vue-flowbite';
|
|
125
128
|
import { onMounted, ref } from 'vue';
|
|
126
129
|
import { useRoute,useRouter } from 'vue-router';
|
|
127
130
|
import {callAdminForthApi} from '@/utils';
|
|
128
131
|
import { showSuccesTost, showErrorTost } from '@/composables/useFrontendApi';
|
|
132
|
+
import ThreeDotsMenu from '@/components/ThreeDotsMenu.vue';
|
|
129
133
|
|
|
130
134
|
|
|
131
|
-
const item = ref(null);
|
|
132
135
|
const route = useRoute();
|
|
133
136
|
const router = useRouter();
|
|
134
137
|
const loading = ref(true);
|
|
135
138
|
|
|
136
|
-
console.log(route.params,'showWiev');
|
|
137
|
-
|
|
138
|
-
|
|
139
139
|
const coreStore = useCoreStore();
|
|
140
|
-
const modalStore = useModalStore();
|
|
141
140
|
|
|
142
141
|
onMounted(async () => {
|
|
143
142
|
loading.value = true;
|
|
144
143
|
await coreStore.fetchResourceFull({
|
|
145
144
|
resourceId: route.params.resourceId
|
|
146
145
|
});
|
|
146
|
+
initThreeDotsDropdown();
|
|
147
147
|
await coreStore.fetchRecord({
|
|
148
148
|
resourceId: route.params.resourceId,
|
|
149
149
|
primaryKey: route.params.primaryKey,
|
|
@@ -176,9 +176,8 @@ async function deleteRecord(row) {
|
|
|
176
176
|
|
|
177
177
|
} catch (e) {
|
|
178
178
|
console.error(e);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
};
|
|
180
|
+
}
|
|
182
181
|
|
|
183
182
|
}
|
|
184
183
|
|
|
@@ -1043,6 +1043,7 @@ export type AdminForthResource = {
|
|
|
1043
1043
|
beforeBreadcrumbs?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
|
|
1044
1044
|
afterBreadcrumbs?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
|
|
1045
1045
|
bottom?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
|
|
1046
|
+
threeDotsDropdownItems?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
|
|
1046
1047
|
},
|
|
1047
1048
|
|
|
1048
1049
|
/**
|
package/types/FrontendAPI.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface FrontendAPIInterface {
|
|
|
19
19
|
* @returns A promise that resolves when the user confirms the dialog
|
|
20
20
|
*/
|
|
21
21
|
confirm(params:ConfirmParams ): Promise<void>;
|
|
22
|
+
|
|
22
23
|
/**
|
|
23
24
|
* Show an alert
|
|
24
25
|
*
|
|
@@ -33,37 +34,53 @@ export interface FrontendAPIInterface {
|
|
|
33
34
|
* @param params - The parameters of the alert
|
|
34
35
|
*/
|
|
35
36
|
alert(params:AlertParams): void;
|
|
36
|
-
/**
|
|
37
|
-
* Add a filter to the list of filters.
|
|
38
|
-
* Works only when user located on the list page.
|
|
39
|
-
* Can be used to set filter from charts or other components in pageInjections.
|
|
40
|
-
*
|
|
41
|
-
* Example:
|
|
42
|
-
*
|
|
43
|
-
* ```ts
|
|
44
|
-
* window.adminforth.updateListFilter({field: 'name', operator: 'ilike', value: 'john'})
|
|
45
|
-
* ```
|
|
46
|
-
*
|
|
47
|
-
* @param filter - The filter to add
|
|
48
|
-
*/
|
|
49
|
-
setListFilter(filter: any): void;
|
|
50
|
-
/**
|
|
51
|
-
* Update a filter in the list of filters
|
|
52
|
-
*
|
|
53
|
-
* Example:
|
|
54
|
-
*
|
|
55
|
-
* ```ts
|
|
56
|
-
* window.adminforth.updateListFilter({field: 'name', operator: 'ilike', value: 'john'})
|
|
57
|
-
* ```
|
|
58
|
-
*
|
|
59
|
-
* @param filter - The filter to update
|
|
60
|
-
*/
|
|
61
|
-
updateListFilter(filter: any): void;
|
|
62
|
-
/**
|
|
63
|
-
* Clear all filters from the list
|
|
64
|
-
*/
|
|
65
|
-
clearListFilters(): void;
|
|
66
37
|
|
|
38
|
+
|
|
39
|
+
list: {
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Refresh the list
|
|
43
|
+
*/
|
|
44
|
+
refresh(): void;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Close the three dots dropdown
|
|
48
|
+
*/
|
|
49
|
+
closeThreeDotsDropdown(): void;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Set a filter in the list
|
|
53
|
+
* Works only when user located on the list page.
|
|
54
|
+
* Can be used to set filter from charts or other components in pageInjections.
|
|
55
|
+
*
|
|
56
|
+
* Example:
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* window.adminforth.list.setFilter({field: 'name', operator: 'ilike', value: 'john'})
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* @param filter - The filter to set
|
|
63
|
+
*/
|
|
64
|
+
setFilter(filter: any): void;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Update a filter in the list
|
|
68
|
+
*
|
|
69
|
+
* Example:
|
|
70
|
+
*
|
|
71
|
+
* ```ts
|
|
72
|
+
* window.adminforth.list.updateFilter({field: 'name', operator: 'ilike', value: 'john'})
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @param filter - The filter to update
|
|
76
|
+
*/
|
|
77
|
+
updateFilter(filter: any): void;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Clear all filters from the list
|
|
81
|
+
*/
|
|
82
|
+
clearFilters(): void;
|
|
83
|
+
}
|
|
67
84
|
}
|
|
68
85
|
|
|
69
86
|
export type ConfirmParams = {
|