adminforth 1.1.4 → 1.1.5
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/index.js +6 -0
- package/dist/modules/styles.js +7 -6
- package/dist/plugins/ForeignInlineListPlugin/index.js +32 -1
- package/dist/plugins/base.js +4 -0
- package/dist/spa/spa/src/App.vue +2 -2
- package/dist/spa/spa/src/components/BreadcrumbsWithButtons.vue +1 -1
- package/dist/spa/spa/src/components/Filters.vue +19 -15
- package/dist/spa/spa/src/components/ResourceForm.vue +12 -12
- package/dist/spa/spa/src/components/ResourceListTable.vue +29 -15
- package/dist/spa/spa/src/composables/useStores.ts +63 -3
- package/dist/spa/spa/src/stores/core.ts +7 -0
- package/dist/spa/spa/src/stores/filters.ts +22 -0
- package/dist/spa/spa/src/stores/modal.ts +0 -1
- package/dist/spa/spa/src/views/CreateView.vue +8 -3
- package/dist/spa/spa/src/views/EditView.vue +2 -2
- package/dist/spa/spa/src/views/ListView.vue +21 -21
- package/dist/spa/spa/src/views/ShowView.vue +3 -3
- package/index.ts +10 -0
- package/modules/styles.ts +7 -6
- package/package.json +1 -1
- package/plugins/ForeignInlineListPlugin/custom/InlineList.vue +105 -13
- package/plugins/ForeignInlineListPlugin/index.ts +32 -5
- package/plugins/base.ts +7 -0
- package/spa/src/App.vue +2 -2
- package/spa/src/components/BreadcrumbsWithButtons.vue +1 -1
- package/spa/src/components/Filters.vue +19 -15
- package/spa/src/components/ResourceForm.vue +12 -12
- package/spa/src/components/ResourceListTable.vue +29 -15
- package/spa/src/composables/useStores.ts +63 -3
- package/spa/src/stores/core.ts +7 -0
- package/spa/src/stores/filters.ts +22 -0
- package/spa/src/stores/modal.ts +0 -1
- package/spa/src/views/CreateView.vue +8 -3
- package/spa/src/views/EditView.vue +2 -2
- package/spa/src/views/ListView.vue +21 -21
- package/spa/src/views/ShowView.vue +3 -3
- package/types/AdminForthConfig.ts +32 -1
- package/types/FrontendAPI.ts +47 -0
|
@@ -30,18 +30,18 @@
|
|
|
30
30
|
/>
|
|
31
31
|
</template>
|
|
32
32
|
<template v-else>
|
|
33
|
-
<td class="px-6 py-4 whitespace-nowrap">
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
<td class="px-6 py-4 whitespace-nowrap "> <!--align-top-->
|
|
34
|
+
{{ column.label }}
|
|
35
|
+
<span :data-tooltip-target="`tooltip-show-${i}`" class="relative inline-block">
|
|
36
|
+
<IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
|
|
37
|
+
:class="(columnError(column) && validating) ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'"
|
|
38
|
+
/>
|
|
39
|
+
</span>
|
|
40
|
+
<div :id="`tooltip-show-${i}`"
|
|
41
|
+
role="tooltip" 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">
|
|
42
|
+
Required field
|
|
43
|
+
<div class="tooltip-arrow" data-popper-arrow></div>
|
|
44
|
+
</div>
|
|
45
45
|
</td>
|
|
46
46
|
<td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap relative">
|
|
47
47
|
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
<input
|
|
126
126
|
id="checkbox-table-search-1"
|
|
127
127
|
type="checkbox"
|
|
128
|
-
:checked="
|
|
128
|
+
:checked="checkboxesInternal.includes(row.id)"
|
|
129
129
|
@change="(e)=>{addToCheckedValues(row.id)}"
|
|
130
130
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
|
131
131
|
<label for="checkbox-table-search-1" class="sr-only">checkbox</label>
|
|
@@ -161,10 +161,12 @@
|
|
|
161
161
|
</div>
|
|
162
162
|
|
|
163
163
|
<RouterLink v-if="resource.options?.allowedActions.edit"
|
|
164
|
-
:to="{
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
:to="{
|
|
165
|
+
name: 'resource-edit',
|
|
166
|
+
params: {
|
|
167
|
+
resourceId: resource.id,
|
|
168
|
+
primaryKey: row._primaryKeyValue
|
|
169
|
+
}
|
|
168
170
|
}"
|
|
169
171
|
class="font-medium text-blue-600 dark:text-blue-500 hover:underline ms-3"
|
|
170
172
|
:data-tooltip-target="`tooltip-edit-${rowI}`"
|
|
@@ -239,6 +241,7 @@
|
|
|
239
241
|
class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-l-0 border border-gray-300 hover:bg-gray-100 hover:text-blue-700 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 disabled:opacity-50"
|
|
240
242
|
@click="page = totalPages" :disabled="page >= totalPages">
|
|
241
243
|
{{ totalPages }}
|
|
244
|
+
|
|
242
245
|
<!-- <IconChevronDoubleRightOutline class="w-4 h-4" /> -->
|
|
243
246
|
</button>
|
|
244
247
|
<button
|
|
@@ -253,6 +256,7 @@
|
|
|
253
256
|
</svg>
|
|
254
257
|
</button>
|
|
255
258
|
</div>
|
|
259
|
+
|
|
256
260
|
</div>
|
|
257
261
|
</template>
|
|
258
262
|
|
|
@@ -287,6 +291,7 @@ const props = defineProps([
|
|
|
287
291
|
'rows',
|
|
288
292
|
'totalRows',
|
|
289
293
|
'pageSize',
|
|
294
|
+
'checkboxes'
|
|
290
295
|
])
|
|
291
296
|
|
|
292
297
|
// emits, update page
|
|
@@ -297,7 +302,7 @@ const emits = defineEmits([
|
|
|
297
302
|
|
|
298
303
|
]);
|
|
299
304
|
|
|
300
|
-
const
|
|
305
|
+
const checkboxesInternal = ref([]);
|
|
301
306
|
const page = ref(1);
|
|
302
307
|
const sort = ref([]);
|
|
303
308
|
|
|
@@ -310,32 +315,41 @@ watch(() => sort.value, (newSort) => {
|
|
|
310
315
|
emits('update:sort', newSort);
|
|
311
316
|
});
|
|
312
317
|
|
|
313
|
-
watch(() =>
|
|
318
|
+
watch(() => checkboxesInternal.value, (newCheckboxes) => {
|
|
319
|
+
console.log('checkboxesInternal ch changed, emiting', newCheckboxes)
|
|
320
|
+
|
|
314
321
|
emits('update:checkboxes', newCheckboxes);
|
|
315
322
|
});
|
|
316
323
|
|
|
324
|
+
watch(() => props.checkboxes, (newCheckboxes) => {
|
|
325
|
+
console.log('Props ch changed', newCheckboxes)
|
|
326
|
+
checkboxesInternal.value = newCheckboxes;
|
|
327
|
+
});
|
|
328
|
+
|
|
317
329
|
function addToCheckedValues(id) {
|
|
318
|
-
if (
|
|
319
|
-
|
|
330
|
+
if (checkboxesInternal.value.includes(id)) {
|
|
331
|
+
checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== id);
|
|
320
332
|
} else {
|
|
321
|
-
|
|
333
|
+
checkboxesInternal.value.push(id);
|
|
322
334
|
}
|
|
335
|
+
checkboxesInternal.value = [ ...checkboxesInternal.value ]
|
|
323
336
|
}
|
|
324
337
|
|
|
325
338
|
const columnsListed = computed(() => props.resource?.columns?.filter(c => c.showIn.includes('list')));
|
|
326
339
|
|
|
327
340
|
async function selectAll(value) {
|
|
328
|
-
rows.
|
|
329
|
-
if (!
|
|
330
|
-
|
|
341
|
+
props.rows.forEach((r) => {
|
|
342
|
+
if (!checkboxesInternal.value.includes(r.id)) {
|
|
343
|
+
checkboxesInternal.value.push(r.id)
|
|
331
344
|
} else {
|
|
332
|
-
|
|
345
|
+
checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== r.id)
|
|
333
346
|
}
|
|
334
347
|
});
|
|
348
|
+
checkboxesInternal.value = [ ...checkboxesInternal.value ]
|
|
335
349
|
// checkboxes.value = rows.value.map((v) => v.id);
|
|
336
350
|
}
|
|
337
351
|
|
|
338
|
-
const totalPages = computed(() => Math.ceil(props.totalRows
|
|
352
|
+
const totalPages = computed(() => Math.ceil(props.totalRows / props.pageSize));
|
|
339
353
|
|
|
340
354
|
|
|
341
355
|
const allFromThisPageChecked = computed(() => {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import type { FrontendAPIInterface, ConfirmParams, AlertParams } from '../types/FrontendAPI';
|
|
1
|
+
import type { FrontendAPIInterface, ConfirmParams, AlertParams, FilterParams,Operator } from '../types/FrontendAPI';
|
|
2
2
|
import { useToastStore } from '../stores/toast';
|
|
3
3
|
import { useModalStore } from '../stores/modal';
|
|
4
|
+
import { useCoreStore } from '@/stores/core';
|
|
5
|
+
import { useFiltersStore } from '@/stores/filters';
|
|
6
|
+
import router from '@/router'
|
|
7
|
+
|
|
8
|
+
|
|
4
9
|
|
|
5
10
|
|
|
6
11
|
|
|
@@ -9,13 +14,19 @@ declare global {
|
|
|
9
14
|
adminforth: {
|
|
10
15
|
confirm: (params: ConfirmParams) => Promise<void>;
|
|
11
16
|
alert: (params: AlertParams) => void;
|
|
17
|
+
setListFilter: (filter: any) => void;
|
|
18
|
+
updateListFilter: (filter: any) => void;
|
|
19
|
+
clearListFilters: () => void;
|
|
12
20
|
};
|
|
13
21
|
}
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
export class FrontendAPI implements FrontendAPIInterface {
|
|
25
|
+
private validOperators: Operator[] = ['lte', 'gte', 'in','ilike'];
|
|
17
26
|
private toastStore:any
|
|
18
27
|
private modalStore:any
|
|
28
|
+
private filtersStore:any
|
|
29
|
+
private coreStore:any
|
|
19
30
|
init() {
|
|
20
31
|
if (window.adminforth) {
|
|
21
32
|
throw new Error('adminforth already initialized');
|
|
@@ -25,7 +36,10 @@ export class FrontendAPI implements FrontendAPIInterface {
|
|
|
25
36
|
console.log(this.toastStore, this.modalStore,'init of adminforth frontend api')
|
|
26
37
|
window.adminforth = {
|
|
27
38
|
confirm: this.confirm.bind(this),
|
|
28
|
-
alert: this.alert.bind(this)
|
|
39
|
+
alert: this.alert.bind(this),
|
|
40
|
+
setListFilter: this.setListFilter.bind(this),
|
|
41
|
+
updateListFilter: this.updateListFilter.bind(this),
|
|
42
|
+
clearListFilters: this.clearListFilters.bind(this),
|
|
29
43
|
}
|
|
30
44
|
}
|
|
31
45
|
|
|
@@ -44,8 +58,54 @@ export class FrontendAPI implements FrontendAPIInterface {
|
|
|
44
58
|
variant: params.variant
|
|
45
59
|
})
|
|
46
60
|
}
|
|
47
|
-
}
|
|
48
61
|
|
|
62
|
+
listFilterValidation(filter: FilterParams): boolean {
|
|
63
|
+
if(router.currentRoute.value.meta.type !== 'list'){
|
|
64
|
+
throw new Error(`Cannot use ${this.setListFilter.name} filter on a list page`)
|
|
65
|
+
} else {
|
|
66
|
+
if(!this.coreStore) this.coreStore = useCoreStore()
|
|
67
|
+
console.log(this.coreStore.resourceColumnsWithFilters,'core store')
|
|
68
|
+
const filterField = this.coreStore.resourceColumnsWithFilters.find((col) => col.name === filter.field)
|
|
69
|
+
if(!filterField){
|
|
70
|
+
throw new Error(`Field ${filter.field} is not available for filtering`)
|
|
71
|
+
}
|
|
72
|
+
if(filterField) {
|
|
73
|
+
if(!this.validOperators.includes(filter.operator)){
|
|
74
|
+
throw new Error(`Operator ${filter.operator} is not valid`)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setListFilter(filter: FilterParams): void {
|
|
82
|
+
if(this.listFilterValidation(filter)){
|
|
83
|
+
this.filtersStore = useFiltersStore()
|
|
84
|
+
if(this.filtersStore.filters.some((f) => {return f.field === filter.field && f.operator === filter.operator})){
|
|
85
|
+
throw new Error(`Filter ${filter.field} with operator ${filter.operator} already exists`)
|
|
86
|
+
} else {
|
|
87
|
+
this.filtersStore.setFilter(filter)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
clearListFilters(): void {
|
|
93
|
+
this.filtersStore = useFiltersStore()
|
|
94
|
+
this.filtersStore.clearFilters()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
updateListFilter(filter: FilterParams): void {
|
|
98
|
+
if(this.listFilterValidation(filter)){
|
|
99
|
+
this.filtersStore = useFiltersStore()
|
|
100
|
+
const index = this.filtersStore.filters.findIndex((f) => f.field === filter.field)
|
|
101
|
+
if(index === -1) {
|
|
102
|
+
this.filtersStore.setFilter(filter)
|
|
103
|
+
} else {
|
|
104
|
+
this.filtersStore.setFilters([...this.filtersStore.filters.slice(0, index), filter, ...this.filtersStore.filters.slice(index + 1)])
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
49
108
|
|
|
50
109
|
|
|
51
110
|
|
|
111
|
+
}
|
package/spa/src/stores/core.ts
CHANGED
|
@@ -9,6 +9,12 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
9
9
|
const record = ref({});
|
|
10
10
|
const resourceColumns = ref(null);
|
|
11
11
|
const resource = ref(null);
|
|
12
|
+
const resourceColumnsWithFilters = computed(() => {
|
|
13
|
+
if (!resourceColumns.value) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
return resourceColumns.value.filter((col) => col.showIn.includes('filter'));
|
|
17
|
+
})
|
|
12
18
|
|
|
13
19
|
const resourceOptions = ref(null);
|
|
14
20
|
const resourceColumnsError = ref('');
|
|
@@ -132,5 +138,6 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
132
138
|
logout,
|
|
133
139
|
resource,
|
|
134
140
|
adminUser,
|
|
141
|
+
resourceColumnsWithFilters
|
|
135
142
|
}
|
|
136
143
|
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
import { defineStore } from 'pinia'
|
|
3
|
+
import { callAdminForthApi } from '@/utils';
|
|
4
|
+
|
|
5
|
+
export const useFiltersStore = defineStore('filters', () => {
|
|
6
|
+
const filters = ref([]);
|
|
7
|
+
const setFilter = (filter: any) => {
|
|
8
|
+
filters.value = [...filters.value, filter];
|
|
9
|
+
}
|
|
10
|
+
const setFilters = (f: any) => {
|
|
11
|
+
filters.value = [...f];
|
|
12
|
+
}
|
|
13
|
+
const getFilters = () => {
|
|
14
|
+
return filters.value;
|
|
15
|
+
}
|
|
16
|
+
const clearFilters = () => {
|
|
17
|
+
filters.value = [];
|
|
18
|
+
}
|
|
19
|
+
return {setFilter, getFilters,clearFilters, filters,setFilters}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
|
package/spa/src/stores/modal.ts
CHANGED
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
<BreadcrumbsWithButtons>
|
|
14
14
|
<!-- save and cancle -->
|
|
15
15
|
<button @click="$router.back()"
|
|
16
|
-
class="flex items-center py-1 px-3 me-2
|
|
16
|
+
class="flex items-center py-1 px-3 me-2 text-sm font-medium rounded-default text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 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"
|
|
17
17
|
>
|
|
18
18
|
Cancel
|
|
19
19
|
</button>
|
|
20
20
|
|
|
21
21
|
<button @click="saveRecord"
|
|
22
|
-
class="flex items-center py-1 px-3
|
|
22
|
+
class="flex items-center py-1 px-3 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50"
|
|
23
23
|
:disabled="saving || (validating && !isValid)"
|
|
24
24
|
>
|
|
25
25
|
<svg v-if="saving" x
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
|
|
46
46
|
<ResourceForm
|
|
47
47
|
v-else
|
|
48
|
-
:record="
|
|
48
|
+
:record="initalValues"
|
|
49
49
|
:resourceColumns="coreStore.resourceColumns"
|
|
50
50
|
@update:record="onUpdateRecord"
|
|
51
51
|
@update:isValid="isValid = $event"
|
|
@@ -77,6 +77,7 @@ import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions } from
|
|
|
77
77
|
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
|
|
78
78
|
import { onMounted, ref } from 'vue';
|
|
79
79
|
import { useRoute, useRouter } from 'vue-router';
|
|
80
|
+
import { computed } from 'vue';
|
|
80
81
|
|
|
81
82
|
|
|
82
83
|
const isValid = ref(false);
|
|
@@ -93,6 +94,10 @@ let createComponentsPerColumn = {};
|
|
|
93
94
|
|
|
94
95
|
const coreStore = useCoreStore();
|
|
95
96
|
|
|
97
|
+
const initalValues = computed(() => {
|
|
98
|
+
return route.query || {};
|
|
99
|
+
});
|
|
100
|
+
|
|
96
101
|
async function onUpdateRecord(newRecord) {
|
|
97
102
|
console.log('newRecord', newRecord);
|
|
98
103
|
record.value = newRecord;
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
<BreadcrumbsWithButtons>
|
|
13
13
|
<!-- save and cancle -->
|
|
14
14
|
<button @click="$router.back()"
|
|
15
|
-
class="flex items-center py-1 px-3 me-2
|
|
15
|
+
class="flex items-center py-1 px-3 me-2 text-sm font-medium text-gray-900 rounded-default focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 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"
|
|
16
16
|
>
|
|
17
17
|
Cancel
|
|
18
18
|
</button>
|
|
19
19
|
|
|
20
20
|
<button @click="saveRecord"
|
|
21
|
-
class="flex items-center py-1 px-3
|
|
21
|
+
class="flex items-center py-1 px-3 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
|
22
22
|
:disabled="saving || (validating && !isValid)"
|
|
23
23
|
>
|
|
24
24
|
<IconFloppyDiskSolid class="w-4 h-4" />
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
<Teleport to="body">
|
|
4
4
|
<Filters
|
|
5
5
|
:columns="coreStore.resourceColumns"
|
|
6
|
-
v-model:filters="filters"
|
|
7
6
|
:columnsMinMax="columnsMinMax" :show="filtersShow"
|
|
8
7
|
@hide="filtersShow = false"
|
|
9
8
|
/>
|
|
@@ -23,7 +22,7 @@
|
|
|
23
22
|
v-if="checkboxes.length"
|
|
24
23
|
data-tooltip-target="tooltip-remove-all"
|
|
25
24
|
data-tooltip-placement="bottom"
|
|
26
|
-
class="flex gap-1 items-center py-1 px-3 me-2
|
|
25
|
+
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-blue-700 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"
|
|
27
26
|
>
|
|
28
27
|
<IconBanOutline class="w-5 h-5 "/>
|
|
29
28
|
|
|
@@ -39,7 +38,7 @@
|
|
|
39
38
|
v-for="(action,i) in coreStore.resource?.options?.bulkActions"
|
|
40
39
|
:key="action.id"
|
|
41
40
|
@click="startBulkAction(action.id)"
|
|
42
|
-
class="flex gap-1 items-center py-1 px-3
|
|
41
|
+
class="flex gap-1 items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 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"
|
|
43
42
|
:class="{'bg-red-100 text-red-800 border-red-400 dark:bg-red-700 dark:text-red-400 dark:border-red-400':action.state==='danger', 'bg-green-100 text-green-800 border-green-400 dark:bg-green-700 dark:text-green-400 dark:border-green-400':action.state==='success',
|
|
44
43
|
'bg-blue-100 text-blue-800 border-blue-400 dark:bg-blue-700 dark:text-blue-400 dark:border-blue-400':action.state==='active',
|
|
45
44
|
}"
|
|
@@ -52,24 +51,24 @@
|
|
|
52
51
|
{{ `${action.label} (${checkboxes.length})` }}
|
|
53
52
|
</button>
|
|
54
53
|
|
|
55
|
-
<RouterLink v-if="coreStore.resource?.options?.create"
|
|
54
|
+
<RouterLink v-if="coreStore.resource?.options?.allowedActions?.create"
|
|
56
55
|
:to="{ name: 'resource-create', params: { resourceId: $route.params.resourceId } }"
|
|
57
|
-
class="flex items-center py-1 px-3
|
|
56
|
+
class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 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"
|
|
58
57
|
>
|
|
59
58
|
<IconPlusOutline class="w-4 h-4 me-2"/>
|
|
60
59
|
Create
|
|
61
60
|
</RouterLink>
|
|
62
61
|
|
|
63
62
|
<button
|
|
64
|
-
class="flex gap-1 items-center py-1 px-3 me-2
|
|
63
|
+
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-blue-700 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"
|
|
65
64
|
@click="()=>{filtersShow = !filtersShow}"
|
|
66
65
|
>
|
|
67
66
|
<IconFilterOutline class="w-4 h-4 me-2"/>
|
|
68
67
|
Filter
|
|
69
68
|
<span
|
|
70
69
|
class="bg-red-100 text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400"
|
|
71
|
-
v-if="filters.length">
|
|
72
|
-
{{ filters.length }}
|
|
70
|
+
v-if="filtersStore.filters.length">
|
|
71
|
+
{{ filtersStore.filters.length }}
|
|
73
72
|
</span>
|
|
74
73
|
</button>
|
|
75
74
|
</BreadcrumbsWithButtons>
|
|
@@ -90,6 +89,7 @@
|
|
|
90
89
|
@update:checkboxes="checkboxes = $event"
|
|
91
90
|
:pageSize="pageSize"
|
|
92
91
|
:totalRows="totalRows"
|
|
92
|
+
:checkboxes="checkboxes"
|
|
93
93
|
/>
|
|
94
94
|
|
|
95
95
|
<component
|
|
@@ -108,6 +108,7 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
|
108
108
|
import ResourceListTable from '@/components/ResourceListTable.vue';
|
|
109
109
|
import { useCoreStore } from '@/stores/core';
|
|
110
110
|
import { useModalStore } from '@/stores/modal';
|
|
111
|
+
import { useFiltersStore } from '@/stores/filters';
|
|
111
112
|
import { callAdminForthApi, getIcon } from '@/utils';
|
|
112
113
|
import { initFlowbite } from 'flowbite';
|
|
113
114
|
import { computed, onMounted, ref, watch } from 'vue';
|
|
@@ -116,17 +117,15 @@ import { useRoute } from 'vue-router';
|
|
|
116
117
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
117
118
|
import { getCustomComponent } from '@/utils';
|
|
118
119
|
|
|
119
|
-
import {
|
|
120
|
-
IconInboxOutline,
|
|
121
|
-
IconPlusOutline
|
|
122
|
-
} from '@iconify-prerendered/vue-flowbite';
|
|
123
120
|
|
|
124
121
|
import {
|
|
125
|
-
IconBanOutline,
|
|
126
|
-
IconEyeSolid,
|
|
127
|
-
IconFilterOutline,
|
|
128
|
-
IconPenSolid,
|
|
129
|
-
IconTrashBinSolid
|
|
122
|
+
IconBanOutline,
|
|
123
|
+
IconEyeSolid,
|
|
124
|
+
IconFilterOutline,
|
|
125
|
+
IconPenSolid,
|
|
126
|
+
IconTrashBinSolid,
|
|
127
|
+
IconInboxOutline,
|
|
128
|
+
IconPlusOutline
|
|
130
129
|
} from '@iconify-prerendered/vue-flowbite';
|
|
131
130
|
|
|
132
131
|
import Filters from '@/components/Filters.vue';
|
|
@@ -135,11 +134,12 @@ const filtersShow = ref(false);
|
|
|
135
134
|
|
|
136
135
|
const coreStore = useCoreStore();
|
|
137
136
|
const modalStore = useModalStore();
|
|
137
|
+
const filtersStore = useFiltersStore();
|
|
138
138
|
|
|
139
139
|
const route = useRoute();
|
|
140
140
|
|
|
141
141
|
const page = ref(1);
|
|
142
|
-
const filters =
|
|
142
|
+
const filters = filtersStore.filters;
|
|
143
143
|
const columnsMinMax = ref({});
|
|
144
144
|
const sort = ref([]);
|
|
145
145
|
|
|
@@ -157,7 +157,7 @@ watch([page], async () => {
|
|
|
157
157
|
await getList();
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
-
watch(
|
|
160
|
+
watch(()=>filtersStore.filters, async () => {
|
|
161
161
|
page.value = 1;
|
|
162
162
|
await getList();
|
|
163
163
|
}, {deep: true});
|
|
@@ -177,7 +177,7 @@ async function getList() {
|
|
|
177
177
|
resourceId: route.params.resourceId,
|
|
178
178
|
limit: pageSize.value,
|
|
179
179
|
offset: (page.value - 1) * pageSize.value,
|
|
180
|
-
filters: filters
|
|
180
|
+
filters: filtersStore.filters,
|
|
181
181
|
sort: sort.value,
|
|
182
182
|
}
|
|
183
183
|
});
|
|
@@ -265,7 +265,7 @@ onMounted(async () => {
|
|
|
265
265
|
|
|
266
266
|
// on route param change
|
|
267
267
|
watch(() => route.params.resourceId, async () => {
|
|
268
|
-
|
|
268
|
+
filtersStore.setFilters([]);
|
|
269
269
|
checkboxes.value = [];
|
|
270
270
|
sort.value = [];
|
|
271
271
|
await init();
|
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
/>
|
|
11
11
|
<BreadcrumbsWithButtons>
|
|
12
12
|
<RouterLink v-if="coreStore?.resourceOptions?.allowedActions?.edit" :to="{ name: 'resource-edit', params: { resourceId: $route.params.resourceId, primaryKey: $route.params.primaryKey } }"
|
|
13
|
-
class="flex items-center py-1 px-3
|
|
13
|
+
class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 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"
|
|
14
14
|
>
|
|
15
15
|
<IconPenSolid class="w-4 h-4" />
|
|
16
16
|
Edit
|
|
17
17
|
</RouterLink>
|
|
18
18
|
|
|
19
19
|
<button v-if="coreStore?.resourceOptions?.allowedActions?.delete" @click="showDeleteModal"
|
|
20
|
-
class="flex items-center py-1 px-3
|
|
20
|
+
class="flex items-center py-1 px-3 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
|
21
21
|
>
|
|
22
22
|
<IconTrashBinSolid class="w-4 h-4" />
|
|
23
23
|
Delete
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
:record="coreStore.record"
|
|
71
71
|
/>
|
|
72
72
|
<template v-else>
|
|
73
|
-
<td class="px-6 py-4 whitespace-nowrap">
|
|
73
|
+
<td class="px-6 py-4 whitespace-nowrap "> <!--align-top-->
|
|
74
74
|
{{ column.label }}
|
|
75
75
|
</td>
|
|
76
76
|
<td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap">
|
|
@@ -19,6 +19,17 @@ export interface GenericHttpServer {
|
|
|
19
19
|
*/
|
|
20
20
|
setupSpaServer(): void;
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Method which should register endpoint in HTTP server.
|
|
24
|
+
*
|
|
25
|
+
* @param options : Object with method, path and handler properties.
|
|
26
|
+
*/
|
|
27
|
+
endpoint(options: {
|
|
28
|
+
method: string,
|
|
29
|
+
path: string,
|
|
30
|
+
handler: Function,
|
|
31
|
+
}): void;
|
|
32
|
+
|
|
22
33
|
}
|
|
23
34
|
|
|
24
35
|
export interface ExpressHttpServer extends GenericHttpServer {
|
|
@@ -53,6 +64,7 @@ export interface AdminForthClass {
|
|
|
53
64
|
codeInjector: CodeInjectorType;
|
|
54
65
|
express: GenericHttpServer;
|
|
55
66
|
|
|
67
|
+
|
|
56
68
|
auth: {
|
|
57
69
|
|
|
58
70
|
verify(jwt : string): any;
|
|
@@ -91,8 +103,26 @@ export interface AdminForthPluginType {
|
|
|
91
103
|
adminforth: AdminForthClass;
|
|
92
104
|
pluginDir: string;
|
|
93
105
|
customFolderName: string;
|
|
106
|
+
pluginInstanceId: string;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* AdminForth plugins concept is based on modification of full AdminForth configuration
|
|
110
|
+
* to add some custom functionality. For example plugin might simply add custom field to resource by reusing
|
|
111
|
+
* {@link AdminForthResourceColumn.components} object, then add some hook which will modify record before getting or saving it to database.
|
|
112
|
+
*
|
|
113
|
+
* So this method is core of AdminForth plugins. It allows to modify full resource configuration.
|
|
114
|
+
* @param adminforth Instance of AdminForthClass
|
|
115
|
+
* @param resourceConfig Resource configuration object which will be modified by plugin
|
|
116
|
+
*/
|
|
94
117
|
modifyResourceConfig(adminforth: AdminForthClass, resourceConfig: AdminForthResource): void;
|
|
95
118
|
componentPath(componentFile: string): string;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Here you can register custom endpoints for your plugin.
|
|
122
|
+
*
|
|
123
|
+
* @param server
|
|
124
|
+
*/
|
|
125
|
+
setupEndpoints(server: GenericHttpServer): void;
|
|
96
126
|
}
|
|
97
127
|
|
|
98
128
|
export enum AdminForthMenuTypes {
|
|
@@ -450,9 +480,10 @@ export type AdminForthResource = {
|
|
|
450
480
|
bulkActions?: Array<{
|
|
451
481
|
label: string,
|
|
452
482
|
state: string,
|
|
453
|
-
icon
|
|
483
|
+
icon?: string,
|
|
454
484
|
action: Function,
|
|
455
485
|
id?: string,
|
|
486
|
+
confirm?: string,
|
|
456
487
|
}>,
|
|
457
488
|
allowedActions?: AllowedActions,
|
|
458
489
|
|
package/types/FrontendAPI.ts
CHANGED
|
@@ -33,6 +33,35 @@ export interface FrontendAPIInterface {
|
|
|
33
33
|
* @param params - The parameters of the alert
|
|
34
34
|
*/
|
|
35
35
|
alert(params:AlertParams): void;
|
|
36
|
+
/**
|
|
37
|
+
* Add a filter to the list of filters
|
|
38
|
+
*
|
|
39
|
+
* Example:
|
|
40
|
+
*
|
|
41
|
+
* ```ts
|
|
42
|
+
* window.adminforth.updateListFilter({field: 'name', operator: 'ilike', value: 'john'})
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @param filter - The filter to add
|
|
46
|
+
*/
|
|
47
|
+
setListFilter(filter: any): void;
|
|
48
|
+
/**
|
|
49
|
+
* Update a filter in the list of filters
|
|
50
|
+
*
|
|
51
|
+
* Example:
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* window.adminforth.updateListFilter({field: 'name', operator: 'ilike', value: 'john'})
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* @param filter - The filter to update
|
|
58
|
+
*/
|
|
59
|
+
updateListFilter(filter: any): void;
|
|
60
|
+
/**
|
|
61
|
+
* Clear all filters from the list
|
|
62
|
+
*/
|
|
63
|
+
clearListFilters(): void;
|
|
64
|
+
|
|
36
65
|
}
|
|
37
66
|
|
|
38
67
|
export type ConfirmParams = {
|
|
@@ -62,12 +91,30 @@ export type AlertParams = {
|
|
|
62
91
|
variant?: AlertVariant;
|
|
63
92
|
}
|
|
64
93
|
|
|
94
|
+
export type FilterParams = {
|
|
95
|
+
/**
|
|
96
|
+
* Field of resource to filter
|
|
97
|
+
*/
|
|
98
|
+
field: string;
|
|
99
|
+
/**
|
|
100
|
+
* Operator of filter
|
|
101
|
+
*/
|
|
102
|
+
operator: Operator;
|
|
103
|
+
/**
|
|
104
|
+
* Value of filter
|
|
105
|
+
*/
|
|
106
|
+
value: string | number | boolean ;
|
|
107
|
+
}
|
|
108
|
+
|
|
65
109
|
export enum AlertVariant {
|
|
66
110
|
Danger = 'danger',
|
|
67
111
|
Success = 'success',
|
|
68
112
|
Warning = 'warning',
|
|
69
113
|
Info = 'info'
|
|
70
114
|
}
|
|
115
|
+
|
|
116
|
+
export type Operator = 'in' | 'ilike' | 'gte' | 'lte' ;
|
|
117
|
+
|
|
71
118
|
|
|
72
119
|
|
|
73
120
|
|