adminforth 2.26.4 → 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/commands/createApp/templates/package.json.hbs +1 -1
- package/dist/modules/restApi.d.ts +1 -0
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +65 -3
- package/dist/modules/restApi.js.map +1 -1
- package/dist/modules/styles.js +2 -2
- package/dist/modules/styles.js.map +1 -1
- package/dist/servers/express.d.ts.map +1 -1
- package/dist/servers/express.js +7 -1
- package/dist/servers/express.js.map +1 -1
- package/dist/spa/package-lock.json +85 -7
- package/dist/spa/package.json +4 -1
- package/dist/spa/pnpm-lock.yaml +339 -299
- package/dist/spa/src/App.vue +1 -1
- package/dist/spa/src/adminforth.ts +17 -29
- package/dist/spa/src/afcl/Input.vue +1 -1
- package/dist/spa/src/afcl/Modal.vue +12 -1
- package/dist/spa/src/afcl/Select.vue +4 -2
- package/dist/spa/src/afcl/Table.vue +27 -13
- package/dist/spa/src/components/AcceptModal.vue +2 -0
- package/dist/spa/src/components/ColumnValueInputWrapper.vue +35 -4
- package/dist/spa/src/components/CustomRangePicker.vue +22 -67
- package/dist/spa/src/components/GroupsTable.vue +7 -4
- package/dist/spa/src/components/ListActionsThreeDots.vue +9 -8
- package/dist/spa/src/components/RangePicker.vue +236 -0
- package/dist/spa/src/components/ResourceForm.vue +100 -6
- package/dist/spa/src/components/ResourceListTable.vue +45 -70
- package/dist/spa/src/components/Sidebar.vue +1 -1
- package/dist/spa/src/components/ThreeDotsMenu.vue +54 -57
- package/dist/spa/src/i18n.ts +1 -1
- package/dist/spa/src/stores/core.ts +4 -2
- package/dist/spa/src/types/Back.ts +10 -3
- package/dist/spa/src/types/Common.ts +43 -8
- package/dist/spa/src/types/FrontendAPI.ts +6 -1
- 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/listUtils.ts +8 -2
- package/dist/spa/src/utils/utils.ts +192 -12
- package/dist/spa/src/utils.ts +2 -1
- package/dist/spa/src/views/CreateView.vue +32 -59
- package/dist/spa/src/views/EditView.vue +30 -47
- package/dist/spa/src/views/ListView.vue +119 -18
- package/dist/spa/src/views/LoginView.vue +13 -13
- package/dist/spa/src/views/ShowView.vue +67 -61
- package/dist/spa/tsconfig.app.json +1 -1
- package/dist/types/Back.d.ts +7 -4
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +43 -8
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js.map +1 -1
- package/dist/types/FrontendAPI.d.ts +13 -1
- package/dist/types/FrontendAPI.d.ts.map +1 -1
- package/dist/types/FrontendAPI.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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<template >
|
|
2
|
-
<div class="relative" v-if="threeDotsDropdownItems?.length || customActions?.length || (bulkActions?.some((action:
|
|
2
|
+
<div class="relative" v-if="threeDotsDropdownItems?.length || customActions?.length || (bulkActions?.some((action: AdminForthBulkActionFront) => action.showInThreeDotsDropdown))">
|
|
3
3
|
<button
|
|
4
4
|
ref="buttonTriggerRef"
|
|
5
5
|
@click="toggleDropdownVisibility"
|
|
6
|
-
class="flex items-center py-2 px-2 text-sm font-medium text-lightThreeDotsMenuIconDots focus:outline-none bg-lightThreeDotsMenuIconBackground rounded border border-lightThreeDotsMenuIconBackgroundBorder hover:bg-lightThreeDotsMenuIconBackgroundHover hover:text-lightThreeDotsMenuIconDotsHover focus:z-10 focus:ring-4 focus:ring-lightThreeDotsMenuIconFocus dark:focus:ring-darkThreeDotsMenuIconFocus dark:bg-darkThreeDotsMenuIconBackground dark:text-darkThreeDotsMenuIconDots dark:border-darkThreeDotsMenuIconBackgroundBorder dark:hover:text-darkThreeDotsMenuIconDotsHover dark:hover:bg-darkThreeDotsMenuIconBackgroundHover rounded-default"
|
|
6
|
+
class="flex transition-all items-center af-button-shadow py-2.5 px-2.5 text-sm font-medium text-lightThreeDotsMenuIconDots focus:outline-none bg-lightThreeDotsMenuIconBackground rounded border border-lightThreeDotsMenuIconBackgroundBorder hover:bg-lightThreeDotsMenuIconBackgroundHover hover:text-lightThreeDotsMenuIconDotsHover focus:z-10 focus:ring-4 focus:ring-lightThreeDotsMenuIconFocus dark:focus:ring-darkThreeDotsMenuIconFocus dark:bg-darkThreeDotsMenuIconBackground dark:text-darkThreeDotsMenuIconDots dark:border-darkThreeDotsMenuIconBackgroundBorder dark:hover:text-darkThreeDotsMenuIconDotsHover dark:hover:bg-darkThreeDotsMenuIconBackgroundHover rounded-default"
|
|
7
7
|
>
|
|
8
8
|
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 4 15">
|
|
9
9
|
<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"/>
|
|
@@ -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,17 +46,30 @@
|
|
|
46
46
|
<li v-for="action in customActions" :key="action.id">
|
|
47
47
|
<div class="wrapper">
|
|
48
48
|
<component
|
|
49
|
-
:is="(action.customComponent && getCustomComponent(action.customComponent)) || CallActionWrapper"
|
|
50
|
-
:meta="action.customComponent
|
|
49
|
+
:is="(action.customComponent && getCustomComponent(formatComponent(action.customComponent))) || CallActionWrapper"
|
|
50
|
+
:meta="formatComponent(action.customComponent).meta"
|
|
51
51
|
@callAction="(payload? : Object) => handleActionClick(action, payload)"
|
|
52
52
|
>
|
|
53
|
-
<a @click.prevent class="block
|
|
53
|
+
<a @click.prevent class="block">
|
|
54
54
|
<div class="flex items-center gap-2">
|
|
55
55
|
<component
|
|
56
|
-
v-if="action.icon"
|
|
56
|
+
v-if="action.icon && !actionLoadingStates[action.id!]"
|
|
57
57
|
:is="getIcon(action.icon)"
|
|
58
58
|
class="w-4 h-4 text-lightPrimary dark:text-darkPrimary"
|
|
59
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>
|
|
60
73
|
{{ action.name }}
|
|
61
74
|
</div>
|
|
62
75
|
</a>
|
|
@@ -88,14 +101,14 @@
|
|
|
88
101
|
|
|
89
102
|
|
|
90
103
|
<script setup lang="ts">
|
|
91
|
-
import { getCustomComponent, getIcon } from '@/utils';
|
|
104
|
+
import { getCustomComponent, getIcon, formatComponent, executeCustomAction } from '@/utils';
|
|
92
105
|
import { useCoreStore } from '@/stores/core';
|
|
93
106
|
import { useAdminforth } from '@/adminforth';
|
|
94
107
|
import { callAdminForthApi } from '@/utils';
|
|
95
108
|
import { useRoute, useRouter } from 'vue-router';
|
|
96
109
|
import CallActionWrapper from '@/components/CallActionWrapper.vue'
|
|
97
110
|
import { ref, type ComponentPublicInstance, onMounted, onUnmounted } from 'vue';
|
|
98
|
-
import type {
|
|
111
|
+
import type { AdminForthActionFront, AdminForthBulkActionFront, AdminForthComponentDeclarationFull } from '@/types/Common';
|
|
99
112
|
import type { AdminForthActionInput } from '@/types/Back';
|
|
100
113
|
|
|
101
114
|
const { list, alert} = useAdminforth();
|
|
@@ -104,13 +117,14 @@ const coreStore = useCoreStore();
|
|
|
104
117
|
const router = useRouter();
|
|
105
118
|
const threeDotsDropdownItemsRefs = ref<Array<ComponentPublicInstance | null>>([]);
|
|
106
119
|
const showDropdown = ref(false);
|
|
120
|
+
const actionLoadingStates = ref<Record<string, boolean>>({});
|
|
107
121
|
const dropdownRef = ref<HTMLElement | null>(null);
|
|
108
122
|
const buttonTriggerRef = ref<HTMLElement | null>(null);
|
|
109
123
|
|
|
110
124
|
const props = defineProps({
|
|
111
125
|
threeDotsDropdownItems: Array<AdminForthComponentDeclarationFull>,
|
|
112
|
-
customActions: Array<
|
|
113
|
-
bulkActions: Array<
|
|
126
|
+
customActions: Array<AdminForthActionFront>,
|
|
127
|
+
bulkActions: Array<AdminForthBulkActionFront>,
|
|
114
128
|
checkboxes: Array,
|
|
115
129
|
updateList: {
|
|
116
130
|
type: Function,
|
|
@@ -130,55 +144,35 @@ function setComponentRef(el: ComponentPublicInstance | null, index: number) {
|
|
|
130
144
|
|
|
131
145
|
async function handleActionClick(action: AdminForthActionInput, payload: any) {
|
|
132
146
|
list.closeThreeDotsDropdown();
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
});
|
|
145
161
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// Navigate within the app
|
|
152
|
-
if (data.redirectUrl.startsWith('http')) {
|
|
153
|
-
window.location.href = data.redirectUrl;
|
|
154
|
-
} else {
|
|
155
|
-
router.push(data.redirectUrl);
|
|
162
|
+
if (data.successMessage) {
|
|
163
|
+
alert({
|
|
164
|
+
message: data.successMessage,
|
|
165
|
+
variant: 'success'
|
|
166
|
+
});
|
|
156
167
|
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (data?.ok) {
|
|
162
|
-
await coreStore.fetchRecord({
|
|
163
|
-
resourceId: route.params.resourceId as string,
|
|
164
|
-
primaryKey: route.params.primaryKey as string,
|
|
165
|
-
source: 'show',
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
if (data.successMessage) {
|
|
168
|
+
},
|
|
169
|
+
onError: (error: string) => {
|
|
169
170
|
alert({
|
|
170
|
-
message:
|
|
171
|
-
variant: '
|
|
171
|
+
message: error,
|
|
172
|
+
variant: 'danger'
|
|
172
173
|
});
|
|
173
174
|
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (data?.error) {
|
|
177
|
-
alert({
|
|
178
|
-
message: data.error,
|
|
179
|
-
variant: 'danger'
|
|
180
|
-
});
|
|
181
|
-
}
|
|
175
|
+
});
|
|
182
176
|
}
|
|
183
177
|
|
|
184
178
|
function startBulkAction(actionId: string) {
|
|
@@ -219,7 +213,10 @@ onUnmounted(() => {
|
|
|
219
213
|
|
|
220
214
|
<style lang="scss" scoped>
|
|
221
215
|
.wrapper > * {
|
|
222
|
-
@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;
|
|
223
220
|
}
|
|
224
221
|
</style>
|
|
225
222
|
|
package/dist/spa/src/i18n.ts
CHANGED
|
@@ -4,9 +4,11 @@ import { callAdminForthApi } from '@/utils';
|
|
|
4
4
|
import websocket from '@/websocket';
|
|
5
5
|
import { useAdminforth } from '@/adminforth';
|
|
6
6
|
|
|
7
|
-
import type { AdminForthResourceCommon, AdminForthResourceColumnCommon, GetBaseConfigResponse, ResourceVeryShort, AdminUser, UserData, AdminForthConfigMenuItem, AdminForthConfigForFrontend } from '@/types/Common';
|
|
7
|
+
import type { AdminForthResourceCommon, AdminForthResourceColumnCommon, GetBaseConfigResponse, ResourceVeryShort, AdminUser, UserData, AdminForthConfigMenuItem, AdminForthConfigForFrontend, AdminForthResourceFrontend } from '@/types/Common';
|
|
8
8
|
import type { Ref } from 'vue'
|
|
9
9
|
|
|
10
|
+
|
|
11
|
+
|
|
10
12
|
export const useCoreStore = defineStore('core', () => {
|
|
11
13
|
const { alert } = useAdminforth();
|
|
12
14
|
const resourceById: Ref<Record<string, ResourceVeryShort>> = ref({});
|
|
@@ -15,7 +17,7 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
15
17
|
const menu: Ref<AdminForthConfigMenuItem[]> = ref([]);
|
|
16
18
|
const config: Ref<AdminForthConfigForFrontend | null> = ref(null);
|
|
17
19
|
const record: Ref<any | null> = ref({});
|
|
18
|
-
const resource: Ref<
|
|
20
|
+
const resource: Ref<AdminForthResourceFrontend | null> = ref(null);
|
|
19
21
|
const userData: Ref<UserData | null> = ref(null);
|
|
20
22
|
const isResourceFetching = ref(false);
|
|
21
23
|
const isInternetError = ref(false);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Express, Request } from 'express';
|
|
1
|
+
import type { Express, Request, Response } from 'express';
|
|
2
2
|
import type { Writable } from 'stream';
|
|
3
3
|
|
|
4
4
|
import { ActionCheckSource, AdminForthFilterOperators, AdminForthSortDirections, AllowedActionsEnum, AdminForthResourcePages,
|
|
@@ -67,6 +67,10 @@ export interface IHttpServer {
|
|
|
67
67
|
headers: {[key: string]: string},
|
|
68
68
|
cookies: {[key: string]: string},
|
|
69
69
|
response: IAdminForthHttpResponse,
|
|
70
|
+
requestUrl: string,
|
|
71
|
+
abortSignal: AbortSignal,
|
|
72
|
+
_raw_express_req: Request,
|
|
73
|
+
_raw_express_res: Response,
|
|
70
74
|
) => void,
|
|
71
75
|
}): void;
|
|
72
76
|
|
|
@@ -1287,11 +1291,14 @@ interface AdminForthInputConfigCustomization {
|
|
|
1287
1291
|
|
|
1288
1292
|
export interface AdminForthActionInput {
|
|
1289
1293
|
name: string;
|
|
1294
|
+
bulkConfirmationMessage?: string;
|
|
1295
|
+
bulkSuccessMessage?: string;
|
|
1290
1296
|
showIn?: {
|
|
1291
1297
|
list?: boolean,
|
|
1292
1298
|
listThreeDotsMenu?: boolean,
|
|
1293
1299
|
showButton?: boolean,
|
|
1294
1300
|
showThreeDotsMenu?: boolean,
|
|
1301
|
+
bulkButton?: boolean,
|
|
1295
1302
|
};
|
|
1296
1303
|
allowed?: (params: {
|
|
1297
1304
|
adminUser: AdminUser;
|
|
@@ -1637,7 +1644,7 @@ export interface AdminForthConfigCustomization extends Omit<AdminForthInputConfi
|
|
|
1637
1644
|
|
|
1638
1645
|
loginPageInjections: {
|
|
1639
1646
|
underInputs: Array<AdminForthComponentDeclarationFull>,
|
|
1640
|
-
underLoginButton
|
|
1647
|
+
underLoginButton: Array<AdminForthComponentDeclarationFull>,
|
|
1641
1648
|
panelHeader: Array<AdminForthComponentDeclarationFull>,
|
|
1642
1649
|
},
|
|
1643
1650
|
|
|
@@ -1825,7 +1832,7 @@ export type AllowedActions = {
|
|
|
1825
1832
|
/**
|
|
1826
1833
|
* General options for resource.
|
|
1827
1834
|
*/
|
|
1828
|
-
export interface ResourceOptionsInput extends Omit<NonNullable<AdminForthResourceInputCommon['options']>, 'allowedActions' | 'bulkActions'> {
|
|
1835
|
+
export interface ResourceOptionsInput extends Omit<NonNullable<AdminForthResourceInputCommon['options']>, 'allowedActions' | 'bulkActions' | 'actions'> {
|
|
1829
1836
|
|
|
1830
1837
|
/**
|
|
1831
1838
|
* Custom bulk actions list. Bulk actions available in list view when user selects multiple records by
|
|
@@ -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;
|
|
@@ -314,6 +314,25 @@ export type FieldGroup = {
|
|
|
314
314
|
noTitle?: boolean;
|
|
315
315
|
};
|
|
316
316
|
|
|
317
|
+
export interface AdminForthActionFront extends Omit<AdminForthActionInput, 'id'> {
|
|
318
|
+
id: string;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export interface AdminForthBulkActionFront extends Omit<AdminForthBulkActionCommon, 'id'> {
|
|
322
|
+
id: string,
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
type AdminforthOptionsCommon = NonNullable<AdminForthResourceCommon['options']>;
|
|
326
|
+
|
|
327
|
+
export interface AdminForthOptionsForFrontend extends Omit<AdminforthOptionsCommon, 'actions' | 'bulkActions'> {
|
|
328
|
+
actions?: AdminForthActionFront[],
|
|
329
|
+
bulkActions?: AdminForthBulkActionFront[],
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export interface AdminForthResourceFrontend extends Omit<AdminForthResourceCommon, 'options'> {
|
|
333
|
+
options: AdminForthOptionsForFrontend;
|
|
334
|
+
}
|
|
335
|
+
|
|
317
336
|
/**
|
|
318
337
|
* Resource describes one table or collection in database.
|
|
319
338
|
* AdminForth generates set of pages for 'list', 'show', 'edit', 'create', 'filter' operations for each resource.
|
|
@@ -361,17 +380,17 @@ export interface AdminForthResourceInputCommon {
|
|
|
361
380
|
recordLabel?: (item: any) => string,
|
|
362
381
|
|
|
363
382
|
|
|
364
|
-
/**
|
|
365
|
-
* If true, user will not see warning about unsaved changes when tries to leave edit or create page with unsaved changes.
|
|
366
|
-
* default is false
|
|
367
|
-
*/
|
|
368
|
-
dontShowWarningAboutUnsavedChanges?: boolean,
|
|
369
383
|
|
|
370
384
|
/**
|
|
371
385
|
* General options for resource.
|
|
372
386
|
*/
|
|
373
387
|
options?: {
|
|
374
388
|
|
|
389
|
+
/**
|
|
390
|
+
* If true, user will not see warning about unsaved changes when tries to leave edit or create page with unsaved changes.
|
|
391
|
+
* default is false
|
|
392
|
+
*/
|
|
393
|
+
dontShowWarningAboutUnsavedChanges?: boolean,
|
|
375
394
|
|
|
376
395
|
/**
|
|
377
396
|
* Show quick action icons for base actions (show, edit, delete) in list view.
|
|
@@ -417,6 +436,7 @@ export interface AdminForthResourceInputCommon {
|
|
|
417
436
|
/**
|
|
418
437
|
* Custom bulk actions list. Bulk actions available in list view when user selects multiple records by
|
|
419
438
|
* using checkboxes.
|
|
439
|
+
* @deprecated in favor of defining .
|
|
420
440
|
*/
|
|
421
441
|
bulkActions?: AdminForthBulkActionCommon[],
|
|
422
442
|
|
|
@@ -590,14 +610,14 @@ export type ValidationObject = {
|
|
|
590
610
|
* ```
|
|
591
611
|
*
|
|
592
612
|
*/
|
|
593
|
-
regExp
|
|
613
|
+
regExp?: string,
|
|
594
614
|
|
|
595
615
|
/**
|
|
596
616
|
* Error message shown to user if validation fails
|
|
597
617
|
*
|
|
598
618
|
* Example: "Invalid email format"
|
|
599
619
|
*/
|
|
600
|
-
message
|
|
620
|
+
message?: string,
|
|
601
621
|
|
|
602
622
|
/**
|
|
603
623
|
* Whether to check case sensitivity (i flag)
|
|
@@ -613,6 +633,20 @@ export type ValidationObject = {
|
|
|
613
633
|
* Whether to check global strings (g flag)
|
|
614
634
|
*/
|
|
615
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,
|
|
616
650
|
}
|
|
617
651
|
|
|
618
652
|
|
|
@@ -1172,6 +1206,7 @@ export interface AdminForthConfigForFrontend {
|
|
|
1172
1206
|
loginPageInjections: {
|
|
1173
1207
|
underInputs: Array<AdminForthComponentDeclaration>,
|
|
1174
1208
|
panelHeader: Array<AdminForthComponentDeclaration>,
|
|
1209
|
+
underLoginButton: Array<AdminForthComponentDeclaration>,
|
|
1175
1210
|
},
|
|
1176
1211
|
rememberMeDuration: string,
|
|
1177
1212
|
showBrandNameInSidebar: boolean,
|
|
@@ -144,7 +144,7 @@ export interface FrontendAPIInterface {
|
|
|
144
144
|
/**
|
|
145
145
|
* Run save interceptors for a specific resource or all resources if no resourceId is provided
|
|
146
146
|
*/
|
|
147
|
-
runSaveInterceptors(params: { action: 'create'|'edit'; values: any; resource: any; resourceId: string; }): Promise<{ ok: boolean; error?: string | null; extra?:
|
|
147
|
+
runSaveInterceptors(params: { action: 'create'|'edit'; values: any; resource: any; resourceId: string; }): Promise<{ ok: boolean; error?: string | null; extra?: any; }>;
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
150
|
* Clear save interceptors for a specific resource or all resources if no resourceId is provided
|
|
@@ -152,6 +152,11 @@ export interface FrontendAPIInterface {
|
|
|
152
152
|
* @param resourceId - The resource ID to clear interceptors for
|
|
153
153
|
*/
|
|
154
154
|
clearSaveInterceptors(resourceId?: string): void;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Register a save interceptor for a specific resource
|
|
158
|
+
*/
|
|
159
|
+
registerSaveInterceptor(handler: (ctx: { action: 'create'|'edit'; values: any; resource: any; }) => Promise<{ ok: boolean; error?: string | null; extra?: any; }>): void;
|
|
155
160
|
}
|
|
156
161
|
|
|
157
162
|
export type ConfirmParams = {
|
|
@@ -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
|
+
}
|
|
@@ -4,13 +4,18 @@ import { type AdminForthResourceCommon } from '../types/Common';
|
|
|
4
4
|
import { useAdminforth } from '@/adminforth';
|
|
5
5
|
import { showErrorTost } from '@/composables/useFrontendApi'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
let getResourceDataLastAbortController: AbortController | null = null;
|
|
8
8
|
export async function getList(resource: AdminForthResourceCommon, isPageLoaded: boolean, page: number | null , pageSize: number, sort: any, checkboxes:{ value: any[] }, filters: any = [] ) {
|
|
9
9
|
let rows: any[] = [];
|
|
10
10
|
let totalRows: number | null = null;
|
|
11
11
|
if (!isPageLoaded) {
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
|
+
const abortController = new AbortController();
|
|
15
|
+
if (getResourceDataLastAbortController) {
|
|
16
|
+
getResourceDataLastAbortController.abort();
|
|
17
|
+
}
|
|
18
|
+
getResourceDataLastAbortController = abortController;
|
|
14
19
|
const data = await callAdminForthApi({
|
|
15
20
|
path: '/get_resource_data',
|
|
16
21
|
method: 'POST',
|
|
@@ -21,7 +26,8 @@ export async function getList(resource: AdminForthResourceCommon, isPageLoaded:
|
|
|
21
26
|
offset: ((page || 1) - 1) * pageSize,
|
|
22
27
|
filters: filters,
|
|
23
28
|
sort: sort,
|
|
24
|
-
}
|
|
29
|
+
},
|
|
30
|
+
abortSignal: abortController.signal
|
|
25
31
|
});
|
|
26
32
|
if (data.error) {
|
|
27
33
|
showErrorTost(data.error);
|