adminforth 2.13.0-next.5 → 2.13.0-next.50

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 (39) hide show
  1. package/dist/modules/configValidator.d.ts.map +1 -1
  2. package/dist/modules/configValidator.js +23 -3
  3. package/dist/modules/configValidator.js.map +1 -1
  4. package/dist/modules/restApi.d.ts.map +1 -1
  5. package/dist/modules/restApi.js +15 -7
  6. package/dist/modules/restApi.js.map +1 -1
  7. package/dist/modules/utils.d.ts +2 -0
  8. package/dist/modules/utils.d.ts.map +1 -1
  9. package/dist/modules/utils.js +51 -0
  10. package/dist/modules/utils.js.map +1 -1
  11. package/dist/spa/package-lock.json +768 -1412
  12. package/dist/spa/package.json +33 -32
  13. package/dist/spa/src/App.vue +45 -23
  14. package/dist/spa/src/afcl/Dialog.vue +65 -5
  15. package/dist/spa/src/afcl/Select.vue +3 -2
  16. package/dist/spa/src/components/ResourceListTable.vue +17 -9
  17. package/dist/spa/src/components/ResourceListTableVirtual.vue +18 -10
  18. package/dist/spa/src/components/Sidebar.vue +15 -3
  19. package/dist/spa/src/components/ThreeDotsMenu.vue +34 -6
  20. package/dist/spa/src/components/ValueRenderer.vue +1 -1
  21. package/dist/spa/src/stores/core.ts +8 -1
  22. package/dist/spa/src/stores/filters.ts +5 -0
  23. package/dist/spa/src/types/Back.ts +6 -0
  24. package/dist/spa/src/types/Common.ts +1 -0
  25. package/dist/spa/src/types/adapters/OAuth2Adapter.ts +1 -1
  26. package/dist/spa/src/utils.ts +18 -4
  27. package/dist/spa/src/views/ListView.vue +6 -0
  28. package/dist/spa/src/views/LoginView.vue +8 -2
  29. package/dist/spa/src/views/PageNotFound.vue +2 -2
  30. package/dist/spa/tsconfig.json +1 -1
  31. package/dist/types/Back.d.ts +4 -0
  32. package/dist/types/Back.d.ts.map +1 -1
  33. package/dist/types/Back.js.map +1 -1
  34. package/dist/types/Common.d.ts +1 -0
  35. package/dist/types/Common.d.ts.map +1 -1
  36. package/dist/types/Common.js.map +1 -1
  37. package/dist/types/adapters/OAuth2Adapter.d.ts +2 -0
  38. package/dist/types/adapters/OAuth2Adapter.d.ts.map +1 -1
  39. package/package.json +1 -1
@@ -13,47 +13,48 @@
13
13
  "i18n:extract": "echo {} > i18n-empty.json && vue-i18n-extract report --vueFiles \"./src/**/*.{js,vue,ts}\" --output ./i18n-messages.json --languageFiles \"i18n-empty.json\" --add"
14
14
  },
15
15
  "dependencies": {
16
- "@iconify-prerendered/vue-flag": "^0.28.1754899047",
16
+ "@iconify-prerendered/vue-flag": "^0.28.1748584105",
17
17
  "@iconify-prerendered/vue-flowbite": "^0.28.1754899090",
18
- "@unhead/vue": "^1.11.20",
19
- "@vueuse/core": "^10.11.1",
18
+ "@iconify-prerendered/vue-humbleicons": "^0.28.1754108846",
19
+ "@unhead/vue": "^1.9.12",
20
+ "@vueuse/core": "^10.10.0",
20
21
  "apexcharts": "^4.7.0",
21
- "dayjs": "^1.11.19",
22
- "debounce": "^2.2.0",
23
- "flowbite-datepicker": "^1.3.2",
24
- "javascript-time-ago": "^2.5.12",
25
- "pinia": "^2.3.1",
26
- "sanitize-html": "^2.17.0",
27
- "unhead": "^1.11.20",
22
+ "dayjs": "^1.11.11",
23
+ "debounce": "^2.1.0",
24
+ "flowbite-datepicker": "^1.2.6",
25
+ "javascript-time-ago": "^2.5.11",
26
+ "pinia": "^2.1.7",
27
+ "sanitize-html": "^2.13.0",
28
+ "unhead": "^1.9.12",
28
29
  "uuid": "^10.0.0",
29
- "vue": "^3.5.22",
30
+ "vue": "^3.5.12",
30
31
  "vue-diff": "^1.2.4",
31
- "vue-i18n": "^10.0.8",
32
- "vue-router": "^4.6.3",
32
+ "vue-i18n": "^10.0.5",
33
+ "vue-router": "^4.3.0",
33
34
  "vue-slider-component": "^4.1.0-beta.7"
34
35
  },
35
36
  "devDependencies": {
36
- "@rushstack/eslint-patch": "^1.14.1",
37
- "@tsconfig/node20": "^20.1.6",
38
- "@types/node": "^20.19.24",
39
- "@vitejs/plugin-vue": "^6.0.2",
37
+ "@rushstack/eslint-patch": "^1.8.0",
38
+ "@tsconfig/node20": "^20.1.4",
39
+ "@types/node": "^20.12.5",
40
+ "@vitejs/plugin-vue": "^5.0.4",
40
41
  "@vue/eslint-config-typescript": "^13.0.0",
41
- "@vue/tsconfig": "^0.8.1",
42
- "autoprefixer": "^10.4.21",
43
- "eslint": "^8.57.1",
44
- "eslint-plugin-vue": "^9.33.0",
45
- "flag-icons": "^7.5.0",
42
+ "@vue/tsconfig": "^0.5.1",
43
+ "autoprefixer": "^10.4.19",
44
+ "eslint": "^8.57.0",
45
+ "eslint-plugin-vue": "^9.23.0",
46
+ "flag-icons": "^7.2.3",
46
47
  "flowbite": "^3.1.2",
47
- "i18n-iso-countries": "^7.14.0",
48
- "npm-run-all2": "^6.2.6",
49
- "portfinder": "^1.0.38",
50
- "postcss": "^8.5.6",
51
- "sass": "^1.93.3",
52
- "tailwindcss": "^3.4.18",
53
- "typescript": "~5.9.3",
54
- "vite": "^7.2.4",
48
+ "i18n-iso-countries": "^7.12.0",
49
+ "npm-run-all2": "^6.1.2",
50
+ "portfinder": "^1.0.32",
51
+ "postcss": "^8.4.38",
52
+ "sass": "^1.77.2",
53
+ "tailwindcss": "^3.4.17",
54
+ "typescript": "~5.4.0",
55
+ "vite": "^5.2.13",
55
56
  "vue-i18n-extract": "^2.0.7",
56
- "vue-tsc": "^2.2.12",
57
- "vue3-json-viewer": "^2.4.1"
57
+ "vue-tsc": "^2.0.11",
58
+ "vue3-json-viewer": "^2.2.2"
58
59
  }
59
60
  }
@@ -24,6 +24,12 @@
24
24
  />
25
25
 
26
26
  <div class="flex items-center ms-3 ">
27
+ <Tooltip>
28
+ <IconWifiOff v-if="coreStore.isInternetError" class="blinking-icon w-8 h-8 text-red-500" />
29
+ <template #tooltip>
30
+ {{$t('Internet connection lost')}}
31
+ </template>
32
+ </Tooltip>
27
33
  <span
28
34
  v-if="!coreStore.config?.singleTheme"
29
35
  @click="toggleTheme" class="cursor-pointer flex items-center gap-1 block px-4 py-2 text-sm text-black dark:text-darkSidebarTextHover dark:hover:text-darkSidebarTextActive" role="menuitem">
@@ -35,12 +41,17 @@
35
41
  ref="dropdownUserButton"
36
42
  type="button" class="flex text-sm bg- rounded-full focus:ring-4 focus:ring-lightSidebarDevider dark:focus:ring-darkSidebarDevider dark:bg-" aria-expanded="false" data-dropdown-toggle="dropdown-user">
37
43
  <span class="sr-only">{{ $t('Open user menu') }}</span>
38
- <svg class="w-8 h-8 text-lightNavbarIcons dark:text-darkNavbarIcons" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
44
+ <img
45
+ v-if="coreStore.userAvatarUrl"
46
+ class="w-8 h-8 rounded-full object-cover"
47
+ :src="coreStore.userAvatarUrl"
48
+ alt="user photo"
49
+ />
50
+ <svg v-else class="w-8 h-8 text-lightNavbarIcons dark:text-darkNavbarIcons" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
39
51
  <path fill-rule="evenodd" d="M12 20a7.966 7.966 0 0 1-5.002-1.756l.002.001v-.683c0-1.794 1.492-3.25 3.333-3.25h3.334c1.84 0 3.333 1.456 3.333 3.25v.683A7.966 7.966 0 0 1 12 20ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10c0 5.5-4.44 9.963-9.932 10h-.138C6.438 21.962 2 17.5 2 12Zm10-5c-1.84 0-3.333 1.455-3.333 3.25S10.159 13.5 12 13.5c1.84 0 3.333-1.455 3.333-3.25S13.841 7 12 7Z" clip-rule="evenodd"/>
40
52
  </svg>
41
53
  </button>
42
54
  </div>
43
-
44
55
  <div class="z-50 hidden my-4 text-base list-none bg-lightUserMenuBackground divide-y divide-lightUserMenuBorder text-lightUserMenuText rounded shadow dark:shadow-black dark:bg-darkUserMenuBackground dark:divide-darkUserMenuBorder text-darkUserMenuText dark:shadow-black" id="dropdown-user">
45
56
  <div class="px-4 py-3" role="none">
46
57
  <p class="text-sm text-gray-900 dark:text-darkNavbarText" role="none" v-if="coreStore.userFullname">
@@ -73,7 +84,7 @@
73
84
  </nav>
74
85
 
75
86
  <Sidebar
76
- v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady && defaultLayout && !headerOnlyLayout"
87
+ v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady && defaultLayout && !headerOnlyLayout && coreStore.menu.length > 0"
77
88
  :sideBarOpen="sideBarOpen"
78
89
  :forceIconOnly="route.meta?.sidebarAndHeader === 'preferIconOnly'"
79
90
  @hideSidebar="hideSidebar"
@@ -129,6 +140,19 @@
129
140
 
130
141
  <style lang="scss" scoped>
131
142
 
143
+ @keyframes blink {
144
+ 0%, 100% {
145
+ opacity: 1;
146
+ }
147
+ 50% {
148
+ opacity: 0.2;
149
+ }
150
+ }
151
+
152
+ .blinking-icon {
153
+ animation: blink 2s ease-in-out infinite;
154
+ }
155
+
132
156
  .fade-leave-active {
133
157
  @apply transition-opacity duration-500;
134
158
  }
@@ -163,6 +187,7 @@ import './index.scss'
163
187
  import { useCoreStore } from '@/stores/core';
164
188
  import { useUserStore } from '@/stores/user';
165
189
  import { IconMoonSolid, IconSunSolid } from '@iconify-prerendered/vue-flowbite';
190
+ import { IconWifiOff } from '@iconify-prerendered/vue-humbleicons';
166
191
  import AcceptModal from './components/AcceptModal.vue';
167
192
  import Sidebar from './components/Sidebar.vue';
168
193
  import { useRoute, useRouter } from 'vue-router';
@@ -173,6 +198,7 @@ import {useToastStore} from '@/stores/toast';
173
198
  import { initFrontedAPI } from '@/adminforth';
174
199
  import adminforth from '@/adminforth';
175
200
  import UserMenuSettingsButton from './components/UserMenuSettingsButton.vue';
201
+ import { Tooltip } from '@/afcl'
176
202
 
177
203
  const coreStore = useCoreStore();
178
204
  const toastStore = useToastStore();
@@ -182,8 +208,6 @@ initFrontedAPI()
182
208
 
183
209
  createHead()
184
210
  const sideBarOpen = ref(false);
185
- const defaultLayout = ref(true);
186
- const headerOnlyLayout = ref(false);
187
211
  const route = useRoute();
188
212
  const router = useRouter();
189
213
  const publicConfigLoaded = ref(false);
@@ -197,6 +221,14 @@ const isSidebarIconOnly = ref(localStorage.getItem('afIconOnlySidebar') === 'tru
197
221
 
198
222
  const loggedIn = computed(() => !!coreStore?.adminUser);
199
223
 
224
+ const defaultLayout = computed(() => {
225
+ return route.meta?.sidebarAndHeader !== 'none';
226
+ });
227
+
228
+ const headerOnlyLayout = computed(() => {
229
+ return route.meta?.sidebarAndHeader === 'headerOnly';
230
+ });
231
+
200
232
  const expandedWidth = computed(() => coreStore.config?.iconOnlySidebar?.expandedSidebarWidth || '16.5rem');
201
233
 
202
234
  const theme = ref('light');
@@ -308,19 +340,6 @@ async function loadMenu() {
308
340
  loginRedirectCheckIsReady.value = true;
309
341
  }
310
342
 
311
- function handleCustomLayout() {
312
- if (route.meta?.sidebarAndHeader === 'none') {
313
- defaultLayout.value = false;
314
- } else if (route.meta?.sidebarAndHeader === 'preferIconOnly') {
315
- defaultLayout.value = true;
316
- isSidebarIconOnly.value = true;
317
- } else if (route.meta?.sidebarAndHeader === 'headerOnly') {
318
- headerOnlyLayout.value = true;
319
- } else {
320
- defaultLayout.value = true;
321
- }
322
- }
323
-
324
343
  function humanizeSnake(str: string): string {
325
344
  if (!str) {
326
345
  return '';
@@ -345,10 +364,11 @@ watch(title, (title) => {
345
364
  document.title = title;
346
365
  })
347
366
 
348
- watch([route, () => coreStore.resourceById, () => coreStore.config], async () => {
349
- handleCustomLayout()
350
- await new Promise((resolve) => setTimeout(resolve, 0));
351
-
367
+ watch(route, () => {
368
+ // Handle preferIconOnly layout
369
+ if (route.meta?.sidebarAndHeader === 'preferIconOnly') {
370
+ isSidebarIconOnly.value = true;
371
+ }
352
372
  });
353
373
 
354
374
 
@@ -376,11 +396,13 @@ onMounted(async () => {
376
396
  loadPublicConfig(); // and this
377
397
  // before init flowbite we have to wait router initialized because it affects dom(our v-ifs) and fetch menu
378
398
  await initRouter();
379
- handleCustomLayout();
380
399
 
381
400
  adminforth.menu.refreshMenuBadges = async () => {
382
401
  await coreStore.fetchMenuBadges();
383
402
  }
403
+
404
+ window.addEventListener('online', () => coreStore.isInternetError = false);
405
+ window.addEventListener('offline', () => coreStore.isInternetError = true);
384
406
  })
385
407
 
386
408
  onBeforeMount(()=>{
@@ -22,7 +22,7 @@
22
22
  v-if="headerCloseButton"
23
23
  type="button"
24
24
  class="text-lightDialogCloseButton bg-transparent hover:bg-lightDialogCloseButtonHoverBackground hover:text-lightDialogCloseButtonHover rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:text-darkDialogCloseButton dark:hover:bg-darkDialogCloseButtonHoverBackground dark:hover:text-darkDialogCloseButtonHover"
25
- @click="modal?.hide()"
25
+ @click="tryToHideModal"
26
26
  >
27
27
  <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
28
28
  <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
@@ -51,13 +51,41 @@
51
51
  </div>
52
52
  </div>
53
53
  </div>
54
+ <div>
55
+ <!-- Confirmation Modal -->
56
+ <div
57
+ v-if="showConfirmationOnClose"
58
+ class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-60"
59
+ >
60
+ <div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-lg max-w-sm w-full">
61
+ <h2 class="text-lg font-semibold mb-4 text-lightDialogHeaderText dark:text-darkDialogHeaderText">Confirm Close</h2>
62
+ <p class="mb-6 text-lightDialogBodyText dark:text-darkDialogBodyText">{{ props.closeConfirmationText }}</p>
63
+ <div class="flex justify-end">
64
+ <Button
65
+ class="me-3"
66
+ @click="showConfirmationOnClose = false"
67
+ >
68
+ Cancel
69
+ </Button>
70
+ <Button
71
+ @click="
72
+ showConfirmationOnClose = false;
73
+ modal?.hide();
74
+ "
75
+ >
76
+ Confirm
77
+ </Button>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </div>
54
82
  </div>
55
83
  </Teleport>
56
84
  </template>
57
85
 
58
86
  <script setup lang="ts">
59
87
  import Button from "./Button.vue";
60
- import { ref, onMounted, nextTick, onUnmounted, type Ref } from 'vue';
88
+ import { ref, onMounted, nextTick, onUnmounted, computed, type Ref } from 'vue';
61
89
  import { Modal } from 'flowbite';
62
90
 
63
91
  const modalEl = ref(null);
@@ -77,20 +105,42 @@ interface DialogProps {
77
105
  beforeCloseFunction?: (() => void | Promise<void>) | null
78
106
  beforeOpenFunction?: (() => void | Promise<void>) | null
79
107
  closable?: boolean
108
+ askForCloseConfirmation?: boolean
109
+ closeConfirmationText?: string
80
110
  }
81
111
 
82
112
  const props = withDefaults(defineProps<DialogProps>(), {
83
113
  header: '',
84
114
  headerCloseButton: true,
85
- buttons: () => [
86
- { label: 'Close', onclick: (dialog: any) => dialog.hide(), type: '' },
87
- ],
115
+ buttons: () => [],
88
116
  clickToCloseOutside: true,
89
117
  beforeCloseFunction: null,
90
118
  beforeOpenFunction: null,
91
119
  closable: true,
120
+ askForCloseConfirmation: false,
121
+ closeConfirmationText: 'Are you sure you want to close this dialog?',
92
122
  })
93
123
 
124
+ const buttons = computed<DialogButton[]>(() => {
125
+ if (props.buttons && props.buttons.length > 0) {
126
+ return props.buttons;
127
+ }
128
+ return [
129
+ {
130
+ label: 'Close',
131
+ onclick: (dialog: any) => {
132
+ if (!props.askForCloseConfirmation) {
133
+ dialog.hide();
134
+ } else {
135
+ showConfirmationOnClose.value = true;
136
+ }
137
+ },
138
+ options: {}
139
+ }
140
+ ];
141
+ });
142
+
143
+ const showConfirmationOnClose = ref(false);
94
144
  onMounted(async () => {
95
145
  //await one tick when all is mounted
96
146
  await nextTick();
@@ -129,6 +179,16 @@ function close() {
129
179
  defineExpose({
130
180
  open: open,
131
181
  close: close,
182
+ tryToHideModal: tryToHideModal,
132
183
  })
133
184
 
185
+ function tryToHideModal() {
186
+ if (!props.askForCloseConfirmation ) {
187
+ modal.value?.hide();
188
+ } else {
189
+ showConfirmationOnClose.value = true;
190
+ }
191
+ }
192
+
193
+
134
194
  </script>
@@ -13,6 +13,7 @@
13
13
  class="block w-full pl-3 pr-10 py-2.5 border border-lightDropownButtonsBorder rounded-md leading-5 bg-lightDropdownButtonsBackground
14
14
  placeholder-lightDropdownButtonsPlaceholderText text-lightDropdownButtonsText sm:text-sm transition duration-150 ease-in-out dark:bg-darkDropdownButtonsBackground dark:border-darkDropdownButtonsBorder dark:placeholder-darkDropdownButtonsPlaceholderText
15
15
  dark:text-darkDropdownButtonsText focus:ring-lightPrimary focus:border-lightPrimary dark:focus:ring-darkPrimary dark:focus:border-darkPrimary"
16
+ :class="{'cursor-pointer': searchDisabled}"
16
17
  autocomplete="off" data-custom="no-autofill"
17
18
  :placeholder="
18
19
  selectedItems.length && !multiple ? '' : (showDropdown ? $t('Search') : placeholder || $t('Select...'))
@@ -265,11 +266,11 @@ onMounted(() => {
265
266
 
266
267
  watch(() => props.modelValue, (value) => {
267
268
  updateFromProps();
268
- });
269
+ }, {deep: true});
269
270
 
270
271
  watch(() => props.options, () => {
271
272
  updateFromProps();
272
- });
273
+ }, { deep: true });
273
274
 
274
275
  addClickListener();
275
276
 
@@ -83,13 +83,20 @@
83
83
  </td>
84
84
  </tr>
85
85
 
86
- <tr @click="onClick($event,row)"
87
- v-else v-for="(row, rowI) in rows" :key="`row_${row._primaryKeyValue}`"
88
- ref="rowRefs"
89
- class="list-table-body-row bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
90
-
91
- :class="{'border-b': rowI !== rows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
92
- >
86
+ <component
87
+ v-else
88
+ v-for="(row, rowI) in rows"
89
+ :is="tableRowReplaceInjection ? getCustomComponent(tableRowReplaceInjection) : 'tr'"
90
+ :key="`row_${row._primaryKeyValue}`"
91
+ :record="row"
92
+ :resource="resource"
93
+ :adminUser="coreStore.adminUser"
94
+ :meta="tableRowReplaceInjection ? tableRowReplaceInjection.meta : undefined"
95
+ @click="onClick($event, row)"
96
+ ref="rowRefs"
97
+ class="list-table-body-row bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
98
+ :class="{'border-b': rowI !== rows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
99
+ >
93
100
  <td class="w-4 p-4 cursor-default sticky-column bg-lightListTable dark:bg-darkListTable" @click="(e)=>e.stopPropagation()">
94
101
  <Checkbox
95
102
  :model-value="checkboxesInternal.includes(row._primaryKeyValue)"
@@ -210,7 +217,7 @@
210
217
  </div>
211
218
 
212
219
  </td>
213
- </tr>
220
+ </component>
214
221
  </tbody>
215
222
  </table>
216
223
  </div>
@@ -328,7 +335,7 @@ import {
328
335
  } from '@iconify-prerendered/vue-flowbite';
329
336
  import router from '@/router';
330
337
  import { Tooltip } from '@/afcl';
331
- import type { AdminForthResourceCommon, AdminForthResourceColumnInputCommon, AdminForthResourceColumnCommon } from '@/types/Common';
338
+ import type { AdminForthResourceCommon, AdminForthResourceColumnInputCommon, AdminForthResourceColumnCommon, AdminForthComponentDeclaration } from '@/types/Common';
332
339
  import adminforth from '@/adminforth';
333
340
  import Checkbox from '@/afcl/Checkbox.vue';
334
341
 
@@ -345,6 +352,7 @@ const props = defineProps<{
345
352
  noRoundings?: boolean,
346
353
  customActionsInjection?: any[],
347
354
  tableBodyStartInjection?: any[],
355
+ tableRowReplaceInjection?: AdminForthComponentDeclaration,
348
356
  }>();
349
357
 
350
358
  // emits, update page
@@ -93,14 +93,21 @@
93
93
  </tr>
94
94
 
95
95
  <!-- Visible rows -->
96
- <tr @click="onClick($event,row)"
97
- v-for="(row, rowI) in visibleRows"
98
- :key="`row_${row._primaryKeyValue}`"
99
- ref="rowRefs"
100
- class="list-table-body-row bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
101
- :class="{'border-b': rowI !== visibleRows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
102
- @mounted="(el: any) => updateRowHeight(`row_${row._primaryKeyValue}`, el.offsetHeight)"
103
- >
96
+ <component
97
+ v-else
98
+ v-for="(row, rowI) in visibleRows"
99
+ :is="tableRowReplaceInjection ? getCustomComponent(tableRowReplaceInjection) : 'tr'"
100
+ :key="`row_${row._primaryKeyValue}`"
101
+ :record="row"
102
+ :resource="resource"
103
+ :adminUser="coreStore.adminUser"
104
+ :meta="tableRowReplaceInjection ? tableRowReplaceInjection.meta : undefined"
105
+ @click="onClick($event, row)"
106
+ ref="rowRefs"
107
+ class="list-table-body-row bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
108
+ :class="{'border-b': rowI !== visibleRows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
109
+ @mounted="(el: any) => updateRowHeight(`row_${row._primaryKeyValue}`, el.offsetHeight)"
110
+ >
104
111
  <td class="w-4 p-4 cursor-default sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading" @click="(e)=>e.stopPropagation()">
105
112
  <Checkbox
106
113
  :model-value="checkboxesInternal.includes(row._primaryKeyValue)"
@@ -224,7 +231,7 @@
224
231
  </template>
225
232
  </div>
226
233
  </td>
227
- </tr>
234
+ </component>
228
235
 
229
236
  <!-- Bottom spacer -->
230
237
  <tr v-if="totalHeight > 0">
@@ -350,7 +357,7 @@ import {
350
357
  } from '@iconify-prerendered/vue-flowbite';
351
358
  import router from '@/router';
352
359
  import { Tooltip } from '@/afcl';
353
- import type { AdminForthResourceCommon, AdminForthResourceColumnCommon } from '@/types/Common';
360
+ import type { AdminForthResourceCommon, AdminForthResourceColumnCommon, AdminForthComponentDeclaration } from '@/types/Common';
354
361
  import adminforth from '@/adminforth';
355
362
  import Checkbox from '@/afcl/Checkbox.vue';
356
363
 
@@ -370,6 +377,7 @@ const props = defineProps<{
370
377
  containerHeight?: number,
371
378
  itemHeight?: number,
372
379
  bufferSize?: number,
380
+ tableRowReplaceInjection?: AdminForthComponentDeclaration
373
381
  }>();
374
382
 
375
383
  // emits, update page
@@ -14,9 +14,21 @@
14
14
  aria-label="Sidebar"
15
15
  >
16
16
  <div class="h-full px-3 pb-20 md:pb-4 bg-lightSidebar dark:bg-darkSidebar border-r border-lightSidebarBorder dark:border-darkSidebarBorder pt-4" :class="{'sidebar-scroll':!isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering)}">
17
- <div class="af-logo-title-wrapper flex relative transition-all duration-300 ease-in-out h-8 items-center" :class="{'mb-4': isSidebarIconOnly && !isSidebarHovering, 'mx-4 mb-4': !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering)}">
18
- <img :src="loadFile(coreStore.config?.brandLogo || '@/assets/logo.svg')" :alt="`${ coreStore.config?.brandName } Logo`" class="af-logo h-8 me-3" :class="{ 'hidden': !(coreStore.config?.showBrandLogoInSidebar !== false && (!iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))) }" />
19
- <img :src="loadFile(coreStore.config?.iconOnlySidebar?.logo || '')" :alt="`${ coreStore.config?.brandName } Logo`" class="af-sidebar-icon-only-logo h-8 me-3" :class="{ 'hidden': !(coreStore.config?.showBrandLogoInSidebar !== false && coreStore.config?.iconOnlySidebar?.logo && iconOnlySidebarEnabled && isSidebarIconOnly && !isSidebarHovering) }" />
17
+ <div
18
+ class="af-logo-title-wrapper flex relative transition-all duration-300 ease-in-out h-8 items-center"
19
+ :class="{
20
+ 'mb-4': isSidebarIconOnly && !isSidebarHovering, 'mx-4 mb-4': !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering),
21
+ 'justify-center': !(coreStore.config?.showBrandLogoInSidebar !== false && (!iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering)))
22
+ }"
23
+ >
24
+ <img
25
+ :src="loadFile(coreStore.config?.brandLogo || '@/assets/logo.svg')"
26
+ :alt="`${ coreStore.config?.brandName } Logo`"
27
+ class="af-logo h-8 me-3"
28
+ :class="{
29
+ 'hidden': !(coreStore.config?.showBrandLogoInSidebar !== false && (!iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))) }"
30
+ />
31
+ <img :src="loadFile(coreStore.config?.iconOnlySidebar?.logo || '')" :alt="`${ coreStore.config?.brandName } Logo`" class="af-sidebar-icon-only-logo h-8" :class="{ 'hidden': !(coreStore.config?.showBrandLogoInSidebar !== false && coreStore.config?.iconOnlySidebar?.logo && iconOnlySidebarEnabled && isSidebarIconOnly && !isSidebarHovering) }" />
20
32
  <span
21
33
  v-if="coreStore.config?.showBrandNameInSidebar && (!iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))"
22
34
  class="af-title self-center text-lightNavbarText-size font-semibold sm:text-lightNavbarText-size whitespace-nowrap dark:text-darkSidebarText text-lightSidebarText"
@@ -1,7 +1,8 @@
1
1
  <template >
2
- <template v-if="threeDotsDropdownItems?.length || customActions?.length || (bulkActions?.some((action: AdminForthBulkActionCommon) => action.showInThreeDotsDropdown))">
2
+ <div class="relative" v-if="threeDotsDropdownItems?.length || customActions?.length || (bulkActions?.some((action: AdminForthBulkActionCommon) => action.showInThreeDotsDropdown))">
3
3
  <button
4
- data-dropdown-toggle="listThreeDotsDropdown"
4
+ ref="buttonTriggerRef"
5
+ @click="toggleDropdownVisibility"
5
6
  class="flex items-center py-2 px-2 text-sm font-medium text-lightThreeDotsMenuIconDots focus:outline-none bg-lightThreeDotsMenuIconBackground rounded border border-lightThreeDotsMenuIconBackgroundBorder hover:bg-lightThreeDotsMenuIconBackgroundHover hover:text-lightThreeDotsMenuIconDotsHover focus:z-10 focus:ring-4 focus:ring-lightThreeDotsMenuIconFocus dark:focus:ring-darkThreeDotsMenuIconFocus dark:bg-darkThreeDotsMenuIconBackground dark:text-darkThreeDotsMenuIconDots dark:border-darkThreeDotsMenuIconBackgroundBorder dark:hover:text-darkThreeDotsMenuIconDotsHover dark:hover:bg-darkThreeDotsMenuIconBackgroundHover rounded-default"
6
7
  >
7
8
  <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 4 15">
@@ -11,8 +12,9 @@
11
12
 
12
13
  <!-- Dropdown menu -->
13
14
  <div
14
- id="listThreeDotsDropdown"
15
- class="z-30 hidden bg-lightThreeDotsMenuBodyBackground divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-darkThreeDotsMenuBodyBackground dark:divide-gray-600">
15
+ ref="dropdownRef"
16
+ :class="{'hidden': !showDropdown, 'block': showDropdown }"
17
+ class="absolute z-30 right-0 mt-3 bg-lightThreeDotsMenuBodyBackground divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-darkThreeDotsMenuBodyBackground dark:divide-gray-600">
16
18
  <ul class="py-2 text-sm text-lightThreeDotsMenuBodyText dark:text-darkThreeDotsMenuBodyText" aria-labelledby="dropdownMenuIconButton">
17
19
  <li v-for="(item, i) in threeDotsDropdownItems" :key="`dropdown-item-${i}`">
18
20
  <a href="#"
@@ -71,7 +73,7 @@
71
73
  </li>
72
74
  </ul>
73
75
  </div>
74
- </template>
76
+ </div>
75
77
  </template>
76
78
 
77
79
 
@@ -82,7 +84,7 @@ import adminforth from '@/adminforth';
82
84
  import { callAdminForthApi } from '@/utils';
83
85
  import { useRoute, useRouter } from 'vue-router';
84
86
  import CallActionWrapper from '@/components/CallActionWrapper.vue'
85
- import { ref, type ComponentPublicInstance } from 'vue';
87
+ import { ref, type ComponentPublicInstance, onMounted, onUnmounted } from 'vue';
86
88
  import type { AdminForthBulkActionCommon, AdminForthComponentDeclarationFull } from '@/types/Common';
87
89
  import type { AdminForthActionInput } from '@/types/Back';
88
90
 
@@ -91,6 +93,9 @@ const route = useRoute();
91
93
  const coreStore = useCoreStore();
92
94
  const router = useRouter();
93
95
  const threeDotsDropdownItemsRefs = ref<Array<ComponentPublicInstance | null>>([]);
96
+ const showDropdown = ref(false);
97
+ const dropdownRef = ref<HTMLElement | null>(null);
98
+ const buttonTriggerRef = ref<HTMLElement | null>(null);
94
99
 
95
100
  const props = defineProps({
96
101
  threeDotsDropdownItems: Array<AdminForthComponentDeclarationFull>,
@@ -169,6 +174,7 @@ async function handleActionClick(action: AdminForthActionInput, payload: any) {
169
174
  function startBulkAction(actionId: string) {
170
175
  adminforth.list.closeThreeDotsDropdown();
171
176
  emit('startBulkAction', actionId);
177
+ showDropdown.value = false;
172
178
  }
173
179
 
174
180
  async function injectedComponentClick(index: number) {
@@ -176,5 +182,27 @@ async function injectedComponentClick(index: number) {
176
182
  if (componentRef && 'click' in componentRef) {
177
183
  (componentRef as any).click?.();
178
184
  }
185
+ showDropdown.value = false;
179
186
  }
187
+
188
+ function toggleDropdownVisibility() {
189
+ showDropdown.value = !showDropdown.value;
190
+ }
191
+
192
+ function handleClickOutside(e: MouseEvent) {
193
+ if (!dropdownRef.value) return
194
+
195
+ if (!dropdownRef.value.contains(e.target as Node) && !buttonTriggerRef.value?.contains(e.target as Node)) {
196
+ showDropdown.value = false;
197
+ }
198
+ }
199
+
200
+ onMounted(() => {
201
+ document.addEventListener('mousedown', handleClickOutside)
202
+ })
203
+
204
+ onUnmounted(() => {
205
+ document.removeEventListener('mousedown', handleClickOutside)
206
+ })
207
+
180
208
  </script>
@@ -117,7 +117,7 @@ import timezone from 'dayjs/plugin/timezone';
117
117
  import {checkEmptyValues} from '@/utils';
118
118
  import { useRoute, useRouter } from 'vue-router';
119
119
  import { JsonViewer } from "vue3-json-viewer";
120
- import "vue3-json-viewer/dist/vue3-json-viewer.css";
120
+ import "vue3-json-viewer/dist/index.css";
121
121
  import type { AdminForthResourceColumnCommon } from '@/types/Common';
122
122
 
123
123
  import { useCoreStore } from '@/stores/core';
@@ -17,6 +17,7 @@ export const useCoreStore = defineStore('core', () => {
17
17
  const resource: Ref<AdminForthResourceCommon | null> = ref(null);
18
18
  const userData: Ref<UserData | null> = ref(null);
19
19
  const isResourceFetching = ref(false);
20
+ const isInternetError = ref(false);
20
21
 
21
22
  const resourceColumnsWithFilters = computed(() => {
22
23
  if (!resource.value) {
@@ -221,6 +222,10 @@ export const useCoreStore = defineStore('core', () => {
221
222
  return userData.value && userFullnameField && userData.value[userFullnameField];
222
223
  })
223
224
 
225
+ const userAvatarUrl = computed(() => {
226
+ return userData.value?.userAvatarUrl || null;
227
+ });
228
+
224
229
  const isIos = computed(() => {
225
230
  return (
226
231
  /iPad|iPhone|iPod/.test(navigator.userAgent) ||
@@ -234,6 +239,7 @@ export const useCoreStore = defineStore('core', () => {
234
239
  menu,
235
240
  username,
236
241
  userFullname,
242
+ userAvatarUrl,
237
243
  getPublicConfig,
238
244
  fetchMenuAndResource,
239
245
  getLoginFormConfig,
@@ -251,6 +257,7 @@ export const useCoreStore = defineStore('core', () => {
251
257
  resetAdminUser,
252
258
  resetResource,
253
259
  isResourceFetching,
254
- isIos
260
+ isIos,
261
+ isInternetError,
255
262
  }
256
263
  })
@@ -14,6 +14,11 @@ export const useFiltersStore = defineStore('filters', () => {
14
14
  return sort.value;
15
15
  }
16
16
  const setFilter = (filter: any) => {
17
+ const index = filters.value.findIndex(f => f.field === filter.field);
18
+ if (filters.value[index]) {
19
+ filters.value[index] = filter;
20
+ return;
21
+ }
17
22
  filters.value.push(filter);
18
23
  }
19
24
  const setFilters = (f: any) => {