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.
- package/commands/createApp/templates/api.ts.hbs +10 -0
- package/commands/createApp/templates/index.ts.hbs +4 -1
- package/commands/createApp/utils.js +5 -0
- package/commands/createCustomComponent/main.js +12 -7
- package/commands/createCustomComponent/templates/customCrud/saveButton.vue.hbs +28 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +6 -0
- package/dist/auth.js.map +1 -1
- package/dist/dataConnectors/baseConnector.d.ts +1 -1
- package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
- package/dist/dataConnectors/baseConnector.js +23 -2
- package/dist/dataConnectors/baseConnector.js.map +1 -1
- package/dist/dataConnectors/postgres.d.ts.map +1 -1
- package/dist/dataConnectors/postgres.js +32 -14
- package/dist/dataConnectors/postgres.js.map +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -12
- package/dist/index.js.map +1 -1
- package/dist/modules/codeInjector.d.ts.map +1 -1
- package/dist/modules/codeInjector.js +12 -0
- package/dist/modules/codeInjector.js.map +1 -1
- package/dist/modules/configValidator.d.ts.map +1 -1
- package/dist/modules/configValidator.js +66 -10
- package/dist/modules/configValidator.js.map +1 -1
- package/dist/modules/restApi.d.ts +1 -1
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +18 -5
- package/dist/modules/restApi.js.map +1 -1
- package/dist/modules/styles.d.ts +0 -18
- package/dist/modules/styles.d.ts.map +1 -1
- package/dist/modules/styles.js +1 -19
- package/dist/modules/styles.js.map +1 -1
- package/dist/servers/express.d.ts +5 -0
- package/dist/servers/express.d.ts.map +1 -1
- package/dist/servers/express.js +26 -1
- package/dist/servers/express.js.map +1 -1
- package/dist/spa/package.json +1 -1
- package/dist/spa/src/App.vue +1 -1
- package/dist/spa/src/afcl/Input.vue +10 -2
- package/dist/spa/src/afcl/Select.vue +17 -3
- package/dist/spa/src/afcl/Table.vue +1 -1
- package/dist/spa/src/afcl/Tooltip.vue +4 -2
- package/dist/spa/src/components/AcceptModal.vue +43 -9
- package/dist/spa/src/components/CallActionWrapper.vue +15 -0
- package/dist/spa/src/components/ColumnValueInput.vue +1 -1
- package/dist/spa/src/components/CustomRangePicker.vue +3 -16
- package/dist/spa/src/components/Filters.vue +129 -112
- package/dist/spa/src/components/ResourceForm.vue +2 -2
- package/dist/spa/src/components/ResourceListTable.vue +45 -13
- package/dist/spa/src/components/ResourceListTableVirtual.vue +52 -16
- package/dist/spa/src/components/ShowTable.vue +5 -4
- package/dist/spa/src/components/Sidebar.vue +27 -5
- package/dist/spa/src/components/ThreeDotsMenu.vue +27 -17
- package/dist/spa/src/components/Toast.vue +15 -22
- package/dist/spa/src/components/UserMenuSettingsButton.vue +9 -10
- package/dist/spa/src/i18n.ts +4 -2
- package/dist/spa/src/main.ts +1 -1
- package/dist/spa/src/stores/core.ts +12 -0
- package/dist/spa/src/stores/filters.ts +5 -1
- package/dist/spa/src/types/Back.ts +22 -1
- package/dist/spa/src/types/Common.ts +24 -0
- package/dist/spa/src/utils.ts +69 -2
- package/dist/spa/src/views/CreateView.vue +47 -4
- package/dist/spa/src/views/EditView.vue +30 -3
- package/dist/spa/src/views/ListView.vue +6 -2
- package/dist/spa/src/views/LoginView.vue +4 -13
- package/dist/spa/src/views/SettingsView.vue +1 -1
- package/dist/spa/src/views/ShowView.vue +27 -17
- package/dist/types/Back.d.ts +27 -0
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +47 -0
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js.map +1 -1
- package/package.json +2 -1
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<!-- Dropdown menu -->
|
|
13
13
|
<div
|
|
14
14
|
id="listThreeDotsDropdown"
|
|
15
|
-
class="z-
|
|
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-
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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
|
|
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-
|
|
8
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
21
|
-
|
|
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
|
-
<
|
|
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-
|
|
5
|
-
text-
|
|
6
|
-
dark:
|
|
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-
|
|
22
|
-
text-
|
|
23
|
-
dark:
|
|
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']);
|
package/dist/spa/src/i18n.ts
CHANGED
|
@@ -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
|
}
|
package/dist/spa/src/main.ts
CHANGED
|
@@ -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 {
|
package/dist/spa/src/utils.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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') {
|