adminforth 2.17.0-next.7 → 2.17.0-next.71

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 (120) hide show
  1. package/commands/callTsProxy.js +2 -1
  2. package/commands/createApp/templates/.env.local.hbs +3 -0
  3. package/commands/createApp/templates/adminuser.ts.hbs +2 -1
  4. package/commands/createApp/templates/index.ts.hbs +3 -2
  5. package/commands/createCustomComponent/main.js +0 -3
  6. package/commands/createCustomComponent/templates/customCrud/afterBreadcrumbs.vue.hbs +4 -2
  7. package/commands/createCustomComponent/templates/customCrud/beforeActionButtons.vue.hbs +3 -2
  8. package/commands/createCustomComponent/templates/customCrud/beforeBreadcrumbs.vue.hbs +4 -2
  9. package/commands/createCustomComponent/templates/customCrud/bottom.vue.hbs +4 -2
  10. package/commands/createCustomComponent/templates/customCrud/threeDotsDropdownItems.vue.hbs +4 -2
  11. package/commands/createPlugin/templates/index.ts.hbs +4 -0
  12. package/dist/auth.d.ts +2 -2
  13. package/dist/auth.d.ts.map +1 -1
  14. package/dist/auth.js +17 -10
  15. package/dist/auth.js.map +1 -1
  16. package/dist/basePlugin.d.ts +1 -0
  17. package/dist/basePlugin.d.ts.map +1 -1
  18. package/dist/basePlugin.js +4 -2
  19. package/dist/basePlugin.js.map +1 -1
  20. package/dist/dataConnectors/baseConnector.d.ts +4 -0
  21. package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  22. package/dist/dataConnectors/baseConnector.js +103 -14
  23. package/dist/dataConnectors/baseConnector.js.map +1 -1
  24. package/dist/dataConnectors/clickhouse.d.ts +2 -0
  25. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  26. package/dist/dataConnectors/clickhouse.js +15 -4
  27. package/dist/dataConnectors/clickhouse.js.map +1 -1
  28. package/dist/dataConnectors/mongo.d.ts +8 -1
  29. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  30. package/dist/dataConnectors/mongo.js +72 -28
  31. package/dist/dataConnectors/mongo.js.map +1 -1
  32. package/dist/dataConnectors/mysql.d.ts +2 -0
  33. package/dist/dataConnectors/mysql.d.ts.map +1 -1
  34. package/dist/dataConnectors/mysql.js +22 -23
  35. package/dist/dataConnectors/mysql.js.map +1 -1
  36. package/dist/dataConnectors/postgres.d.ts +2 -0
  37. package/dist/dataConnectors/postgres.d.ts.map +1 -1
  38. package/dist/dataConnectors/postgres.js +23 -26
  39. package/dist/dataConnectors/postgres.js.map +1 -1
  40. package/dist/dataConnectors/sqlite.d.ts +2 -0
  41. package/dist/dataConnectors/sqlite.d.ts.map +1 -1
  42. package/dist/dataConnectors/sqlite.js +19 -19
  43. package/dist/dataConnectors/sqlite.js.map +1 -1
  44. package/dist/index.d.ts +21 -40
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +77 -54
  47. package/dist/index.js.map +1 -1
  48. package/dist/modules/codeInjector.d.ts.map +1 -1
  49. package/dist/modules/codeInjector.js +45 -63
  50. package/dist/modules/codeInjector.js.map +1 -1
  51. package/dist/modules/configValidator.d.ts.map +1 -1
  52. package/dist/modules/configValidator.js +14 -9
  53. package/dist/modules/configValidator.js.map +1 -1
  54. package/dist/modules/logger.d.ts +5 -0
  55. package/dist/modules/logger.d.ts.map +1 -0
  56. package/dist/modules/logger.js +14 -0
  57. package/dist/modules/logger.js.map +1 -0
  58. package/dist/modules/restApi.d.ts.map +1 -1
  59. package/dist/modules/restApi.js +31 -24
  60. package/dist/modules/restApi.js.map +1 -1
  61. package/dist/modules/socketBroker.d.ts.map +1 -1
  62. package/dist/modules/socketBroker.js +6 -5
  63. package/dist/modules/socketBroker.js.map +1 -1
  64. package/dist/modules/styles.js +1 -1
  65. package/dist/servers/express.d.ts +2 -2
  66. package/dist/servers/express.d.ts.map +1 -1
  67. package/dist/servers/express.js +20 -12
  68. package/dist/servers/express.js.map +1 -1
  69. package/dist/spa/package-lock.json +0 -13
  70. package/dist/spa/package.json +0 -1
  71. package/dist/spa/src/App.vue +6 -3
  72. package/dist/spa/src/adminforth.ts +60 -1
  73. package/dist/spa/src/afcl/DatePicker.vue +0 -1
  74. package/dist/spa/src/afcl/Dialog.vue +2 -2
  75. package/dist/spa/src/afcl/Dropzone.vue +6 -4
  76. package/dist/spa/src/afcl/Select.vue +1 -1
  77. package/dist/spa/src/afcl/Table.vue +7 -5
  78. package/dist/spa/src/afcl/Tooltip.vue +38 -4
  79. package/dist/spa/src/components/ColumnValueInput.vue +14 -1
  80. package/dist/spa/src/components/CustomDateRangePicker.vue +0 -2
  81. package/dist/spa/src/components/CustomRangePicker.vue +9 -6
  82. package/dist/spa/src/components/Filters.vue +4 -4
  83. package/dist/spa/src/components/ListActionsThreeDots.vue +235 -0
  84. package/dist/spa/src/components/MenuLink.vue +4 -0
  85. package/dist/spa/src/components/ResourceForm.vue +4 -4
  86. package/dist/spa/src/components/ResourceListTable.vue +30 -16
  87. package/dist/spa/src/components/ResourceListTableVirtual.vue +34 -18
  88. package/dist/spa/src/components/Sidebar.vue +4 -2
  89. package/dist/spa/src/components/ThreeDotsMenu.vue +35 -20
  90. package/dist/spa/src/components/Toast.vue +9 -10
  91. package/dist/spa/src/composables/useFrontendApi.ts +8 -4
  92. package/dist/spa/src/renderers/CompactField.vue +3 -2
  93. package/dist/spa/src/renderers/CompactUUID.vue +3 -2
  94. package/dist/spa/src/stores/core.ts +3 -2
  95. package/dist/spa/src/stores/filters.ts +1 -1
  96. package/dist/spa/src/stores/toast.ts +1 -2
  97. package/dist/spa/src/types/Back.ts +419 -18
  98. package/dist/spa/src/types/Common.ts +7 -14
  99. package/dist/spa/src/types/FrontendAPI.ts +27 -11
  100. package/dist/spa/src/utils/index.ts +2 -0
  101. package/dist/spa/src/utils/listUtils.ts +90 -0
  102. package/dist/spa/src/{utils.ts → utils/utils.ts} +18 -12
  103. package/dist/spa/src/views/CreateView.vue +39 -45
  104. package/dist/spa/src/views/EditView.vue +28 -31
  105. package/dist/spa/src/views/ListView.vue +38 -96
  106. package/dist/spa/src/views/SettingsView.vue +3 -2
  107. package/dist/spa/src/views/ShowView.vue +7 -6
  108. package/dist/types/Back.d.ts +371 -44
  109. package/dist/types/Back.d.ts.map +1 -1
  110. package/dist/types/Back.js +6 -0
  111. package/dist/types/Back.js.map +1 -1
  112. package/dist/types/Common.d.ts +8 -15
  113. package/dist/types/Common.d.ts.map +1 -1
  114. package/dist/types/Common.js +2 -0
  115. package/dist/types/Common.js.map +1 -1
  116. package/dist/types/FrontendAPI.d.ts +34 -11
  117. package/dist/types/FrontendAPI.d.ts.map +1 -1
  118. package/dist/types/FrontendAPI.js.map +1 -1
  119. package/package.json +4 -1
  120. package/commands/createCustomComponent/templates/customCrud/saveButton.vue.hbs +0 -28
@@ -196,7 +196,7 @@ import { getCustomComponent } from '@/utils';
196
196
  import Toast from './components/Toast.vue';
197
197
  import {useToastStore} from '@/stores/toast';
198
198
  import { initFrontedAPI } from '@/adminforth';
199
- import adminforth from '@/adminforth';
199
+ import { useAdminforth } from '@/adminforth';
200
200
  import UserMenuSettingsButton from './components/UserMenuSettingsButton.vue';
201
201
  import { Tooltip } from '@/afcl'
202
202
 
@@ -204,6 +204,9 @@ const coreStore = useCoreStore();
204
204
  const toastStore = useToastStore();
205
205
  const userStore = useUserStore();
206
206
 
207
+ const { menu } = useAdminforth();
208
+ let { closeUserMenuDropdown } = useAdminforth();
209
+
207
210
  initFrontedAPI()
208
211
 
209
212
  createHead()
@@ -310,7 +313,7 @@ watch(dropdownUserButton, (dropdownUserButton) => {
310
313
  document.querySelector('#dropdown-user') as HTMLElement,
311
314
  document.querySelector('[data-dropdown-toggle="dropdown-user"]') as HTMLElement,
312
315
  );
313
- adminforth.closeUserMenuDropdown = () => {
316
+ closeUserMenuDropdown = () => {
314
317
  dd.hide();
315
318
  }
316
319
  }
@@ -329,7 +332,7 @@ onMounted(async () => {
329
332
  // before init flowbite we have to wait router initialized because it affects dom(our v-ifs) and fetch menu
330
333
  await initRouter();
331
334
 
332
- adminforth.menu.refreshMenuBadges = async () => {
335
+ menu.refreshMenuBadges = async () => {
333
336
  await coreStore.fetchMenuBadges();
334
337
  }
335
338
 
@@ -19,11 +19,12 @@ class FrontendAPI implements FrontendAPIInterface {
19
19
  public modalStore:any
20
20
  public filtersStore:any
21
21
  public coreStore:any
22
+ private saveInterceptors: Record<string, Array<(ctx: { action: 'create'|'edit'; values: any; resource: any; resourceId: string; }) => Promise<{ ok: boolean; error?: string | null; extra?: object; }>>> = {};
22
23
 
23
24
  public list: {
24
25
  refresh(): Promise<{ error? : string }>;
25
26
  silentRefresh(): Promise<{ error? : string }>;
26
- silentRefreshRow(): Promise<{ error? : string }>;
27
+ silentRefreshRow(pk: any): Promise<{ error? : string }>;
27
28
  closeThreeDotsDropdown(): Promise<{ error? : string }>;
28
29
  closeUserMenuDropdown: () => void;
29
30
  setFilter: (filter: FilterParams) => void;
@@ -84,6 +85,49 @@ class FrontendAPI implements FrontendAPIInterface {
84
85
  }
85
86
  }
86
87
 
88
+ registerSaveInterceptor(
89
+ handler: (ctx: { action: 'create'|'edit'; values: any; resource: any; }) => Promise<{ ok: boolean; error?: string | null; extra?: object; }>,
90
+ ): void {
91
+ const rid = router.currentRoute.value?.params?.resourceId as string;
92
+ if (!rid) {
93
+ return;
94
+ }
95
+ if (!this.saveInterceptors[rid]) {
96
+ this.saveInterceptors[rid] = [];
97
+ }
98
+ this.saveInterceptors[rid].push(handler);
99
+ }
100
+
101
+ async runSaveInterceptors(params: { action: 'create'|'edit'; values: any; resource: any; resourceId: string; }): Promise<{ ok: boolean; error?: string | null; extra?: object; }> {
102
+ const list = this.saveInterceptors[params.resourceId] || [];
103
+ const aggregatedExtra: Record<string, any> = {};
104
+ for (const fn of list) {
105
+ try {
106
+ const res = await fn(params);
107
+ if (typeof res !== 'object' || typeof res.ok !== 'boolean') {
108
+ return { ok: false, error: 'Invalid interceptor return value' };
109
+ }
110
+ if (!res.ok) {
111
+ return { ok: false, error: res.error ?? 'Interceptor failed' };
112
+ }
113
+ if (res.extra) {
114
+ Object.assign(aggregatedExtra, res.extra);
115
+ }
116
+ } catch (e: any) {
117
+ return { ok: false, error: e?.message || String(e) };
118
+ }
119
+ }
120
+ return { ok: true, extra: aggregatedExtra };
121
+ }
122
+
123
+ clearSaveInterceptors(resourceId?: string): void {
124
+ if (resourceId) {
125
+ delete this.saveInterceptors[resourceId];
126
+ } else {
127
+ this.saveInterceptors = {};
128
+ }
129
+ }
130
+
87
131
  confirm(params: ConfirmParams): Promise<boolean> {
88
132
  return new Promise((resolve, reject) => {
89
133
  this.modalStore.setModalContent({
@@ -180,5 +224,20 @@ export function initFrontedAPI() {
180
224
  api.filtersStore = useFiltersStore();
181
225
  }
182
226
 
227
+ export function useAdminforth() {
228
+ const api = frontendAPI as FrontendAPI;
229
+ return {
230
+ registerSaveInterceptor: (handler: (ctx: { action: 'create'|'edit'; values: any; resource: any; }) => Promise<{ ok: boolean; error?: string | null; extra?: object; }>) => api.registerSaveInterceptor(handler),
231
+ alert: (params: AlertParams) => api.alert(params),
232
+ confirm: (params: ConfirmParams) => api.confirm(params),
233
+ list: api.list,
234
+ show: api.show,
235
+ menu: api.menu,
236
+ closeUserMenuDropdown: () => api.closeUserMenuDropdown(),
237
+ runSaveInterceptors: (params: { action: 'create'|'edit'; values: any; resource: any; resourceId: string; }) => api.runSaveInterceptors(params),
238
+ clearSaveInterceptors: (resourceId?: string) => api.clearSaveInterceptors(resourceId),
239
+ };
240
+ }
241
+
183
242
 
184
243
  export default frontendAPI;
@@ -134,7 +134,6 @@ onMounted(() => {
134
134
  })
135
135
 
136
136
  watch(start, () => {
137
- //console.log('⚡ emit', start.value)
138
137
  emit('update:valueStart', start.value)
139
138
  })
140
139
 
@@ -55,14 +55,14 @@
55
55
  <!-- Confirmation Modal -->
56
56
  <div
57
57
  v-if="showConfirmationOnClose"
58
- class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-60"
58
+ class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-[60]"
59
59
  >
60
60
  <div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-lg max-w-sm w-full">
61
61
  <h2 class="text-lg font-semibold mb-4 text-lightDialogHeaderText dark:text-darkDialogHeaderText">Confirm Close</h2>
62
62
  <p class="mb-6 text-lightDialogBodyText dark:text-darkDialogBodyText">{{ props.closeConfirmationText }}</p>
63
63
  <div class="flex justify-end">
64
64
  <Button
65
- class="me-3"
65
+ class="me-3 !bg-gray-50 dark:!bg-gray-700 !text-lightDialogBodyText dark:!text-darkDialogBodyText hover:!bg-gray-100 dark:hover:!bg-gray-600 !border-gray-200 dark:!border-gray-600"
66
66
  @click="showConfirmationOnClose = false"
67
67
  >
68
68
  Cancel
@@ -100,7 +100,9 @@ import { humanifySize } from '@/utils';
100
100
  import { ref, type Ref, computed } from 'vue';
101
101
  import { IconFileSolid } from '@iconify-prerendered/vue-flowbite';
102
102
  import { watch } from 'vue';
103
- import adminforth from '@/adminforth';
103
+ import { useAdminforth } from '@/adminforth';
104
+
105
+ const { alert } = useAdminforth();
104
106
 
105
107
  const props = defineProps<{
106
108
  extensions: string[],
@@ -175,7 +177,7 @@ function doEmit(filesIn: FileList) {
175
177
  );
176
178
 
177
179
  if (isDuplicate) {
178
- adminforth.alert({
180
+ alert({
179
181
  message: `The file "${file.name}" is already selected.`,
180
182
  variant: 'warning',
181
183
  });
@@ -183,14 +185,14 @@ function doEmit(filesIn: FileList) {
183
185
  }
184
186
 
185
187
  if (!allowedExtensions.includes(`.${extension}`)) {
186
- adminforth.alert({
188
+ alert({
187
189
  message: `Sorry, the file type .${extension} is not allowed. Please upload a file with one of the following extensions: ${allowedExtensions.join(', ')}`,
188
190
  variant: 'danger',
189
191
  });
190
192
  return;
191
193
  }
192
194
  if (size > maxSizeBytes) {
193
- adminforth.alert({
195
+ alert({
194
196
  message: `Sorry, the file size ${humanifySize(size)} exceeds the maximum allowed size of ${humanifySize(maxSizeBytes)}.`,
195
197
  variant: 'danger',
196
198
  });
@@ -38,7 +38,7 @@
38
38
  </div>
39
39
  </div>
40
40
  <teleport to="body" v-if="(teleportToBody || teleportToTop) && showDropdown">
41
- <div ref="dropdownEl" :style="getDropdownPosition" :class="{'shadow-none': isTop, 'z-10': teleportToBody, 'z-[1000]': teleportToTop}"
41
+ <div ref="dropdownEl" :style="getDropdownPosition" :class="{'shadow-none': isTop, 'z-30': teleportToBody, 'z-[1000]': teleportToTop}"
42
42
  class="fixed w-full bg-lightDropdownOptionsBackground shadow-lg dark:shadow-black dark:bg-darkDropdownOptionsBackground
43
43
  dark:border-gray-600 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm max-h-48"
44
44
  @scroll="handleDropdownScroll">
@@ -1,8 +1,7 @@
1
1
  <template>
2
- <div class="afcl-table-container relative overflow-x-auto shadow-md rounded-lg">
3
- <div class="overflow-x-auto w-full">
2
+ <div class="afcl-table-container relative overflow-x-auto overflow-y-auto shadow-md rounded-lg">
4
3
  <table class="afcl-table w-full text-sm text-left rtl:text-right text-lightTableText dark:text-darkTableText overflow-x-auto">
5
- <thead class="afcl-table-thread text-xs text-lightTableHeadingText uppercase bg-lightTableHeadingBackground dark:bg-darkTableHeadingBackground dark:text-darkTableHeadingText">
4
+ <thead class="afcl-table-thread z-40 text-xs text-lightTableHeadingText uppercase bg-lightTableHeadingBackground dark:bg-darkTableHeadingBackground dark:text-darkTableHeadingText" :class="makeHeaderSticky ? 'sticky top-0' : ''">
6
5
  <tr>
7
6
  <th
8
7
  scope="col"
@@ -79,10 +78,11 @@
79
78
  </tr>
80
79
  </tbody>
81
80
  </table>
82
- </div>
83
81
  <nav class="afcl-table-pagination-container bg-lightTableBackground dark:bg-darkTableBackground mt-2 flex flex-col gap-2 items-center sm:flex-row justify-center sm:justify-between px-4 pb-4"
84
82
  v-if="totalPages > 1"
85
- :aria-label="$t('Table navigation')">
83
+ :aria-label="$t('Table navigation')"
84
+ :class="makePaginationSticky ? 'sticky bottom-0 pt-4' : ''"
85
+ >
86
86
  <i18n-t
87
87
  keypath="Showing {from} to {to} of {total}" tag="span" class="afcl-table-pagination-text text-sm font-normal text-center text-lightTablePaginationText dark:text-darkTablePaginationText sm:mb-4 md:mb-0 block w-full md:inline md:w-auto"
88
88
  >
@@ -169,6 +169,8 @@
169
169
  isLoading?: boolean,
170
170
  defaultSortField?: string,
171
171
  defaultSortDirection?: 'asc' | 'desc',
172
+ makeHeaderSticky?: boolean,
173
+ makePaginationSticky?: boolean,
172
174
  }>(), {
173
175
  evenHighlights: true,
174
176
  pageSize: 5,
@@ -9,9 +9,7 @@
9
9
  ref="tooltip"
10
10
  >
11
11
  <slot name="tooltip"></slot>
12
- <div class="tooltip-arrow absolute -top-2" data-popper-arrow>
13
- <div class="absolute top-0 -left-0.5 w-0 h-0 border-l-8 border-r-8 border-b-8 border-l-transparent border-r-transparent border-b-lightTooltipBackground dark:border-b-darkTooltipBackground"></div>
14
- </div>
12
+ <div class="tooltip-arrow" data-popper-arrow></div>
15
13
  </div>
16
14
  </teleport>
17
15
  </template>
@@ -50,4 +48,40 @@ function mouseOff() {
50
48
  showTooltip.value = false;
51
49
  }
52
50
 
53
- </script>
51
+ </script>
52
+
53
+ <style>
54
+ .tooltip .tooltip-arrow,
55
+ .tooltip .tooltip-arrow::before {
56
+ position: absolute;
57
+ width: 8px;
58
+ height: 8px;
59
+ background: inherit;
60
+ }
61
+
62
+ .tooltip .tooltip-arrow {
63
+ visibility: hidden;
64
+ }
65
+
66
+ .tooltip .tooltip-arrow::before {
67
+ visibility: visible;
68
+ content: '';
69
+ transform: rotate(45deg);
70
+ }
71
+
72
+ .tooltip[data-popper-placement^='top'] > .tooltip-arrow {
73
+ bottom: -4px;
74
+ }
75
+
76
+ .tooltip[data-popper-placement^='bottom'] > .tooltip-arrow {
77
+ top: -4px;
78
+ }
79
+
80
+ .tooltip[data-popper-placement^='left'] > .tooltip-arrow {
81
+ right: -4px;
82
+ }
83
+
84
+ .tooltip[data-popper-placement^='right'] > .tooltip-arrow {
85
+ left: -4px;
86
+ }
87
+ </style>
@@ -86,7 +86,20 @@
86
86
  :readonly="(column.editReadonly && source === 'edit') || readonly"
87
87
  />
88
88
  <Input
89
- v-else-if="['decimal', 'float'].includes(type || column.type)"
89
+ v-else-if="(type || column.type) === 'decimal'"
90
+ ref="input"
91
+ type="number"
92
+ inputmode="decimal"
93
+ class="w-40"
94
+ placeholder="0.0"
95
+ :fullWidth="true"
96
+ :prefix="column.inputPrefix"
97
+ :suffix="column.inputSuffix"
98
+ :modelValue="String(value)"
99
+ @update:modelValue="$emit('update:modelValue', String($event))"
100
+ />
101
+ <Input
102
+ v-else-if="(type || column.type) === 'float'"
90
103
  ref="input"
91
104
  type="number"
92
105
  step="0.1"
@@ -197,12 +197,10 @@ onMounted(() => {
197
197
  })
198
198
 
199
199
  watch(start, () => {
200
- //console.log('⚡ emit', start.value)
201
200
  emit('update:valueStart', start.value)
202
201
  })
203
202
 
204
203
  watch(end, () => {
205
- //console.log('⚡ emit', end.value)
206
204
  emit('update:valueEnd', end.value)
207
205
  })
208
206
 
@@ -53,9 +53,6 @@ const emit = defineEmits(['update:valueStart', 'update:valueEnd']);
53
53
  const minFormatted = computed(() => Math.floor(<number>props.min));
54
54
  const maxFormatted = computed(() => Math.ceil(<number>props.max));
55
55
 
56
- const isChanged = computed(() => {
57
- return start.value && start.value !== minFormatted.value || end.value && end.value !== maxFormatted.value;
58
- });
59
56
 
60
57
  const start = ref<string | number>(props.valueStart);
61
58
  const end = ref<string | number>(props.valueEnd);
@@ -92,17 +89,23 @@ function updateEndFromProps() {
92
89
  }
93
90
 
94
91
  watch(start, () => {
95
- console.log('⚡ emit', start.value)
96
92
  emit('update:valueStart', start.value)
97
93
  })
98
94
 
99
95
  watch(end, () => {
100
- console.log('⚡ emit', end.value)
101
96
  emit('update:valueEnd', end.value);
102
97
  })
103
98
 
104
99
  watch([minFormatted,maxFormatted], () => {
105
- setSliderValues(minFormatted.value, maxFormatted.value)
100
+ if ( !start.value && end.value ) {
101
+ setSliderValues(minFormatted.value, end.value);
102
+ } else if ( start.value && !end.value ) {
103
+ setSliderValues(start.value, maxFormatted.value);
104
+ } else if ( !start.value && !end.value ) {
105
+ setSliderValues(minFormatted.value, maxFormatted.value);
106
+ } else {
107
+ setSliderValues(start.value, end.value);
108
+ }
106
109
  })
107
110
 
108
111
  function setSliderValues(start: any, end: any) {
@@ -123,9 +123,9 @@
123
123
  :min="getFilterMinValue(c.name)"
124
124
  :max="getFilterMaxValue(c.name)"
125
125
  :valueStart="getFilterItem({ column: c, operator: 'gte' })"
126
- @update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? $event : undefined })"
126
+ @update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? (c.type === 'decimal' ? String($event) : $event) : undefined })"
127
127
  :valueEnd="getFilterItem({ column: c, operator: 'lte' })"
128
- @update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? $event : undefined })"
128
+ @update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? (c.type === 'decimal' ? String($event) : $event) : undefined })"
129
129
  />
130
130
 
131
131
  <div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
@@ -133,14 +133,14 @@
133
133
  type="number"
134
134
  aria-describedby="helper-text-explanation"
135
135
  :placeholder="$t('From')"
136
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? $event : undefined })"
136
+ @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? (c.type === 'decimal' ? String($event) : $event) : undefined })"
137
137
  :modelValue="getFilterItem({ column: c, operator: 'gte' })"
138
138
  />
139
139
  <Input
140
140
  type="number"
141
141
  aria-describedby="helper-text-explanation"
142
142
  :placeholder="$t('To')"
143
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? $event : undefined })"
143
+ @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? (c.type === 'decimal' ? String($event) : $event) : undefined })"
144
144
  :modelValue="getFilterItem({ column: c, operator: 'lte' })"
145
145
  />
146
146
  </div>
@@ -0,0 +1,235 @@
1
+ <template>
2
+ <div class="relative inline-block">
3
+ <div
4
+ ref="triggerRef"
5
+ class="border border-gray-300 dark:border-gray-700 dark:border-opacity-0 border-opacity-0 hover:border-opacity-100 dark:hover:border-opacity-100 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
6
+ @click="toggleMenu"
7
+ >
8
+ <IconDotsHorizontalOutline class="w-6 h-6 text-lightPrimary dark:text-darkPrimary" />
9
+ </div>
10
+ <teleport to="body">
11
+ <div
12
+ v-if="showMenu"
13
+ ref="menuRef"
14
+ class="z-50 bg-white dark:bg-gray-900 rounded-md shadow-lg border dark:border-gray-700 py-1"
15
+ :style="menuStyles"
16
+ >
17
+ <template v-if="!resourceOptions?.baseActionsAsQuickIcons || (resourceOptions?.baseActionsAsQuickIcons && !resourceOptions?.baseActionsAsQuickIcons.includes('show'))">
18
+ <RouterLink
19
+ v-if="resourceOptions?.allowedActions?.show"
20
+ class="flex text-nowrap p-1 hover:bg-gray-100 dark:hover:bg-gray-800 w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300"
21
+ :to="{
22
+ name: 'resource-show',
23
+ params: {
24
+ resourceId: props.resourceId,
25
+ primaryKey: record._primaryKeyValue,
26
+ }
27
+ }"
28
+
29
+ >
30
+ <IconEyeSolid class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"/>
31
+ {{ $t('Show item') }}
32
+ </RouterLink>
33
+ </template>
34
+ <template v-if="!resourceOptions?.baseActionsAsQuickIcons || (resourceOptions?.baseActionsAsQuickIcons && !resourceOptions?.baseActionsAsQuickIcons.includes('edit'))">
35
+ <RouterLink
36
+ v-if="resourceOptions?.allowedActions?.edit"
37
+ class="flex text-nowrap p-1 hover:bg-gray-100 dark:hover:bg-gray-800 w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300"
38
+ :to="{
39
+ name: 'resource-edit',
40
+ params: {
41
+ resourceId: props.resourceId,
42
+ primaryKey: record._primaryKeyValue,
43
+ }
44
+ }"
45
+ >
46
+ <IconPenSolid class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"/>
47
+ {{ $t('Edit item') }}
48
+ </RouterLink>
49
+ </template>
50
+ <template v-if="!resourceOptions?.baseActionsAsQuickIcons || (resourceOptions?.baseActionsAsQuickIcons && !resourceOptions?.baseActionsAsQuickIcons.includes('delete'))">
51
+ <button
52
+ v-if="resourceOptions?.allowedActions?.delete"
53
+ class="flex text-nowrap p-1 hover:bg-gray-100 dark:hover:bg-gray-800 w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300"
54
+ @click="deleteRecord(record)"
55
+ >
56
+ <IconTrashBinSolid class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"/>
57
+ {{ $t('Delete item') }}
58
+ </button>
59
+ </template>
60
+ <div v-for="action in (resourceOptions.actions ?? []).filter(a => a.showIn?.listThreeDotsMenu)" :key="action.id" >
61
+ <button class="flex text-nowrap p-1 hover:bg-gray-100 dark:hover:bg-gray-800 w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300" @click="() => { startCustomAction(action.id, record); showMenu = false; }">
62
+ <component
63
+ :is="action.customComponent ? getCustomComponent(action.customComponent) : CallActionWrapper"
64
+ :meta="action.customComponent?.meta"
65
+ :row="record"
66
+ :resource="resource"
67
+ :adminUser="adminUser"
68
+ @callAction="(payload? : Object) => startCustomAction(action.id, record, payload)"
69
+ >
70
+ <component
71
+ v-if="action.icon"
72
+ :is="getIcon(action.icon)"
73
+ class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"
74
+ />
75
+ {{ $t(action.name) }}
76
+ </component>
77
+ </button>
78
+ </div>
79
+ <template v-if="customActionIconsThreeDotsMenuItems">
80
+ <component
81
+ v-for="c in customActionIconsThreeDotsMenuItems"
82
+ :is="getCustomComponent(c)"
83
+ :meta="c.meta"
84
+ :resource="coreStore.resource"
85
+ :adminUser="coreStore.adminUser"
86
+ :record="record"
87
+ :updateRecords="props.updateRecords"
88
+ />
89
+ </template>
90
+ </div>
91
+ </teleport>
92
+ </div>
93
+ </template>
94
+
95
+ <script lang="ts" setup>
96
+ import {
97
+ IconEyeSolid,
98
+ IconPenSolid,
99
+ IconTrashBinSolid,
100
+ IconDotsHorizontalOutline
101
+ } from '@iconify-prerendered/vue-flowbite';
102
+ import { onMounted, onBeforeUnmount, ref, nextTick, watch } from 'vue';
103
+ import { getIcon, getCustomComponent } from '@/utils';
104
+ import { useCoreStore } from '@/stores/core';
105
+ import CallActionWrapper from '@/components/CallActionWrapper.vue'
106
+
107
+ const coreStore = useCoreStore();
108
+ const showMenu = ref(false);
109
+ const triggerRef = ref<HTMLElement | null>(null);
110
+ const menuRef = ref<HTMLElement | null>(null);
111
+ const menuStyles = ref<Record<string, string>>({});
112
+
113
+ const props = defineProps<{
114
+ resourceOptions: any;
115
+ record: any;
116
+ customActionIconsThreeDotsMenuItems: any[];
117
+ resourceId: string;
118
+ deleteRecord: (record: any) => void;
119
+ updateRecords: () => void;
120
+ startCustomAction: (actionId: string, record: any) => void;
121
+ }>();
122
+
123
+ onMounted(() => {
124
+ window.addEventListener('scroll', handleScrollOrResize, true);
125
+ window.addEventListener('resize', handleScrollOrResize);
126
+ document.addEventListener('click', handleOutsideClick, true);
127
+ });
128
+
129
+ onBeforeUnmount(() => {
130
+ window.removeEventListener('scroll', handleScrollOrResize, true);
131
+ window.removeEventListener('resize', handleScrollOrResize);
132
+ document.removeEventListener('click', handleOutsideClick, true);
133
+ });
134
+
135
+ watch(showMenu, async (isOpen) => {
136
+ if (isOpen) {
137
+ await nextTick();
138
+ // First pass: after DOM mount
139
+ updateMenuPosition();
140
+ // Second pass: after layout/paint to catch width changes (fonts/icons)
141
+ requestAnimationFrame(() => {
142
+ updateMenuPosition();
143
+ // Final safety: one micro-delay retry if width was still 0
144
+ setTimeout(() => updateMenuPosition(), 0);
145
+ });
146
+ }
147
+ });
148
+
149
+ function toggleMenu() {
150
+ if (!showMenu.value) {
151
+ // Provisional position to avoid flashing at left:0 on first open
152
+ const el = triggerRef.value;
153
+ if (el) {
154
+ const rect = el.getBoundingClientRect();
155
+ const gap = 8;
156
+ menuStyles.value = {
157
+ position: 'fixed',
158
+ top: `${Math.round(rect.bottom)}px`,
159
+ left: `${Math.round(rect.left)}px`,
160
+ };
161
+ }
162
+ }
163
+ showMenu.value = !showMenu.value;
164
+ }
165
+
166
+ function updateMenuPosition() {
167
+ const el = triggerRef.value;
168
+ if (!el) return;
169
+ const rect = el.getBoundingClientRect();
170
+ const margin = 8; // gap around the trigger/menu
171
+ const menuEl = menuRef.value;
172
+ // Measure current menu size to align and decide flipping
173
+ let menuWidth = rect.width; // fallback to trigger width
174
+ let menuHeight = 0;
175
+ if (menuEl) {
176
+ const menuRect = menuEl.getBoundingClientRect();
177
+ // Prefer bounding rect; fallback to offset/scroll width if needed
178
+ const measuredW = menuRect.width || menuEl.offsetWidth || menuEl.scrollWidth;
179
+ if (measuredW > 0) menuWidth = measuredW;
180
+ const measuredH = menuRect.height || menuEl.offsetHeight || menuEl.scrollHeight;
181
+ if (measuredH > 0) menuHeight = measuredH;
182
+ }
183
+ // Right-align: right edge of menu == right edge of trigger
184
+ let left = rect.right - menuWidth;
185
+ // Clamp within viewport with small margin so it doesn't render off-screen
186
+ const minLeft = margin;
187
+ const maxLeft = Math.max(minLeft, window.innerWidth - margin - menuWidth);
188
+ left = Math.min(Math.max(left, minLeft), maxLeft);
189
+
190
+ // Determine whether to place above or below based on available space
191
+ const spaceBelow = window.innerHeight - rect.bottom - margin;
192
+ const spaceAbove = rect.top - margin;
193
+ const maxMenuHeight = Math.max(0, window.innerHeight - 2 * margin);
194
+
195
+ let top: number;
196
+ if (menuHeight === 0) {
197
+ // Unknown height yet (first pass). Prefer placing below; a subsequent pass will correct if needed.
198
+ top = rect.bottom + margin;
199
+ } else if (menuHeight <= spaceBelow) {
200
+ // Enough space below
201
+ top = rect.bottom + margin;
202
+ } else if (menuHeight <= spaceAbove) {
203
+ // Not enough below but enough above -> flip
204
+ top = rect.top - margin - menuHeight;
205
+ } else {
206
+ // Not enough space on either side: pick the side with more room and clamp within viewport
207
+ if (spaceBelow >= spaceAbove) {
208
+ top = Math.min(rect.bottom + margin, window.innerHeight - margin - menuHeight);
209
+ } else {
210
+ top = Math.max(margin, rect.top - margin - menuHeight);
211
+ }
212
+ }
213
+
214
+ menuStyles.value = {
215
+ position: 'fixed',
216
+ top: `${Math.round(top)}px`,
217
+ left: `${Math.round(left)}px`,
218
+ maxHeight: `${Math.round(maxMenuHeight)}px`,
219
+ overflowY: 'auto',
220
+ };
221
+ }
222
+
223
+ function handleScrollOrResize() {
224
+ showMenu.value = false;
225
+ }
226
+
227
+ function handleOutsideClick(e: MouseEvent) {
228
+ const target = e.target as Node | null;
229
+ if (!target) return;
230
+ if (menuRef.value?.contains(target)) return;
231
+ if (triggerRef.value?.contains(target)) return;
232
+ showMenu.value = false;
233
+ }
234
+
235
+ </script>
@@ -16,6 +16,9 @@
16
16
  class="min-w-5 min-h-5 text-lightSidebarIcons dark:text-darkSidebarIcons group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover transition-all duration-200 ease-in-out"
17
17
  >
18
18
  </component>
19
+ <IconFileImageOutline v-else
20
+ class="min-w-5 min-h-5 text-lightSidebarIcons dark:text-darkSidebarIcons group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover transition-all duration-200 ease-in-out"
21
+ />
19
22
  <div
20
23
  class="overflow-hidden block ms-3 pr-4 text-left rtl:text-right transition-all duration-200 ease-in-out"
21
24
  :class="{
@@ -57,6 +60,7 @@ import { getIcon } from '@/utils';
57
60
  import { Tooltip } from '@/afcl';
58
61
  import { ref, watch, computed } from 'vue';
59
62
  import { useCoreStore } from '@/stores/core';
63
+ import { IconFileImageOutline } from '@iconify-prerendered/vue-flowbite';
60
64
 
61
65
  const props = defineProps(['item', 'isChild', 'isSidebarIconOnly', 'isSidebarHovering']);
62
66