adminforth 2.27.0-next.1 → 2.27.0-next.10
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/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +40 -2
- package/dist/modules/restApi.js.map +1 -1
- package/dist/spa/package-lock.json +41 -0
- package/dist/spa/package.json +3 -0
- package/dist/spa/pnpm-lock.yaml +38 -0
- package/dist/spa/src/components/ColumnValueInputWrapper.vue +24 -1
- package/dist/spa/src/components/CustomRangePicker.vue +6 -0
- package/dist/spa/src/components/GroupsTable.vue +7 -4
- package/dist/spa/src/components/ResourceForm.vue +100 -6
- package/dist/spa/src/components/ResourceListTable.vue +21 -43
- package/dist/spa/src/components/ThreeDotsMenu.vue +47 -51
- package/dist/spa/src/types/Back.ts +3 -0
- package/dist/spa/src/types/Common.ts +18 -3
- package/dist/spa/src/types/adapters/StorageAdapter.ts +12 -0
- package/dist/spa/src/utils/createEditUtils.ts +65 -0
- package/dist/spa/src/utils/index.ts +2 -1
- package/dist/spa/src/utils/utils.ts +165 -4
- package/dist/spa/src/utils.ts +2 -1
- package/dist/spa/src/views/CreateView.vue +22 -49
- package/dist/spa/src/views/EditView.vue +20 -38
- package/dist/spa/src/views/ListView.vue +72 -13
- package/dist/spa/src/views/ShowView.vue +52 -46
- package/dist/types/Back.d.ts +3 -0
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +23 -3
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js.map +1 -1
- package/dist/types/adapters/StorageAdapter.d.ts +11 -0
- package/dist/types/adapters/StorageAdapter.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<Tooltip v-if="column.required[mode]">
|
|
31
31
|
|
|
32
32
|
<IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
|
|
33
|
-
:class="(
|
|
33
|
+
:class="(columnsWithErrors[column.name] && validatingMode && !isValidating) ? 'text-lightInputErrorColor dark:text-darkInputErrorColor' : 'text-lightRequiredIconColor dark:text-darkRequiredIconColor'"
|
|
34
34
|
/>
|
|
35
35
|
|
|
36
36
|
<template #tooltip>
|
|
@@ -56,9 +56,10 @@
|
|
|
56
56
|
@update:inValidity="customComponentsInValidity[$event.name] = $event.value"
|
|
57
57
|
@update:emptiness="customComponentsEmptiness[$event.name] = $event.value"
|
|
58
58
|
:readonly="readonlyColumns?.includes(column.name)"
|
|
59
|
+
:columnsWithErrors="columnsWithErrors"
|
|
60
|
+
:isValidating="isValidating"
|
|
61
|
+
:validatingMode="validatingMode"
|
|
59
62
|
/>
|
|
60
|
-
<div v-if="columnError(column) && validating" class="af-invalid-field-message mt-1 text-xs text-lightInputErrorColor dark:text-darkInputErrorColor">{{ columnError(column) }}</div>
|
|
61
|
-
<div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-lightFormFieldTextColor dark:text-darkFormFieldTextColor">{{ column.editingNote[mode] }}</div>
|
|
62
63
|
</td>
|
|
63
64
|
</tr>
|
|
64
65
|
</tbody>
|
|
@@ -80,13 +81,15 @@
|
|
|
80
81
|
source: 'create' | 'edit',
|
|
81
82
|
group: any,
|
|
82
83
|
mode: string,
|
|
83
|
-
|
|
84
|
+
validatingMode: boolean,
|
|
84
85
|
currentValues: any,
|
|
85
86
|
unmasked: any,
|
|
86
87
|
columnError: (column: any) => string,
|
|
87
88
|
setCurrentValue: (columnName: string, value: any) => void,
|
|
88
89
|
columnOptions: any,
|
|
89
90
|
readonlyColumns?: string[],
|
|
91
|
+
columnsWithErrors: Record<string, string>,
|
|
92
|
+
isValidating: boolean
|
|
90
93
|
}>();
|
|
91
94
|
|
|
92
95
|
const customComponentsInValidity: Ref<Record<string, boolean>> = ref({});
|
|
@@ -13,11 +13,13 @@
|
|
|
13
13
|
:mode="mode"
|
|
14
14
|
:unmasked="unmasked"
|
|
15
15
|
:columnOptions="columnOptions"
|
|
16
|
-
:
|
|
16
|
+
:validatingMode="validatingMode"
|
|
17
17
|
:columnError="columnError"
|
|
18
18
|
:setCurrentValue="setCurrentValue"
|
|
19
19
|
@update:customComponentsInValidity="(data) => customComponentsInValidity = { ...customComponentsInValidity, ...data }"
|
|
20
20
|
@update:customComponentsEmptiness="(data) => customComponentsEmptiness = { ...customComponentsEmptiness, ...data }"
|
|
21
|
+
:columnsWithErrors="columnsWithErrors"
|
|
22
|
+
:isValidating="isValidating"
|
|
21
23
|
/>
|
|
22
24
|
</div>
|
|
23
25
|
<div v-else class="flex flex-col gap-4">
|
|
@@ -31,11 +33,13 @@
|
|
|
31
33
|
:mode="mode"
|
|
32
34
|
:unmasked="unmasked"
|
|
33
35
|
:columnOptions="columnOptions"
|
|
34
|
-
:
|
|
36
|
+
:validatingMode="validatingMode"
|
|
35
37
|
:columnError="columnError"
|
|
36
38
|
:setCurrentValue="setCurrentValue"
|
|
37
39
|
@update:customComponentsInValidity="(data) => customComponentsInValidity = { ...customComponentsInValidity, ...data }"
|
|
38
40
|
@update:customComponentsEmptiness="(data) => customComponentsEmptiness = { ...customComponentsEmptiness, ...data }"
|
|
41
|
+
:columnsWithErrors="columnsWithErrors"
|
|
42
|
+
:isValidating="isValidating"
|
|
39
43
|
/>
|
|
40
44
|
</template>
|
|
41
45
|
<div v-if="otherColumns?.length || 0 > 0">
|
|
@@ -48,11 +52,13 @@
|
|
|
48
52
|
:mode="mode"
|
|
49
53
|
:unmasked="unmasked"
|
|
50
54
|
:columnOptions="columnOptions"
|
|
51
|
-
:
|
|
55
|
+
:validatingMode="validatingMode"
|
|
52
56
|
:columnError="columnError"
|
|
53
57
|
:setCurrentValue="setCurrentValue"
|
|
54
58
|
@update:customComponentsInValidity="(data) => customComponentsInValidity = { ...customComponentsInValidity, ...data }"
|
|
55
59
|
@update:customComponentsEmptiness="(data) => customComponentsEmptiness = { ...customComponentsEmptiness, ...data }"
|
|
60
|
+
:columnsWithErrors="columnsWithErrors"
|
|
61
|
+
:isValidating="isValidating"
|
|
56
62
|
/>
|
|
57
63
|
</div>
|
|
58
64
|
</div>
|
|
@@ -71,16 +77,20 @@ import { useCoreStore } from "@/stores/core";
|
|
|
71
77
|
import GroupsTable from '@/components/GroupsTable.vue';
|
|
72
78
|
import { useI18n } from 'vue-i18n';
|
|
73
79
|
import { type AdminForthResourceColumnCommon, type AdminForthResourceCommon } from '@/types/Common';
|
|
80
|
+
import { Mutex } from 'async-mutex';
|
|
81
|
+
import debounce from 'lodash.debounce';
|
|
74
82
|
|
|
75
83
|
const { t } = useI18n();
|
|
76
84
|
|
|
85
|
+
const mutex = new Mutex();
|
|
86
|
+
|
|
77
87
|
const coreStore = useCoreStore();
|
|
78
88
|
const router = useRouter();
|
|
79
89
|
const route = useRoute();
|
|
80
90
|
const props = defineProps<{
|
|
81
91
|
resource: AdminForthResourceCommon,
|
|
82
92
|
record: any,
|
|
83
|
-
|
|
93
|
+
validatingMode: boolean,
|
|
84
94
|
source: 'create' | 'edit',
|
|
85
95
|
readonlyColumns?: string[],
|
|
86
96
|
}>();
|
|
@@ -99,6 +109,11 @@ const columnOptions = ref<Record<string, any[]>>({});
|
|
|
99
109
|
const columnLoadingState = reactive<Record<string, { loading: boolean; hasMore: boolean }>>({});
|
|
100
110
|
const columnOffsets = reactive<Record<string, number>>({});
|
|
101
111
|
const columnEmptyResultsCount = reactive<Record<string, number>>({});
|
|
112
|
+
const columnsWithErrors = ref<Record<string, string>>({});
|
|
113
|
+
const isValidating = ref(false);
|
|
114
|
+
const blockSettingIsValidating = ref(false);
|
|
115
|
+
const isValid = ref(true);
|
|
116
|
+
const doesUserHaveCustomValidation = computed(() => props.resource.columns.some(column => column.validation && column.validation.some((val) => val.validator)));
|
|
102
117
|
|
|
103
118
|
const columnError = (column: AdminForthResourceColumnCommon) => {
|
|
104
119
|
const val = computed(() => {
|
|
@@ -329,10 +344,48 @@ const editableColumns = computed(() => {
|
|
|
329
344
|
return props.resource?.columns?.filter(column => column.showIn?.[mode.value] && (currentValues.value ? checkShowIf(column, currentValues.value, props.resource.columns) : true));
|
|
330
345
|
});
|
|
331
346
|
|
|
332
|
-
|
|
333
|
-
|
|
347
|
+
function checkIfColumnHasError(column: AdminForthResourceColumnCommon) {
|
|
348
|
+
const error = columnError(column);
|
|
349
|
+
if (error) {
|
|
350
|
+
columnsWithErrors.value[column.name] = error;
|
|
351
|
+
} else {
|
|
352
|
+
delete columnsWithErrors.value[column.name];
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const checkIfAnyColumnHasErrors = () => {
|
|
357
|
+
return Object.keys(columnsWithErrors.value).length > 0 ? false : true;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const debouncedValidation = debounce(async (columns: AdminForthResourceColumnCommon[]) => {
|
|
361
|
+
await mutex.runExclusive(async () => {
|
|
362
|
+
await validateUsingUserValidationFunction(columns);
|
|
363
|
+
});
|
|
364
|
+
setIsValidatingValue(false);
|
|
365
|
+
isValid.value = checkIfAnyColumnHasErrors();
|
|
366
|
+
}, 500);
|
|
367
|
+
|
|
368
|
+
watch(() => [editableColumns.value, props.validatingMode], async () => {
|
|
369
|
+
setIsValidatingValue(true);
|
|
370
|
+
|
|
371
|
+
editableColumns.value?.forEach(column => {
|
|
372
|
+
checkIfColumnHasError(column);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
if (props.validatingMode && doesUserHaveCustomValidation.value) {
|
|
376
|
+
debouncedValidation(editableColumns.value);
|
|
377
|
+
} else {
|
|
378
|
+
setIsValidatingValue(false);
|
|
379
|
+
isValid.value = checkIfAnyColumnHasErrors();
|
|
380
|
+
}
|
|
334
381
|
});
|
|
335
382
|
|
|
383
|
+
const setIsValidatingValue = (value: boolean) => {
|
|
384
|
+
if (!blockSettingIsValidating.value) {
|
|
385
|
+
isValidating.value = value;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
336
389
|
|
|
337
390
|
const groups = computed(() => {
|
|
338
391
|
let fieldGroupType;
|
|
@@ -381,9 +434,50 @@ watch(() => isValid.value, (value) => {
|
|
|
381
434
|
emit('update:isValid', value);
|
|
382
435
|
});
|
|
383
436
|
|
|
437
|
+
async function validateUsingUserValidationFunction(editableColumnsInner: AdminForthResourceColumnCommon[]): Promise<void> {
|
|
438
|
+
if (doesUserHaveCustomValidation.value) {
|
|
439
|
+
try {
|
|
440
|
+
blockSettingIsValidating.value = true;
|
|
441
|
+
const res = await callAdminForthApi({
|
|
442
|
+
method: 'POST',
|
|
443
|
+
path: '/validate_columns',
|
|
444
|
+
body: {
|
|
445
|
+
resourceId: props.resource.resourceId,
|
|
446
|
+
editableColumns: editableColumnsInner.map(col => {return {name: col.name, value: currentValues.value?.[col.name]} }),
|
|
447
|
+
record: currentValues.value,
|
|
448
|
+
}
|
|
449
|
+
})
|
|
450
|
+
if (res.validationResults && Object.keys(res.validationResults).length > 0) {
|
|
451
|
+
for (const [columnName, validationResult] of Object.entries(res.validationResults) as [string, any][]) {
|
|
452
|
+
if (!validationResult.isValid) {
|
|
453
|
+
columnsWithErrors.value[columnName] = validationResult.message || 'Invalid value';
|
|
454
|
+
} else {
|
|
455
|
+
delete columnsWithErrors.value[columnName];
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const columnsToProcess = editableColumns.value.filter(col => res.validationResults[col.name] === undefined);
|
|
459
|
+
columnsToProcess.forEach(column => {
|
|
460
|
+
checkIfColumnHasError(column);
|
|
461
|
+
});
|
|
462
|
+
} else {
|
|
463
|
+
editableColumnsInner.forEach(column => {
|
|
464
|
+
checkIfColumnHasError(column);
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
blockSettingIsValidating.value = false;
|
|
468
|
+
} catch (e) {
|
|
469
|
+
console.error('Error during custom validation', e);
|
|
470
|
+
blockSettingIsValidating.value = false;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
384
475
|
defineExpose({
|
|
385
476
|
columnError,
|
|
386
477
|
editableColumns,
|
|
478
|
+
columnsWithErrors,
|
|
479
|
+
isValidating,
|
|
480
|
+
validateUsingUserValidationFunction
|
|
387
481
|
})
|
|
388
482
|
|
|
389
483
|
</script>
|
|
@@ -341,7 +341,7 @@
|
|
|
341
341
|
|
|
342
342
|
|
|
343
343
|
import { computed, onMounted, ref, watch, useTemplateRef, nextTick, type Ref } from 'vue';
|
|
344
|
-
import { callAdminForthApi } from '@/utils';
|
|
344
|
+
import { callAdminForthApi, executeCustomAction } from '@/utils';
|
|
345
345
|
import { useI18n } from 'vue-i18n';
|
|
346
346
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
347
347
|
import { getCustomComponent, formatComponent } from '@/utils';
|
|
@@ -607,50 +607,28 @@ async function deleteRecord(row: any) {
|
|
|
607
607
|
const actionLoadingStates = ref<Record<string | number, boolean>>({});
|
|
608
608
|
|
|
609
609
|
async function startCustomAction(actionId: string | number, row: any, extraData: Record<string, any> = {}) {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
if (data?.redirectUrl) {
|
|
627
|
-
// Check if the URL should open in a new tab
|
|
628
|
-
if (data.redirectUrl.includes('target=_blank')) {
|
|
629
|
-
window.open(data.redirectUrl.replace('&target=_blank', '').replace('?target=_blank', ''), '_blank');
|
|
630
|
-
} else {
|
|
631
|
-
// Navigate within the app
|
|
632
|
-
if (data.redirectUrl.startsWith('http')) {
|
|
633
|
-
window.location.href = data.redirectUrl;
|
|
634
|
-
} else {
|
|
635
|
-
router.push(data.redirectUrl);
|
|
610
|
+
await executeCustomAction({
|
|
611
|
+
actionId,
|
|
612
|
+
resourceId: props.resource?.resourceId || '',
|
|
613
|
+
recordId: row._primaryKeyValue,
|
|
614
|
+
extra: extraData,
|
|
615
|
+
setLoadingState: (loading: boolean) => {
|
|
616
|
+
actionLoadingStates.value[actionId] = loading;
|
|
617
|
+
},
|
|
618
|
+
onSuccess: async (data: any) => {
|
|
619
|
+
emits('update:records', true);
|
|
620
|
+
|
|
621
|
+
if (data.successMessage) {
|
|
622
|
+
alert({
|
|
623
|
+
message: data.successMessage,
|
|
624
|
+
variant: 'success'
|
|
625
|
+
});
|
|
636
626
|
}
|
|
627
|
+
},
|
|
628
|
+
onError: (error: string) => {
|
|
629
|
+
showErrorTost(error);
|
|
637
630
|
}
|
|
638
|
-
|
|
639
|
-
}
|
|
640
|
-
if (data?.ok) {
|
|
641
|
-
emits('update:records', true);
|
|
642
|
-
|
|
643
|
-
if (data.successMessage) {
|
|
644
|
-
alert({
|
|
645
|
-
message: data.successMessage,
|
|
646
|
-
variant: 'success'
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
if (data?.error) {
|
|
652
|
-
showErrorTost(data.error);
|
|
653
|
-
}
|
|
631
|
+
});
|
|
654
632
|
}
|
|
655
633
|
|
|
656
634
|
function validatePageInput() {
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
}"
|
|
31
31
|
@click="injectedComponentClick(i)"
|
|
32
32
|
>
|
|
33
|
-
<div class="wrapper">
|
|
33
|
+
<div class="wrapper" v-if="getCustomComponent(item)">
|
|
34
34
|
<component
|
|
35
|
-
:ref="(el: any) => setComponentRef(el, i)" :is="getCustomComponent(item)"
|
|
35
|
+
:ref="(el: any) => setComponentRef(el, i)" :is="getCustomComponent(item)!"
|
|
36
36
|
:meta="item.meta"
|
|
37
37
|
:resource="coreStore.resource"
|
|
38
38
|
:adminUser="coreStore.adminUser"
|
|
@@ -46,18 +46,30 @@
|
|
|
46
46
|
<li v-for="action in customActions" :key="action.id">
|
|
47
47
|
<div class="wrapper">
|
|
48
48
|
<component
|
|
49
|
-
v-if="action.customComponent"
|
|
50
49
|
:is="(action.customComponent && getCustomComponent(formatComponent(action.customComponent))) || CallActionWrapper"
|
|
51
50
|
:meta="formatComponent(action.customComponent).meta"
|
|
52
51
|
@callAction="(payload? : Object) => handleActionClick(action, payload)"
|
|
53
52
|
>
|
|
54
|
-
<a @click.prevent class="block
|
|
53
|
+
<a @click.prevent class="block">
|
|
55
54
|
<div class="flex items-center gap-2">
|
|
56
55
|
<component
|
|
57
|
-
v-if="action.icon"
|
|
56
|
+
v-if="action.icon && !actionLoadingStates[action.id!]"
|
|
58
57
|
:is="getIcon(action.icon)"
|
|
59
58
|
class="w-4 h-4 text-lightPrimary dark:text-darkPrimary"
|
|
60
59
|
/>
|
|
60
|
+
<div v-if="actionLoadingStates[action.id!]">
|
|
61
|
+
<svg
|
|
62
|
+
aria-hidden="true"
|
|
63
|
+
class="w-4 h-4 animate-spin text-gray-200 dark:text-gray-500 fill-gray-500 dark:fill-gray-300"
|
|
64
|
+
viewBox="0 0 100 101"
|
|
65
|
+
fill="none"
|
|
66
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
67
|
+
>
|
|
68
|
+
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
|
|
69
|
+
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
|
|
70
|
+
</svg>
|
|
71
|
+
<span class="sr-only">Loading...</span>
|
|
72
|
+
</div>
|
|
61
73
|
{{ action.name }}
|
|
62
74
|
</div>
|
|
63
75
|
</a>
|
|
@@ -89,7 +101,7 @@
|
|
|
89
101
|
|
|
90
102
|
|
|
91
103
|
<script setup lang="ts">
|
|
92
|
-
import { getCustomComponent, getIcon, formatComponent } from '@/utils';
|
|
104
|
+
import { getCustomComponent, getIcon, formatComponent, executeCustomAction } from '@/utils';
|
|
93
105
|
import { useCoreStore } from '@/stores/core';
|
|
94
106
|
import { useAdminforth } from '@/adminforth';
|
|
95
107
|
import { callAdminForthApi } from '@/utils';
|
|
@@ -105,6 +117,7 @@ const coreStore = useCoreStore();
|
|
|
105
117
|
const router = useRouter();
|
|
106
118
|
const threeDotsDropdownItemsRefs = ref<Array<ComponentPublicInstance | null>>([]);
|
|
107
119
|
const showDropdown = ref(false);
|
|
120
|
+
const actionLoadingStates = ref<Record<string, boolean>>({});
|
|
108
121
|
const dropdownRef = ref<HTMLElement | null>(null);
|
|
109
122
|
const buttonTriggerRef = ref<HTMLElement | null>(null);
|
|
110
123
|
|
|
@@ -131,55 +144,35 @@ function setComponentRef(el: ComponentPublicInstance | null, index: number) {
|
|
|
131
144
|
|
|
132
145
|
async function handleActionClick(action: AdminForthActionInput, payload: any) {
|
|
133
146
|
list.closeThreeDotsDropdown();
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
await executeCustomAction({
|
|
148
|
+
actionId: action.id,
|
|
149
|
+
resourceId: route.params.resourceId as string,
|
|
150
|
+
recordId: route.params.primaryKey as string,
|
|
151
|
+
extra: payload || {},
|
|
152
|
+
setLoadingState: (loading: boolean) => {
|
|
153
|
+
actionLoadingStates.value[action.id!] = loading;
|
|
154
|
+
},
|
|
155
|
+
onSuccess: async (data: any) => {
|
|
156
|
+
await coreStore.fetchRecord({
|
|
157
|
+
resourceId: route.params.resourceId as string,
|
|
158
|
+
primaryKey: route.params.primaryKey as string,
|
|
159
|
+
source: 'show',
|
|
160
|
+
});
|
|
146
161
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
// Navigate within the app
|
|
153
|
-
if (data.redirectUrl.startsWith('http')) {
|
|
154
|
-
window.location.href = data.redirectUrl;
|
|
155
|
-
} else {
|
|
156
|
-
router.push(data.redirectUrl);
|
|
162
|
+
if (data.successMessage) {
|
|
163
|
+
alert({
|
|
164
|
+
message: data.successMessage,
|
|
165
|
+
variant: 'success'
|
|
166
|
+
});
|
|
157
167
|
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (data?.ok) {
|
|
163
|
-
await coreStore.fetchRecord({
|
|
164
|
-
resourceId: route.params.resourceId as string,
|
|
165
|
-
primaryKey: route.params.primaryKey as string,
|
|
166
|
-
source: 'show',
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
if (data.successMessage) {
|
|
168
|
+
},
|
|
169
|
+
onError: (error: string) => {
|
|
170
170
|
alert({
|
|
171
|
-
message:
|
|
172
|
-
variant: '
|
|
171
|
+
message: error,
|
|
172
|
+
variant: 'danger'
|
|
173
173
|
});
|
|
174
174
|
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (data?.error) {
|
|
178
|
-
alert({
|
|
179
|
-
message: data.error,
|
|
180
|
-
variant: 'danger'
|
|
181
|
-
});
|
|
182
|
-
}
|
|
175
|
+
});
|
|
183
176
|
}
|
|
184
177
|
|
|
185
178
|
function startBulkAction(actionId: string) {
|
|
@@ -220,7 +213,10 @@ onUnmounted(() => {
|
|
|
220
213
|
|
|
221
214
|
<style lang="scss" scoped>
|
|
222
215
|
.wrapper > * {
|
|
223
|
-
@apply px-4 py-2
|
|
216
|
+
@apply px-4 py-2
|
|
217
|
+
hover:text-lightThreeDotsMenuBodyTextHover hover:bg-lightThreeDotsMenuBodyBackgroundHover
|
|
218
|
+
dark:hover:bg-darkThreeDotsMenuBodyBackgroundHover dark:hover:text-darkThreeDotsMenuBodyTextHover
|
|
219
|
+
cursor-pointer;
|
|
224
220
|
}
|
|
225
221
|
</style>
|
|
226
222
|
|
|
@@ -1291,11 +1291,14 @@ interface AdminForthInputConfigCustomization {
|
|
|
1291
1291
|
|
|
1292
1292
|
export interface AdminForthActionInput {
|
|
1293
1293
|
name: string;
|
|
1294
|
+
bulkConfirmationMessage?: string;
|
|
1295
|
+
bulkSuccessMessage?: string;
|
|
1294
1296
|
showIn?: {
|
|
1295
1297
|
list?: boolean,
|
|
1296
1298
|
listThreeDotsMenu?: boolean,
|
|
1297
1299
|
showButton?: boolean,
|
|
1298
1300
|
showThreeDotsMenu?: boolean,
|
|
1301
|
+
bulkButton?: boolean,
|
|
1299
1302
|
};
|
|
1300
1303
|
allowed?: (params: {
|
|
1301
1304
|
adminUser: AdminUser;
|
|
@@ -303,7 +303,7 @@ export interface AdminForthComponentDeclarationFull {
|
|
|
303
303
|
[key: string]: any,
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
|
-
import { type AdminForthActionInput, type AdminForthResource } from './Back.js'
|
|
306
|
+
import { type IAdminForth, type AdminForthActionInput, type AdminForthResource } from './Back.js'
|
|
307
307
|
export { type AdminForthActionInput } from './Back.js'
|
|
308
308
|
|
|
309
309
|
export type AdminForthComponentDeclaration = AdminForthComponentDeclarationFull | string;
|
|
@@ -436,6 +436,7 @@ export interface AdminForthResourceInputCommon {
|
|
|
436
436
|
/**
|
|
437
437
|
* Custom bulk actions list. Bulk actions available in list view when user selects multiple records by
|
|
438
438
|
* using checkboxes.
|
|
439
|
+
* @deprecated in favor of defining .
|
|
439
440
|
*/
|
|
440
441
|
bulkActions?: AdminForthBulkActionCommon[],
|
|
441
442
|
|
|
@@ -609,14 +610,14 @@ export type ValidationObject = {
|
|
|
609
610
|
* ```
|
|
610
611
|
*
|
|
611
612
|
*/
|
|
612
|
-
regExp
|
|
613
|
+
regExp?: string,
|
|
613
614
|
|
|
614
615
|
/**
|
|
615
616
|
* Error message shown to user if validation fails
|
|
616
617
|
*
|
|
617
618
|
* Example: "Invalid email format"
|
|
618
619
|
*/
|
|
619
|
-
message
|
|
620
|
+
message?: string,
|
|
620
621
|
|
|
621
622
|
/**
|
|
622
623
|
* Whether to check case sensitivity (i flag)
|
|
@@ -632,6 +633,20 @@ export type ValidationObject = {
|
|
|
632
633
|
* Whether to check global strings (g flag)
|
|
633
634
|
*/
|
|
634
635
|
global?: boolean
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Custom validator function.
|
|
639
|
+
*
|
|
640
|
+
* Example:
|
|
641
|
+
*
|
|
642
|
+
* ```ts
|
|
643
|
+
* validator: async (value) => {
|
|
644
|
+
* // custom validation logic
|
|
645
|
+
* return { isValid: true, message: 'Validation passed' }; // or { isValid: false, message: 'Validation failed' }
|
|
646
|
+
* }
|
|
647
|
+
* ```
|
|
648
|
+
*/
|
|
649
|
+
validator?: (value: any, record: any, adminForth: IAdminForth) => {isValid: boolean, message?: string} | Promise<{isValid: boolean, message?: string}> | boolean,
|
|
635
650
|
}
|
|
636
651
|
|
|
637
652
|
|
|
@@ -70,6 +70,18 @@ export interface StorageAdapter {
|
|
|
70
70
|
* @returns A promise that resolves to a string containing the data URL
|
|
71
71
|
*/
|
|
72
72
|
getKeyAsDataURL(key: string): Promise<string>;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Determines whether the given URL points to a resource managed by this storage adapter.
|
|
76
|
+
* * This method is important for plugins (such as MarkdownPlugin) to distinguish between
|
|
77
|
+
* "own" resources (stored in your S3 bucket or local storage) and external links * (such as images from Unsplash or Google).
|
|
78
|
+
* * The implementation logic typically includes:
|
|
79
|
+
* 1. Checking whether the hostname of the URL matches the configured bucket domain or custom CDN.
|
|
80
|
+
* 2. Checking whether the URL path contains the adapter's specific download prefix.
|
|
81
|
+
* * @param url - The full URL string to check (can be a public URL or a pre-signed URL).
|
|
82
|
+
* @returns A promise that returns true if the URL belongs to this adapter, false otherwise.
|
|
83
|
+
*/
|
|
84
|
+
isInternalUrl (url: string): Promise<boolean>;
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { AdminForthResourceColumn } from '@/types/Back';
|
|
2
|
+
import { useAdminforth } from '@/adminforth';
|
|
3
|
+
import { type Ref, nextTick } from 'vue';
|
|
4
|
+
|
|
5
|
+
export function scrollToInvalidField(resourceFormRef: any, t: (key: string) => string) {
|
|
6
|
+
const { alert } = useAdminforth();
|
|
7
|
+
let columnsWithErrors: {column: AdminForthResourceColumn, error: string}[] = [];
|
|
8
|
+
for (const column of resourceFormRef.value?.editableColumns || []) {
|
|
9
|
+
if (resourceFormRef.value?.columnsWithErrors[column.name]) {
|
|
10
|
+
columnsWithErrors.push({
|
|
11
|
+
column,
|
|
12
|
+
error: resourceFormRef.value?.columnsWithErrors[column.name]
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const errorMessage = t('Failed to save. Please fix errors for the following fields:') + '<ul class="mt-2 list-disc list-inside">' + columnsWithErrors.map(c => `<li><strong>${c.column.label || c.column.name}</strong>: ${c.error}</li>`).join('') + '</ul>';
|
|
17
|
+
alert({
|
|
18
|
+
messageHtml: errorMessage,
|
|
19
|
+
variant: 'danger'
|
|
20
|
+
});
|
|
21
|
+
const firstInvalidElement = document.querySelector('.af-invalid-field-message');
|
|
22
|
+
if (firstInvalidElement) {
|
|
23
|
+
firstInvalidElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function saveRecordPreparations(
|
|
28
|
+
viewMode: 'create' | 'edit',
|
|
29
|
+
validatingMode: Ref<boolean>,
|
|
30
|
+
resourceFormRef: Ref<any>,
|
|
31
|
+
isValid: Ref<boolean>,
|
|
32
|
+
t: (key: string) => string,
|
|
33
|
+
saving: Ref<boolean>,
|
|
34
|
+
runSaveInterceptors: any,
|
|
35
|
+
record: Ref<Record<string, any>>,
|
|
36
|
+
coreStore: any,
|
|
37
|
+
route: any
|
|
38
|
+
) {
|
|
39
|
+
validatingMode.value = true;
|
|
40
|
+
await nextTick();
|
|
41
|
+
//wait for response for the user validation function if it exists
|
|
42
|
+
while (1) {
|
|
43
|
+
if (resourceFormRef.value?.isValidating) {
|
|
44
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
45
|
+
} else {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!isValid.value) {
|
|
50
|
+
await nextTick();
|
|
51
|
+
scrollToInvalidField(resourceFormRef, t);
|
|
52
|
+
return;
|
|
53
|
+
} else {
|
|
54
|
+
validatingMode.value = false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
saving.value = true;
|
|
58
|
+
const interceptorsResult = await runSaveInterceptors({
|
|
59
|
+
action: viewMode,
|
|
60
|
+
values: record.value,
|
|
61
|
+
resource: coreStore.resource,
|
|
62
|
+
resourceId: route.params.resourceId as string,
|
|
63
|
+
});
|
|
64
|
+
return interceptorsResult;
|
|
65
|
+
}
|