adminforth 2.11.18 → 2.12.1

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 (76) hide show
  1. package/commands/createApp/templates/api.ts.hbs +10 -0
  2. package/commands/createApp/templates/index.ts.hbs +4 -1
  3. package/commands/createApp/utils.js +5 -0
  4. package/commands/createCustomComponent/main.js +12 -7
  5. package/commands/createCustomComponent/templates/customCrud/saveButton.vue.hbs +28 -0
  6. package/dist/auth.d.ts.map +1 -1
  7. package/dist/auth.js +6 -0
  8. package/dist/auth.js.map +1 -1
  9. package/dist/dataConnectors/baseConnector.d.ts +1 -1
  10. package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  11. package/dist/dataConnectors/baseConnector.js +23 -2
  12. package/dist/dataConnectors/baseConnector.js.map +1 -1
  13. package/dist/dataConnectors/postgres.d.ts.map +1 -1
  14. package/dist/dataConnectors/postgres.js +32 -14
  15. package/dist/dataConnectors/postgres.js.map +1 -1
  16. package/dist/index.d.ts +10 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +19 -12
  19. package/dist/index.js.map +1 -1
  20. package/dist/modules/codeInjector.d.ts.map +1 -1
  21. package/dist/modules/codeInjector.js +12 -0
  22. package/dist/modules/codeInjector.js.map +1 -1
  23. package/dist/modules/configValidator.d.ts.map +1 -1
  24. package/dist/modules/configValidator.js +66 -10
  25. package/dist/modules/configValidator.js.map +1 -1
  26. package/dist/modules/restApi.d.ts +1 -1
  27. package/dist/modules/restApi.d.ts.map +1 -1
  28. package/dist/modules/restApi.js +18 -5
  29. package/dist/modules/restApi.js.map +1 -1
  30. package/dist/modules/styles.d.ts +0 -18
  31. package/dist/modules/styles.d.ts.map +1 -1
  32. package/dist/modules/styles.js +1 -19
  33. package/dist/modules/styles.js.map +1 -1
  34. package/dist/servers/express.d.ts +5 -0
  35. package/dist/servers/express.d.ts.map +1 -1
  36. package/dist/servers/express.js +26 -1
  37. package/dist/servers/express.js.map +1 -1
  38. package/dist/spa/package.json +1 -1
  39. package/dist/spa/src/App.vue +1 -1
  40. package/dist/spa/src/afcl/Input.vue +10 -2
  41. package/dist/spa/src/afcl/Select.vue +17 -3
  42. package/dist/spa/src/afcl/Table.vue +1 -1
  43. package/dist/spa/src/afcl/Tooltip.vue +4 -2
  44. package/dist/spa/src/components/AcceptModal.vue +43 -9
  45. package/dist/spa/src/components/CallActionWrapper.vue +15 -0
  46. package/dist/spa/src/components/ColumnValueInput.vue +1 -1
  47. package/dist/spa/src/components/CustomRangePicker.vue +3 -16
  48. package/dist/spa/src/components/Filters.vue +129 -112
  49. package/dist/spa/src/components/ResourceForm.vue +2 -2
  50. package/dist/spa/src/components/ResourceListTable.vue +45 -13
  51. package/dist/spa/src/components/ResourceListTableVirtual.vue +52 -16
  52. package/dist/spa/src/components/ShowTable.vue +5 -4
  53. package/dist/spa/src/components/Sidebar.vue +27 -5
  54. package/dist/spa/src/components/ThreeDotsMenu.vue +27 -17
  55. package/dist/spa/src/components/Toast.vue +15 -22
  56. package/dist/spa/src/components/UserMenuSettingsButton.vue +9 -10
  57. package/dist/spa/src/i18n.ts +4 -2
  58. package/dist/spa/src/main.ts +1 -1
  59. package/dist/spa/src/stores/core.ts +12 -0
  60. package/dist/spa/src/stores/filters.ts +5 -1
  61. package/dist/spa/src/types/Back.ts +22 -1
  62. package/dist/spa/src/types/Common.ts +24 -0
  63. package/dist/spa/src/utils.ts +69 -2
  64. package/dist/spa/src/views/CreateView.vue +47 -4
  65. package/dist/spa/src/views/EditView.vue +30 -3
  66. package/dist/spa/src/views/ListView.vue +6 -2
  67. package/dist/spa/src/views/LoginView.vue +4 -13
  68. package/dist/spa/src/views/SettingsView.vue +1 -1
  69. package/dist/spa/src/views/ShowView.vue +27 -17
  70. package/dist/types/Back.d.ts +27 -0
  71. package/dist/types/Back.d.ts.map +1 -1
  72. package/dist/types/Back.js.map +1 -1
  73. package/dist/types/Common.d.ts +47 -0
  74. package/dist/types/Common.d.ts.map +1 -1
  75. package/dist/types/Common.js.map +1 -1
  76. package/package.json +2 -1
@@ -12,7 +12,7 @@
12
12
  <!-- Dropdown menu -->
13
13
  <div
14
14
  id="listThreeDotsDropdown"
15
- class="z-20 hidden bg-lightThreeDotsMenuBodyBackground divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-darkThreeDotsMenuBodyBackground dark:divide-gray-600">
15
+ class="z-30 hidden bg-lightThreeDotsMenuBodyBackground divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-darkThreeDotsMenuBodyBackground dark:divide-gray-600">
16
16
  <ul class="py-2 text-sm text-lightThreeDotsMenuBodyText dark:text-darkThreeDotsMenuBodyText" aria-labelledby="dropdownMenuIconButton">
17
17
  <li v-for="(item, i) in threeDotsDropdownItems" :key="`dropdown-item-${i}`">
18
18
  <a href="#"
@@ -23,7 +23,7 @@
23
23
  'cursor-not-allowed': checkboxes && checkboxes.length === 0 && item.meta?.disabledWhenNoCheckboxes,
24
24
  }"
25
25
  @click="injectedComponentClick(i)">
26
- <component :ref="(el: any) => setComponentRef(el, i)" :is="getCustomComponent(item)"
26
+ <component :ref="(el: any) => setComponentRef(el, i)" :is="getCustomComponent(item)"
27
27
  :meta="item.meta"
28
28
  :resource="coreStore.resource"
29
29
  :adminUser="coreStore.adminUser"
@@ -33,19 +33,25 @@
33
33
  />
34
34
  </a>
35
35
  </li>
36
- <li v-if="customActions" v-for="action in customActions" :key="action.id">
37
- <a href="#" @click.prevent="handleActionClick(action)" class="block px-4 py-2 hover:text-lightThreeDotsMenuBodyTextHover hover:bg-lightThreeDotsMenuBodyBackgroundHover dark:hover:bg-darkThreeDotsMenuBodyBackgroundHover dark:hover:text-darkThreeDotsMenuBodyTextHover">
38
- <div class="flex items-center gap-2">
39
- <component
40
- v-if="action.icon"
41
- :is="getIcon(action.icon)"
42
- class="w-4 h-4 text-lightPrimary dark:text-darkPrimary"
43
- />
44
- {{ action.name }}
45
- </div>
46
- </a>
36
+ <li v-for="action in customActions" :key="action.id">
37
+ <component
38
+ :is="(action.customComponent && getCustomComponent(action.customComponent)) || CallActionWrapper"
39
+ :meta="action.customComponent?.meta"
40
+ @callAction="(payload? : Object) => handleActionClick(action, payload)"
41
+ >
42
+ <a href="#" @click.prevent class="block px-4 py-2 hover:text-lightThreeDotsMenuBodyTextHover hover:bg-lightThreeDotsMenuBodyBackgroundHover dark:hover:bg-darkThreeDotsMenuBodyBackgroundHover dark:hover:text-darkThreeDotsMenuBodyTextHover">
43
+ <div class="flex items-center gap-2">
44
+ <component
45
+ v-if="action.icon"
46
+ :is="getIcon(action.icon)"
47
+ class="w-4 h-4 text-lightPrimary dark:text-darkPrimary"
48
+ />
49
+ {{ action.name }}
50
+ </div>
51
+ </a>
52
+ </component>
47
53
  </li>
48
- <li v-for="action in bulkActions?.filter((a:AdminForthBulkActionCommon ) => a.showInThreeDotsDropdown)" :key="action.id">
54
+ <li v-for="action in (bulkActions ?? []).filter(a => a.showInThreeDotsDropdown)" :key="action.id">
49
55
  <a href="#" @click.prevent="startBulkAction(action.id)"
50
56
  class="block px-4 py-2 hover:text-lightThreeDotsMenuBodyTextHover hover:bg-lightThreeDotsMenuBodyBackgroundHover dark:hover:bg-darkThreeDotsMenuBodyBackgroundHover dark:hover:text-darkThreeDotsMenuBodyTextHover"
51
57
  :class="{
@@ -75,8 +81,11 @@ import { useCoreStore } from '@/stores/core';
75
81
  import adminforth from '@/adminforth';
76
82
  import { callAdminForthApi } from '@/utils';
77
83
  import { useRoute, useRouter } from 'vue-router';
78
- import type { AdminForthComponentDeclarationFull, AdminForthBulkActionCommon, AdminForthActionInput } from '@/types/Common.js';
84
+ import CallActionWrapper from '@/components/CallActionWrapper.vue'
79
85
  import { ref, type ComponentPublicInstance } from 'vue';
86
+ import type { AdminForthBulkActionCommon, AdminForthComponentDeclarationFull } from '@/types/Common';
87
+ import type { AdminForthActionInput } from '@/types/Back';
88
+
80
89
 
81
90
  const route = useRoute();
82
91
  const coreStore = useCoreStore();
@@ -104,7 +113,7 @@ function setComponentRef(el: ComponentPublicInstance | null, index: number) {
104
113
  }
105
114
  }
106
115
 
107
- async function handleActionClick(action: AdminForthActionInput) {
116
+ async function handleActionClick(action: AdminForthActionInput, payload: any) {
108
117
  adminforth.list.closeThreeDotsDropdown();
109
118
 
110
119
  const actionId = action.id;
@@ -114,7 +123,8 @@ async function handleActionClick(action: AdminForthActionInput) {
114
123
  body: {
115
124
  resourceId: route.params.resourceId,
116
125
  actionId: actionId,
117
- recordId: route.params.primaryKey
126
+ recordId: route.params.primaryKey,
127
+ extra: payload || {},
118
128
  }
119
129
  });
120
130
 
@@ -1,32 +1,22 @@
1
1
  <template>
2
2
 
3
3
 
4
- <div class="flex items-center w-full p-4 rounded-lg shadow-lg dark:text-darkToastText dark:bg-darkToastBackground bg-lightToastBackground text-lightToastText"
4
+ <div class="afcl-toast flex items-center w-full p-4 rounded-lg shadow-lg dark:text-darkToastText dark:bg-darkToastBackground bg-lightToastBackground text-lightToastText border-l-4"
5
+ :class="toast.variant == 'info' ? 'border-lightPrimary dark:border-darkPrimary' : toast.variant == 'danger' ? 'border-red-500 dark:border-red-800' : toast.variant == 'warning' ? 'border-orange-500 dark:border-orange-700' : 'border-green-500 dark:border-green-800'"
5
6
  role="alert"
6
7
  >
7
- <div v-if="toast.variant == 'info'" class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-lightPrimary dark:text-darkPrimary bg-lightPrimaryOpacity rounded-lg dark:bg-blue-800 dark:text-blue-200">
8
- <svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
9
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.147 15.085a7.159 7.159 0 0 1-6.189 3.307A6.713 6.713 0 0 1 3.1 15.444c-2.679-4.513.287-8.737.888-9.548A4.373 4.373 0 0 0 5 1.608c1.287.953 6.445 3.218 5.537 10.5 1.5-1.122 2.706-3.01 2.853-6.14 1.433 1.049 3.993 5.395 1.757 9.117Z"/>
10
- </svg>
11
- <span class="sr-only">{{ $t('Fire icon') }}</span>
8
+ <div v-if="toast.variant == 'info'" class="af-toast-icon inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-lightPrimary dark:text-darkPrimary bg-lightPrimaryOpacity rounded-lg dark:bg-darkPrimary dark:!text-blue-100">
9
+ <IconInfoCircleSolid class="w-5 h-5" aria-hidden="true" />
12
10
  </div>
13
- <div v-else-if="toast.variant == 'danger'" class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-red-500 bg-red-100 rounded-lg dark:bg-red-800 dark:text-red-200">
14
- <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
15
- <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 11.793a1 1 0 1 1-1.414 1.414L10 11.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L8.586 10 6.293 7.707a1 1 0 0 1 1.414-1.414L10 8.586l2.293-2.293a1 1 0 0 1 1.414 1.414L11.414 10l2.293 2.293Z"/>
16
- </svg>
17
- <span class="sr-only">{{ $t('Error icon') }}</span>
11
+ <div v-else-if="toast.variant == 'danger'" class="af-toast-icon inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-red-500 bg-red-100 rounded-lg dark:bg-red-800 dark:text-red-200">
12
+ <IconCloseCircleSolid class="w-5 h-5" aria-hidden="true" />
18
13
  </div>
19
- <div v-else-if="toast.variant == 'warning'"class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-orange-500 bg-orange-100 rounded-lg dark:bg-orange-700 dark:text-orange-200">
20
- <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
21
- <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM10 15a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-4a1 1 0 0 1-2 0V6a1 1 0 0 1 2 0v5Z"/>
22
- </svg>
23
- <span class="sr-only">{{ $t('Warning icon') }}</span>
14
+ <div v-else-if="toast.variant == 'warning'" class="af-toast-icon inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-orange-500 bg-orange-100 rounded-lg dark:bg-orange-700 dark:text-orange-200">
15
+ <IconExclamationCircleSolid class="w-5 h-5" aria-hidden="true" />
16
+
24
17
  </div>
25
- <div v-else class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg dark:bg-green-800 dark:text-green-200">
26
- <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
27
- <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z"/>
28
- </svg>
29
- <span class="sr-only">{{ $t('Check icon') }}</span>
18
+ <div v-else class="af-toast-icon inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg dark:bg-green-800 dark:text-green-200">
19
+ <IconCheckCircleSolid class="w-5 h-5" aria-hidden="true" />
30
20
  </div>
31
21
 
32
22
  <div class="ms-3 text-sm font-normal max-w-xs pr-2" v-if="toast.messageHtml" v-html="toast.messageHtml"></div>
@@ -34,7 +24,7 @@
34
24
  <div class="flex flex-col items-center justify-center">
35
25
  {{toast.message}}
36
26
  <div v-if="toast.buttons" class="flex justify-center mt-2 gap-2">
37
- <div v-for="button in toast.buttons" class="rounded-md bg-lightButtonsBackground hover:bg-lightButtonsHover text-lightButtonsText dark:bg-darkPrimary dark:hover:bg-darkButtonsBackground dark:text-darkButtonsText">
27
+ <div v-for="button in toast.buttons" class="af-toast-button rounded-md bg-lightButtonsBackground hover:bg-lightButtonsHover text-lightButtonsText dark:bg-darkPrimary dark:hover:bg-darkButtonsBackground dark:text-darkButtonsText">
38
28
  <button @click="onButtonClick(button.value)" class="px-2 py-1 rounded hover:bg-black/5 dark:hover:bg-white/10">
39
29
  {{ button.label }}
40
30
  </button>
@@ -47,6 +37,7 @@
47
37
  <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
48
38
  </svg>
49
39
  </button>
40
+ <!-- <div class="h-full ml-3 w-1 rounded-r-lg" :class="toast.variant == 'info' ? 'bg-lightPrimary dark:bg-darkPrimary' : toast.variant == 'danger' ? 'bg-red-500 dark:bg-red-800' : toast.variant == 'warning' ? 'bg-orange-500 dark:bg-orange-700' : 'bg-green-500 dark:bg-green-800'"></div> -->
50
41
  </div>
51
42
 
52
43
 
@@ -55,6 +46,8 @@
55
46
  <script setup lang="ts">
56
47
  import { onMounted } from 'vue';
57
48
  import { useToastStore } from '@/stores/toast';
49
+ import { IconInfoCircleSolid, IconCloseCircleSolid, IconExclamationCircleSolid, IconCheckCircleSolid } from '@iconify-prerendered/vue-flowbite';
50
+
58
51
  const toastStore = useToastStore();
59
52
  const emit = defineEmits(['close']);
60
53
  const props = defineProps<{
@@ -1,15 +1,13 @@
1
1
  <template>
2
2
  <div class="min-w-40">
3
3
  <div class="cursor-pointer flex items-center justify-between gap-1 block px-4 py-2 text-sm
4
- bg-lightUserMenuSettingsButtonBackground hover:bg-lightUserMenuSettingsButtonBackgroundHover
5
- text-lightUserMenuSettingsButtonText hover:text-lightUserMenuSettingsButtonTextHover
6
- dark:bg-darkUserMenuSettingsButtonBackground dark:hover:bg-darkUserMenuSettingsButtonBackgroundHover
7
- dark:text-darkUserMenuSettingsButtonText dark:hover:text-darkUserMenuSettingsButtonTextHover
4
+ bg-lightUserMenuItemBackground hover:bg-lightUserMenuItemBackgroundHover text-lightUserMenuItemText
5
+ hover:text-lightUserMenuItemText dark:bg-darkUserMenuItemBackground dark:hover:bg-darkUserMenuItemBackgroundHover
6
+ dark:text-darkUserMenuItemText dark:hover:darkUserMenuItemTextHover
8
7
  w-full select-none "
9
- :class="{ 'bg-lightUserMenuSettingsButtonBackgroundExpanded hover:bg-lightUserMenuSettingsButtonBackgroundExpanded dark:bg-darkUserMenuSettingsButtonBackgroundExpanded hover:dark:bg-darkUserMenuSettingsButtonBackgroundExpanded ': showDropdown }"
10
8
  @click="showDropdown = !showDropdown"
11
9
  >
12
- <span>Settings</span>
10
+ <span>{{ $t('Settings') }}</span>
13
11
  <IconCaretDownSolid class="h-5 w-5 text-lightPrimary dark:text-gray-400 opacity-50 transition duration-150 ease-in"
14
12
  :class="{ 'transform rotate-180': showDropdown }"
15
13
  />
@@ -18,10 +16,9 @@
18
16
  <div v-if="showDropdown" >
19
17
 
20
18
  <router-link class="cursor-pointer flex items-center gap-1 block px-4 py-1 text-sm
21
- bg-lightUserMenuSettingsButtonDropdownItemBackground hover:bg-lightUserMenuSettingsButtonDropdownItemBackgroundHover
22
- text-lightUserMenuSettingsButtonDropdownItemText hover:text-lightUserMenuSettingsButtonDropdownItemTextHover
23
- dark:bg-darkUserMenuSettingsButtonDropdownItemBackground dark:hover:bg-darkUserMenuSettingsButtonDropdownItemBackgroundHover
24
- dark:text-darkUserMenuSettingsButtonDropdownItemText dark:hover:text-darkUserMenuSettingsButtonDropdownItemTextHover
19
+ bg-lightUserMenuItemBackground hover:bg-lightUserMenuItemBackgroundHover text-lightUserMenuItemText
20
+ hover:text-lightUserMenuItemText dark:bg-darkUserMenuItemBackground dark:hover:bg-darkUserMenuItemBackgroundHover
21
+ dark:text-darkUserMenuItemText dark:hover:darkUserMenuItemTextHover
25
22
  w-full text-select-none pl-5 select-none"
26
23
  v-for="option in options"
27
24
  :to="getRoute(option)"
@@ -43,9 +40,11 @@ import { computed, ref, onMounted, watch } from 'vue';
43
40
  import { useCoreStore } from '@/stores/core';
44
41
  import { getIcon } from '@/utils';
45
42
  import { useRouter } from 'vue-router';
43
+ import { useI18n } from 'vue-i18n';
46
44
 
47
45
  const router = useRouter();
48
46
  const coreStore = useCoreStore();
47
+ const { t } = useI18n();
49
48
 
50
49
  const showDropdown = ref(false);
51
50
  const props = defineProps(['meta', 'resource']);
@@ -7,7 +7,7 @@ function slavicPluralRule(choice: number, choicesLength: number, orgRule: any) {
7
7
  if (choice === 0) {
8
8
  return 0
9
9
  }
10
-
10
+
11
11
  const teen = choice > 10 && choice < 20
12
12
  const endsWithOne = choice % 10 === 1
13
13
 
@@ -21,6 +21,8 @@ function slavicPluralRule(choice: number, choicesLength: number, orgRule: any) {
21
21
  return choicesLength < 4 ? 2 : 3
22
22
  }
23
23
 
24
+ export let i18nInstance: ReturnType<typeof createI18n> | null = null
25
+
24
26
  export function initI18n(app: ReturnType<typeof createApp>) {
25
27
  const i18n = createI18n({
26
28
  legacy: false,
@@ -48,7 +50,7 @@ export function initI18n(app: ReturnType<typeof createApp>) {
48
50
  return key + ' ';
49
51
  },
50
52
  });
51
-
52
53
  app.use(i18n);
54
+ i18nInstance = i18n
53
55
  return i18n
54
56
  }
@@ -14,7 +14,7 @@ app.use(createPinia())
14
14
  app.use(router)
15
15
 
16
16
  // get access to i18n instance outside components
17
- window.i18n = initI18n(app);
17
+ initI18n(app);
18
18
 
19
19
 
20
20
  /* IMPORTANT:ADMINFORTH CUSTOM USES */
@@ -16,6 +16,7 @@ export const useCoreStore = defineStore('core', () => {
16
16
  const record: Ref<any | null> = ref({});
17
17
  const resource: Ref<AdminForthResourceCommon | null> = ref(null);
18
18
  const userData: Ref<UserData | null> = ref(null);
19
+ const isResourceFetching = ref(false);
19
20
 
20
21
  const resourceColumnsWithFilters = computed(() => {
21
22
  if (!resource.value) {
@@ -172,6 +173,7 @@ export const useCoreStore = defineStore('core', () => {
172
173
  // already fetched
173
174
  return;
174
175
  }
176
+ isResourceFetching.value = true;
175
177
  resourceColumnsId.value = resourceId;
176
178
  resourceColumnsError.value = '';
177
179
  const res = await callAdminForthApi({
@@ -188,6 +190,7 @@ export const useCoreStore = defineStore('core', () => {
188
190
  resource.value = res.resource;
189
191
  resourceOptions.value = res.resource.options;
190
192
  }
193
+ isResourceFetching.value = false;
191
194
  }
192
195
 
193
196
  async function getPublicConfig() {
@@ -198,6 +201,13 @@ export const useCoreStore = defineStore('core', () => {
198
201
  config.value = {...config.value, ...res};
199
202
  }
200
203
 
204
+ async function getLoginFormConfig() {
205
+ const res = await callAdminForthApi({
206
+ path: '/get_login_form_config',
207
+ method: 'GET',
208
+ });
209
+ config.value = {...config.value, ...res};
210
+ }
201
211
 
202
212
  const username = computed(() => {
203
213
  const usernameField = config.value?.usernameField;
@@ -218,6 +228,7 @@ export const useCoreStore = defineStore('core', () => {
218
228
  userFullname,
219
229
  getPublicConfig,
220
230
  fetchMenuAndResource,
231
+ getLoginFormConfig,
221
232
  fetchRecord,
222
233
  record,
223
234
  fetchResourceFull,
@@ -231,5 +242,6 @@ export const useCoreStore = defineStore('core', () => {
231
242
  fetchMenuBadges,
232
243
  resetAdminUser,
233
244
  resetResource,
245
+ isResourceFetching,
234
246
  }
235
247
  })
@@ -22,6 +22,9 @@ export const useFiltersStore = defineStore('filters', () => {
22
22
  const getFilters = () => {
23
23
  return filters.value;
24
24
  }
25
+ const clearFilter = (fieldName: string) => {
26
+ filters.value = filters.value.filter(f => f.field !== fieldName);
27
+ }
25
28
  const clearFilters = () => {
26
29
  filters.value = [];
27
30
  }
@@ -49,6 +52,7 @@ export const useFiltersStore = defineStore('filters', () => {
49
52
  setSort,
50
53
  getSort,
51
54
  visibleFiltersCount,
52
- shouldFilterBeHidden
55
+ shouldFilterBeHidden,
56
+ clearFilter
53
57
  }
54
58
  })
@@ -363,7 +363,8 @@ export interface IAdminForth {
363
363
  ): Promise<{ error?: string, createdRecord?: any, newRecordId?: any }>;
364
364
 
365
365
  updateResourceRecord(
366
- params: { resource: AdminForthResource, recordId: any, record: any, oldRecord: any, adminUser: AdminUser, extra?: HttpExtra }
366
+ params: { resource: AdminForthResource, recordId: any, record: any, oldRecord: any, adminUser: AdminUser, extra?: HttpExtra, updates?: never }
367
+ | { resource: AdminForthResource, recordId: any, record?: never, oldRecord: any, adminUser: AdminUser, extra?: HttpExtra, updates: any }
367
368
  ): Promise<{ error?: string }>;
368
369
 
369
370
  deleteResourceRecord(
@@ -603,6 +604,7 @@ export type BeforeLoginConfirmationFunction = (params?: {
603
604
  response: IAdminForthHttpResponse,
604
605
  adminforth: IAdminForth,
605
606
  extra?: HttpExtra,
607
+ rememberMeDays?: number,
606
608
  }) => Promise<{
607
609
  error?: string,
608
610
  body: {
@@ -611,6 +613,19 @@ export type BeforeLoginConfirmationFunction = (params?: {
611
613
  }
612
614
  }>;
613
615
 
616
+ /**
617
+ * Allow to make extra authorization
618
+ */
619
+ export type AdminUserAuthorizeFunction = ((params?: {
620
+ adminUser: AdminUser,
621
+ response: IAdminForthHttpResponse,
622
+ adminforth: IAdminForth,
623
+ extra?: HttpExtra,
624
+ }) => Promise<{
625
+ error?: string,
626
+ allowed?: boolean,
627
+ }>);
628
+
614
629
 
615
630
  /**
616
631
  * Data source describes database connection which will be used to fetch data for resources.
@@ -848,6 +863,7 @@ export interface AdminForthActionInput {
848
863
  }>;
849
864
  icon?: string;
850
865
  id?: string;
866
+ customComponent?: AdminForthComponentDeclaration;
851
867
  }
852
868
 
853
869
  export interface AdminForthResourceInput extends Omit<NonNullable<AdminForthResourceInputCommon>, 'columns' | 'hooks' | 'options'> {
@@ -1009,6 +1025,11 @@ export interface AdminForthInputConfig {
1009
1025
  */
1010
1026
  beforeLoginConfirmation?: BeforeLoginConfirmationFunction | Array<BeforeLoginConfirmationFunction>,
1011
1027
 
1028
+ /**
1029
+ * Array of functions which will be called before any request to AdminForth API.
1030
+ */
1031
+ adminUserAuthorize?: AdminUserAuthorizeFunction | Array<AdminUserAuthorizeFunction>,
1032
+
1012
1033
  /**
1013
1034
  * Optionally if your users table has a field(column) with full name, you can set it here.
1014
1035
  * This field will be used to display user name in the top right corner of the admin panel.
@@ -67,6 +67,11 @@ export type AllowedActionsResolved = {
67
67
  [key in AllowedActionsEnum]: boolean
68
68
  }
69
69
 
70
+ // conditional operators for predicates
71
+ type Value = any;
72
+ type Operators = { $eq: Value } | { $not: Value } | { $gt: Value } | { $gte: Value } | { $lt: Value } | { $lte: Value } | { $in: Value[] } | { $nin: Value[] } | { $includes: Value } | { $nincludes: Value };
73
+ export type Predicate = { $and: Predicate[] } | { $or: Predicate[] } | { [key: string]: Operators | Value };
74
+
70
75
  export interface AdminUser {
71
76
  /**
72
77
  * primaryKey field value of user in table which is defined by {@link AdminForthConfig.auth.usersResourceId}
@@ -507,6 +512,11 @@ export interface AdminForthResourceInputCommon {
507
512
  afterBreadcrumbs?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
508
513
  bottom?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
509
514
  threeDotsDropdownItems?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
515
+ /**
516
+ * Custom Save button component for Edit page.
517
+ * Accepts props: [record, resource, adminUser, meta, saving, validating, isValid, disabled, saveRecord]
518
+ */
519
+ saveButton?: AdminForthComponentDeclaration,
510
520
  },
511
521
 
512
522
  /**
@@ -519,6 +529,11 @@ export interface AdminForthResourceInputCommon {
519
529
  afterBreadcrumbs?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
520
530
  bottom?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
521
531
  threeDotsDropdownItems?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
532
+ /**
533
+ * Custom Save button component for Create page.
534
+ * Accepts props: [record, resource, adminUser, meta, saving, validating, isValid, disabled, saveRecord]
535
+ */
536
+ saveButton?: AdminForthComponentDeclaration,
522
537
  },
523
538
  }
524
539
  },
@@ -872,6 +887,15 @@ export interface AdminForthResourceColumnInputCommon {
872
887
  */
873
888
  masked?: boolean,
874
889
 
890
+ /**
891
+ * Sticky position for column
892
+ */
893
+ listSticky?: boolean;
894
+
895
+ /**
896
+ * Show field only if certain conditions are met.
897
+ */
898
+ showIf?: Predicate;
875
899
  }
876
900
 
877
901
  export interface AdminForthResourceColumnCommon extends AdminForthResourceColumnInputCommon {
@@ -8,6 +8,8 @@ import { Dropdown } from 'flowbite';
8
8
  import adminforth from './adminforth';
9
9
  import sanitizeHtml from 'sanitize-html'
10
10
  import debounce from 'debounce';
11
+ import type { AdminForthResourceColumnInputCommon, Predicate } from '@/types/Common';
12
+ import { i18nInstance } from './i18n'
11
13
 
12
14
  const LS_LANG_KEY = `afLanguage`;
13
15
  const MAX_CONSECUTIVE_EMPTY_RESULTS = 2;
@@ -18,6 +20,7 @@ export async function callApi({path, method, body, headers}: {
18
20
  body?: any
19
21
  headers?: Record<string, string>
20
22
  }): Promise<any> {
23
+ const t = i18nInstance?.global.t || ((s: string) => s)
21
24
  const options = {
22
25
  method,
23
26
  headers: {
@@ -41,11 +44,11 @@ export async function callApi({path, method, body, headers}: {
41
44
  // if it is internal error, say to user
42
45
  if (e instanceof TypeError && e.message === 'Failed to fetch') {
43
46
  // this is a network error
44
- adminforth.alert({variant:'danger', message: window.i18n?.global?.t('Network error, please check your Internet connection and try again'),})
47
+ adminforth.alert({variant:'danger', message: t('Network error, please check your Internet connection and try again'),})
45
48
  return null;
46
49
  }
47
50
 
48
- adminforth.alert({variant:'danger', message: window.i18n?.global?.t('Something went wrong, please try again later'),})
51
+ adminforth.alert({variant:'danger', message: t('Something went wrong, please try again later'),})
49
52
  console.error(`error in callApi ${path}`, e);
50
53
  }
51
54
  }
@@ -421,4 +424,68 @@ export function createSearchInputHandlers(
421
424
  }
422
425
  return acc;
423
426
  }, {} as Record<string, (searchTerm: string) => void>);
427
+ }
428
+
429
+ export function checkShowIf(c: AdminForthResourceColumnInputCommon, record: Record<string, any>) {
430
+ if (!c.showIf) return true;
431
+
432
+ const evaluatePredicate = (predicate: Predicate): boolean => {
433
+ const results: boolean[] = [];
434
+
435
+ if ("$and" in predicate) {
436
+ results.push(predicate.$and.every(evaluatePredicate));
437
+ }
438
+
439
+ if ("$or" in predicate) {
440
+ results.push(predicate.$or.some(evaluatePredicate));
441
+ }
442
+
443
+ const fieldEntries = Object.entries(predicate).filter(([key]) => !key.startsWith('$'));
444
+ if (fieldEntries.length > 0) {
445
+ const fieldResult = fieldEntries.every(([field, condition]) => {
446
+ const recordValue = record[field];
447
+
448
+ if (condition === undefined) {
449
+ return true;
450
+ }
451
+ if (typeof condition !== "object" || condition === null) {
452
+ return recordValue === condition;
453
+ }
454
+
455
+ if ("$eq" in condition) return recordValue === condition.$eq;
456
+ if ("$not" in condition) return recordValue !== condition.$not;
457
+ if ("$gt" in condition) return recordValue > condition.$gt;
458
+ if ("$gte" in condition) return recordValue >= condition.$gte;
459
+ if ("$lt" in condition) return recordValue < condition.$lt;
460
+ if ("$lte" in condition) return recordValue <= condition.$lte;
461
+ if ("$in" in condition) return (Array.isArray(condition.$in) && condition.$in.includes(recordValue));
462
+ if ("$nin" in condition) return (Array.isArray(condition.$nin) && !condition.$nin.includes(recordValue));
463
+ if ("$includes" in condition)
464
+ return (
465
+ Array.isArray(recordValue) &&
466
+ recordValue.includes(condition.$includes)
467
+ );
468
+ if ("$nincludes" in condition)
469
+ return (
470
+ Array.isArray(recordValue) &&
471
+ !recordValue.includes(condition.$nicludes)
472
+ );
473
+
474
+ return true;
475
+ });
476
+ results.push(fieldResult);
477
+ }
478
+
479
+ return results.every(result => result);
480
+ };
481
+
482
+ return evaluatePredicate(c.showIf);
483
+ }
484
+
485
+ export function btoa_function(source: string): string {
486
+ return btoa(source);
487
+ }
488
+
489
+ export function atob_function(source: string): string {
490
+ return atob(source);
424
491
  }
@@ -18,8 +18,25 @@
18
18
  {{ $t('Cancel') }}
19
19
  </button>
20
20
 
21
+ <!-- Custom Save Button injection -->
22
+ <component
23
+ v-if="createSaveButtonInjection"
24
+ :is="getCustomComponent(createSaveButtonInjection)"
25
+ :meta="createSaveButtonInjection.meta"
26
+ :record="record"
27
+ :resource="coreStore.resource"
28
+ :adminUser="coreStore.adminUser"
29
+ :saving="saving"
30
+ :validating="validating"
31
+ :isValid="isValid"
32
+ :disabled="saving || (validating && !isValid)"
33
+ :saveRecord="saveRecord"
34
+ />
35
+
36
+ <!-- Default Save Button fallback -->
21
37
  <button
22
- @click="saveRecord"
38
+ v-else
39
+ @click="() => saveRecord()"
23
40
  class="af-save-button flex items-center py-1 px-3 text-sm font-medium rounded-default text-lightCreateViewSaveButtonText focus:outline-none bg-lightCreateViewButtonBackground rounded border border-lightCreateViewButtonBorder hover:bg-lightCreateViewButtonBackgroundHover hover:text-lightCreateViewSaveButtonTextHover focus:z-10 focus:ring-4 focus:ring-lightCreateViewButtonFocusRing dark:focus:ring-darkCreateViewButtonFocusRing dark:bg-darkCreateViewButtonBackground dark:text-darkCreateViewSaveButtonText dark:border-darkCreateViewButtonBorder dark:hover:text-darkCreateViewSaveButtonTextHover dark:hover:bg-darkCreateViewButtonBackgroundHover disabled:opacity-50 gap-1"
24
41
  :disabled="saving || (validating && !isValid)"
25
42
  >
@@ -105,6 +122,13 @@ const coreStore = useCoreStore();
105
122
 
106
123
  const { t } = useI18n();
107
124
 
125
+ const createSaveButtonInjection = computed<AdminForthComponentDeclarationFull | null>(() => {
126
+ const raw: any = coreStore.resourceOptions?.pageInjections?.create?.saveButton as any;
127
+ if (!raw) return null;
128
+ const item = Array.isArray(raw) ? raw[0] : raw;
129
+ return item as AdminForthComponentDeclarationFull;
130
+ });
131
+
108
132
  const initialValues = ref({});
109
133
 
110
134
  const readonlyColumns = ref([]);
@@ -125,11 +149,27 @@ onMounted(async () => {
125
149
  }
126
150
  return acc;
127
151
  }, {});
152
+ let userUseMultipleEncoding = true; //TODO remove this in future versions
128
153
  if (route.query.values) {
129
- initialValues.value = { ...initialValues.value, ...JSON.parse(decodeURIComponent(route.query.values as string)) };
154
+ try {
155
+ JSON.parse(decodeURIComponent(route.query.values as string));
156
+ console.warn('You are using an outdated format for the query vales. Please update your links and don`t use multiple URL encoding.');
157
+ } catch (e) {
158
+ userUseMultipleEncoding = false;
159
+ console.warn('You are using an outdated format for the query vales. Please update your links and don`t use multiple URL encoding.');
160
+ }
161
+ if (userUseMultipleEncoding) {
162
+ initialValues.value = { ...initialValues.value, ...JSON.parse(decodeURIComponent((route.query.values as string))) };
163
+ } else {
164
+ initialValues.value = { ...initialValues.value, ...JSON.parse(atob(route.query.values as string)) };
165
+ }
130
166
  }
131
167
  if (route.query.readonlyColumns) {
132
- readonlyColumns.value = JSON.parse(decodeURIComponent(route.query.readonlyColumns as string));
168
+ if (userUseMultipleEncoding) {
169
+ readonlyColumns.value = JSON.parse(decodeURIComponent((route.query.readonlyColumns as string)));
170
+ } else {
171
+ readonlyColumns.value = JSON.parse(atob(route.query.readonlyColumns as string));
172
+ }
133
173
  }
134
174
  record.value = initialValues.value;
135
175
  loading.value = false;
@@ -137,7 +177,7 @@ onMounted(async () => {
137
177
  initThreeDotsDropdown();
138
178
  });
139
179
 
140
- async function saveRecord() {
180
+ async function saveRecord(opts?: { confirmationResult?: any }) {
141
181
  if (!isValid.value) {
142
182
  validating.value = true;
143
183
  return;
@@ -151,6 +191,9 @@ async function saveRecord() {
151
191
  body: {
152
192
  resourceId: route.params.resourceId,
153
193
  record: record.value,
194
+ meta: {
195
+ ...(opts?.confirmationResult ? { confirmationResult: opts.confirmationResult } : {}),
196
+ },
154
197
  },
155
198
  });
156
199
  if (response?.error && response?.error !== 'Operation aborted by hook') {