adminforth 2.26.4 → 2.27.0-next.10

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 (58) hide show
  1. package/commands/createApp/templates/package.json.hbs +1 -1
  2. package/dist/modules/restApi.d.ts +1 -0
  3. package/dist/modules/restApi.d.ts.map +1 -1
  4. package/dist/modules/restApi.js +65 -3
  5. package/dist/modules/restApi.js.map +1 -1
  6. package/dist/modules/styles.js +2 -2
  7. package/dist/modules/styles.js.map +1 -1
  8. package/dist/servers/express.d.ts.map +1 -1
  9. package/dist/servers/express.js +7 -1
  10. package/dist/servers/express.js.map +1 -1
  11. package/dist/spa/package-lock.json +85 -7
  12. package/dist/spa/package.json +4 -1
  13. package/dist/spa/pnpm-lock.yaml +339 -299
  14. package/dist/spa/src/App.vue +1 -1
  15. package/dist/spa/src/adminforth.ts +17 -29
  16. package/dist/spa/src/afcl/Input.vue +1 -1
  17. package/dist/spa/src/afcl/Modal.vue +12 -1
  18. package/dist/spa/src/afcl/Select.vue +4 -2
  19. package/dist/spa/src/afcl/Table.vue +27 -13
  20. package/dist/spa/src/components/AcceptModal.vue +2 -0
  21. package/dist/spa/src/components/ColumnValueInputWrapper.vue +35 -4
  22. package/dist/spa/src/components/CustomRangePicker.vue +22 -67
  23. package/dist/spa/src/components/GroupsTable.vue +7 -4
  24. package/dist/spa/src/components/ListActionsThreeDots.vue +9 -8
  25. package/dist/spa/src/components/RangePicker.vue +236 -0
  26. package/dist/spa/src/components/ResourceForm.vue +100 -6
  27. package/dist/spa/src/components/ResourceListTable.vue +45 -70
  28. package/dist/spa/src/components/Sidebar.vue +1 -1
  29. package/dist/spa/src/components/ThreeDotsMenu.vue +54 -57
  30. package/dist/spa/src/i18n.ts +1 -1
  31. package/dist/spa/src/stores/core.ts +4 -2
  32. package/dist/spa/src/types/Back.ts +10 -3
  33. package/dist/spa/src/types/Common.ts +43 -8
  34. package/dist/spa/src/types/FrontendAPI.ts +6 -1
  35. package/dist/spa/src/types/adapters/StorageAdapter.ts +12 -0
  36. package/dist/spa/src/utils/createEditUtils.ts +65 -0
  37. package/dist/spa/src/utils/index.ts +2 -1
  38. package/dist/spa/src/utils/listUtils.ts +8 -2
  39. package/dist/spa/src/utils/utils.ts +192 -12
  40. package/dist/spa/src/utils.ts +2 -1
  41. package/dist/spa/src/views/CreateView.vue +32 -59
  42. package/dist/spa/src/views/EditView.vue +30 -47
  43. package/dist/spa/src/views/ListView.vue +119 -18
  44. package/dist/spa/src/views/LoginView.vue +13 -13
  45. package/dist/spa/src/views/ShowView.vue +67 -61
  46. package/dist/spa/tsconfig.app.json +1 -1
  47. package/dist/types/Back.d.ts +7 -4
  48. package/dist/types/Back.d.ts.map +1 -1
  49. package/dist/types/Back.js.map +1 -1
  50. package/dist/types/Common.d.ts +43 -8
  51. package/dist/types/Common.d.ts.map +1 -1
  52. package/dist/types/Common.js.map +1 -1
  53. package/dist/types/FrontendAPI.d.ts +13 -1
  54. package/dist/types/FrontendAPI.d.ts.map +1 -1
  55. package/dist/types/FrontendAPI.js.map +1 -1
  56. package/dist/types/adapters/StorageAdapter.d.ts +11 -0
  57. package/dist/types/adapters/StorageAdapter.d.ts.map +1 -1
  58. package/package.json +1 -1
@@ -0,0 +1,236 @@
1
+ <template>
2
+ <div class="range-slider" ref="trackRef" @mousedown="onTrackMouseDown">
3
+ <div class="track"></div>
4
+ <div class="range bg-lightPrimary/30" :style="rangeStyle"></div>
5
+
6
+ <div
7
+ class="bg-lightPrimary thumb"
8
+ :style="minThumbStyle"
9
+ @mousedown.stop.prevent="startDrag('min', $event)"
10
+ @mouseenter="minHovered = true"
11
+ @mouseleave="minHovered = false"
12
+ ></div>
13
+ <div v-if="minHovered || activeThumb === 'min'" class="thumb-tooltip" :style="minTooltipStyle">{{ minVal }}</div>
14
+
15
+ <div
16
+ class="bg-lightPrimary thumb"
17
+ :style="maxThumbStyle"
18
+ @mousedown.stop.prevent="startDrag('max', $event)"
19
+ @mouseenter="maxHovered = true"
20
+ @mouseleave="maxHovered = false"
21
+ ></div>
22
+ <div v-if="maxHovered || activeThumb === 'max'" class="thumb-tooltip" :style="maxTooltipStyle">{{ maxVal }}</div>
23
+
24
+ </div>
25
+ </template>
26
+
27
+ <script setup lang="ts">
28
+ import { computed, ref, watch, onBeforeUnmount } from 'vue'
29
+
30
+ const props = defineProps({
31
+ modelValue: {
32
+ type: Array as unknown as () => [number, number],
33
+ default: () => [0, 100]
34
+ },
35
+ min: { type: Number, default: 0 },
36
+ max: { type: Number, default: 100 },
37
+ dotSize: { type: Number, default: 20 },
38
+ height: { type: String, default: '8px' }
39
+ })
40
+
41
+ const emit = defineEmits(['update:modelValue'])
42
+
43
+ const trackRef = ref<HTMLElement | null>(null)
44
+
45
+ const minVal = ref(props.modelValue[0])
46
+ const maxVal = ref(props.modelValue[1])
47
+
48
+ watch(() => props.modelValue, (val) => {
49
+ if (!val) return
50
+ minVal.value = val[0]
51
+ maxVal.value = val[1]
52
+ })
53
+
54
+ function clamp(val: number) {
55
+ return Math.min(props.max, Math.max(props.min, val))
56
+ }
57
+
58
+ function valueToPercent(val: number) {
59
+ return ((val - props.min) / (props.max - props.min)) * 100
60
+ }
61
+
62
+ function percentToValue(percent: number) {
63
+ return props.min + ((props.max - props.min) * percent) / 100
64
+ }
65
+
66
+ const minPercent = computed(() => valueToPercent(minVal.value))
67
+ const maxPercent = computed(() => valueToPercent(maxVal.value))
68
+
69
+ const rangeStyle = computed(() => ({
70
+ left: `${minPercent.value}%`,
71
+ width: `${maxPercent.value - minPercent.value}%`,
72
+ transition: isAnimating.value ? 'left 0.18s ease, width 0.18s ease' : 'none'
73
+ }))
74
+
75
+ const minThumbStyle = computed(() => ({
76
+ left: `calc(${minPercent.value}% - ${props.dotSize / 2}px)`,
77
+ width: `${props.dotSize}px`,
78
+ height: `${props.dotSize}px`,
79
+ transition: isAnimating.value ? 'left 0.18s ease' : 'none',
80
+ zIndex: activeThumb.value === 'min' ? 3 : 2
81
+ }))
82
+
83
+ const maxThumbStyle = computed(() => ({
84
+ left: `calc(${maxPercent.value}% - ${props.dotSize / 2}px)`,
85
+ width: `${props.dotSize}px`,
86
+ height: `${props.dotSize}px`,
87
+ transition: isAnimating.value ? 'left 0.18s ease' : 'none',
88
+ zIndex: activeThumb.value === 'max' ? 3 : 2
89
+ }))
90
+
91
+ const minTooltipStyle = computed(() => ({
92
+ left: `${minPercent.value}%`,
93
+ transition: isAnimating.value ? 'left 0.18s ease' : 'none'
94
+ }))
95
+
96
+ const maxTooltipStyle = computed(() => ({
97
+ left: `${maxPercent.value}%`,
98
+ transition: isAnimating.value ? 'left 0.18s ease' : 'none'
99
+ }))
100
+
101
+ const activeThumb = ref<'min' | 'max' | null>(null)
102
+ const isAnimating = ref(false)
103
+ const minHovered = ref(false)
104
+ const maxHovered = ref(false)
105
+
106
+ function startDrag(type: 'min' | 'max', e: MouseEvent) {
107
+ activeThumb.value = type
108
+ document.addEventListener('mousemove', onMouseMove)
109
+ document.addEventListener('mouseup', stopDrag)
110
+ }
111
+
112
+ function onMouseMove(e: MouseEvent) {
113
+ if (!trackRef.value || !activeThumb.value) return
114
+
115
+ const rect = trackRef.value.getBoundingClientRect()
116
+ const percent = ((e.clientX - rect.left) / rect.width) * 100
117
+ const value = Math.round(clamp(percentToValue(percent)))
118
+
119
+ if (activeThumb.value === 'min') {
120
+ if (value > maxVal.value) {
121
+ // cross over: become the max thumb
122
+ minVal.value = maxVal.value
123
+ maxVal.value = value
124
+ activeThumb.value = 'max'
125
+ } else {
126
+ minVal.value = value
127
+ }
128
+ } else {
129
+ if (value < minVal.value) {
130
+ // cross over: become the min thumb
131
+ maxVal.value = minVal.value
132
+ minVal.value = value
133
+ activeThumb.value = 'min'
134
+ } else {
135
+ maxVal.value = value
136
+ }
137
+ }
138
+
139
+ emit('update:modelValue', [minVal.value, maxVal.value])
140
+ }
141
+
142
+ function stopDrag() {
143
+ document.removeEventListener('mousemove', onMouseMove)
144
+ document.removeEventListener('mouseup', stopDrag)
145
+ activeThumb.value = null
146
+ }
147
+
148
+ function onTrackMouseDown(e: MouseEvent) {
149
+ if (!trackRef.value) return
150
+
151
+ const rect = trackRef.value.getBoundingClientRect()
152
+ const percent = ((e.clientX - rect.left) / rect.width) * 100
153
+ const value = percentToValue(percent)
154
+
155
+ const distToMin = Math.abs(value - minVal.value)
156
+ const distToMax = Math.abs(value - maxVal.value)
157
+
158
+ isAnimating.value = true
159
+ if (distToMin < distToMax) {
160
+ minVal.value = Math.round(Math.min(clamp(value), maxVal.value))
161
+ } else {
162
+ maxVal.value = Math.round(Math.max(clamp(value), minVal.value))
163
+ }
164
+
165
+ emit('update:modelValue', [minVal.value, maxVal.value])
166
+
167
+ setTimeout(() => { isAnimating.value = false }, 200)
168
+ }
169
+
170
+ onBeforeUnmount(() => {
171
+ stopDrag()
172
+ })
173
+ </script>
174
+
175
+ <style scoped>
176
+ .range-slider {
177
+ position: relative;
178
+ width: 100%;
179
+ height: 20px;
180
+ display: flex;
181
+ align-items: center;
182
+ }
183
+
184
+ .track {
185
+ position: absolute;
186
+ width: 100%;
187
+ height: 8px;
188
+ background: #e5e7eb;
189
+ border-radius: 9999px;
190
+ }
191
+
192
+ .range {
193
+ position: absolute;
194
+ height: 8px;
195
+ border-radius: 9999px;
196
+ }
197
+
198
+ .thumb {
199
+ position: absolute;
200
+ top: 50%;
201
+ transform: translateY(-50%);
202
+ border-radius: 9999px;
203
+ cursor: pointer;
204
+ }
205
+
206
+ .thumb-tooltip {
207
+ position: absolute;
208
+ top: -28px;
209
+ transform: translateX(-50%);
210
+ background: rgba(0, 0, 0, 0.75);
211
+ color: #fff;
212
+ font-size: 14px;
213
+ font-weight: 500;
214
+ line-height: 1;
215
+ padding: 6px 6px;
216
+ border-radius: 4px;
217
+ white-space: nowrap;
218
+ pointer-events: none;
219
+ animation: tooltip-in 0.12s ease;
220
+ }
221
+
222
+ .thumb-tooltip::after {
223
+ content: '';
224
+ position: absolute;
225
+ top: 100%;
226
+ left: 50%;
227
+ transform: translateX(-50%);
228
+ border: 4px solid transparent;
229
+ border-top-color: rgba(0, 0, 0, 0.75);
230
+ }
231
+
232
+ @keyframes tooltip-in {
233
+ from { opacity: 0; transform: translateX(-50%) translateY(4px); }
234
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
235
+ }
236
+ </style>
@@ -13,11 +13,13 @@
13
13
  :mode="mode"
14
14
  :unmasked="unmasked"
15
15
  :columnOptions="columnOptions"
16
- :validating="validating"
16
+ :validatingMode="validatingMode"
17
17
  :columnError="columnError"
18
18
  :setCurrentValue="setCurrentValue"
19
19
  @update:customComponentsInValidity="(data) => customComponentsInValidity = { ...customComponentsInValidity, ...data }"
20
20
  @update:customComponentsEmptiness="(data) => customComponentsEmptiness = { ...customComponentsEmptiness, ...data }"
21
+ :columnsWithErrors="columnsWithErrors"
22
+ :isValidating="isValidating"
21
23
  />
22
24
  </div>
23
25
  <div v-else class="flex flex-col gap-4">
@@ -31,11 +33,13 @@
31
33
  :mode="mode"
32
34
  :unmasked="unmasked"
33
35
  :columnOptions="columnOptions"
34
- :validating="validating"
36
+ :validatingMode="validatingMode"
35
37
  :columnError="columnError"
36
38
  :setCurrentValue="setCurrentValue"
37
39
  @update:customComponentsInValidity="(data) => customComponentsInValidity = { ...customComponentsInValidity, ...data }"
38
40
  @update:customComponentsEmptiness="(data) => customComponentsEmptiness = { ...customComponentsEmptiness, ...data }"
41
+ :columnsWithErrors="columnsWithErrors"
42
+ :isValidating="isValidating"
39
43
  />
40
44
  </template>
41
45
  <div v-if="otherColumns?.length || 0 > 0">
@@ -48,11 +52,13 @@
48
52
  :mode="mode"
49
53
  :unmasked="unmasked"
50
54
  :columnOptions="columnOptions"
51
- :validating="validating"
55
+ :validatingMode="validatingMode"
52
56
  :columnError="columnError"
53
57
  :setCurrentValue="setCurrentValue"
54
58
  @update:customComponentsInValidity="(data) => customComponentsInValidity = { ...customComponentsInValidity, ...data }"
55
59
  @update:customComponentsEmptiness="(data) => customComponentsEmptiness = { ...customComponentsEmptiness, ...data }"
60
+ :columnsWithErrors="columnsWithErrors"
61
+ :isValidating="isValidating"
56
62
  />
57
63
  </div>
58
64
  </div>
@@ -71,16 +77,20 @@ import { useCoreStore } from "@/stores/core";
71
77
  import GroupsTable from '@/components/GroupsTable.vue';
72
78
  import { useI18n } from 'vue-i18n';
73
79
  import { type AdminForthResourceColumnCommon, type AdminForthResourceCommon } from '@/types/Common';
80
+ import { Mutex } from 'async-mutex';
81
+ import debounce from 'lodash.debounce';
74
82
 
75
83
  const { t } = useI18n();
76
84
 
85
+ const mutex = new Mutex();
86
+
77
87
  const coreStore = useCoreStore();
78
88
  const router = useRouter();
79
89
  const route = useRoute();
80
90
  const props = defineProps<{
81
91
  resource: AdminForthResourceCommon,
82
92
  record: any,
83
- validating: boolean,
93
+ validatingMode: boolean,
84
94
  source: 'create' | 'edit',
85
95
  readonlyColumns?: string[],
86
96
  }>();
@@ -99,6 +109,11 @@ const columnOptions = ref<Record<string, any[]>>({});
99
109
  const columnLoadingState = reactive<Record<string, { loading: boolean; hasMore: boolean }>>({});
100
110
  const columnOffsets = reactive<Record<string, number>>({});
101
111
  const columnEmptyResultsCount = reactive<Record<string, number>>({});
112
+ const columnsWithErrors = ref<Record<string, string>>({});
113
+ const isValidating = ref(false);
114
+ const blockSettingIsValidating = ref(false);
115
+ const isValid = ref(true);
116
+ const doesUserHaveCustomValidation = computed(() => props.resource.columns.some(column => column.validation && column.validation.some((val) => val.validator)));
102
117
 
103
118
  const columnError = (column: AdminForthResourceColumnCommon) => {
104
119
  const val = computed(() => {
@@ -329,10 +344,48 @@ const editableColumns = computed(() => {
329
344
  return props.resource?.columns?.filter(column => column.showIn?.[mode.value] && (currentValues.value ? checkShowIf(column, currentValues.value, props.resource.columns) : true));
330
345
  });
331
346
 
332
- const isValid = computed(() => {
333
- return editableColumns.value?.every(column => !columnError(column));
347
+ function checkIfColumnHasError(column: AdminForthResourceColumnCommon) {
348
+ const error = columnError(column);
349
+ if (error) {
350
+ columnsWithErrors.value[column.name] = error;
351
+ } else {
352
+ delete columnsWithErrors.value[column.name];
353
+ }
354
+ }
355
+
356
+ const checkIfAnyColumnHasErrors = () => {
357
+ return Object.keys(columnsWithErrors.value).length > 0 ? false : true;
358
+ }
359
+
360
+ const debouncedValidation = debounce(async (columns: AdminForthResourceColumnCommon[]) => {
361
+ await mutex.runExclusive(async () => {
362
+ await validateUsingUserValidationFunction(columns);
363
+ });
364
+ setIsValidatingValue(false);
365
+ isValid.value = checkIfAnyColumnHasErrors();
366
+ }, 500);
367
+
368
+ watch(() => [editableColumns.value, props.validatingMode], async () => {
369
+ setIsValidatingValue(true);
370
+
371
+ editableColumns.value?.forEach(column => {
372
+ checkIfColumnHasError(column);
373
+ });
374
+
375
+ if (props.validatingMode && doesUserHaveCustomValidation.value) {
376
+ debouncedValidation(editableColumns.value);
377
+ } else {
378
+ setIsValidatingValue(false);
379
+ isValid.value = checkIfAnyColumnHasErrors();
380
+ }
334
381
  });
335
382
 
383
+ const setIsValidatingValue = (value: boolean) => {
384
+ if (!blockSettingIsValidating.value) {
385
+ isValidating.value = value;
386
+ }
387
+ }
388
+
336
389
 
337
390
  const groups = computed(() => {
338
391
  let fieldGroupType;
@@ -381,9 +434,50 @@ watch(() => isValid.value, (value) => {
381
434
  emit('update:isValid', value);
382
435
  });
383
436
 
437
+ async function validateUsingUserValidationFunction(editableColumnsInner: AdminForthResourceColumnCommon[]): Promise<void> {
438
+ if (doesUserHaveCustomValidation.value) {
439
+ try {
440
+ blockSettingIsValidating.value = true;
441
+ const res = await callAdminForthApi({
442
+ method: 'POST',
443
+ path: '/validate_columns',
444
+ body: {
445
+ resourceId: props.resource.resourceId,
446
+ editableColumns: editableColumnsInner.map(col => {return {name: col.name, value: currentValues.value?.[col.name]} }),
447
+ record: currentValues.value,
448
+ }
449
+ })
450
+ if (res.validationResults && Object.keys(res.validationResults).length > 0) {
451
+ for (const [columnName, validationResult] of Object.entries(res.validationResults) as [string, any][]) {
452
+ if (!validationResult.isValid) {
453
+ columnsWithErrors.value[columnName] = validationResult.message || 'Invalid value';
454
+ } else {
455
+ delete columnsWithErrors.value[columnName];
456
+ }
457
+ }
458
+ const columnsToProcess = editableColumns.value.filter(col => res.validationResults[col.name] === undefined);
459
+ columnsToProcess.forEach(column => {
460
+ checkIfColumnHasError(column);
461
+ });
462
+ } else {
463
+ editableColumnsInner.forEach(column => {
464
+ checkIfColumnHasError(column);
465
+ });
466
+ }
467
+ blockSettingIsValidating.value = false;
468
+ } catch (e) {
469
+ console.error('Error during custom validation', e);
470
+ blockSettingIsValidating.value = false;
471
+ }
472
+ }
473
+ }
474
+
384
475
  defineExpose({
385
476
  columnError,
386
477
  editableColumns,
478
+ columnsWithErrors,
479
+ isValidating,
480
+ validateUsingUserValidationFunction
387
481
  })
388
482
 
389
483
  </script>
@@ -1,7 +1,6 @@
1
1
  <template>
2
2
  <!-- table -->
3
- <div
4
- class="relative shadow-listTableShadow dark:shadow-darkListTableShadow overflow-auto "
3
+ <div class="relative shadow-listTableShadow dark:shadow-darkListTableShadow overflow-auto border dark:border-gray-700"
5
4
  :class="{'rounded-default': !noRoundings}"
6
5
  :style="isVirtualScrollEnabled ? { maxHeight: `${containerHeight}px` } : {}"
7
6
  @scroll="handleScroll"
@@ -21,7 +20,7 @@
21
20
 
22
21
  <tbody>
23
22
  <!-- table header -->
24
- <tr class="t-header sticky z-20 top-0 text-xs text-lightListTableHeadingText bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-darkListTableHeadingText">
23
+ <tr class="border-b dark:border-gray-700 t-header sticky z-20 top-0 text-xs text-lightListTableHeadingText bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-darkListTableHeadingText">
25
24
  <td scope="col" class="list-table-header-cell p-4 sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading">
26
25
  <Checkbox
27
26
  :modelValue="allFromThisPageChecked"
@@ -34,10 +33,8 @@
34
33
 
35
34
  <td v-for="c in columnsListed" ref="headerRefs" scope="col" class="list-table-header-cell px-2 md:px-3 lg:px-6 py-3" :class="{'sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading': c.listSticky}">
36
35
 
37
- <div
38
- @click="(evt) => c.sortable && onSortButtonClick(evt, c.name)"
39
- class="flex items-center " :class="{'cursor-pointer':c.sortable}"
40
- >
36
+ <div @click="(evt) => c.sortable && onSortButtonClick(evt, c.name)"
37
+ class="flex items-center font-semibold" :class="{'cursor-pointer':c.sortable}">
41
38
  {{ c.label }}
42
39
 
43
40
  <div v-if="c.sortable">
@@ -67,7 +64,7 @@
67
64
  </div>
68
65
  </td>
69
66
 
70
- <td scope="col" class="px-6 py-3">
67
+ <td scope="col" class="px-6 py-3 font-semibold">
71
68
  {{ $t('Actions') }}
72
69
  </td>
73
70
  </tr>
@@ -103,12 +100,12 @@
103
100
 
104
101
  <component
105
102
  v-for="(row, rowI) in rowsToRender"
106
- :is="tableRowReplaceInjection ? getCustomComponent(tableRowReplaceInjection) : 'tr'"
103
+ :is="tableRowReplaceInjection ? getCustomComponent(formatComponent(tableRowReplaceInjection)) : 'tr'"
107
104
  :key="`row_${row._primaryKeyValue}`"
108
105
  :record="row"
109
106
  :resource="resource"
110
107
  :adminUser="coreStore.adminUser"
111
- :meta="tableRowReplaceInjection ? tableRowReplaceInjection.meta : undefined"
108
+ :meta="tableRowReplaceInjection ? formatComponent(tableRowReplaceInjection).meta : undefined"
112
109
  @click="onClick($event, row)"
113
110
  ref="rowRefs"
114
111
  class="list-table-body-row bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
@@ -203,17 +200,17 @@
203
200
  :key="action.id"
204
201
  >
205
202
  <component
206
- :is="action.customComponent ? getCustomComponent(action.customComponent) : CallActionWrapper"
207
- :meta="action.customComponent?.meta"
203
+ v-if="action.customComponent"
204
+ :is="action.customComponent ? getCustomComponent(formatComponent(action.customComponent)) : CallActionWrapper"
205
+ :meta="formatComponent(action.customComponent).meta"
208
206
  :row="row"
209
207
  :resource="resource"
210
- :adminUser="adminUser"
211
- @callAction="(payload? : Object) => startCustomAction(action.id, row, payload)"
208
+ :adminUser="coreStore.adminUser"
209
+ @callAction="(payload? : Object) => startCustomAction(action.id as string | number, row, payload)"
212
210
  >
213
211
  <button
214
212
  type="button"
215
213
  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"
216
- :disabled="rowActionLoadingStates?.[action.id]"
217
214
  >
218
215
  <component
219
216
  v-if="action.icon"
@@ -236,7 +233,7 @@
236
233
  :deleteRecord="deleteRecord"
237
234
  :resourceId="resource.resourceId"
238
235
  :startCustomAction="startCustomAction"
239
- :customActionIconsThreeDotsMenuItems="customActionIconsThreeDotsMenuItems"
236
+ :customActionIconsThreeDotsMenuItems="customActionIconsThreeDotsMenuItems ?? []"
240
237
  />
241
238
  </div>
242
239
 
@@ -256,12 +253,12 @@
256
253
  -->
257
254
  <div class="af-pagination-container flex flex-row items-center mt-4 xs:flex-row xs:justify-between xs:items-center gap-3">
258
255
 
259
- <div class="af-pagination-buttons-container inline-flex "
256
+ <div class="af-pagination-buttons-container af-button-shadow inline-flex rounded "
260
257
  v-if="(rows || totalRows) && totalRows >= pageSize && totalRows > 0"
261
258
  >
262
259
  <!-- Buttons -->
263
260
  <button
264
- class="af-pagination-prev-button flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightListTablePaginationText focus:outline-none bg-lightListTablePaginationBackgoround border-r-0 rounded-s border border-lightListTablePaginationBorder hover:bg-lightListTablePaginationBackgoroundHover hover:text-lightListTablePaginationTextHover focus:z-10 focus:ring-4 focus:ring-lightListTablePaginationFocusRing dark:focus:ring-darkListTablePaginationFocusRing dark:bg-darkListTablePaginationBackgoround dark:text-darkListTablePaginationText dark:border-darkListTablePaginationBorder dark:hover:text-darkListTablePaginationTextHover dark:hover:bg-darkListTablePaginationBackgoroundHover disabled:opacity-50"
261
+ class="af-pagination-prev-button flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightListTablePaginationText focus:outline-none bg-lightListTablePaginationBackgoround border-r-0 rounded-s border border-lightListTablePaginationBorder hover:bg-lightListTablePaginationBackgoroundHover hover:text-lightListTablePaginationTextHover focus:z-20 focus:ring-4 focus:ring-lightListTablePaginationFocusRing dark:focus:ring-darkListTablePaginationFocusRing dark:bg-darkListTablePaginationBackgoround dark:text-darkListTablePaginationText dark:border-darkListTablePaginationBorder dark:hover:text-darkListTablePaginationTextHover dark:hover:bg-darkListTablePaginationBackgoroundHover disabled:opacity-50"
265
262
  @click="page--; pageInput = page.toString();"
266
263
  :disabled="page <= 1"
267
264
  >
@@ -273,7 +270,7 @@
273
270
  </span>
274
271
  </button>
275
272
  <button
276
- class="af-pagination-first-page-button flex items-center py-1 px-3 text-sm font-medium text-lightListTablePaginationText focus:outline-none bg-lightListTablePaginationBackgoround border-r-0 border border-lightListTablePaginationBorder hover:bg-lightListTablePaginationBackgoroundHover hover:text-lightListTablePaginationTextHover focus:z-10 focus:ring-4 focus:ring-lightListTablePaginationFocusRing dark:focus:ring-darkListTablePaginationFocusRing dark:bg-darkListTablePaginationBackgoround dark:text-darkListTablePaginationText dark:border-darkListTablePaginationBorder dark:hover:text-darkListTablePaginationTextHover dark:hover:bg-darkListTablePaginationBackgoroundHover disabled:opacity-50"
273
+ class="af-pagination-first-page-button flex items-center py-1 px-3 text-sm font-medium text-lightListTablePaginationText focus:outline-none bg-lightListTablePaginationBackgoround border-r-0 border border-lightListTablePaginationBorder hover:bg-lightListTablePaginationBackgoroundHover hover:text-lightListTablePaginationTextHover z-10 focus:z-20 focus:ring-4 focus:ring-lightListTablePaginationFocusRing dark:focus:ring-darkListTablePaginationFocusRing dark:bg-darkListTablePaginationBackgoround dark:text-darkListTablePaginationText dark:border-darkListTablePaginationBorder dark:hover:text-darkListTablePaginationTextHover dark:hover:bg-darkListTablePaginationBackgoroundHover disabled:opacity-50"
277
274
  @click="page = 1;
278
275
  pageInput = page.toString();"
279
276
  :disabled="page <= 1"
@@ -284,13 +281,13 @@
284
281
  type="text"
285
282
  v-model="pageInput"
286
283
  :style="{ width: `${Math.max(1, pageInput.length+4)}ch` }"
287
- class="af-pagination-input min-w-10 outline-none inline-block py-1.5 px-3 text-sm text-center text-lightListTablePaginationCurrentPageText border border-lightListTablePaginationBorder dark:border-darkListTablePaginationBorder dark:text-darkListTablePaginationCurrentPageText dark:bg-darkListTablePaginationBackgoround z-10"
284
+ class="af-pagination-input z-10 min-w-10 outline-none inline-block py-1.5 px-3 text-sm text-center text-lightListTablePaginationCurrentPageText border border-lightListTablePaginationBorder dark:border-darkListTablePaginationBorder dark:text-darkListTablePaginationCurrentPageText dark:bg-darkListTablePaginationBackgoround"
288
285
  @keydown="onPageKeydown($event)"
289
286
  @blur="validatePageInput()"
290
287
  />
291
288
 
292
289
  <button
293
- class="af-pagination-last-page-button flex items-center py-1 px-3 text-sm font-medium text-lightListTablePaginationText focus:outline-none bg-lightListTablePaginationBackgoround border-l-0 border border-lightListTablePaginationBorder hover:bg-lightListTablePaginationBackgoroundHover hover:text-lightListTablePaginationTextHover focus:z-10 focus:ring-4 focus:ring-lightListTablePaginationFocusRing dark:focus:ring-darkListTablePaginationFocusRing dark:bg-darkListTablePaginationBackgoround dark:text-darkListTablePaginationText dark:border-darkListTablePaginationBorder dark:hover:text-white dark:hover:bg-darkListTablePaginationBackgoroundHover disabled:opacity-50"
290
+ class="af-pagination-last-page-button z-10 flex items-center py-1 px-3 text-sm font-medium text-lightListTablePaginationText focus:outline-none bg-lightListTablePaginationBackgoround border-l-0 border border-lightListTablePaginationBorder hover:bg-lightListTablePaginationBackgoroundHover hover:text-lightListTablePaginationTextHover focus:ring-4 focus:z-20 focus:ring-lightListTablePaginationFocusRing dark:focus:ring-darkListTablePaginationFocusRing dark:bg-darkListTablePaginationBackgoround dark:text-darkListTablePaginationText dark:border-darkListTablePaginationBorder dark:hover:text-white dark:hover:bg-darkListTablePaginationBackgoroundHover disabled:opacity-50"
294
291
  @click="page = totalPages; pageInput = page.toString();" :disabled="page >= totalPages">
295
292
  {{ totalPages }}
296
293
  </button>
@@ -344,10 +341,10 @@
344
341
 
345
342
 
346
343
  import { computed, onMounted, ref, watch, useTemplateRef, nextTick, type Ref } from 'vue';
347
- import { callAdminForthApi } from '@/utils';
344
+ import { callAdminForthApi, executeCustomAction } from '@/utils';
348
345
  import { useI18n } from 'vue-i18n';
349
346
  import ValueRenderer from '@/components/ValueRenderer.vue';
350
- import { getCustomComponent } from '@/utils';
347
+ import { getCustomComponent, formatComponent } from '@/utils';
351
348
  import { useCoreStore } from '@/stores/core';
352
349
  import { showSuccesTost, showErrorTost } from '@/composables/useFrontendApi';
353
350
  import SkeleteLoader from '@/components/SkeleteLoader.vue';
@@ -360,7 +357,7 @@ import {
360
357
  } from '@iconify-prerendered/vue-flowbite';
361
358
  import router from '@/router';
362
359
  import { Tooltip } from '@/afcl';
363
- import type { AdminForthResourceCommon, AdminForthResourceColumnCommon, AdminForthComponentDeclarationFull } from '@/types/Common';
360
+ import type { AdminForthResourceCommon, AdminForthResourceColumnCommon, AdminForthComponentDeclarationFull, AdminForthComponentDeclaration } from '@/types/Common';
364
361
  import { useAdminforth } from '@/adminforth';
365
362
  import Checkbox from '@/afcl/Checkbox.vue';
366
363
  import ListActionsThreeDots from '@/components/ListActionsThreeDots.vue';
@@ -383,8 +380,8 @@ const props = defineProps<{
383
380
  containerHeight?: number,
384
381
  itemHeight?: number,
385
382
  bufferSize?: number,
386
- customActionIconsThreeDotsMenuItems?: any[]
387
- tableRowReplaceInjection?: AdminForthComponentDeclarationFull,
383
+ customActionIconsThreeDotsMenuItems?: AdminForthComponentDeclaration[]
384
+ tableRowReplaceInjection?: AdminForthComponentDeclaration,
388
385
  isVirtualScrollEnabled: boolean
389
386
  }>();
390
387
 
@@ -414,7 +411,7 @@ const sort: Ref<Array<{field: string, direction: string}>> = ref([]);
414
411
  const showListActionsThreeDots = computed(() => {
415
412
  return props.resource?.options?.actions?.some(a => a.showIn?.listThreeDotsMenu) // show if any action is set to show in three dots menu
416
413
  || (props.customActionIconsThreeDotsMenuItems && props.customActionIconsThreeDotsMenuItems.length > 0) // or if there are custom action icons for three dots menu
417
- || !props.resource?.options.baseActionsAsQuickIcons // or if there is no baseActionsAsQuickIcons
414
+ || !props.resource?.options?.baseActionsAsQuickIcons // or if there is no baseActionsAsQuickIcons
418
415
  || (props.resource?.options.baseActionsAsQuickIcons && props.resource?.options.baseActionsAsQuickIcons.length < 3) // if there all 3 base actions are shown as quick icons - hide three dots icon
419
416
  })
420
417
 
@@ -609,51 +606,29 @@ async function deleteRecord(row: any) {
609
606
 
610
607
  const actionLoadingStates = ref<Record<string | number, boolean>>({});
611
608
 
612
- async function startCustomAction(actionId: string, row: any, extraData: Record<string, any> = {}) {
613
-
614
- actionLoadingStates.value[actionId] = true;
615
-
616
- const data = await callAdminForthApi({
617
- path: '/start_custom_action',
618
- method: 'POST',
619
- body: {
620
- resourceId: props.resource?.resourceId,
621
- actionId: actionId,
622
- recordId: row._primaryKeyValue,
623
- extra: extraData
624
- }
625
- });
626
-
627
- actionLoadingStates.value[actionId] = false;
628
-
629
- if (data?.redirectUrl) {
630
- // Check if the URL should open in a new tab
631
- if (data.redirectUrl.includes('target=_blank')) {
632
- window.open(data.redirectUrl.replace('&target=_blank', '').replace('?target=_blank', ''), '_blank');
633
- } else {
634
- // Navigate within the app
635
- if (data.redirectUrl.startsWith('http')) {
636
- window.location.href = data.redirectUrl;
637
- } else {
638
- router.push(data.redirectUrl);
609
+ async function startCustomAction(actionId: string | number, row: any, extraData: Record<string, any> = {}) {
610
+ await executeCustomAction({
611
+ actionId,
612
+ resourceId: props.resource?.resourceId || '',
613
+ recordId: row._primaryKeyValue,
614
+ extra: extraData,
615
+ setLoadingState: (loading: boolean) => {
616
+ actionLoadingStates.value[actionId] = loading;
617
+ },
618
+ onSuccess: async (data: any) => {
619
+ emits('update:records', true);
620
+
621
+ if (data.successMessage) {
622
+ alert({
623
+ message: data.successMessage,
624
+ variant: 'success'
625
+ });
639
626
  }
627
+ },
628
+ onError: (error: string) => {
629
+ showErrorTost(error);
640
630
  }
641
- return;
642
- }
643
- if (data?.ok) {
644
- emits('update:records', true);
645
-
646
- if (data.successMessage) {
647
- alert({
648
- message: data.successMessage,
649
- variant: 'success'
650
- });
651
- }
652
- }
653
-
654
- if (data?.error) {
655
- showErrorTost(data.error);
656
- }
631
+ });
657
632
  }
658
633
 
659
634
  function validatePageInput() {
@@ -52,7 +52,7 @@
52
52
  </div>
53
53
  </div>
54
54
 
55
- <div v-if="coreStore.config.defaultUserExists && !isLocalhost" class="p-4 mb-4 text-white rounded-lg bg-red-700/80 fill-white text-sm">
55
+ <div v-if="coreStore?.config?.defaultUserExists && !isLocalhost" class="p-4 mb-4 text-white rounded-lg bg-red-700/80 fill-white text-sm">
56
56
  <IconExclamationCircleOutline class="inline-block align-text-bottom mr-0,5 w-5 h-5" />
57
57
  Default user <strong>"adminforth"</strong> detected. Delete it and create your own account.
58
58
  </div>