adminforth 2.13.18 → 2.14.1

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.
@@ -13,47 +13,47 @@
13
13
  "i18n:extract": "echo {} > i18n-empty.json && vue-i18n-extract report --vueFiles \"./src/**/*.{js,vue,ts}\" --output ./i18n-messages.json --languageFiles \"i18n-empty.json\" --add"
14
14
  },
15
15
  "dependencies": {
16
- "@iconify-prerendered/vue-flag": "^0.28.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
+ "@unhead/vue": "^1.9.12",
19
+ "@vueuse/core": "^10.10.0",
20
20
  "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",
21
+ "dayjs": "^1.11.11",
22
+ "debounce": "^2.1.0",
23
+ "flowbite-datepicker": "^1.2.6",
24
+ "javascript-time-ago": "^2.5.11",
25
+ "pinia": "^2.1.7",
26
+ "sanitize-html": "^2.13.0",
27
+ "unhead": "^1.9.12",
28
28
  "uuid": "^10.0.0",
29
- "vue": "^3.5.22",
29
+ "vue": "^3.5.12",
30
30
  "vue-diff": "^1.2.4",
31
- "vue-i18n": "^10.0.8",
32
- "vue-router": "^4.6.3",
31
+ "vue-i18n": "^10.0.5",
32
+ "vue-router": "^4.3.0",
33
33
  "vue-slider-component": "^4.1.0-beta.7"
34
34
  },
35
35
  "devDependencies": {
36
- "@rushstack/eslint-patch": "^1.14.1",
37
- "@tsconfig/node20": "^20.1.6",
38
- "@types/node": "^20.19.24",
39
- "@vitejs/plugin-vue": "^5.2.4",
36
+ "@rushstack/eslint-patch": "^1.8.0",
37
+ "@tsconfig/node20": "^20.1.4",
38
+ "@types/node": "^20.12.5",
39
+ "@vitejs/plugin-vue": "^5.0.4",
40
40
  "@vue/eslint-config-typescript": "^13.0.0",
41
- "@vue/tsconfig": "^0.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",
41
+ "@vue/tsconfig": "^0.5.1",
42
+ "autoprefixer": "^10.4.19",
43
+ "eslint": "^8.57.0",
44
+ "eslint-plugin-vue": "^9.23.0",
45
+ "flag-icons": "^7.2.3",
46
46
  "flowbite": "^3.1.2",
47
- "i18n-iso-countries": "^7.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": "^5.4.21",
47
+ "i18n-iso-countries": "^7.12.0",
48
+ "npm-run-all2": "^6.1.2",
49
+ "portfinder": "^1.0.32",
50
+ "postcss": "^8.4.38",
51
+ "sass": "^1.77.2",
52
+ "tailwindcss": "^3.4.17",
53
+ "typescript": "~5.4.0",
54
+ "vite": "^5.2.13",
55
55
  "vue-i18n-extract": "^2.0.7",
56
- "vue-tsc": "^2.2.12",
57
- "vue3-json-viewer": "^2.4.1"
56
+ "vue-tsc": "^2.0.11",
57
+ "vue3-json-viewer": "^2.2.2"
58
58
  }
59
59
  }
@@ -73,7 +73,7 @@
73
73
  </nav>
74
74
 
75
75
  <Sidebar
76
- v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady && defaultLayout && !headerOnlyLayout"
76
+ v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady && defaultLayout && !headerOnlyLayout && coreStore.menu.length > 0"
77
77
  :sideBarOpen="sideBarOpen"
78
78
  :forceIconOnly="route.meta?.sidebarAndHeader === 'preferIconOnly'"
79
79
  @hideSidebar="hideSidebar"
@@ -182,8 +182,6 @@ initFrontedAPI()
182
182
 
183
183
  createHead()
184
184
  const sideBarOpen = ref(false);
185
- const defaultLayout = ref(true);
186
- const headerOnlyLayout = ref(false);
187
185
  const route = useRoute();
188
186
  const router = useRouter();
189
187
  const publicConfigLoaded = ref(false);
@@ -197,6 +195,14 @@ const isSidebarIconOnly = ref(localStorage.getItem('afIconOnlySidebar') === 'tru
197
195
 
198
196
  const loggedIn = computed(() => !!coreStore?.adminUser);
199
197
 
198
+ const defaultLayout = computed(() => {
199
+ return route.meta?.sidebarAndHeader !== 'none';
200
+ });
201
+
202
+ const headerOnlyLayout = computed(() => {
203
+ return route.meta?.sidebarAndHeader === 'headerOnly';
204
+ });
205
+
200
206
  const expandedWidth = computed(() => coreStore.config?.iconOnlySidebar?.expandedSidebarWidth || '16.5rem');
201
207
 
202
208
  const theme = ref('light');
@@ -308,19 +314,6 @@ async function loadMenu() {
308
314
  loginRedirectCheckIsReady.value = true;
309
315
  }
310
316
 
311
- function handleCustomLayout() {
312
- if (route.meta?.sidebarAndHeader === 'none') {
313
- defaultLayout.value = false;
314
- } else if (route.meta?.sidebarAndHeader === 'preferIconOnly') {
315
- defaultLayout.value = true;
316
- isSidebarIconOnly.value = true;
317
- } else if (route.meta?.sidebarAndHeader === 'headerOnly') {
318
- headerOnlyLayout.value = true;
319
- } else {
320
- defaultLayout.value = true;
321
- }
322
- }
323
-
324
317
  function humanizeSnake(str: string): string {
325
318
  if (!str) {
326
319
  return '';
@@ -345,10 +338,11 @@ watch(title, (title) => {
345
338
  document.title = title;
346
339
  })
347
340
 
348
- watch([route, () => coreStore.resourceById, () => coreStore.config], async () => {
349
- handleCustomLayout()
350
- await new Promise((resolve) => setTimeout(resolve, 0));
351
-
341
+ watch(route, () => {
342
+ // Handle preferIconOnly layout
343
+ if (route.meta?.sidebarAndHeader === 'preferIconOnly') {
344
+ isSidebarIconOnly.value = true;
345
+ }
352
346
  });
353
347
 
354
348
 
@@ -376,7 +370,6 @@ onMounted(async () => {
376
370
  loadPublicConfig(); // and this
377
371
  // before init flowbite we have to wait router initialized because it affects dom(our v-ifs) and fetch menu
378
372
  await initRouter();
379
- handleCustomLayout();
380
373
 
381
374
  adminforth.menu.refreshMenuBadges = async () => {
382
375
  await coreStore.fetchMenuBadges();
@@ -22,7 +22,7 @@
22
22
  v-if="headerCloseButton"
23
23
  type="button"
24
24
  class="text-lightDialogCloseButton bg-transparent hover:bg-lightDialogCloseButtonHoverBackground hover:text-lightDialogCloseButtonHover rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:text-darkDialogCloseButton dark:hover:bg-darkDialogCloseButtonHoverBackground dark:hover:text-darkDialogCloseButtonHover"
25
- @click="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>
@@ -17,7 +17,7 @@
17
17
  aria-describedby="helper-text-explanation"
18
18
  class="afcl-input inline-flex bg-lightInputBackground border border-lightInputBorder rounded-0 focus:ring-lightPrimary focus:border-lightPrimary dark:focus:ring-darkPrimary dark:focus:border-darkPrimary
19
19
  blue-500 focus:border-blue-500 block w-20 p-2.5 dark:bg-darkInputBackground dark:border-darkInputBorder placeholder-lightInputPlaceholderText dark:placeholder-darkInputPlaceholderText dark:text-darkInputText translate-y-0"
20
- :class="{'rounded-l-md': !$slots.prefix && !prefix, 'rounded-r-md': !$slots.suffix && !suffix, 'w-full': fullWidth, 'text-base': isIOSDevice, 'text-sm': !isIOSDevice }"
20
+ :class="{'rounded-l-md': !$slots.prefix && !prefix, 'rounded-r-md': !$slots.suffix && !suffix, 'w-full': fullWidth, 'text-base': isIos, 'text-sm': !isIos }"
21
21
  :disabled="readonly"
22
22
  >
23
23
 
@@ -36,7 +36,10 @@
36
36
  <script setup lang="ts">
37
37
 
38
38
  import { ref } from 'vue';
39
- const isIOSDevice = isIOS();
39
+ import { useCoreStore } from '@/stores/core';
40
+
41
+ const coreStore = useCoreStore();
42
+ const isIos = coreStore.isIos;
40
43
 
41
44
  const props = defineProps<{
42
45
  type: string,
@@ -53,12 +56,5 @@ defineExpose({
53
56
  focus: () => input.value?.focus(),
54
57
  });
55
58
 
56
- function isIOS() {
57
- return (
58
- /iPad|iPhone|iPod/.test(navigator.userAgent) ||
59
- (navigator.userAgent.includes('Mac') && 'ontouchend' in document)
60
- )
61
- }
62
-
63
59
  </script>
64
60
 
@@ -265,11 +265,11 @@ onMounted(() => {
265
265
 
266
266
  watch(() => props.modelValue, (value) => {
267
267
  updateFromProps();
268
- });
268
+ }, {deep: true});
269
269
 
270
270
  watch(() => props.options, () => {
271
271
  updateFromProps();
272
- });
272
+ }, { deep: true });
273
273
 
274
274
  addClickListener();
275
275
 
@@ -1,31 +1,35 @@
1
- <template>
2
-
3
- <textarea
4
- ref="input"
5
- class="bg-lightInputBackground border border-lightInputBorder text-lightInputText placeholder-lightInputPlaceholderText text-sm rounded-lg block w-full p-2.5 dark:bg-darkInputBackground dark:border-darkInputBorder dark:placeholder-darkInputPlaceholderText dark:text-darkInputText dark:border-darkInputBorder focus:ring-lightInputFocusRing focus:border-lightInputFocusBorder dark:focus:ring-darkInputFocusRing dark:focus:border-darkInputFocusBorder"
6
- :placeholder="placeholder"
7
- :value="modelValue"
8
- @input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
9
- :readonly="readonly"
10
- />
11
-
1
+ <template>
2
+ <textarea
3
+ ref="input"
4
+ class="afcl-textarea bg-lightInputBackground border border-lightInputBorder text-lightInputText placeholder-lightInputPlaceholderText text-sm rounded-md block w-full p-2.5 dark:bg-darkInputBackground dark:border-darkInputBorder dark:placeholder-darkInputPlaceholderText dark:text-darkInputText dark:border-darkInputBorder focus:ring-lightInputFocusRing focus:border-lightInputFocusBorder dark:focus:ring-darkInputFocusRing dark:focus:border-darkInputFocusBorder"
5
+ :class="`${readonly ? 'opacity-50' : ''} ${isIos ? 'text-md' : 'text-sm'}`"
6
+ :placeholder="placeholder"
7
+ :value="modelValue"
8
+ @input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
9
+ :readonly="readonly"
10
+ />
12
11
  </template>
13
12
 
14
13
  <script setup lang="ts">
15
-
16
14
  import { ref } from 'vue';
15
+ import { useCoreStore } from '@/stores/core';
16
+
17
+ const coreStore = useCoreStore();
18
+ const isIos = coreStore.isIos;
17
19
 
18
20
  const props = defineProps<{
19
- modelValue: string,
20
- readonly?: boolean,
21
- placeholder?: string,
21
+ modelValue: string
22
+ readonly?: boolean
23
+ placeholder?: string
22
24
  }>()
23
25
 
24
- const input = ref<HTMLInputElement | null>(null)
26
+ const emit = defineEmits<{
27
+ (e: 'update:modelValue', value: string): void
28
+ }>()
29
+
30
+ const input = ref<HTMLTextAreaElement | null>(null)
25
31
 
26
32
  defineExpose({
27
33
  focus: () => input.value?.focus(),
28
- });
29
-
34
+ })
30
35
  </script>
31
-
@@ -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"
@@ -21,7 +21,7 @@
21
21
 
22
22
  <div class="ms-3 text-sm font-normal max-w-xs pr-2" v-if="toast.messageHtml" v-html="toast.messageHtml"></div>
23
23
  <div class="ms-3 text-sm font-normal max-w-xs pr-2" v-else>
24
- <div class="flex flex-col items-center justify-center">
24
+ <div class="flex flex-col items-center justify-center break-all">
25
25
  {{toast.message}}
26
26
  <div v-if="toast.buttons" class="flex justify-center mt-2 gap-2">
27
27
  <div v-for="button in toast.buttons" class="af-toast-button rounded-md bg-lightButtonsBackground hover:bg-lightButtonsHover text-lightButtonsText dark:bg-darkPrimary dark:hover:bg-darkButtonsBackground dark:text-darkButtonsText">
@@ -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';
@@ -221,6 +221,12 @@ export const useCoreStore = defineStore('core', () => {
221
221
  return userData.value && userFullnameField && userData.value[userFullnameField];
222
222
  })
223
223
 
224
+ const isIos = computed(() => {
225
+ return (
226
+ /iPad|iPhone|iPod/.test(navigator.userAgent) ||
227
+ (navigator.userAgent.includes('Mac') && 'ontouchend' in document)
228
+ )});
229
+
224
230
 
225
231
  return {
226
232
  config,
@@ -245,5 +251,6 @@ export const useCoreStore = defineStore('core', () => {
245
251
  resetAdminUser,
246
252
  resetResource,
247
253
  isResourceFetching,
254
+ isIos
248
255
  }
249
256
  })
@@ -504,6 +504,7 @@ export interface AdminForthResourceInputCommon {
504
504
  threeDotsDropdownItems?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
505
505
  customActionIcons?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
506
506
  tableBodyStart?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
507
+ tableRowReplace?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
507
508
  },
508
509
 
509
510
  /**
@@ -36,7 +36,20 @@ export async function callApi({path, method, body, headers}: {
36
36
  if (r.status == 401 ) {
37
37
  useUserStore().unauthorize();
38
38
  useCoreStore().resetAdminUser();
39
- await router.push({ name: 'login' });
39
+ const currentPath = router.currentRoute.value.path;
40
+ const homeRoute = router.getRoutes().find(route => route.name === 'home');
41
+ const homePagePath = (homeRoute?.redirect as string) || '/';
42
+ let next = '';
43
+ if (currentPath !== '/login' && currentPath !== homePagePath) {
44
+ if (Object.keys(router.currentRoute.value.query).length > 0) {
45
+ next = currentPath + '?' + Object.entries(router.currentRoute.value.query).map(([key, value]) => `${key}=${value}`).join('&');
46
+ } else {
47
+ next = currentPath;
48
+ }
49
+ await router.push({ name: 'login', query: { next: next } });
50
+ } else {
51
+ await router.push({ name: 'login' });
52
+ }
40
53
  return null;
41
54
  }
42
55
  return await r.json();
@@ -100,6 +113,7 @@ export const loadFile = (file: string) => {
100
113
  return baseUrl;
101
114
  }
102
115
 
116
+
103
117
  export function checkEmptyValues(value: any, viewType: 'show' | 'list' ) {
104
118
  const config: CoreConfig | {} | null = useCoreStore().config;
105
119
  let emptyFieldPlaceholder = '';
@@ -143,6 +143,9 @@
143
143
  ? [coreStore.resourceOptions.pageInjections.list.tableBodyStart]
144
144
  : []
145
145
  "
146
+ :tableRowReplaceInjection="Array.isArray(coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace)
147
+ ? coreStore.resourceOptions.pageInjections.list.tableRowReplace[0]
148
+ : coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace || undefined"
146
149
  :container-height="1100"
147
150
  :item-height="52.5"
148
151
  :buffer-size="listBufferSize"
@@ -173,6 +176,9 @@
173
176
  ? [coreStore.resourceOptions.pageInjections.list.tableBodyStart]
174
177
  : []
175
178
  "
179
+ :tableRowReplaceInjection="Array.isArray(coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace)
180
+ ? coreStore.resourceOptions.pageInjections.list.tableRowReplace[0]
181
+ : coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace || undefined"
176
182
  />
177
183
 
178
184
  <component
@@ -199,6 +199,9 @@ async function login() {
199
199
  if (resp.error) {
200
200
  error.value = resp.error;
201
201
  } else if (resp.redirectTo) {
202
+ error.value = null;
203
+ user.authorize();
204
+ await coreStore.fetchMenuAndResource();
202
205
  router.push(resp.redirectTo);
203
206
  } else {
204
207
  error.value = null;
@@ -1,12 +1,12 @@
1
1
  <template>
2
- <section class="bg-white dark:bg-gray-900">
2
+ <section class="flex flex-col items-center justify-center">
3
3
  <div class="py-8 px-4 mx-auto max-w-screen-xl lg:py-16 lg:px-6">
4
4
  <div class="mx-auto max-w-screen-sm text-center">
5
5
  <h1 class="mb-4 text-7xl tracking-tight font-extrabold lg:text-9xl text-lightPrimary dark:text-darkPrimary">404</h1>
6
6
  <p class="mb-4 text-3xl tracking-tight font-bold text-gray-900 md:text-4xl dark:text-white">{{ $t("Something's missing.") }}</p>
7
7
  <p class="mb-4 text-lg font-light text-gray-500 dark:text-gray-400">{{ $t("Sorry, we can't find that page. You'll find lots to explore on the home page.") }} </p>
8
8
  <div class="flex justify-center">
9
- <LinkButton to="/">{{ $t('Go back home') }}</LinkButton>
9
+ <LinkButton to="/">{{ $t('Go back home') }}</LinkButton>
10
10
  </div>
11
11
  </div>
12
12
  </div>
@@ -7,5 +7,5 @@
7
7
  {
8
8
  "path": "./tsconfig.app.json"
9
9
  }
10
- ],
10
+ ]
11
11
  }