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.
Files changed (70) hide show
  1. package/dist/auth.d.ts +7 -0
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +5 -0
  4. package/dist/auth.js.map +1 -1
  5. package/dist/modules/codeInjector.d.ts.map +1 -1
  6. package/dist/modules/codeInjector.js +18 -2
  7. package/dist/modules/codeInjector.js.map +1 -1
  8. package/dist/modules/configValidator.d.ts.map +1 -1
  9. package/dist/modules/configValidator.js +21 -5
  10. package/dist/modules/configValidator.js.map +1 -1
  11. package/dist/modules/restApi.d.ts.map +1 -1
  12. package/dist/modules/restApi.js +2 -0
  13. package/dist/modules/restApi.js.map +1 -1
  14. package/dist/modules/styles.d.ts +28 -0
  15. package/dist/modules/styles.d.ts.map +1 -1
  16. package/dist/modules/styles.js +28 -0
  17. package/dist/modules/styles.js.map +1 -1
  18. package/dist/modules/utils.d.ts +1 -0
  19. package/dist/modules/utils.d.ts.map +1 -1
  20. package/dist/modules/utils.js +7 -0
  21. package/dist/modules/utils.js.map +1 -1
  22. package/dist/spa/package-lock.json +5 -4
  23. package/dist/spa/package.json +1 -1
  24. package/dist/spa/src/App.vue +43 -176
  25. package/dist/spa/src/adminforth.ts +14 -10
  26. package/dist/spa/src/afcl/ButtonGroup.vue +91 -0
  27. package/dist/spa/src/afcl/Card.vue +25 -0
  28. package/dist/spa/src/afcl/LinkButton.vue +2 -2
  29. package/dist/spa/src/afcl/Table.vue +19 -10
  30. package/dist/spa/src/afcl/VerticalTabs.vue +15 -6
  31. package/dist/spa/src/afcl/index.ts +2 -0
  32. package/dist/spa/src/components/Filters.vue +2 -2
  33. package/dist/spa/src/components/MenuLink.vue +90 -23
  34. package/dist/spa/src/components/Sidebar.vue +443 -0
  35. package/dist/spa/src/components/UserMenuSettingsButton.vue +68 -0
  36. package/dist/spa/src/renderers/CompactField.vue +1 -1
  37. package/dist/spa/src/renderers/CompactUUID.vue +1 -1
  38. package/dist/spa/src/router/index.ts +9 -0
  39. package/dist/spa/src/spa_types/core.ts +5 -0
  40. package/dist/spa/src/stores/filters.ts +29 -2
  41. package/dist/spa/src/types/Back.ts +29 -0
  42. package/dist/spa/src/types/Common.ts +23 -2
  43. package/dist/spa/src/types/FrontendAPI.ts +10 -0
  44. package/dist/spa/src/types/adapters/CaptchaAdapter.ts +34 -0
  45. package/dist/spa/src/types/adapters/KeyValueAdapter.ts +16 -0
  46. package/dist/spa/src/types/adapters/index.ts +2 -0
  47. package/dist/spa/src/utils.ts +1 -0
  48. package/dist/spa/src/views/ListView.vue +15 -7
  49. package/dist/spa/src/views/LoginView.vue +7 -2
  50. package/dist/spa/src/views/SettingsView.vue +121 -0
  51. package/dist/types/Back.d.ts +38 -0
  52. package/dist/types/Back.d.ts.map +1 -1
  53. package/dist/types/Back.js.map +1 -1
  54. package/dist/types/Common.d.ts +21 -1
  55. package/dist/types/Common.d.ts.map +1 -1
  56. package/dist/types/Common.js.map +1 -1
  57. package/dist/types/FrontendAPI.d.ts +10 -0
  58. package/dist/types/FrontendAPI.d.ts.map +1 -1
  59. package/dist/types/FrontendAPI.js.map +1 -1
  60. package/dist/types/adapters/CaptchaAdapter.d.ts +30 -0
  61. package/dist/types/adapters/CaptchaAdapter.d.ts.map +1 -0
  62. package/dist/types/adapters/CaptchaAdapter.js +5 -0
  63. package/dist/types/adapters/CaptchaAdapter.js.map +1 -0
  64. package/dist/types/adapters/KeyValueAdapter.d.ts +10 -0
  65. package/dist/types/adapters/KeyValueAdapter.d.ts.map +1 -0
  66. package/dist/types/adapters/KeyValueAdapter.js +2 -0
  67. package/dist/types/adapters/KeyValueAdapter.js.map +1 -0
  68. package/dist/types/adapters/index.d.ts +2 -0
  69. package/dist/types/adapters/index.d.ts.map +1 -1
  70. package/package.json +1 -1
@@ -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
- <aside
73
- ref="sidebarAside"
75
+ <Sidebar
74
76
  v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady && defaultLayout"
75
- id="logo-lightSidebar" class="fixed border-none top-0 left-0 z-30 w-64 h-screen transition-transform bg-lightSidebar dark:bg-darkSidebar border-r border-lightSidebarBorder sm:translate-x-0 dark:bg-darkSidebar dark:border-darkSidebarBorder"
76
- :class="{ '-translate-x-full': !sideBarOpen, 'transform-none': sideBarOpen }"
77
- aria-label="Sidebar"
78
- >
79
- <div class="h-full px-3 pb-4 overflow-y-auto bg-lightSidebar dark:bg-darkSidebar border-r border-lightSidebarBorder dark:border-darkSidebarBorder">
80
- <div class="af-logo-title-wrapper flex ms-2 m-4">
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="sm:ml-64 max-w-[100vw] sm:max-w-[calc(100%-16rem)]"
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, nextTick, type Ref } from 'vue';
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 MenuLink from './components/MenuLink.vue';
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 { loadFile } from '@/utils';
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 (!route.meta.customLayout) {
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?.customLayout) {
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
- if(this.filtersStore.filters.some((f: any) => {return f.field === filter.field && f.operator === filter.operator})){
137
- throw new Error(`Filter ${filter.field} with operator ${filter.operator} already exists`)
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-lightPrimaryContrast bg-lightPrimary dark:bg-darkPrimary hover:brightness-110
7
- focus:ring-4 focus:outline-none focus:ring-lightPrimary focus:ring-opacity-50 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:focus:ring-darkPrimary dark:focus:ring-opacity-50"
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 sm:rounded-lg">
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();" :disabled="currentPage <= 1">
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();" :disabled="currentPage <= 1">
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="true"
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();" :disabled="currentPage >= totalPages">
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();" :disabled="currentPage >= totalPages">
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 emites = defineEmits([
189
- 'update:activeTab',
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="activeTab = tab"
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 text-medium text-lightVerticalTabsSlotText dark:text-darkVerticalTabsSlotText w-full ">
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
- import { onMounted, useSlots, ref, type Ref } from 'vue';
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.filters.length"
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.clearFilters();
292
+ filtersStore.filters = [...filtersStore.filters.filter(f => filtersStore.shouldFilterBeHidden(f.field))];
293
293
  emits('update:filters', [...filtersStore.filters]);
294
294
  }
295
295