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.
Files changed (58) hide show
  1. package/commands/createApp/templates/package.json.hbs +1 -1
  2. package/dist/modules/restApi.d.ts +1 -0
  3. package/dist/modules/restApi.d.ts.map +1 -1
  4. package/dist/modules/restApi.js +65 -3
  5. package/dist/modules/restApi.js.map +1 -1
  6. package/dist/modules/styles.js +2 -2
  7. package/dist/modules/styles.js.map +1 -1
  8. package/dist/servers/express.d.ts.map +1 -1
  9. package/dist/servers/express.js +7 -1
  10. package/dist/servers/express.js.map +1 -1
  11. package/dist/spa/package-lock.json +85 -7
  12. package/dist/spa/package.json +4 -1
  13. package/dist/spa/pnpm-lock.yaml +339 -299
  14. package/dist/spa/src/App.vue +1 -1
  15. package/dist/spa/src/adminforth.ts +17 -29
  16. package/dist/spa/src/afcl/Input.vue +1 -1
  17. package/dist/spa/src/afcl/Modal.vue +12 -1
  18. package/dist/spa/src/afcl/Select.vue +4 -2
  19. package/dist/spa/src/afcl/Table.vue +27 -13
  20. package/dist/spa/src/components/AcceptModal.vue +2 -0
  21. package/dist/spa/src/components/ColumnValueInputWrapper.vue +35 -4
  22. package/dist/spa/src/components/CustomRangePicker.vue +22 -67
  23. package/dist/spa/src/components/GroupsTable.vue +7 -4
  24. package/dist/spa/src/components/ListActionsThreeDots.vue +9 -8
  25. package/dist/spa/src/components/RangePicker.vue +236 -0
  26. package/dist/spa/src/components/ResourceForm.vue +100 -6
  27. package/dist/spa/src/components/ResourceListTable.vue +45 -70
  28. package/dist/spa/src/components/Sidebar.vue +1 -1
  29. package/dist/spa/src/components/ThreeDotsMenu.vue +54 -57
  30. package/dist/spa/src/i18n.ts +1 -1
  31. package/dist/spa/src/stores/core.ts +4 -2
  32. package/dist/spa/src/types/Back.ts +10 -3
  33. package/dist/spa/src/types/Common.ts +43 -8
  34. package/dist/spa/src/types/FrontendAPI.ts +6 -1
  35. package/dist/spa/src/types/adapters/StorageAdapter.ts +12 -0
  36. package/dist/spa/src/utils/createEditUtils.ts +65 -0
  37. package/dist/spa/src/utils/index.ts +2 -1
  38. package/dist/spa/src/utils/listUtils.ts +8 -2
  39. package/dist/spa/src/utils/utils.ts +192 -12
  40. package/dist/spa/src/utils.ts +2 -1
  41. package/dist/spa/src/views/CreateView.vue +32 -59
  42. package/dist/spa/src/views/EditView.vue +30 -47
  43. package/dist/spa/src/views/ListView.vue +119 -18
  44. package/dist/spa/src/views/LoginView.vue +13 -13
  45. package/dist/spa/src/views/ShowView.vue +67 -61
  46. package/dist/spa/tsconfig.app.json +1 -1
  47. package/dist/types/Back.d.ts +7 -4
  48. package/dist/types/Back.d.ts.map +1 -1
  49. package/dist/types/Back.js.map +1 -1
  50. package/dist/types/Common.d.ts +43 -8
  51. package/dist/types/Common.d.ts.map +1 -1
  52. package/dist/types/Common.js.map +1 -1
  53. package/dist/types/FrontendAPI.d.ts +13 -1
  54. package/dist/types/FrontendAPI.d.ts.map +1 -1
  55. package/dist/types/FrontendAPI.js.map +1 -1
  56. package/dist/types/adapters/StorageAdapter.d.ts +11 -0
  57. package/dist/types/adapters/StorageAdapter.d.ts.map +1 -1
  58. 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: AdminForthBulkActionCommon) => action.showInThreeDotsDropdown))">
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?.meta"
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 px-4 py-2 hover:text-lightThreeDotsMenuBodyTextHover hover:bg-lightThreeDotsMenuBodyBackgroundHover dark:hover:bg-darkThreeDotsMenuBodyBackgroundHover dark:hover:text-darkThreeDotsMenuBodyTextHover">
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 { AdminForthBulkActionCommon, AdminForthComponentDeclarationFull } from '@/types/Common';
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<AdminForthActionInput>,
113
- bulkActions: Array<AdminForthBulkActionCommon>,
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
- const actionId = action.id;
135
- const data = await callAdminForthApi({
136
- path: '/start_custom_action',
137
- method: 'POST',
138
- body: {
139
- resourceId: route.params.resourceId,
140
- actionId: actionId,
141
- recordId: route.params.primaryKey,
142
- extra: payload || {},
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
- if (data?.redirectUrl) {
147
- // Check if the URL should open in a new tab
148
- if (data.redirectUrl.includes('target=_blank')) {
149
- window.open(data.redirectUrl.replace('&target=_blank', '').replace('?target=_blank', ''), '_blank');
150
- } else {
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
- return;
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: data.successMessage,
171
- variant: 'success'
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
 
@@ -51,6 +51,6 @@ export function initI18n(app: ReturnType<typeof createApp>) {
51
51
  },
52
52
  });
53
53
  app.use(i18n);
54
- i18nInstance = i18n
54
+ i18nInstance = i18n as typeof i18nInstance
55
55
  return i18n
56
56
  }
@@ -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<AdminForthResourceCommon | null> = ref(null);
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?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
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: string,
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: string,
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?: object; }>;
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
+ }
@@ -1,2 +1,3 @@
1
1
  export * from './utils';
2
- export * from './listUtils';
2
+ export * from './listUtils';
3
+ export * from './createEditUtils';
@@ -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);