adminforth 2.8.2 → 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
package/dist/spa/src/App.vue
CHANGED
|
@@ -59,6 +59,9 @@
|
|
|
59
59
|
:adminUser="coreStore.adminUser"
|
|
60
60
|
/>
|
|
61
61
|
</li>
|
|
62
|
+
<li v-if="coreStore?.config?.settingPages && coreStore.config.settingPages.length > 0">
|
|
63
|
+
<UserMenuSettingsButton />
|
|
64
|
+
</li>
|
|
62
65
|
<li>
|
|
63
66
|
<button @click="logout" class="cursor-pointer flex items-center gap-1 block px-4 py-2 text-sm text-black hover:bg-html dark:text-darkSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextActive w-full" role="menuitem">{{ $t('Sign out') }}</button>
|
|
64
67
|
</li>
|
|
@@ -69,123 +72,22 @@
|
|
|
69
72
|
</div>
|
|
70
73
|
</nav>
|
|
71
74
|
|
|
72
|
-
<
|
|
73
|
-
ref="sidebarAside"
|
|
75
|
+
<Sidebar
|
|
74
76
|
v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady && defaultLayout"
|
|
75
|
-
|
|
76
|
-
:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<img v-if="coreStore.config?.showBrandLogoInSidebar !== false" :src="loadFile(coreStore.config?.brandLogo || '@/assets/logo.svg')" :alt="`${ coreStore.config?.brandName } Logo`" class="af-logo h-8 me-3" />
|
|
82
|
-
<span
|
|
83
|
-
v-if="coreStore.config?.showBrandNameInSidebar"
|
|
84
|
-
class="af-title self-center text-lightNavbarText-size font-semibold sm:text-lightNavbarText-size whitespace-nowrap dark:text-darkSidebarText text-lightSidebarText"
|
|
85
|
-
>
|
|
86
|
-
{{ coreStore.config?.brandName }}
|
|
87
|
-
</span>
|
|
88
|
-
<div class="flex items-center gap-2 w-auto" :class="{'w-full justify-end': coreStore.config?.showBrandLogoInSidebar === false}">
|
|
89
|
-
<component
|
|
90
|
-
v-for="c in coreStore?.config?.globalInjections?.sidebarTop || []"
|
|
91
|
-
:is="getCustomComponent(c)"
|
|
92
|
-
:meta="c.meta"
|
|
93
|
-
:adminUser="coreStore.adminUser"
|
|
94
|
-
/>
|
|
95
|
-
</div>
|
|
96
|
-
</div>
|
|
97
|
-
|
|
98
|
-
<ul class="af-sidebar-container space-y-2 font-medium">
|
|
99
|
-
<template v-for="(item, i) in coreStore.menu" :key="`menu-${i}`">
|
|
100
|
-
<div v-if="item.type === 'divider'" class="border-t border-lightSidebarDevider dark:border-darkSidebarDevider"></div>
|
|
101
|
-
<div v-else-if="item.type === 'gap'" class="flex items-center justify-center h-8"></div>
|
|
102
|
-
<div v-else-if="item.type === 'heading'" class="flex items-center justify-left pl-2 h-8 text-lightSidebarHeading dark:text-darkSidebarHeading
|
|
103
|
-
">{{ item.label }}</div>
|
|
104
|
-
<li v-else-if="item.children" class="af-sidebar-expand-container">
|
|
105
|
-
<button @click="clickOnMenuItem(i)" type="button" class="af-sidebar-expand-button flex items-center w-full p-2 text-base text-lightSidebarText rounded-default transition duration-75 group hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:text-darkSidebarText dark:hover:bg-darkSidebarHover dark:hover:text-darkSidebarTextHover"
|
|
106
|
-
:class="opened.includes(i) ? 'af-sidebar-dropdown-expanded' : 'af-sidebar-dropdown-collapsed'"
|
|
107
|
-
:aria-controls="`dropdown-example${i}`"
|
|
108
|
-
:data-collapse-toggle="`dropdown-example${i}`"
|
|
109
|
-
>
|
|
110
|
-
|
|
111
|
-
<component v-if="item.icon" :is="getIcon(item.icon)" class="w-5 h-5 text-lightSidebarIcons group-hover:text-lightSidebarIconsHover transition duration-75 dark:group-hover:text-darkSidebarIconsHover dark:text-darkSidebarIcons" ></component>
|
|
112
|
-
|
|
113
|
-
<span class="text-ellipsis overflow-hidden flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">{{ item.label }}
|
|
114
|
-
|
|
115
|
-
<span v-if="item.badge" class="inline-flex items-center justify-center w-3 h-3 p-3 ms-3 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
|
|
116
|
-
fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent">
|
|
117
|
-
<Tooltip v-if="item.badgeTooltip">
|
|
118
|
-
{{ item.badge }}
|
|
119
|
-
<template #tooltip>
|
|
120
|
-
{{ item.badgeTooltip }}
|
|
121
|
-
</template>
|
|
122
|
-
</Tooltip>
|
|
123
|
-
<template v-else>
|
|
124
|
-
{{ item.badge }}
|
|
125
|
-
</template>
|
|
126
|
-
</span>
|
|
127
|
-
</span>
|
|
128
|
-
|
|
129
|
-
<svg :class="{'rotate-180': opened.includes(i) }" class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
|
130
|
-
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
|
|
131
|
-
</svg>
|
|
132
|
-
</button>
|
|
133
|
-
|
|
134
|
-
<ul :id="`dropdown-example${i}`" role="none" class="af-sidebar-dropdown pt-1 space-y-1" :class="{ 'hidden': !opened.includes(i) }">
|
|
135
|
-
<template v-for="(child, j) in item.children" :key="`menu-${i}-${j}`">
|
|
136
|
-
<li class="af-sidebar-menu-link">
|
|
137
|
-
<MenuLink :item="child" isChild="true" @click="hideSidebar"/>
|
|
138
|
-
</li>
|
|
139
|
-
</template>
|
|
140
|
-
</ul>
|
|
141
|
-
</li>
|
|
142
|
-
<li v-else class="af-sidebar-menu-link">
|
|
143
|
-
<MenuLink :item="item" @click="hideSidebar"/>
|
|
144
|
-
</li>
|
|
145
|
-
</template>
|
|
146
|
-
</ul>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
<div id="dropdown-cta" class="p-4 mt-6 rounded-lg bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
|
|
150
|
-
fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent text-sm" role="alert"
|
|
151
|
-
v-if="ctaBadge"
|
|
152
|
-
>
|
|
153
|
-
<div class="flex items-center mb-3" :class="!ctaBadge.title ? 'float-right' : ''">
|
|
154
|
-
<!-- <span class="bg-lightPrimaryOpacity dark:bg-darkPrimaryOpacity text-sm font-semibold me-2 px-2.5 py-0.5 rounded "
|
|
155
|
-
v-if="ctaBadge.title"
|
|
156
|
-
> -->
|
|
157
|
-
<span>
|
|
158
|
-
{{ctaBadge.title}}
|
|
159
|
-
</span>
|
|
160
|
-
<button type="button"
|
|
161
|
-
class="ms-auto -mx-1.5 -my-1.5 bg-lightPrimaryOpacity dark:bg-darkPrimaryOpacity inline-flex justify-center items-center w-6 h-6 rounded-lg p-1 hover:brightness-110"
|
|
162
|
-
|
|
163
|
-
data-dismiss-target="#dropdown-cta" aria-label="Close"
|
|
164
|
-
v-if="ctaBadge?.closable" @click="closeCTA"
|
|
165
|
-
>
|
|
166
|
-
<span class="sr-only">Close</span>
|
|
167
|
-
<svg class="w-2.5 h-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
|
168
|
-
<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"/>
|
|
169
|
-
</svg>
|
|
170
|
-
</button>
|
|
171
|
-
</div>
|
|
172
|
-
<p class="mb-3 text-sm " v-if="ctaBadge.html" v-html="ctaBadge.html"></p>
|
|
173
|
-
<p class="mb-3 text-sm fill-lightNavbarText dark:fill-darkPrimary text-lightNavbarText dark:text-darkNavbarPrimary" v-else>
|
|
174
|
-
{{ ctaBadge.text }}
|
|
175
|
-
</p>
|
|
176
|
-
<!-- <a class="text-sm text-lightPrimary underline font-medium hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300" href="#">Turn new navigation off</a> -->
|
|
177
|
-
</div>
|
|
178
|
-
|
|
179
|
-
<component
|
|
180
|
-
v-for="c in coreStore?.config?.globalInjections?.sidebar || []"
|
|
181
|
-
:is="getCustomComponent(c)"
|
|
182
|
-
:meta="c.meta"
|
|
183
|
-
:adminUser="coreStore.adminUser"
|
|
184
|
-
/>
|
|
185
|
-
</div>
|
|
186
|
-
</aside>
|
|
77
|
+
:sideBarOpen="sideBarOpen"
|
|
78
|
+
:forceIconOnly="route.meta?.sidebarAndHeader === 'preferIconOnly'"
|
|
79
|
+
@hideSidebar="hideSidebar"
|
|
80
|
+
@loadMenu="loadMenu"
|
|
81
|
+
@sidebarStateChange="handleSidebarStateChange"
|
|
82
|
+
/>
|
|
187
83
|
|
|
188
|
-
<div class="
|
|
84
|
+
<div class="transition-all duration-300 ease-in-out max-w-[100vw]"
|
|
85
|
+
:class="{
|
|
86
|
+
'sm:ml-20': isSidebarIconOnly,
|
|
87
|
+
'sm:ml-[264px]': !isSidebarIconOnly,
|
|
88
|
+
'sm:max-w-[calc(100%-4.5rem)]': isSidebarIconOnly,
|
|
89
|
+
'sm:max-w-[calc(100%-16rem)]': !isSidebarIconOnly
|
|
90
|
+
}"
|
|
189
91
|
v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady && defaultLayout">
|
|
190
92
|
<div class="p-0 dark:border-gray-700 mt-14">
|
|
191
93
|
<RouterView/>
|
|
@@ -245,10 +147,20 @@
|
|
|
245
147
|
@apply opacity-100;
|
|
246
148
|
}
|
|
247
149
|
|
|
150
|
+
@media (min-width: 640px) {
|
|
151
|
+
.sm\:ml-18 {
|
|
152
|
+
margin-left: 4.5rem;
|
|
153
|
+
}
|
|
154
|
+
.sm\:max-w-\[calc\(100\%-4\.5rem\)\] {
|
|
155
|
+
max-width: calc(100% - 4.5rem);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
248
160
|
</style>
|
|
249
161
|
|
|
250
162
|
<script setup lang="ts">
|
|
251
|
-
import { computed, onMounted, ref, watch, onBeforeMount
|
|
163
|
+
import { computed, onMounted, ref, watch, onBeforeMount } from 'vue';
|
|
252
164
|
import { RouterView } from 'vue-router';
|
|
253
165
|
import { Dropdown } from 'flowbite'
|
|
254
166
|
import './index.scss'
|
|
@@ -256,19 +168,15 @@ import { useCoreStore } from '@/stores/core';
|
|
|
256
168
|
import { useUserStore } from '@/stores/user';
|
|
257
169
|
import { IconMoonSolid, IconSunSolid } from '@iconify-prerendered/vue-flowbite';
|
|
258
170
|
import AcceptModal from './components/AcceptModal.vue';
|
|
259
|
-
import
|
|
171
|
+
import Sidebar from './components/Sidebar.vue';
|
|
260
172
|
import { useRoute, useRouter } from 'vue-router';
|
|
261
|
-
import { getIcon, verySimpleHash } from '@/utils';
|
|
262
173
|
import { createHead } from 'unhead'
|
|
263
|
-
import {
|
|
174
|
+
import { getCustomComponent } from '@/utils';
|
|
264
175
|
import Toast from './components/Toast.vue';
|
|
265
176
|
import {useToastStore} from '@/stores/toast';
|
|
266
|
-
import { getCustomComponent } from '@/utils';
|
|
267
|
-
import type { AdminForthConfigMenuItem, AnnouncementBadgeResponse } from './types/Common';
|
|
268
|
-
import { Tooltip } from '@/afcl';
|
|
269
177
|
import { initFrontedAPI } from '@/adminforth';
|
|
270
178
|
import adminforth from '@/adminforth';
|
|
271
|
-
|
|
179
|
+
import UserMenuSettingsButton from './components/UserMenuSettingsButton.vue';
|
|
272
180
|
|
|
273
181
|
const coreStore = useCoreStore();
|
|
274
182
|
const toastStore = useToastStore();
|
|
@@ -281,16 +189,15 @@ const sideBarOpen = ref(false);
|
|
|
281
189
|
const defaultLayout = ref(true);
|
|
282
190
|
const route = useRoute();
|
|
283
191
|
const router = useRouter();
|
|
284
|
-
//create a ref to store the opened menu items with ts type;
|
|
285
|
-
const opened = ref<(string|number)[]>([]);
|
|
286
192
|
const publicConfigLoaded = ref(false);
|
|
287
193
|
const dropdownUserButton = ref(null);
|
|
288
194
|
|
|
289
|
-
const sidebarAside = ref(null);
|
|
290
195
|
|
|
291
196
|
const routerIsReady = ref(false);
|
|
292
197
|
const loginRedirectCheckIsReady = ref(false);
|
|
293
198
|
|
|
199
|
+
const isSidebarIconOnly = ref(localStorage.getItem('afIconOnlySidebar') === 'true');
|
|
200
|
+
|
|
294
201
|
const loggedIn = computed(() => !!coreStore?.adminUser);
|
|
295
202
|
|
|
296
203
|
const theme = ref('light');
|
|
@@ -299,19 +206,16 @@ function hideSidebar(): void {
|
|
|
299
206
|
sideBarOpen.value = false;
|
|
300
207
|
}
|
|
301
208
|
|
|
209
|
+
function handleSidebarStateChange(state: { isSidebarIconOnly: boolean }) {
|
|
210
|
+
isSidebarIconOnly.value = state.isSidebarIconOnly;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
302
214
|
function toggleTheme() {
|
|
303
215
|
theme.value = theme.value === 'light' ? 'dark' : 'light';
|
|
304
216
|
coreStore.toggleTheme();
|
|
305
217
|
}
|
|
306
218
|
|
|
307
|
-
function clickOnMenuItem(label: string | number) {
|
|
308
|
-
if (opened.value.includes(label)) {
|
|
309
|
-
opened.value = opened.value.filter((item) => item !== label);
|
|
310
|
-
} else {
|
|
311
|
-
opened.value.push(label);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
}
|
|
315
219
|
|
|
316
220
|
async function logout() {
|
|
317
221
|
userStore.unauthorize();
|
|
@@ -326,7 +230,7 @@ async function initRouter() {
|
|
|
326
230
|
|
|
327
231
|
async function loadMenu() {
|
|
328
232
|
await initRouter();
|
|
329
|
-
if (
|
|
233
|
+
if (route.meta.sidebarAndHeader !== 'none') {
|
|
330
234
|
// for custom layouts we don't need to fetch menu
|
|
331
235
|
await coreStore.fetchMenuAndResource();
|
|
332
236
|
}
|
|
@@ -334,8 +238,11 @@ async function loadMenu() {
|
|
|
334
238
|
}
|
|
335
239
|
|
|
336
240
|
function handleCustomLayout() {
|
|
337
|
-
if (route.meta?.
|
|
241
|
+
if (route.meta?.sidebarAndHeader === 'none') {
|
|
338
242
|
defaultLayout.value = false;
|
|
243
|
+
} else if (route.meta?.sidebarAndHeader === 'preferIconOnly') {
|
|
244
|
+
defaultLayout.value = true;
|
|
245
|
+
isSidebarIconOnly.value = true;
|
|
339
246
|
} else {
|
|
340
247
|
defaultLayout.value = true;
|
|
341
248
|
}
|
|
@@ -371,13 +278,6 @@ watch([route, () => coreStore.resourceById, () => coreStore.config], async () =>
|
|
|
371
278
|
|
|
372
279
|
});
|
|
373
280
|
|
|
374
|
-
watch(()=>coreStore.menu, () => {
|
|
375
|
-
coreStore.menu.forEach((item, i) => {
|
|
376
|
-
if (item.open) {
|
|
377
|
-
opened.value.push(i);
|
|
378
|
-
};
|
|
379
|
-
});
|
|
380
|
-
})
|
|
381
281
|
|
|
382
282
|
watch(dropdownUserButton, (dropdownUserButton) => {
|
|
383
283
|
if (dropdownUserButton) {
|
|
@@ -396,11 +296,6 @@ async function loadPublicConfig() {
|
|
|
396
296
|
publicConfigLoaded.value = true;
|
|
397
297
|
}
|
|
398
298
|
|
|
399
|
-
watch(sidebarAside, (sidebarAside) => {
|
|
400
|
-
if (sidebarAside) {
|
|
401
|
-
coreStore.fetchMenuBadges();
|
|
402
|
-
}
|
|
403
|
-
})
|
|
404
299
|
|
|
405
300
|
// initialize components based on data attribute selectors
|
|
406
301
|
onMounted(async () => {
|
|
@@ -428,32 +323,4 @@ watch(() => coreStore.config?.singleTheme, (singleTheme) => {
|
|
|
428
323
|
}
|
|
429
324
|
}, { immediate: true })
|
|
430
325
|
|
|
431
|
-
|
|
432
|
-
const ctaBadge: Ref<(AnnouncementBadgeResponse & { hash: string; }) | null> = computed(() => {
|
|
433
|
-
const badge = coreStore.config?.announcementBadge;
|
|
434
|
-
if (!badge) {
|
|
435
|
-
return null;
|
|
436
|
-
}
|
|
437
|
-
const hash = badge.closable ? verySimpleHash(JSON.stringify(badge)) : '';
|
|
438
|
-
if (badge.closable && window.localStorage.getItem(`ctaBadge-${hash}`)) {
|
|
439
|
-
return null;
|
|
440
|
-
}
|
|
441
|
-
return {...badge, hash};
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
function closeCTA() {
|
|
445
|
-
if (!ctaBadge.value) {
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
const hash = ctaBadge.value.hash;
|
|
449
|
-
window.localStorage.setItem(`ctaBadge-${hash}`, '1');
|
|
450
|
-
nextTick( async() => {
|
|
451
|
-
loadMenu();
|
|
452
|
-
await coreStore.fetchMenuBadges();
|
|
453
|
-
adminforth.menu.refreshMenuBadges();
|
|
454
|
-
})
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
|
|
459
326
|
</script>
|
|
@@ -120,23 +120,27 @@ class FrontendAPI implements FrontendAPIInterface {
|
|
|
120
120
|
listFilterValidation(filter: FilterParams): boolean {
|
|
121
121
|
if(router.currentRoute.value.meta.type !== 'list'){
|
|
122
122
|
throw new Error(`Cannot use ${this.setListFilter.name} filter on a list page`)
|
|
123
|
-
} else {
|
|
124
|
-
console.log(this.coreStore.resourceColumnsWithFilters,'core store')
|
|
125
|
-
const filterField = this.coreStore.resourceColumnsWithFilters.find((col: AdminForthResourceColumnCommon) => col.name === filter.field)
|
|
126
|
-
if(!filterField){
|
|
127
|
-
throw new Error(`Field ${filter.field} is not available for filtering`)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
123
|
}
|
|
131
124
|
return true
|
|
132
125
|
}
|
|
133
126
|
|
|
134
127
|
setListFilter(filter: FilterParams): void {
|
|
135
128
|
if(this.listFilterValidation(filter)){
|
|
136
|
-
|
|
137
|
-
|
|
129
|
+
const existingFilterIndex = this.filtersStore.filters.findIndex((f: any) => {
|
|
130
|
+
return f.field === filter.field && f.operator === filter.operator
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if(existingFilterIndex !== -1){
|
|
134
|
+
// Update existing filter instead of throwing error
|
|
135
|
+
const filters = [...this.filtersStore.filters];
|
|
136
|
+
if (filter.value === undefined) {
|
|
137
|
+
filters.splice(existingFilterIndex, 1);
|
|
138
|
+
} else {
|
|
139
|
+
filters[existingFilterIndex] = filter;
|
|
140
|
+
}
|
|
141
|
+
this.filtersStore.setFilters(filters);
|
|
138
142
|
} else {
|
|
139
|
-
this.filtersStore.setFilter(filter)
|
|
143
|
+
this.filtersStore.setFilter(filter);
|
|
140
144
|
}
|
|
141
145
|
}
|
|
142
146
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="isInitFinished" class="inline-flex rounded-md shadow-xs" role="group">
|
|
3
|
+
<button v-for="button in buttons" :key="`${button}-button-controll`"
|
|
4
|
+
:disabled="slotProps[button].disabled"
|
|
5
|
+
class="inline-flex items-center text-sm font-medium border-t border-b border-r border-lightButtonGroupBorder focus:z-10 focus:ring-2 dark:border-darkButtonGroupBorder disabled:opacity-50 disabled:cursor-not-allowed"
|
|
6
|
+
:class="[
|
|
7
|
+
buttonsStyles[button] === 'rounded' ? 'border rounded-lg'
|
|
8
|
+
: buttonsStyles[button] === 'rounded-left' ? 'border rounded-s-lg'
|
|
9
|
+
: buttonsStyles[button] === 'rounded-right' ? 'border rounded-e-lg border-l-lightButtonGroupBackground focus:border-l-lightButtonGroupBorder dark:border-l-darkButtonGroupBackground dark:focus:border-l-darkButtonGroupBorder'
|
|
10
|
+
: buttonsStyles[button] === 'no-rounded' ? 'border border-l-lightButtonGroupBackground focus:border-l-lightButtonGroupBorder dark:border-l-darkButtonGroupBackground dark:focus:border-l-darkButtonGroupBorder'
|
|
11
|
+
: buttonsStyles[button] === 'rounded-left-with-right-border' ? 'border rounded-s-lg' : '',
|
|
12
|
+
(button === activeButton || props.solidColor === true) ? 'bg-lightButtonGroupActiveBackground text-lightButtonGroupActiveText focus:ring-lightButtonGroupActiveFocusRing dark:bg-darkButtonGroupActiveBackground dark:text-darkButtonGroupActiveText dark:focus:ring-darkButtonGroupActiveFocusRing'
|
|
13
|
+
:'text-lightButtonGroupText bg-lightButtonGroupBackground focus:ring-lightButtonGroupFocusRing hover:bg-lightButtonGroupBackgroundHover hover:text-lightButtonGroupTextHover dark:bg-darkButtonGroupBackground dark:text-darkButtonGroupText dark:hover:text-darkButtonGroupTextHover dark:hover:bg-darkButtonGroupBackgroundHover dark:focus:ring-darkButtonGroupFocusRing'
|
|
14
|
+
]"
|
|
15
|
+
@click="setActiveButton(button)"
|
|
16
|
+
>
|
|
17
|
+
<slot :name="`button:${button}`"></slot>
|
|
18
|
+
</button>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script lang="ts" setup>
|
|
23
|
+
import { onMounted, useSlots, reactive, ref, type Ref } from 'vue';
|
|
24
|
+
|
|
25
|
+
const buttons : Ref<string[]> = ref([]);
|
|
26
|
+
const buttonsStyles : Ref<Record<string, string>> = ref({});
|
|
27
|
+
const activeButton = ref('');
|
|
28
|
+
const slotProps = reactive<Record<string, any>>({});
|
|
29
|
+
const isInitFinished = ref(false);
|
|
30
|
+
|
|
31
|
+
const emits = defineEmits(['update:modelValue']);
|
|
32
|
+
|
|
33
|
+
const props = defineProps<{
|
|
34
|
+
solidColor?: boolean;
|
|
35
|
+
modelValue?: string;
|
|
36
|
+
}>();
|
|
37
|
+
|
|
38
|
+
onMounted(() => {
|
|
39
|
+
const slots = useSlots();
|
|
40
|
+
buttons.value = Object.keys(slots).reduce((tbs: string[], tb: string) => {
|
|
41
|
+
if (tb.startsWith('button:')) {
|
|
42
|
+
tbs.push(tb.replace('button:', ''));
|
|
43
|
+
}
|
|
44
|
+
return tbs;
|
|
45
|
+
}, []);
|
|
46
|
+
if (buttons.value.length > 0) {
|
|
47
|
+
activeButton.value = buttons.value[0];
|
|
48
|
+
}
|
|
49
|
+
for (const button of buttons.value) {
|
|
50
|
+
const temp = {
|
|
51
|
+
[button]: getButtonsClasses(button)
|
|
52
|
+
}
|
|
53
|
+
buttonsStyles.value = { ...buttonsStyles.value, ...temp };
|
|
54
|
+
const slot = slots[`button:${button}`];
|
|
55
|
+
if (slot && slot()[0]?.props) {
|
|
56
|
+
slotProps[button] = slot()[0].props;
|
|
57
|
+
} else {
|
|
58
|
+
slotProps[button] = {};
|
|
59
|
+
}
|
|
60
|
+
if (slotProps[button]?.disabled === undefined) {
|
|
61
|
+
slotProps[button].disabled = false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
isInitFinished.value = true;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
function setActiveButton(button: string) {
|
|
69
|
+
if (buttons.value.includes(button)) {
|
|
70
|
+
activeButton.value = button;
|
|
71
|
+
emits('update:modelValue', button);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getButtonsClasses(button: string) {
|
|
76
|
+
const index = buttons.value.indexOf(button);
|
|
77
|
+
const lenght = buttons.value.length;
|
|
78
|
+
if ( lenght === 1 ) {
|
|
79
|
+
return 'rounded'
|
|
80
|
+
} else if ( lenght === 2 && index === 0 ) {
|
|
81
|
+
return 'rounded-left-with-right-border'
|
|
82
|
+
} else if ( index === 0 ) {
|
|
83
|
+
return 'rounded-left'
|
|
84
|
+
} else if ( index === lenght - 1 ) {
|
|
85
|
+
return 'rounded-right'
|
|
86
|
+
} else {
|
|
87
|
+
return 'no-rounded'
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
</script>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<a href="#"
|
|
3
|
+
class="block max-w-sm bg-lightCardBackground border border-lightCardBorder rounded-lg shadow-sm hover:bg-lightCardBackgroundHover dark:bg-darkCardBackground dark:border-darkCardBorder dark:hover:bg-darkCardBackgroundHover"
|
|
4
|
+
:class="[
|
|
5
|
+
props.size === 'sm' ? 'p-2' : props.size === 'md' ? 'p-4' : 'p-6',
|
|
6
|
+
props.hideBorder ? 'border-0' : ''
|
|
7
|
+
]"
|
|
8
|
+
>
|
|
9
|
+
|
|
10
|
+
<h5 class="font-bold tracking-tight text-lightCardTitle dark:text-darkCardTitle" :class="[props.size === 'sm' ? 'text-base' : props.size === 'md' ? 'text-lg mb-1' : 'text-xl mb-2']">{{ props.title }}</h5>
|
|
11
|
+
<p class="font-normal text-lightCardDescription dark:text-darkCardDescription" :class="[props.size === 'sm' ? 'text-sm' : props.size === 'md' ? 'text-base' : 'text-lg']">{{ props.description }}</p>
|
|
12
|
+
<slot></slot>
|
|
13
|
+
</a>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
title?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
size?: 'sm' | 'md' | 'lg';
|
|
22
|
+
hideBorder?: boolean;
|
|
23
|
+
}>();
|
|
24
|
+
|
|
25
|
+
</script>
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
v-bind="$attrs"
|
|
4
4
|
:to="props.to"
|
|
5
5
|
type="submit"
|
|
6
|
-
class="afcl-link-button flex items-center justify-center gap-1 text-
|
|
7
|
-
focus:ring-4 focus:outline-none focus:ring-
|
|
6
|
+
class="afcl-link-button flex items-center justify-center gap-1 text-lightButtonsText bg-lightButtonsBackground border border-lightButtonsBorder dark:bg-darkButtonsBackground hover:bg-lightButtonsHover hover:border-lightButtonsBorderHover
|
|
7
|
+
focus:ring-4 focus:outline-none focus:ring-lightButtonFocusRing focus:ring-opacity-50 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:focus:ring-darkButtonFocusRing dark:text-darkButtonsText dark:border-darkButtonsBorder dark:hover:bg-darkButtonsHover dark:hover:border-darkButtonsBorderHover"
|
|
8
8
|
:class="{
|
|
9
9
|
'cursor-default': props.disabled,
|
|
10
10
|
'opacity-50': props.disabled,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="afcl-table-container relative overflow-x-auto shadow-md
|
|
2
|
+
<div class="afcl-table-container relative overflow-x-auto shadow-md rounded-lg">
|
|
3
3
|
<div class="overflow-x-auto w-full">
|
|
4
4
|
<table class="afcl-table w-full text-sm text-left rtl:text-right text-lightTableText dark:text-darkTableText overflow-x-auto">
|
|
5
5
|
<thead class="afcl-table-thread text-xs text-lightTableHeadingText uppercase bg-lightTableHeadingBackground dark:bg-darkTableHeadingBackground dark:text-darkTableHeadingText">
|
|
@@ -73,11 +73,12 @@
|
|
|
73
73
|
<template #total><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ dataResult.total }}</span></template>
|
|
74
74
|
</i18n-t>
|
|
75
75
|
<div class="af-pagination-container flex flex-row items-center xs:flex-row xs:justify-between xs:items-center gap-3">
|
|
76
|
-
<div class="inline-flex">
|
|
76
|
+
<div class="inline-flex" :class="isLoading || props.isLoading ? 'pointer-events-none select-none opacity-50' : ''">
|
|
77
77
|
<!-- Buttons -->
|
|
78
78
|
<button
|
|
79
79
|
class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightActivePaginationButtonText bg-lightActivePaginationButtonBackground border-r-0 rounded-s hover:opacity-90 dark:bg-darkActivePaginationButtonBackground dark:text-darkActivePaginationButtonText disabled:opacity-50"
|
|
80
|
-
@click="currentPage--; pageInput = currentPage.toString();"
|
|
80
|
+
@click="currentPage--; pageInput = currentPage.toString();"
|
|
81
|
+
:disabled="currentPage <= 1 || isLoading || props.isLoading">
|
|
81
82
|
<svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
|
82
83
|
viewBox="0 0 14 10">
|
|
83
84
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
@@ -86,12 +87,13 @@
|
|
|
86
87
|
</button>
|
|
87
88
|
<button
|
|
88
89
|
class="flex items-center py-1 px-3 text-sm font-medium text-lightUnactivePaginationButtonText focus:outline-none bg-lightUnactivePaginationButtonBackground border-r-0 border border-lightUnactivePaginationButtonBorder hover:bg-lightUnactivePaginationButtonHoverBackground hover:text-lightUnactivePaginationButtonHoverText dark:bg-darkUnactivePaginationButtonBackground dark:text-darkUnactivePaginationButtonText dark:border-darkUnactivePaginationButtonBorder dark:hover:text-darkUnactivePaginationButtonHoverText dark:hover:bg-darkUnactivePaginationButtonHoverBackground disabled:opacity-50"
|
|
89
|
-
@click="switchPage(1); pageInput = currentPage.toString();"
|
|
90
|
+
@click="switchPage(1); pageInput = currentPage.toString();"
|
|
91
|
+
:disabled="currentPage <= 1 || isLoading || props.isLoading">
|
|
90
92
|
<!-- <IconChevronDoubleLeftOutline class="w-4 h-4" /> -->
|
|
91
93
|
1
|
|
92
94
|
</button>
|
|
93
95
|
<div
|
|
94
|
-
contenteditable="
|
|
96
|
+
:contenteditable="!isLoading && !props.isLoading"
|
|
95
97
|
class="min-w-10 outline-none inline-block w-auto py-1.5 px-3 text-sm text-center text-lightTablePaginationInputText border border-lightTablePaginationInputBorder bg-lightTablePaginationInputBackground dark:border-darkTablePaginationInputBorder dark:text-darkTablePaginationInputText dark:bg-darkTablePaginationInputBackground z-10"
|
|
96
98
|
@keydown="onPageKeydown($event)"
|
|
97
99
|
@input="onPageInput($event)"
|
|
@@ -102,14 +104,17 @@
|
|
|
102
104
|
|
|
103
105
|
<button
|
|
104
106
|
class="flex items-center py-1 px-3 text-sm font-medium text-lightUnactivePaginationButtonText focus:outline-none bg-lightUnactivePaginationButtonBackground border-l-0 border border-lightUnactivePaginationButtonBorder hover:bg-lightUnactivePaginationButtonHoverBackground hover:text-lightUnactivePaginationButtonHoverText dark:bg-darkUnactivePaginationButtonBackground dark:text-darkUnactivePaginationButtonText dark:border-darkUnactivePaginationButtonBorder dark:hover:text-darkUnactivePaginationButtonHoverText dark:hover:bg-darkUnactivePaginationButtonHoverBackground disabled:opacity-50"
|
|
105
|
-
@click="currentPage = totalPages; pageInput = currentPage.toString();"
|
|
107
|
+
@click="currentPage = totalPages; pageInput = currentPage.toString();"
|
|
108
|
+
:disabled="currentPage >= totalPages || isLoading || props.isLoading"
|
|
109
|
+
>
|
|
106
110
|
{{ totalPages }}
|
|
107
111
|
|
|
108
|
-
<!-- <IconChevronDoubleRightOutline class="w-4 h-4" /> -->
|
|
109
112
|
</button>
|
|
110
113
|
<button
|
|
111
114
|
class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightActivePaginationButtonText focus:outline-none bg-lightActivePaginationButtonBackground border-l-0 rounded-e hover:opacity-90 dark:bg-darkActivePaginationButtonBackground dark:text-darkActivePaginationButtonText disabled:opacity-50"
|
|
112
|
-
@click="currentPage++; pageInput = currentPage.toString();"
|
|
115
|
+
@click="currentPage++; pageInput = currentPage.toString();"
|
|
116
|
+
:disabled="currentPage >= totalPages || isLoading || props.isLoading"
|
|
117
|
+
>
|
|
113
118
|
<svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
|
114
119
|
viewBox="0 0 14 10">
|
|
115
120
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
@@ -172,6 +177,10 @@
|
|
|
172
177
|
columnWidths.value = !headerRefs.value ? [] : headerRefs.value.map((el: HTMLElement) => el.offsetWidth);
|
|
173
178
|
});
|
|
174
179
|
|
|
180
|
+
watch([isLoading, () => props.isLoading], () => {
|
|
181
|
+
emit('update:tableLoading', isLoading.value || props.isLoading);
|
|
182
|
+
});
|
|
183
|
+
|
|
175
184
|
const totalPages = computed(() => {
|
|
176
185
|
return dataResult.value?.total ? Math.ceil(dataResult.value.total / props.pageSize) : 1;
|
|
177
186
|
});
|
|
@@ -185,8 +194,8 @@
|
|
|
185
194
|
pageInput.value = p.toString();
|
|
186
195
|
}
|
|
187
196
|
|
|
188
|
-
const
|
|
189
|
-
'update:
|
|
197
|
+
const emit = defineEmits([
|
|
198
|
+
'update:tableLoading',
|
|
190
199
|
]);
|
|
191
200
|
|
|
192
201
|
function onPageInput(event: any) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="md:flex">
|
|
3
|
-
<ul class="flex-column space-y space-y-4 text-sm font-medium text-lightVerticalTabsText dark:text-darkVerticalTabsText md:me-4 mb-4 md:mb-0">
|
|
3
|
+
<ul class="ps-6 flex-column space-y space-y-4 text-sm font-medium text-lightVerticalTabsText dark:text-darkVerticalTabsText md:me-4 mb-4 md:mb-0 md:mr-0 mr-6">
|
|
4
4
|
<li v-for="tab in tabs" :key="`${tab}-tab-controll`">
|
|
5
5
|
<a
|
|
6
6
|
href="#"
|
|
7
|
-
@click="
|
|
7
|
+
@click="setActiveTab(tab)"
|
|
8
8
|
class="inline-flex items-center px-4 py-3 rounded-lg w-full"
|
|
9
9
|
:class="tab === activeTab ? 'text-lightVerticalTabsTextActive bg-lightVerticalTabsBackgroundActive active dark:bg-darkVerticalTabsBackgroundActive dark:text-darkVerticalTabsTextActive' : 'text-lightVerticalTabsText dark:text-darkVerticalTabsText hover:text-lightVerticalTabsTextHover bg-lightVerticalTabsBackground hover:bg-lightVerticalTabsBackgroundHover dark:bg-darkVerticalTabsBackground dark:hover:bg-darkVerticalTabsBackgroundHover dark:hover:darkVerticalTabsTextHover'"
|
|
10
10
|
aria-current="page"
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
</a>
|
|
14
14
|
</li>
|
|
15
15
|
</ul>
|
|
16
|
-
<div class="ps-6
|
|
16
|
+
<div class="ps-6 text-medium text-lightVerticalTabsSlotText dark:text-darkVerticalTabsSlotText w-full ">
|
|
17
17
|
<slot :name="activeTab"></slot>
|
|
18
18
|
</div>
|
|
19
19
|
</div>
|
|
20
20
|
</template>
|
|
21
21
|
|
|
22
22
|
<script setup lang="ts">
|
|
23
|
-
|
|
23
|
+
import { onMounted, useSlots, ref, type Ref } from 'vue';
|
|
24
24
|
const tabs : Ref<string[]> = ref([]);
|
|
25
25
|
const activeTab = ref('');
|
|
26
26
|
const props = defineProps({
|
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
const emites = defineEmits([
|
|
32
32
|
'update:activeTab',
|
|
33
33
|
]);
|
|
34
|
+
|
|
35
|
+
defineExpose({
|
|
36
|
+
setActiveTab
|
|
37
|
+
});
|
|
38
|
+
|
|
34
39
|
onMounted(() => {
|
|
35
40
|
const slots = useSlots();
|
|
36
41
|
tabs.value = Object.keys(slots).reduce((tbs: string[], tb: string) => {
|
|
@@ -44,6 +49,10 @@
|
|
|
44
49
|
}
|
|
45
50
|
});
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
function setActiveTab(tab: string) {
|
|
53
|
+
if (tabs.value.includes(tab)) {
|
|
54
|
+
activeTab.value = tab;
|
|
55
|
+
emites('update:activeTab', tab);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
49
58
|
</script>
|
|
@@ -23,3 +23,5 @@ export { default as JsonViewer } from './JsonViewer.vue';
|
|
|
23
23
|
export { default as Toggle } from './Toggle.vue';
|
|
24
24
|
export { default as DatePicker } from './DatePicker.vue';
|
|
25
25
|
export { default as Textarea } from './Textarea.vue';
|
|
26
|
+
export { default as ButtonGroup } from './ButtonGroup.vue';
|
|
27
|
+
export { default as Card } from './Card.vue';
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
|
|
137
137
|
<div class="flex justify-end gap-2">
|
|
138
138
|
<button
|
|
139
|
-
:disabled="!filtersStore.
|
|
139
|
+
:disabled="!filtersStore.visibleFiltersCount"
|
|
140
140
|
type="button"
|
|
141
141
|
class="flex items-center py-1 px-3 text-sm font-medium text-lightFiltersClearAllButtonText focus:outline-none bg-lightFiltersClearAllButtonBackground rounded border border-lightFiltersClearAllButtonBorder hover:bg-lightFiltersClearAllButtonBackgroundHover hover:text-lightFiltersClearAllButtonTextHover focus:z-10 focus:ring-4 focus:ring-lightFiltersClearAllButtonFocus dark:focus:ring-darkFiltersClearAllButtonFocus dark:bg-darkFiltersClearAllButtonBackground dark:text-darkFiltersClearAllButtonText dark:border-darkFiltersClearAllButtonBorder dark:hover:text-darkFiltersClearAllButtonTextHover dark:hover:bg-darkFiltersClearAllButtonBackgroundHover disabled:opacity-50 disabled:cursor-not-allowed"
|
|
142
142
|
@click="clear">{{ $t('Clear all') }}</button>
|
|
@@ -289,7 +289,7 @@ function getFilterItem({ column, operator }) {
|
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
async function clear() {
|
|
292
|
-
filtersStore.
|
|
292
|
+
filtersStore.filters = [...filtersStore.filters.filter(f => filtersStore.shouldFilterBeHidden(f.field))];
|
|
293
293
|
emits('update:filters', [...filtersStore.filters]);
|
|
294
294
|
}
|
|
295
295
|
|