adminforth 2.13.0-next.4 → 2.13.0-next.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/configValidator.d.ts.map +1 -1
- package/dist/modules/configValidator.js +10 -3
- package/dist/modules/configValidator.js.map +1 -1
- package/dist/spa/package-lock.json +755 -1412
- package/dist/spa/package.json +32 -32
- package/dist/spa/src/App.vue +14 -21
- package/dist/spa/src/afcl/Dialog.vue +65 -5
- package/dist/spa/src/afcl/Input.vue +5 -9
- package/dist/spa/src/afcl/Select.vue +2 -2
- package/dist/spa/src/afcl/Textarea.vue +6 -1
- package/dist/spa/src/components/ResourceListTable.vue +17 -9
- package/dist/spa/src/components/ResourceListTableVirtual.vue +18 -10
- package/dist/spa/src/components/Sidebar.vue +15 -3
- package/dist/spa/src/components/ThreeDotsMenu.vue +34 -6
- package/dist/spa/src/components/ValueRenderer.vue +1 -1
- package/dist/spa/src/stores/core.ts +7 -0
- package/dist/spa/src/types/Common.ts +1 -0
- package/dist/spa/src/utils.ts +18 -4
- package/dist/spa/src/views/ListView.vue +6 -0
- package/dist/spa/src/views/LoginView.vue +3 -0
- package/dist/spa/src/views/PageNotFound.vue +2 -2
- package/dist/spa/tsconfig.json +1 -1
- package/dist/types/Common.d.ts +1 -0
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js.map +1 -1
- package/package.json +1 -1
package/dist/spa/package.json
CHANGED
|
@@ -13,47 +13,47 @@
|
|
|
13
13
|
"i18n:extract": "echo {} > i18n-empty.json && vue-i18n-extract report --vueFiles \"./src/**/*.{js,vue,ts}\" --output ./i18n-messages.json --languageFiles \"i18n-empty.json\" --add"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@iconify-prerendered/vue-flag": "^0.28.
|
|
16
|
+
"@iconify-prerendered/vue-flag": "^0.28.1748584105",
|
|
17
17
|
"@iconify-prerendered/vue-flowbite": "^0.28.1754899090",
|
|
18
|
-
"@unhead/vue": "^1.
|
|
19
|
-
"@vueuse/core": "^10.
|
|
18
|
+
"@unhead/vue": "^1.9.12",
|
|
19
|
+
"@vueuse/core": "^10.10.0",
|
|
20
20
|
"apexcharts": "^4.7.0",
|
|
21
|
-
"dayjs": "^1.11.
|
|
22
|
-
"debounce": "^2.
|
|
23
|
-
"flowbite-datepicker": "^1.
|
|
24
|
-
"javascript-time-ago": "^2.5.
|
|
25
|
-
"pinia": "^2.
|
|
26
|
-
"sanitize-html": "^2.
|
|
27
|
-
"unhead": "^1.
|
|
21
|
+
"dayjs": "^1.11.11",
|
|
22
|
+
"debounce": "^2.1.0",
|
|
23
|
+
"flowbite-datepicker": "^1.2.6",
|
|
24
|
+
"javascript-time-ago": "^2.5.11",
|
|
25
|
+
"pinia": "^2.1.7",
|
|
26
|
+
"sanitize-html": "^2.13.0",
|
|
27
|
+
"unhead": "^1.9.12",
|
|
28
28
|
"uuid": "^10.0.0",
|
|
29
|
-
"vue": "^3.5.
|
|
29
|
+
"vue": "^3.5.12",
|
|
30
30
|
"vue-diff": "^1.2.4",
|
|
31
|
-
"vue-i18n": "^10.0.
|
|
32
|
-
"vue-router": "^4.
|
|
31
|
+
"vue-i18n": "^10.0.5",
|
|
32
|
+
"vue-router": "^4.3.0",
|
|
33
33
|
"vue-slider-component": "^4.1.0-beta.7"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@rushstack/eslint-patch": "^1.
|
|
37
|
-
"@tsconfig/node20": "^20.1.
|
|
38
|
-
"@types/node": "^20.
|
|
39
|
-
"@vitejs/plugin-vue": "^
|
|
36
|
+
"@rushstack/eslint-patch": "^1.8.0",
|
|
37
|
+
"@tsconfig/node20": "^20.1.4",
|
|
38
|
+
"@types/node": "^20.12.5",
|
|
39
|
+
"@vitejs/plugin-vue": "^5.0.4",
|
|
40
40
|
"@vue/eslint-config-typescript": "^13.0.0",
|
|
41
|
-
"@vue/tsconfig": "^0.
|
|
42
|
-
"autoprefixer": "^10.4.
|
|
43
|
-
"eslint": "^8.57.
|
|
44
|
-
"eslint-plugin-vue": "^9.
|
|
45
|
-
"flag-icons": "^7.
|
|
41
|
+
"@vue/tsconfig": "^0.5.1",
|
|
42
|
+
"autoprefixer": "^10.4.19",
|
|
43
|
+
"eslint": "^8.57.0",
|
|
44
|
+
"eslint-plugin-vue": "^9.23.0",
|
|
45
|
+
"flag-icons": "^7.2.3",
|
|
46
46
|
"flowbite": "^3.1.2",
|
|
47
|
-
"i18n-iso-countries": "^7.
|
|
48
|
-
"npm-run-all2": "^6.2
|
|
49
|
-
"portfinder": "^1.0.
|
|
50
|
-
"postcss": "^8.
|
|
51
|
-
"sass": "^1.
|
|
52
|
-
"tailwindcss": "^3.4.
|
|
53
|
-
"typescript": "~5.
|
|
54
|
-
"vite": "^
|
|
47
|
+
"i18n-iso-countries": "^7.12.0",
|
|
48
|
+
"npm-run-all2": "^6.1.2",
|
|
49
|
+
"portfinder": "^1.0.32",
|
|
50
|
+
"postcss": "^8.4.38",
|
|
51
|
+
"sass": "^1.77.2",
|
|
52
|
+
"tailwindcss": "^3.4.17",
|
|
53
|
+
"typescript": "~5.4.0",
|
|
54
|
+
"vite": "^5.2.13",
|
|
55
55
|
"vue-i18n-extract": "^2.0.7",
|
|
56
|
-
"vue-tsc": "^2.
|
|
57
|
-
"vue3-json-viewer": "^2.
|
|
56
|
+
"vue-tsc": "^2.0.11",
|
|
57
|
+
"vue3-json-viewer": "^2.2.2"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/dist/spa/src/App.vue
CHANGED
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
</nav>
|
|
74
74
|
|
|
75
75
|
<Sidebar
|
|
76
|
-
v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady && defaultLayout && !headerOnlyLayout"
|
|
76
|
+
v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady && defaultLayout && !headerOnlyLayout && coreStore.menu.length > 0"
|
|
77
77
|
:sideBarOpen="sideBarOpen"
|
|
78
78
|
:forceIconOnly="route.meta?.sidebarAndHeader === 'preferIconOnly'"
|
|
79
79
|
@hideSidebar="hideSidebar"
|
|
@@ -182,8 +182,6 @@ initFrontedAPI()
|
|
|
182
182
|
|
|
183
183
|
createHead()
|
|
184
184
|
const sideBarOpen = ref(false);
|
|
185
|
-
const defaultLayout = ref(true);
|
|
186
|
-
const headerOnlyLayout = ref(false);
|
|
187
185
|
const route = useRoute();
|
|
188
186
|
const router = useRouter();
|
|
189
187
|
const publicConfigLoaded = ref(false);
|
|
@@ -197,6 +195,14 @@ const isSidebarIconOnly = ref(localStorage.getItem('afIconOnlySidebar') === 'tru
|
|
|
197
195
|
|
|
198
196
|
const loggedIn = computed(() => !!coreStore?.adminUser);
|
|
199
197
|
|
|
198
|
+
const defaultLayout = computed(() => {
|
|
199
|
+
return route.meta?.sidebarAndHeader !== 'none';
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const headerOnlyLayout = computed(() => {
|
|
203
|
+
return route.meta?.sidebarAndHeader === 'headerOnly';
|
|
204
|
+
});
|
|
205
|
+
|
|
200
206
|
const expandedWidth = computed(() => coreStore.config?.iconOnlySidebar?.expandedSidebarWidth || '16.5rem');
|
|
201
207
|
|
|
202
208
|
const theme = ref('light');
|
|
@@ -308,19 +314,6 @@ async function loadMenu() {
|
|
|
308
314
|
loginRedirectCheckIsReady.value = true;
|
|
309
315
|
}
|
|
310
316
|
|
|
311
|
-
function handleCustomLayout() {
|
|
312
|
-
if (route.meta?.sidebarAndHeader === 'none') {
|
|
313
|
-
defaultLayout.value = false;
|
|
314
|
-
} else if (route.meta?.sidebarAndHeader === 'preferIconOnly') {
|
|
315
|
-
defaultLayout.value = true;
|
|
316
|
-
isSidebarIconOnly.value = true;
|
|
317
|
-
} else if (route.meta?.sidebarAndHeader === 'headerOnly') {
|
|
318
|
-
headerOnlyLayout.value = true;
|
|
319
|
-
} else {
|
|
320
|
-
defaultLayout.value = true;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
317
|
function humanizeSnake(str: string): string {
|
|
325
318
|
if (!str) {
|
|
326
319
|
return '';
|
|
@@ -345,10 +338,11 @@ watch(title, (title) => {
|
|
|
345
338
|
document.title = title;
|
|
346
339
|
})
|
|
347
340
|
|
|
348
|
-
watch(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
341
|
+
watch(route, () => {
|
|
342
|
+
// Handle preferIconOnly layout
|
|
343
|
+
if (route.meta?.sidebarAndHeader === 'preferIconOnly') {
|
|
344
|
+
isSidebarIconOnly.value = true;
|
|
345
|
+
}
|
|
352
346
|
});
|
|
353
347
|
|
|
354
348
|
|
|
@@ -376,7 +370,6 @@ onMounted(async () => {
|
|
|
376
370
|
loadPublicConfig(); // and this
|
|
377
371
|
// before init flowbite we have to wait router initialized because it affects dom(our v-ifs) and fetch menu
|
|
378
372
|
await initRouter();
|
|
379
|
-
handleCustomLayout();
|
|
380
373
|
|
|
381
374
|
adminforth.menu.refreshMenuBadges = async () => {
|
|
382
375
|
await coreStore.fetchMenuBadges();
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
v-if="headerCloseButton"
|
|
23
23
|
type="button"
|
|
24
24
|
class="text-lightDialogCloseButton bg-transparent hover:bg-lightDialogCloseButtonHoverBackground hover:text-lightDialogCloseButtonHover rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:text-darkDialogCloseButton dark:hover:bg-darkDialogCloseButtonHoverBackground dark:hover:text-darkDialogCloseButtonHover"
|
|
25
|
-
@click="
|
|
25
|
+
@click="tryToHideModal"
|
|
26
26
|
>
|
|
27
27
|
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
|
28
28
|
<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"/>
|
|
@@ -51,13 +51,41 @@
|
|
|
51
51
|
</div>
|
|
52
52
|
</div>
|
|
53
53
|
</div>
|
|
54
|
+
<div>
|
|
55
|
+
<!-- Confirmation Modal -->
|
|
56
|
+
<div
|
|
57
|
+
v-if="showConfirmationOnClose"
|
|
58
|
+
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-60"
|
|
59
|
+
>
|
|
60
|
+
<div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-lg max-w-sm w-full">
|
|
61
|
+
<h2 class="text-lg font-semibold mb-4 text-lightDialogHeaderText dark:text-darkDialogHeaderText">Confirm Close</h2>
|
|
62
|
+
<p class="mb-6 text-lightDialogBodyText dark:text-darkDialogBodyText">{{ props.closeConfirmationText }}</p>
|
|
63
|
+
<div class="flex justify-end">
|
|
64
|
+
<Button
|
|
65
|
+
class="me-3"
|
|
66
|
+
@click="showConfirmationOnClose = false"
|
|
67
|
+
>
|
|
68
|
+
Cancel
|
|
69
|
+
</Button>
|
|
70
|
+
<Button
|
|
71
|
+
@click="
|
|
72
|
+
showConfirmationOnClose = false;
|
|
73
|
+
modal?.hide();
|
|
74
|
+
"
|
|
75
|
+
>
|
|
76
|
+
Confirm
|
|
77
|
+
</Button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
54
82
|
</div>
|
|
55
83
|
</Teleport>
|
|
56
84
|
</template>
|
|
57
85
|
|
|
58
86
|
<script setup lang="ts">
|
|
59
87
|
import Button from "./Button.vue";
|
|
60
|
-
import { ref, onMounted, nextTick, onUnmounted, type Ref } from 'vue';
|
|
88
|
+
import { ref, onMounted, nextTick, onUnmounted, computed, type Ref } from 'vue';
|
|
61
89
|
import { Modal } from 'flowbite';
|
|
62
90
|
|
|
63
91
|
const modalEl = ref(null);
|
|
@@ -77,20 +105,42 @@ interface DialogProps {
|
|
|
77
105
|
beforeCloseFunction?: (() => void | Promise<void>) | null
|
|
78
106
|
beforeOpenFunction?: (() => void | Promise<void>) | null
|
|
79
107
|
closable?: boolean
|
|
108
|
+
askForCloseConfirmation?: boolean
|
|
109
|
+
closeConfirmationText?: string
|
|
80
110
|
}
|
|
81
111
|
|
|
82
112
|
const props = withDefaults(defineProps<DialogProps>(), {
|
|
83
113
|
header: '',
|
|
84
114
|
headerCloseButton: true,
|
|
85
|
-
buttons: () => [
|
|
86
|
-
{ label: 'Close', onclick: (dialog: any) => dialog.hide(), type: '' },
|
|
87
|
-
],
|
|
115
|
+
buttons: () => [],
|
|
88
116
|
clickToCloseOutside: true,
|
|
89
117
|
beforeCloseFunction: null,
|
|
90
118
|
beforeOpenFunction: null,
|
|
91
119
|
closable: true,
|
|
120
|
+
askForCloseConfirmation: false,
|
|
121
|
+
closeConfirmationText: 'Are you sure you want to close this dialog?',
|
|
92
122
|
})
|
|
93
123
|
|
|
124
|
+
const buttons = computed<DialogButton[]>(() => {
|
|
125
|
+
if (props.buttons && props.buttons.length > 0) {
|
|
126
|
+
return props.buttons;
|
|
127
|
+
}
|
|
128
|
+
return [
|
|
129
|
+
{
|
|
130
|
+
label: 'Close',
|
|
131
|
+
onclick: (dialog: any) => {
|
|
132
|
+
if (!props.askForCloseConfirmation) {
|
|
133
|
+
dialog.hide();
|
|
134
|
+
} else {
|
|
135
|
+
showConfirmationOnClose.value = true;
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
options: {}
|
|
139
|
+
}
|
|
140
|
+
];
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const showConfirmationOnClose = ref(false);
|
|
94
144
|
onMounted(async () => {
|
|
95
145
|
//await one tick when all is mounted
|
|
96
146
|
await nextTick();
|
|
@@ -129,6 +179,16 @@ function close() {
|
|
|
129
179
|
defineExpose({
|
|
130
180
|
open: open,
|
|
131
181
|
close: close,
|
|
182
|
+
tryToHideModal: tryToHideModal,
|
|
132
183
|
})
|
|
133
184
|
|
|
185
|
+
function tryToHideModal() {
|
|
186
|
+
if (!props.askForCloseConfirmation ) {
|
|
187
|
+
modal.value?.hide();
|
|
188
|
+
} else {
|
|
189
|
+
showConfirmationOnClose.value = true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
134
194
|
</script>
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
aria-describedby="helper-text-explanation"
|
|
18
18
|
class="afcl-input inline-flex bg-lightInputBackground border border-lightInputBorder rounded-0 focus:ring-lightPrimary focus:border-lightPrimary dark:focus:ring-darkPrimary dark:focus:border-darkPrimary
|
|
19
19
|
blue-500 focus:border-blue-500 block w-20 p-2.5 dark:bg-darkInputBackground dark:border-darkInputBorder placeholder-lightInputPlaceholderText dark:placeholder-darkInputPlaceholderText dark:text-darkInputText translate-y-0"
|
|
20
|
-
:class="{'rounded-l-md': !$slots.prefix && !prefix, 'rounded-r-md': !$slots.suffix && !suffix, 'w-full': fullWidth, 'text-base':
|
|
20
|
+
:class="{'rounded-l-md': !$slots.prefix && !prefix, 'rounded-r-md': !$slots.suffix && !suffix, 'w-full': fullWidth, 'text-base': isIos, 'text-sm': !isIos }"
|
|
21
21
|
:disabled="readonly"
|
|
22
22
|
>
|
|
23
23
|
|
|
@@ -36,7 +36,10 @@
|
|
|
36
36
|
<script setup lang="ts">
|
|
37
37
|
|
|
38
38
|
import { ref } from 'vue';
|
|
39
|
-
|
|
39
|
+
import { useCoreStore } from '@/stores/core';
|
|
40
|
+
|
|
41
|
+
const coreStore = useCoreStore();
|
|
42
|
+
const isIos = coreStore.isIos;
|
|
40
43
|
|
|
41
44
|
const props = defineProps<{
|
|
42
45
|
type: string,
|
|
@@ -53,12 +56,5 @@ defineExpose({
|
|
|
53
56
|
focus: () => input.value?.focus(),
|
|
54
57
|
});
|
|
55
58
|
|
|
56
|
-
function isIOS() {
|
|
57
|
-
return (
|
|
58
|
-
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
|
59
|
-
(navigator.userAgent.includes('Mac') && 'ontouchend' in document)
|
|
60
|
-
)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
59
|
</script>
|
|
64
60
|
|
|
@@ -265,11 +265,11 @@ onMounted(() => {
|
|
|
265
265
|
|
|
266
266
|
watch(() => props.modelValue, (value) => {
|
|
267
267
|
updateFromProps();
|
|
268
|
-
});
|
|
268
|
+
}, {deep: true});
|
|
269
269
|
|
|
270
270
|
watch(() => props.options, () => {
|
|
271
271
|
updateFromProps();
|
|
272
|
-
});
|
|
272
|
+
}, { deep: true });
|
|
273
273
|
|
|
274
274
|
addClickListener();
|
|
275
275
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<textarea
|
|
3
3
|
ref="input"
|
|
4
|
-
class="bg-lightInputBackground border border-lightInputBorder text-lightInputText placeholder-lightInputPlaceholderText text-sm rounded-
|
|
4
|
+
class="afcl-textarea bg-lightInputBackground border border-lightInputBorder text-lightInputText placeholder-lightInputPlaceholderText text-sm rounded-md block w-full p-2.5 dark:bg-darkInputBackground dark:border-darkInputBorder dark:placeholder-darkInputPlaceholderText dark:text-darkInputText dark:border-darkInputBorder focus:ring-lightInputFocusRing focus:border-lightInputFocusBorder dark:focus:ring-darkInputFocusRing dark:focus:border-darkInputFocusBorder"
|
|
5
|
+
:class="`${readonly ? 'opacity-50' : ''} ${isIos ? 'text-md' : 'text-sm'}`"
|
|
5
6
|
:placeholder="placeholder"
|
|
6
7
|
:value="modelValue"
|
|
7
8
|
@input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
|
|
@@ -11,6 +12,10 @@
|
|
|
11
12
|
|
|
12
13
|
<script setup lang="ts">
|
|
13
14
|
import { ref } from 'vue';
|
|
15
|
+
import { useCoreStore } from '@/stores/core';
|
|
16
|
+
|
|
17
|
+
const coreStore = useCoreStore();
|
|
18
|
+
const isIos = coreStore.isIos;
|
|
14
19
|
|
|
15
20
|
const props = defineProps<{
|
|
16
21
|
modelValue: string
|
|
@@ -83,13 +83,20 @@
|
|
|
83
83
|
</td>
|
|
84
84
|
</tr>
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
<component
|
|
87
|
+
v-else
|
|
88
|
+
v-for="(row, rowI) in rows"
|
|
89
|
+
:is="tableRowReplaceInjection ? getCustomComponent(tableRowReplaceInjection) : 'tr'"
|
|
90
|
+
:key="`row_${row._primaryKeyValue}`"
|
|
91
|
+
:record="row"
|
|
92
|
+
:resource="resource"
|
|
93
|
+
:adminUser="coreStore.adminUser"
|
|
94
|
+
:meta="tableRowReplaceInjection ? tableRowReplaceInjection.meta : undefined"
|
|
95
|
+
@click="onClick($event, row)"
|
|
96
|
+
ref="rowRefs"
|
|
97
|
+
class="list-table-body-row bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
|
|
98
|
+
:class="{'border-b': rowI !== rows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
|
|
99
|
+
>
|
|
93
100
|
<td class="w-4 p-4 cursor-default sticky-column bg-lightListTable dark:bg-darkListTable" @click="(e)=>e.stopPropagation()">
|
|
94
101
|
<Checkbox
|
|
95
102
|
:model-value="checkboxesInternal.includes(row._primaryKeyValue)"
|
|
@@ -210,7 +217,7 @@
|
|
|
210
217
|
</div>
|
|
211
218
|
|
|
212
219
|
</td>
|
|
213
|
-
|
|
220
|
+
</component>
|
|
214
221
|
</tbody>
|
|
215
222
|
</table>
|
|
216
223
|
</div>
|
|
@@ -328,7 +335,7 @@ import {
|
|
|
328
335
|
} from '@iconify-prerendered/vue-flowbite';
|
|
329
336
|
import router from '@/router';
|
|
330
337
|
import { Tooltip } from '@/afcl';
|
|
331
|
-
import type { AdminForthResourceCommon, AdminForthResourceColumnInputCommon, AdminForthResourceColumnCommon } from '@/types/Common';
|
|
338
|
+
import type { AdminForthResourceCommon, AdminForthResourceColumnInputCommon, AdminForthResourceColumnCommon, AdminForthComponentDeclaration } from '@/types/Common';
|
|
332
339
|
import adminforth from '@/adminforth';
|
|
333
340
|
import Checkbox from '@/afcl/Checkbox.vue';
|
|
334
341
|
|
|
@@ -345,6 +352,7 @@ const props = defineProps<{
|
|
|
345
352
|
noRoundings?: boolean,
|
|
346
353
|
customActionsInjection?: any[],
|
|
347
354
|
tableBodyStartInjection?: any[],
|
|
355
|
+
tableRowReplaceInjection?: AdminForthComponentDeclaration,
|
|
348
356
|
}>();
|
|
349
357
|
|
|
350
358
|
// emits, update page
|
|
@@ -93,14 +93,21 @@
|
|
|
93
93
|
</tr>
|
|
94
94
|
|
|
95
95
|
<!-- Visible rows -->
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
<component
|
|
97
|
+
v-else
|
|
98
|
+
v-for="(row, rowI) in visibleRows"
|
|
99
|
+
:is="tableRowReplaceInjection ? getCustomComponent(tableRowReplaceInjection) : 'tr'"
|
|
100
|
+
:key="`row_${row._primaryKeyValue}`"
|
|
101
|
+
:record="row"
|
|
102
|
+
:resource="resource"
|
|
103
|
+
:adminUser="coreStore.adminUser"
|
|
104
|
+
:meta="tableRowReplaceInjection ? tableRowReplaceInjection.meta : undefined"
|
|
105
|
+
@click="onClick($event, row)"
|
|
106
|
+
ref="rowRefs"
|
|
107
|
+
class="list-table-body-row bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
|
|
108
|
+
:class="{'border-b': rowI !== visibleRows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
|
|
109
|
+
@mounted="(el: any) => updateRowHeight(`row_${row._primaryKeyValue}`, el.offsetHeight)"
|
|
110
|
+
>
|
|
104
111
|
<td class="w-4 p-4 cursor-default sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading" @click="(e)=>e.stopPropagation()">
|
|
105
112
|
<Checkbox
|
|
106
113
|
:model-value="checkboxesInternal.includes(row._primaryKeyValue)"
|
|
@@ -224,7 +231,7 @@
|
|
|
224
231
|
</template>
|
|
225
232
|
</div>
|
|
226
233
|
</td>
|
|
227
|
-
</
|
|
234
|
+
</component>
|
|
228
235
|
|
|
229
236
|
<!-- Bottom spacer -->
|
|
230
237
|
<tr v-if="totalHeight > 0">
|
|
@@ -350,7 +357,7 @@ import {
|
|
|
350
357
|
} from '@iconify-prerendered/vue-flowbite';
|
|
351
358
|
import router from '@/router';
|
|
352
359
|
import { Tooltip } from '@/afcl';
|
|
353
|
-
import type { AdminForthResourceCommon, AdminForthResourceColumnCommon } from '@/types/Common';
|
|
360
|
+
import type { AdminForthResourceCommon, AdminForthResourceColumnCommon, AdminForthComponentDeclaration } from '@/types/Common';
|
|
354
361
|
import adminforth from '@/adminforth';
|
|
355
362
|
import Checkbox from '@/afcl/Checkbox.vue';
|
|
356
363
|
|
|
@@ -370,6 +377,7 @@ const props = defineProps<{
|
|
|
370
377
|
containerHeight?: number,
|
|
371
378
|
itemHeight?: number,
|
|
372
379
|
bufferSize?: number,
|
|
380
|
+
tableRowReplaceInjection?: AdminForthComponentDeclaration
|
|
373
381
|
}>();
|
|
374
382
|
|
|
375
383
|
// emits, update page
|
|
@@ -14,9 +14,21 @@
|
|
|
14
14
|
aria-label="Sidebar"
|
|
15
15
|
>
|
|
16
16
|
<div class="h-full px-3 pb-20 md:pb-4 bg-lightSidebar dark:bg-darkSidebar border-r border-lightSidebarBorder dark:border-darkSidebarBorder pt-4" :class="{'sidebar-scroll':!isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering)}">
|
|
17
|
-
<div
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
<div
|
|
18
|
+
class="af-logo-title-wrapper flex relative transition-all duration-300 ease-in-out h-8 items-center"
|
|
19
|
+
:class="{
|
|
20
|
+
'mb-4': isSidebarIconOnly && !isSidebarHovering, 'mx-4 mb-4': !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering),
|
|
21
|
+
'justify-center': !(coreStore.config?.showBrandLogoInSidebar !== false && (!iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering)))
|
|
22
|
+
}"
|
|
23
|
+
>
|
|
24
|
+
<img
|
|
25
|
+
:src="loadFile(coreStore.config?.brandLogo || '@/assets/logo.svg')"
|
|
26
|
+
:alt="`${ coreStore.config?.brandName } Logo`"
|
|
27
|
+
class="af-logo h-8 me-3"
|
|
28
|
+
:class="{
|
|
29
|
+
'hidden': !(coreStore.config?.showBrandLogoInSidebar !== false && (!iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))) }"
|
|
30
|
+
/>
|
|
31
|
+
<img :src="loadFile(coreStore.config?.iconOnlySidebar?.logo || '')" :alt="`${ coreStore.config?.brandName } Logo`" class="af-sidebar-icon-only-logo h-8" :class="{ 'hidden': !(coreStore.config?.showBrandLogoInSidebar !== false && coreStore.config?.iconOnlySidebar?.logo && iconOnlySidebarEnabled && isSidebarIconOnly && !isSidebarHovering) }" />
|
|
20
32
|
<span
|
|
21
33
|
v-if="coreStore.config?.showBrandNameInSidebar && (!iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))"
|
|
22
34
|
class="af-title self-center text-lightNavbarText-size font-semibold sm:text-lightNavbarText-size whitespace-nowrap dark:text-darkSidebarText text-lightSidebarText"
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<template >
|
|
2
|
-
<
|
|
2
|
+
<div class="relative" v-if="threeDotsDropdownItems?.length || customActions?.length || (bulkActions?.some((action: AdminForthBulkActionCommon) => action.showInThreeDotsDropdown))">
|
|
3
3
|
<button
|
|
4
|
-
|
|
4
|
+
ref="buttonTriggerRef"
|
|
5
|
+
@click="toggleDropdownVisibility"
|
|
5
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
7
|
>
|
|
7
8
|
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 4 15">
|
|
@@ -11,8 +12,9 @@
|
|
|
11
12
|
|
|
12
13
|
<!-- Dropdown menu -->
|
|
13
14
|
<div
|
|
14
|
-
|
|
15
|
-
class="
|
|
15
|
+
ref="dropdownRef"
|
|
16
|
+
:class="{'hidden': !showDropdown, 'block': showDropdown }"
|
|
17
|
+
class="absolute z-30 right-0 mt-3 bg-lightThreeDotsMenuBodyBackground divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-darkThreeDotsMenuBodyBackground dark:divide-gray-600">
|
|
16
18
|
<ul class="py-2 text-sm text-lightThreeDotsMenuBodyText dark:text-darkThreeDotsMenuBodyText" aria-labelledby="dropdownMenuIconButton">
|
|
17
19
|
<li v-for="(item, i) in threeDotsDropdownItems" :key="`dropdown-item-${i}`">
|
|
18
20
|
<a href="#"
|
|
@@ -71,7 +73,7 @@
|
|
|
71
73
|
</li>
|
|
72
74
|
</ul>
|
|
73
75
|
</div>
|
|
74
|
-
</
|
|
76
|
+
</div>
|
|
75
77
|
</template>
|
|
76
78
|
|
|
77
79
|
|
|
@@ -82,7 +84,7 @@ import adminforth from '@/adminforth';
|
|
|
82
84
|
import { callAdminForthApi } from '@/utils';
|
|
83
85
|
import { useRoute, useRouter } from 'vue-router';
|
|
84
86
|
import CallActionWrapper from '@/components/CallActionWrapper.vue'
|
|
85
|
-
import { ref, type ComponentPublicInstance } from 'vue';
|
|
87
|
+
import { ref, type ComponentPublicInstance, onMounted, onUnmounted } from 'vue';
|
|
86
88
|
import type { AdminForthBulkActionCommon, AdminForthComponentDeclarationFull } from '@/types/Common';
|
|
87
89
|
import type { AdminForthActionInput } from '@/types/Back';
|
|
88
90
|
|
|
@@ -91,6 +93,9 @@ const route = useRoute();
|
|
|
91
93
|
const coreStore = useCoreStore();
|
|
92
94
|
const router = useRouter();
|
|
93
95
|
const threeDotsDropdownItemsRefs = ref<Array<ComponentPublicInstance | null>>([]);
|
|
96
|
+
const showDropdown = ref(false);
|
|
97
|
+
const dropdownRef = ref<HTMLElement | null>(null);
|
|
98
|
+
const buttonTriggerRef = ref<HTMLElement | null>(null);
|
|
94
99
|
|
|
95
100
|
const props = defineProps({
|
|
96
101
|
threeDotsDropdownItems: Array<AdminForthComponentDeclarationFull>,
|
|
@@ -169,6 +174,7 @@ async function handleActionClick(action: AdminForthActionInput, payload: any) {
|
|
|
169
174
|
function startBulkAction(actionId: string) {
|
|
170
175
|
adminforth.list.closeThreeDotsDropdown();
|
|
171
176
|
emit('startBulkAction', actionId);
|
|
177
|
+
showDropdown.value = false;
|
|
172
178
|
}
|
|
173
179
|
|
|
174
180
|
async function injectedComponentClick(index: number) {
|
|
@@ -176,5 +182,27 @@ async function injectedComponentClick(index: number) {
|
|
|
176
182
|
if (componentRef && 'click' in componentRef) {
|
|
177
183
|
(componentRef as any).click?.();
|
|
178
184
|
}
|
|
185
|
+
showDropdown.value = false;
|
|
179
186
|
}
|
|
187
|
+
|
|
188
|
+
function toggleDropdownVisibility() {
|
|
189
|
+
showDropdown.value = !showDropdown.value;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function handleClickOutside(e: MouseEvent) {
|
|
193
|
+
if (!dropdownRef.value) return
|
|
194
|
+
|
|
195
|
+
if (!dropdownRef.value.contains(e.target as Node) && !buttonTriggerRef.value?.contains(e.target as Node)) {
|
|
196
|
+
showDropdown.value = false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
onMounted(() => {
|
|
201
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
onUnmounted(() => {
|
|
205
|
+
document.removeEventListener('mousedown', handleClickOutside)
|
|
206
|
+
})
|
|
207
|
+
|
|
180
208
|
</script>
|
|
@@ -117,7 +117,7 @@ import timezone from 'dayjs/plugin/timezone';
|
|
|
117
117
|
import {checkEmptyValues} from '@/utils';
|
|
118
118
|
import { useRoute, useRouter } from 'vue-router';
|
|
119
119
|
import { JsonViewer } from "vue3-json-viewer";
|
|
120
|
-
import "vue3-json-viewer/dist/
|
|
120
|
+
import "vue3-json-viewer/dist/index.css";
|
|
121
121
|
import type { AdminForthResourceColumnCommon } from '@/types/Common';
|
|
122
122
|
|
|
123
123
|
import { useCoreStore } from '@/stores/core';
|
|
@@ -221,6 +221,12 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
221
221
|
return userData.value && userFullnameField && userData.value[userFullnameField];
|
|
222
222
|
})
|
|
223
223
|
|
|
224
|
+
const isIos = computed(() => {
|
|
225
|
+
return (
|
|
226
|
+
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
|
227
|
+
(navigator.userAgent.includes('Mac') && 'ontouchend' in document)
|
|
228
|
+
)});
|
|
229
|
+
|
|
224
230
|
|
|
225
231
|
return {
|
|
226
232
|
config,
|
|
@@ -245,5 +251,6 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
245
251
|
resetAdminUser,
|
|
246
252
|
resetResource,
|
|
247
253
|
isResourceFetching,
|
|
254
|
+
isIos
|
|
248
255
|
}
|
|
249
256
|
})
|
|
@@ -504,6 +504,7 @@ export interface AdminForthResourceInputCommon {
|
|
|
504
504
|
threeDotsDropdownItems?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
|
|
505
505
|
customActionIcons?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
|
|
506
506
|
tableBodyStart?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
|
|
507
|
+
tableRowReplace?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
|
|
507
508
|
},
|
|
508
509
|
|
|
509
510
|
/**
|
package/dist/spa/src/utils.ts
CHANGED
|
@@ -36,7 +36,20 @@ export async function callApi({path, method, body, headers}: {
|
|
|
36
36
|
if (r.status == 401 ) {
|
|
37
37
|
useUserStore().unauthorize();
|
|
38
38
|
useCoreStore().resetAdminUser();
|
|
39
|
-
|
|
39
|
+
const currentPath = router.currentRoute.value.path;
|
|
40
|
+
const homeRoute = router.getRoutes().find(route => route.name === 'home');
|
|
41
|
+
const homePagePath = (homeRoute?.redirect as string) || '/';
|
|
42
|
+
let next = '';
|
|
43
|
+
if (currentPath !== '/login' && currentPath !== homePagePath) {
|
|
44
|
+
if (Object.keys(router.currentRoute.value.query).length > 0) {
|
|
45
|
+
next = currentPath + '?' + Object.entries(router.currentRoute.value.query).map(([key, value]) => `${key}=${value}`).join('&');
|
|
46
|
+
} else {
|
|
47
|
+
next = currentPath;
|
|
48
|
+
}
|
|
49
|
+
await router.push({ name: 'login', query: { next: next } });
|
|
50
|
+
} else {
|
|
51
|
+
await router.push({ name: 'login' });
|
|
52
|
+
}
|
|
40
53
|
return null;
|
|
41
54
|
}
|
|
42
55
|
return await r.json();
|
|
@@ -90,16 +103,17 @@ export const loadFile = (file: string) => {
|
|
|
90
103
|
let baseUrl = '';
|
|
91
104
|
if (file.startsWith('@/')) {
|
|
92
105
|
path = file.replace('@/', '');
|
|
93
|
-
baseUrl = new URL(`./${path}`, import.meta.url
|
|
106
|
+
baseUrl = new URL(`./${path}`, import.meta.url).href;
|
|
94
107
|
} else if (file.startsWith('@@/')) {
|
|
95
108
|
path = file.replace('@@/', '');
|
|
96
|
-
baseUrl = new URL(`./custom/${path}`, import.meta.url
|
|
109
|
+
baseUrl = new URL(`./custom/${path}`, import.meta.url).href;
|
|
97
110
|
} else {
|
|
98
|
-
baseUrl = new URL(`./${file}`, import.meta.url
|
|
111
|
+
baseUrl = new URL(`./${file}`, import.meta.url).href;
|
|
99
112
|
}
|
|
100
113
|
return baseUrl;
|
|
101
114
|
}
|
|
102
115
|
|
|
116
|
+
|
|
103
117
|
export function checkEmptyValues(value: any, viewType: 'show' | 'list' ) {
|
|
104
118
|
const config: CoreConfig | {} | null = useCoreStore().config;
|
|
105
119
|
let emptyFieldPlaceholder = '';
|