adminforth 2.8.1 → 2.9.0
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/auth.d.ts +7 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +5 -0
- package/dist/auth.js.map +1 -1
- package/dist/modules/codeInjector.d.ts.map +1 -1
- package/dist/modules/codeInjector.js +18 -2
- package/dist/modules/codeInjector.js.map +1 -1
- package/dist/modules/configValidator.d.ts.map +1 -1
- package/dist/modules/configValidator.js +21 -5
- package/dist/modules/configValidator.js.map +1 -1
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +2 -0
- package/dist/modules/restApi.js.map +1 -1
- package/dist/modules/styles.d.ts +28 -0
- package/dist/modules/styles.d.ts.map +1 -1
- package/dist/modules/styles.js +28 -0
- package/dist/modules/styles.js.map +1 -1
- package/dist/modules/utils.d.ts +1 -0
- package/dist/modules/utils.d.ts.map +1 -1
- package/dist/modules/utils.js +7 -0
- package/dist/modules/utils.js.map +1 -1
- package/dist/spa/package-lock.json +5 -4
- package/dist/spa/package.json +1 -1
- package/dist/spa/src/App.vue +43 -176
- package/dist/spa/src/adminforth.ts +14 -10
- package/dist/spa/src/afcl/ButtonGroup.vue +91 -0
- package/dist/spa/src/afcl/Card.vue +25 -0
- package/dist/spa/src/afcl/LinkButton.vue +2 -2
- package/dist/spa/src/afcl/Table.vue +19 -10
- package/dist/spa/src/afcl/VerticalTabs.vue +15 -6
- package/dist/spa/src/afcl/index.ts +2 -0
- package/dist/spa/src/components/Filters.vue +2 -2
- package/dist/spa/src/components/MenuLink.vue +90 -23
- package/dist/spa/src/components/Sidebar.vue +443 -0
- package/dist/spa/src/components/UserMenuSettingsButton.vue +68 -0
- package/dist/spa/src/renderers/CompactField.vue +1 -1
- package/dist/spa/src/renderers/CompactUUID.vue +1 -1
- package/dist/spa/src/router/index.ts +9 -0
- package/dist/spa/src/spa_types/core.ts +5 -0
- package/dist/spa/src/stores/filters.ts +29 -2
- package/dist/spa/src/types/Back.ts +29 -0
- package/dist/spa/src/types/Common.ts +23 -2
- package/dist/spa/src/types/FrontendAPI.ts +10 -0
- package/dist/spa/src/types/adapters/CaptchaAdapter.ts +34 -0
- package/dist/spa/src/types/adapters/KeyValueAdapter.ts +16 -0
- package/dist/spa/src/types/adapters/index.ts +2 -0
- package/dist/spa/src/utils.ts +1 -0
- package/dist/spa/src/views/ListView.vue +15 -7
- package/dist/spa/src/views/LoginView.vue +7 -2
- package/dist/spa/src/views/SettingsView.vue +121 -0
- package/dist/types/Back.d.ts +38 -0
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +21 -1
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js.map +1 -1
- package/dist/types/FrontendAPI.d.ts +10 -0
- package/dist/types/FrontendAPI.d.ts.map +1 -1
- package/dist/types/FrontendAPI.js.map +1 -1
- package/dist/types/adapters/CaptchaAdapter.d.ts +30 -0
- package/dist/types/adapters/CaptchaAdapter.d.ts.map +1 -0
- package/dist/types/adapters/CaptchaAdapter.js +5 -0
- package/dist/types/adapters/CaptchaAdapter.js.map +1 -0
- package/dist/types/adapters/KeyValueAdapter.d.ts +10 -0
- package/dist/types/adapters/KeyValueAdapter.d.ts.map +1 -0
- package/dist/types/adapters/KeyValueAdapter.js +2 -0
- package/dist/types/adapters/KeyValueAdapter.js.map +1 -0
- package/dist/types/adapters/index.d.ts +2 -0
- package/dist/types/adapters/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -62,6 +62,15 @@ const router = createRouter({
|
|
|
62
62
|
},
|
|
63
63
|
]
|
|
64
64
|
},
|
|
65
|
+
{
|
|
66
|
+
path: '/settings/:page?',
|
|
67
|
+
name: 'settings',
|
|
68
|
+
component: () => import('@/views/SettingsView.vue'),
|
|
69
|
+
meta: {
|
|
70
|
+
title: 'Settings',
|
|
71
|
+
sidebarAndHeader: 'preferIconOnly',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
65
74
|
/* IMPORTANT:ADMINFORTH ROUTES */
|
|
66
75
|
{ path: "/:pathMatch(.*)*", component: PageNotFound },
|
|
67
76
|
]
|
|
@@ -23,6 +23,10 @@ export type CoreConfig = {
|
|
|
23
23
|
brandName: string,
|
|
24
24
|
singleTheme?: 'light' | 'dark',
|
|
25
25
|
brandLogo: string,
|
|
26
|
+
iconOnlySidebar: {
|
|
27
|
+
logo?: string,
|
|
28
|
+
enabled?: boolean,
|
|
29
|
+
},
|
|
26
30
|
title: string,
|
|
27
31
|
datesFormat: string,
|
|
28
32
|
timeFormat: string,
|
|
@@ -45,6 +49,7 @@ export type CoreConfig = {
|
|
|
45
49
|
customHeadItems?: {
|
|
46
50
|
tagName: string;
|
|
47
51
|
attributes: { [key: string]: string | boolean };
|
|
52
|
+
innerCode?: string;
|
|
48
53
|
}[],
|
|
49
54
|
}
|
|
50
55
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { ref, type Ref } from 'vue';
|
|
1
|
+
import { ref, computed, type Ref } from 'vue';
|
|
2
2
|
import { defineStore } from 'pinia';
|
|
3
|
+
import { useCoreStore } from './core';
|
|
3
4
|
|
|
4
5
|
export const useFiltersStore = defineStore('filters', () => {
|
|
5
6
|
const filters: Ref<any[]> = ref([]);
|
|
6
7
|
const sort: Ref<any> = ref({});
|
|
8
|
+
const coreStore = useCoreStore();
|
|
7
9
|
|
|
8
10
|
const setSort = (s: any) => {
|
|
9
11
|
sort.value = s;
|
|
@@ -23,5 +25,30 @@ export const useFiltersStore = defineStore('filters', () => {
|
|
|
23
25
|
const clearFilters = () => {
|
|
24
26
|
filters.value = [];
|
|
25
27
|
}
|
|
26
|
-
|
|
28
|
+
|
|
29
|
+
const shouldFilterBeHidden = (fieldName: string) => {
|
|
30
|
+
if (coreStore.resource?.columns) {
|
|
31
|
+
const column = coreStore.resource.columns.find((col: any) => col.name === fieldName);
|
|
32
|
+
if (column?.showIn?.filter !== true) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const visibleFiltersCount = computed(() => {
|
|
40
|
+
return filters.value.filter(f => !shouldFilterBeHidden(f.field)).length;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
setFilter,
|
|
45
|
+
getFilters,
|
|
46
|
+
clearFilters,
|
|
47
|
+
filters,
|
|
48
|
+
setFilters,
|
|
49
|
+
setSort,
|
|
50
|
+
getSort,
|
|
51
|
+
visibleFiltersCount,
|
|
52
|
+
shouldFilterBeHidden
|
|
53
|
+
}
|
|
27
54
|
})
|
|
@@ -306,6 +306,10 @@ export interface IAdminForthAuth {
|
|
|
306
306
|
|
|
307
307
|
removeCustomCookie({response, name}: {response: any, name: string}): void;
|
|
308
308
|
|
|
309
|
+
setCustomCookie({response, payload}: {response: any, payload: {name: string, value: string, expiry: number, httpOnly: boolean}}): void;
|
|
310
|
+
|
|
311
|
+
getCustomCookie({cookies, name}: {cookies: {key: string, value: string}[], name: string}): string | null;
|
|
312
|
+
|
|
309
313
|
setAuthCookie({expireInDays, response, username, pk,}: {expireInDays?: number, response: any, username: string, pk: string}): void;
|
|
310
314
|
|
|
311
315
|
removeAuthCookie(response: any): void;
|
|
@@ -668,6 +672,19 @@ interface AdminForthInputConfigCustomization {
|
|
|
668
672
|
*/
|
|
669
673
|
brandLogo?: string,
|
|
670
674
|
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Path to your app logo for icon only sidebar
|
|
678
|
+
*
|
|
679
|
+
* Example:
|
|
680
|
+
* Place file `logo.svg` to `./custom` folder and set this option:
|
|
681
|
+
*
|
|
682
|
+
*/
|
|
683
|
+
iconOnlySidebar?: {
|
|
684
|
+
logo?: string,
|
|
685
|
+
enabled?: boolean,
|
|
686
|
+
},
|
|
687
|
+
|
|
671
688
|
/**
|
|
672
689
|
* Path to your app favicon
|
|
673
690
|
*
|
|
@@ -798,6 +815,7 @@ interface AdminForthInputConfigCustomization {
|
|
|
798
815
|
customHeadItems?: {
|
|
799
816
|
tagName: string;
|
|
800
817
|
attributes: Record<string, string | boolean>;
|
|
818
|
+
innerCode?: string;
|
|
801
819
|
}[];
|
|
802
820
|
|
|
803
821
|
}
|
|
@@ -1036,6 +1054,16 @@ export interface AdminForthInputConfig {
|
|
|
1036
1054
|
* If you are using Cloudflare, set this to 'CF-Connecting-IP'. Case-insensitive.
|
|
1037
1055
|
*/
|
|
1038
1056
|
clientIpHeader?: string,
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Add custom page to the settings page
|
|
1060
|
+
*/
|
|
1061
|
+
userMenuSettingsPages: {
|
|
1062
|
+
icon?: string,
|
|
1063
|
+
pageLabel: string,
|
|
1064
|
+
slug?: string,
|
|
1065
|
+
component: string
|
|
1066
|
+
}[],
|
|
1039
1067
|
},
|
|
1040
1068
|
|
|
1041
1069
|
/**
|
|
@@ -1117,6 +1145,7 @@ export interface AdminForthConfigCustomization extends Omit<AdminForthInputConfi
|
|
|
1117
1145
|
customHeadItems?: {
|
|
1118
1146
|
tagName: string;
|
|
1119
1147
|
attributes: Record<string, string | boolean>;
|
|
1148
|
+
innerCode?: string;
|
|
1120
1149
|
}[];
|
|
1121
1150
|
|
|
1122
1151
|
}
|
|
@@ -261,7 +261,17 @@ export interface AdminForthComponentDeclarationFull {
|
|
|
261
261
|
* </script>
|
|
262
262
|
*
|
|
263
263
|
*/
|
|
264
|
-
meta?:
|
|
264
|
+
meta?: {
|
|
265
|
+
/**
|
|
266
|
+
* Controls sidebar and header visibility for custom pages
|
|
267
|
+
* - 'default': Show both sidebar and header (default behavior)
|
|
268
|
+
* - 'none': Hide both sidebar and header (full custom layout)
|
|
269
|
+
* - 'preferIconOnly': Show header but prefer icon-only sidebar
|
|
270
|
+
*/
|
|
271
|
+
sidebarAndHeader?: 'default' | 'none' | 'preferIconOnly',
|
|
272
|
+
|
|
273
|
+
[key: string]: any,
|
|
274
|
+
}
|
|
265
275
|
}
|
|
266
276
|
import { type AdminForthActionInput } from './Back.js'
|
|
267
277
|
export { type AdminForthActionInput } from './Back.js'
|
|
@@ -1073,6 +1083,10 @@ export interface AdminForthConfigForFrontend {
|
|
|
1073
1083
|
showBrandNameInSidebar: boolean,
|
|
1074
1084
|
showBrandLogoInSidebar: boolean,
|
|
1075
1085
|
brandLogo?: string,
|
|
1086
|
+
iconOnlySidebar?: {
|
|
1087
|
+
logo?: string,
|
|
1088
|
+
enabled?: boolean,
|
|
1089
|
+
},
|
|
1076
1090
|
singleTheme?: 'light' | 'dark',
|
|
1077
1091
|
datesFormat: string,
|
|
1078
1092
|
timeFormat: string,
|
|
@@ -1094,7 +1108,14 @@ export interface AdminForthConfigForFrontend {
|
|
|
1094
1108
|
customHeadItems?: {
|
|
1095
1109
|
tagName: string;
|
|
1096
1110
|
attributes: Record<string, string | boolean>;
|
|
1097
|
-
|
|
1111
|
+
innerCode?: string;
|
|
1112
|
+
}[],
|
|
1113
|
+
settingPages?:{
|
|
1114
|
+
icon?: string,
|
|
1115
|
+
pageLabel: string,
|
|
1116
|
+
slug?: string,
|
|
1117
|
+
component: string,
|
|
1118
|
+
}[],
|
|
1098
1119
|
}
|
|
1099
1120
|
|
|
1100
1121
|
export interface GetBaseConfigResponse {
|
|
@@ -87,12 +87,19 @@ export interface FrontendAPIInterface {
|
|
|
87
87
|
* Works only when user located on the list page. If filter already exists, it will be replaced with the new one.
|
|
88
88
|
* Can be used to set filter from charts or other components in pageInjections.
|
|
89
89
|
*
|
|
90
|
+
* Filters are automatically marked as hidden (won't count in badge) if:
|
|
91
|
+
* - Column has showIn.filter: false
|
|
92
|
+
*
|
|
90
93
|
* Example:
|
|
91
94
|
*
|
|
92
95
|
* ```ts
|
|
93
96
|
* import adminforth from '@/adminforth'
|
|
94
97
|
*
|
|
98
|
+
* // Regular filter (will show in badge if column.showIn.filter !== false)
|
|
95
99
|
* adminforth.list.setFilter({field: 'name', operator: 'ilike', value: 'john'})
|
|
100
|
+
*
|
|
101
|
+
* // Hidden filter (won't show in badge if column.showIn.filter === false)
|
|
102
|
+
* adminforth.list.setFilter({field: 'internal_status', operator: 'eq', value: 'active'})
|
|
96
103
|
* ```
|
|
97
104
|
*
|
|
98
105
|
* Please note that you can set/update filter even for fields which have showIn.filter=false in resource configuration.
|
|
@@ -106,6 +113,9 @@ export interface FrontendAPIInterface {
|
|
|
106
113
|
* DEPRECATED: does the same as setFilter, kept for backward compatibility
|
|
107
114
|
* Update a filter in the list
|
|
108
115
|
*
|
|
116
|
+
* Filters visibility in badge is automatically determined by column configuration:
|
|
117
|
+
* - Hidden if column has showIn.filter: false
|
|
118
|
+
*
|
|
109
119
|
* Example:
|
|
110
120
|
*
|
|
111
121
|
* ```ts
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface for Captcha adapters.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface CaptchaAdapter {
|
|
6
|
+
/**
|
|
7
|
+
* Returns the script source URL for the captcha widget.
|
|
8
|
+
*/
|
|
9
|
+
getScriptSrc(): string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns the site key for the captcha.
|
|
13
|
+
*/
|
|
14
|
+
getSiteKey(): string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns the widget ID for the captcha.
|
|
18
|
+
*/
|
|
19
|
+
getWidgetId(): string;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns the script HTML for the captcha widget.
|
|
23
|
+
*/
|
|
24
|
+
getRenderWidgetCode(): string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Returns the function name to render the captcha widget.
|
|
28
|
+
*/
|
|
29
|
+
getRenderWidgetFunctionName(): string;
|
|
30
|
+
/**
|
|
31
|
+
* Validates the captcha token.
|
|
32
|
+
*/
|
|
33
|
+
validate(token: string, ip: string): Promise<Record<string, any>>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Might have implementations like RAM, Redis, Memcached,
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
export interface KeyValueAdapter {
|
|
7
|
+
|
|
8
|
+
get(key: string): Promise<string | null>;
|
|
9
|
+
|
|
10
|
+
set(key: string, value: string, expiresInSeconds?: number): Promise<void>;
|
|
11
|
+
|
|
12
|
+
delete(key: string): Promise<void>;
|
|
13
|
+
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export type { EmailAdapter } from './EmailAdapter.js';
|
|
2
2
|
export type { CompletionAdapter } from './CompletionAdapter.js';
|
|
3
3
|
export type { ImageGenerationAdapter } from './ImageGenerationAdapter.js';
|
|
4
|
+
export type { KeyValueAdapter } from './KeyValueAdapter.js';
|
|
4
5
|
export type { ImageVisionAdapter } from './ImageVisionAdapter.js';
|
|
5
6
|
export type { OAuth2Adapter } from './OAuth2Adapter.js';
|
|
6
7
|
export type { StorageAdapter } from './StorageAdapter.js';
|
|
8
|
+
export type { CaptchaAdapter } from './CaptchaAdapter.js';
|
package/dist/spa/src/utils.ts
CHANGED
|
@@ -30,6 +30,7 @@ export async function callApi({path, method, body=undefined}: {
|
|
|
30
30
|
const r = await fetch(fullPath, options);
|
|
31
31
|
if (r.status == 401 ) {
|
|
32
32
|
useUserStore().unauthorize();
|
|
33
|
+
useCoreStore().resetAdminUser();
|
|
33
34
|
await router.push({ name: 'login' });
|
|
34
35
|
return null;
|
|
35
36
|
}
|
|
@@ -92,8 +92,8 @@
|
|
|
92
92
|
{{ $t('Filter') }}
|
|
93
93
|
<span
|
|
94
94
|
class="bg-red-100 text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400"
|
|
95
|
-
v-if="filtersStore.
|
|
96
|
-
{{ filtersStore.
|
|
95
|
+
v-if="filtersStore.visibleFiltersCount">
|
|
96
|
+
{{ filtersStore.visibleFiltersCount }}
|
|
97
97
|
</span>
|
|
98
98
|
</button>
|
|
99
99
|
|
|
@@ -189,7 +189,7 @@ import ResourceListTable from '@/components/ResourceListTable.vue';
|
|
|
189
189
|
import { useCoreStore } from '@/stores/core';
|
|
190
190
|
import { useFiltersStore } from '@/stores/filters';
|
|
191
191
|
import { callAdminForthApi, currentQuery, getIcon, setQuery } from '@/utils';
|
|
192
|
-
import { computed, onMounted, ref, watch, nextTick, type Ref } from 'vue';
|
|
192
|
+
import { computed, onMounted, onUnmounted, ref, watch, nextTick, type Ref } from 'vue';
|
|
193
193
|
import { useRoute } from 'vue-router';
|
|
194
194
|
import { showErrorTost } from '@/composables/useFrontendApi'
|
|
195
195
|
import { getCustomComponent, initThreeDotsDropdown } from '@/utils';
|
|
@@ -387,6 +387,13 @@ class SortQuerySerializer {
|
|
|
387
387
|
|
|
388
388
|
let listAutorefresher: any = null;
|
|
389
389
|
|
|
390
|
+
function clearAutoRefresher() {
|
|
391
|
+
if (listAutorefresher) {
|
|
392
|
+
clearInterval(listAutorefresher);
|
|
393
|
+
listAutorefresher = null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
390
397
|
async function init() {
|
|
391
398
|
|
|
392
399
|
await coreStore.fetchResourceFull({
|
|
@@ -434,10 +441,7 @@ async function init() {
|
|
|
434
441
|
}
|
|
435
442
|
});
|
|
436
443
|
|
|
437
|
-
|
|
438
|
-
clearInterval(listAutorefresher);
|
|
439
|
-
listAutorefresher = null;
|
|
440
|
-
}
|
|
444
|
+
clearAutoRefresher();
|
|
441
445
|
if (coreStore.resource!.options?.listRowsAutoRefreshSeconds) {
|
|
442
446
|
listAutorefresher = setInterval(async () => {
|
|
443
447
|
await adminforth.list.silentRefresh();
|
|
@@ -505,6 +509,10 @@ onMounted(async () => {
|
|
|
505
509
|
initInProcess = false;
|
|
506
510
|
});
|
|
507
511
|
|
|
512
|
+
onUnmounted(() => {
|
|
513
|
+
clearAutoRefresher();
|
|
514
|
+
});
|
|
515
|
+
|
|
508
516
|
watch([page], async () => {
|
|
509
517
|
setQuery({ page: page.value });
|
|
510
518
|
});
|
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
v-for="c in coreStore?.config?.loginPageInjections?.underInputs || []"
|
|
91
91
|
:is="getCustomComponent(c)"
|
|
92
92
|
:meta="c.meta"
|
|
93
|
+
@update:disableLoginButton="setDisableLoginButton($event)"
|
|
93
94
|
/>
|
|
94
95
|
|
|
95
96
|
<ErrorMessage :error="error" />
|
|
@@ -103,7 +104,7 @@
|
|
|
103
104
|
<span class="sr-only">{{ $t('Info') }}</span>
|
|
104
105
|
<div v-html="loginPromptHTML"></div>
|
|
105
106
|
</div>
|
|
106
|
-
<Button @click="login" :loader="inProgress" :disabled="inProgress" class="w-full">
|
|
107
|
+
<Button @click="login" :loader="inProgress" :disabled="inProgress || disableLoginButton" class="w-full">
|
|
107
108
|
{{ $t('Login to your account') }}
|
|
108
109
|
</Button>
|
|
109
110
|
</form>
|
|
@@ -117,7 +118,7 @@
|
|
|
117
118
|
</template>
|
|
118
119
|
|
|
119
120
|
|
|
120
|
-
<script setup>
|
|
121
|
+
<script setup lang="ts">
|
|
121
122
|
|
|
122
123
|
import { getCustomComponent } from '@/utils';
|
|
123
124
|
import { onBeforeMount, onMounted, ref, computed } from 'vue';
|
|
@@ -148,6 +149,7 @@ const user = useUserStore();
|
|
|
148
149
|
const showPw = ref(false);
|
|
149
150
|
|
|
150
151
|
const error = ref(null);
|
|
152
|
+
const disableLoginButton = ref(false);
|
|
151
153
|
|
|
152
154
|
const backgroundPosition = computed(() => {
|
|
153
155
|
return coreStore.config?.loginBackgroundPosition || '1/2';
|
|
@@ -211,5 +213,8 @@ async function login() {
|
|
|
211
213
|
|
|
212
214
|
}
|
|
213
215
|
|
|
216
|
+
function setDisableLoginButton(value: boolean) {
|
|
217
|
+
disableLoginButton.value = value;
|
|
218
|
+
}
|
|
214
219
|
|
|
215
220
|
</script>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mt-16 h-full w-full" :class="{ 'hidden': initialTabSet === false }">
|
|
3
|
+
<div v-if="!coreStore?.config?.settingPages || coreStore?.config?.settingPages.length === 0">
|
|
4
|
+
<p>No setting pages configured or still loading...</p>
|
|
5
|
+
</div>
|
|
6
|
+
<VerticalTabs v-else ref="VerticalTabsRef" v-model:active-tab="activeTab" @update:active-tab="setURL({slug: $event, pageLabel: ''})">
|
|
7
|
+
<template v-for="(c,i) in coreStore?.config?.settingPages" :key="`tab:${settingPageSlotName(c,i)}`" v-slot:['tab:'+c.slug]>
|
|
8
|
+
<div class="flex items-center justify-center whitespace-nowrap w-full px-4 gap-2" @click="setURL(c)">
|
|
9
|
+
<component v-if="c.icon" :is="getIcon(c.icon)" class="w-5 h-5 group-hover:text-lightSidebarIconsHover transition duration-75 dark:group-hover:text-darkSidebarIconsHover dark:text-darkSidebarIcons" ></component>
|
|
10
|
+
{{ c.pageLabel }}
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<template v-for="(c,i) in coreStore?.config?.settingPages" :key="`${settingPageSlotName(c,i)}-content`" v-slot:[c.slug]>
|
|
15
|
+
<component
|
|
16
|
+
:is="getCustomComponent({file: c.component || ''})"
|
|
17
|
+
:resource="coreStore.resource"
|
|
18
|
+
:adminUser="coreStore.adminUser"
|
|
19
|
+
/>
|
|
20
|
+
</template>
|
|
21
|
+
</VerticalTabs>
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
import { ref, onMounted, watch } from 'vue';
|
|
27
|
+
import { useRouter } from 'vue-router';
|
|
28
|
+
import { useCoreStore } from '@/stores/core';
|
|
29
|
+
import { getCustomComponent, getIcon } from '@/utils';
|
|
30
|
+
import { Dropdown } from 'flowbite';
|
|
31
|
+
import adminforth from '@/adminforth';
|
|
32
|
+
import { VerticalTabs } from '@/afcl'
|
|
33
|
+
import { useRoute } from 'vue-router'
|
|
34
|
+
|
|
35
|
+
const route = useRoute()
|
|
36
|
+
const coreStore = useCoreStore();
|
|
37
|
+
const router = useRouter();
|
|
38
|
+
|
|
39
|
+
const routerIsReady = ref(false);
|
|
40
|
+
const loginRedirectCheckIsReady = ref(false);
|
|
41
|
+
const dropdownUserButton = ref<HTMLElement | null>(null);
|
|
42
|
+
const VerticalTabsRef = ref();
|
|
43
|
+
const activeTab = ref('');
|
|
44
|
+
const initialTabSet = ref(false);
|
|
45
|
+
|
|
46
|
+
watch(() => route?.params?.page, (val) => {
|
|
47
|
+
handleURLChange(val as string | null);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
async function initRouter() {
|
|
51
|
+
await router.isReady();
|
|
52
|
+
routerIsReady.value = true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
watch(dropdownUserButton, (el) => {
|
|
56
|
+
if (el) {
|
|
57
|
+
const dd = new Dropdown(
|
|
58
|
+
document.querySelector('#dropdown-user') as HTMLElement,
|
|
59
|
+
document.querySelector('[data-dropdown-toggle="dropdown-user"]') as HTMLElement,
|
|
60
|
+
);
|
|
61
|
+
adminforth.closeUserMenuDropdown = () => dd.hide();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
onMounted(async () => {
|
|
66
|
+
if (coreStore.adminUser) {
|
|
67
|
+
await loadMenu();
|
|
68
|
+
loginRedirectCheckIsReady.value = true;
|
|
69
|
+
const routeParamsPage = route?.params?.page;
|
|
70
|
+
if (!routeParamsPage) {
|
|
71
|
+
if (coreStore.config?.settingPages?.[0]) {
|
|
72
|
+
setURL(coreStore.config.settingPages[0]);
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
handleURLChange(routeParamsPage as string | null);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
async function loadMenu() {
|
|
81
|
+
await initRouter();
|
|
82
|
+
await coreStore.fetchMenuAndResource();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function slugifyString(str: string): string {
|
|
86
|
+
return str
|
|
87
|
+
.toString()
|
|
88
|
+
.toLowerCase()
|
|
89
|
+
.replace(/\s+/g, '-')
|
|
90
|
+
.replace(/[^a-z0-9-_]/g, '-');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function setURL(item: {
|
|
94
|
+
pageLabel: string;
|
|
95
|
+
slug?: string | undefined;
|
|
96
|
+
}) {
|
|
97
|
+
router.replace({
|
|
98
|
+
name: 'settings',
|
|
99
|
+
params: { page: item?.slug }
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function handleURLChange(val: string | null) {
|
|
104
|
+
let isParamInTabs;
|
|
105
|
+
for (const c of coreStore?.config?.settingPages || []) {
|
|
106
|
+
if (c.slug ? c.slug === val : slugifyString(c.pageLabel) === val) {
|
|
107
|
+
isParamInTabs = true;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (isParamInTabs) {
|
|
112
|
+
VerticalTabsRef.value.setActiveTab(val);
|
|
113
|
+
activeTab.value = val as string;
|
|
114
|
+
if (!initialTabSet.value) initialTabSet.value = true;
|
|
115
|
+
} else {
|
|
116
|
+
if (coreStore.config?.settingPages?.[0]) {
|
|
117
|
+
setURL(coreStore.config.settingPages[0]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
</script>
|
package/dist/types/Back.d.ts
CHANGED
|
@@ -277,6 +277,22 @@ export interface IAdminForthAuth {
|
|
|
277
277
|
response: any;
|
|
278
278
|
name: string;
|
|
279
279
|
}): void;
|
|
280
|
+
setCustomCookie({ response, payload }: {
|
|
281
|
+
response: any;
|
|
282
|
+
payload: {
|
|
283
|
+
name: string;
|
|
284
|
+
value: string;
|
|
285
|
+
expiry: number;
|
|
286
|
+
httpOnly: boolean;
|
|
287
|
+
};
|
|
288
|
+
}): void;
|
|
289
|
+
getCustomCookie({ cookies, name }: {
|
|
290
|
+
cookies: {
|
|
291
|
+
key: string;
|
|
292
|
+
value: string;
|
|
293
|
+
}[];
|
|
294
|
+
name: string;
|
|
295
|
+
}): string | null;
|
|
280
296
|
setAuthCookie({ expireInDays, response, username, pk, }: {
|
|
281
297
|
expireInDays?: number;
|
|
282
298
|
response: any;
|
|
@@ -643,6 +659,17 @@ interface AdminForthInputConfigCustomization {
|
|
|
643
659
|
*
|
|
644
660
|
*/
|
|
645
661
|
brandLogo?: string;
|
|
662
|
+
/**
|
|
663
|
+
* Path to your app logo for icon only sidebar
|
|
664
|
+
*
|
|
665
|
+
* Example:
|
|
666
|
+
* Place file `logo.svg` to `./custom` folder and set this option:
|
|
667
|
+
*
|
|
668
|
+
*/
|
|
669
|
+
iconOnlySidebar?: {
|
|
670
|
+
logo?: string;
|
|
671
|
+
enabled?: boolean;
|
|
672
|
+
};
|
|
646
673
|
/**
|
|
647
674
|
* Path to your app favicon
|
|
648
675
|
*
|
|
@@ -761,6 +788,7 @@ interface AdminForthInputConfigCustomization {
|
|
|
761
788
|
customHeadItems?: {
|
|
762
789
|
tagName: string;
|
|
763
790
|
attributes: Record<string, string | boolean>;
|
|
791
|
+
innerCode?: string;
|
|
764
792
|
}[];
|
|
765
793
|
}
|
|
766
794
|
export interface AdminForthActionInput {
|
|
@@ -969,6 +997,15 @@ export interface AdminForthInputConfig {
|
|
|
969
997
|
* If you are using Cloudflare, set this to 'CF-Connecting-IP'. Case-insensitive.
|
|
970
998
|
*/
|
|
971
999
|
clientIpHeader?: string;
|
|
1000
|
+
/**
|
|
1001
|
+
* Add custom page to the settings page
|
|
1002
|
+
*/
|
|
1003
|
+
userMenuSettingsPages: {
|
|
1004
|
+
icon?: string;
|
|
1005
|
+
pageLabel: string;
|
|
1006
|
+
slug?: string;
|
|
1007
|
+
component: string;
|
|
1008
|
+
}[];
|
|
972
1009
|
};
|
|
973
1010
|
/**
|
|
974
1011
|
* Array of resources which will be displayed in the admin panel.
|
|
@@ -1036,6 +1073,7 @@ export interface AdminForthConfigCustomization extends Omit<AdminForthInputConfi
|
|
|
1036
1073
|
customHeadItems?: {
|
|
1037
1074
|
tagName: string;
|
|
1038
1075
|
attributes: Record<string, string | boolean>;
|
|
1076
|
+
innerCode?: string;
|
|
1039
1077
|
}[];
|
|
1040
1078
|
}
|
|
1041
1079
|
export interface AdminForthConfig extends Omit<AdminForthInputConfig, 'customization' | 'resources'> {
|