adminforth 2.26.2 → 2.27.0-next.2

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 (50) 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 +25 -1
  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 +44 -7
  12. package/dist/spa/package.json +1 -1
  13. package/dist/spa/pnpm-lock.yaml +301 -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 +11 -3
  22. package/dist/spa/src/components/CustomRangePicker.vue +16 -67
  23. package/dist/spa/src/components/ListActionsThreeDots.vue +9 -8
  24. package/dist/spa/src/components/RangePicker.vue +236 -0
  25. package/dist/spa/src/components/ResourceListTable.vue +45 -70
  26. package/dist/spa/src/components/Sidebar.vue +1 -1
  27. package/dist/spa/src/components/ThreeDotsMenu.vue +30 -52
  28. package/dist/spa/src/i18n.ts +1 -1
  29. package/dist/spa/src/stores/core.ts +4 -2
  30. package/dist/spa/src/types/Back.ts +11 -4
  31. package/dist/spa/src/types/Common.ts +26 -5
  32. package/dist/spa/src/types/FrontendAPI.ts +6 -1
  33. package/dist/spa/src/utils/listUtils.ts +8 -2
  34. package/dist/spa/src/utils/utils.ts +187 -10
  35. package/dist/spa/src/views/CreateView.vue +10 -10
  36. package/dist/spa/src/views/EditView.vue +10 -9
  37. package/dist/spa/src/views/ListView.vue +122 -18
  38. package/dist/spa/src/views/LoginView.vue +13 -13
  39. package/dist/spa/src/views/ShowView.vue +53 -60
  40. package/dist/spa/tsconfig.app.json +1 -1
  41. package/dist/types/Back.d.ts +8 -5
  42. package/dist/types/Back.d.ts.map +1 -1
  43. package/dist/types/Back.js.map +1 -1
  44. package/dist/types/Common.d.ts +21 -5
  45. package/dist/types/Common.d.ts.map +1 -1
  46. package/dist/types/Common.js.map +1 -1
  47. package/dist/types/FrontendAPI.d.ts +13 -1
  48. package/dist/types/FrontendAPI.d.ts.map +1 -1
  49. package/dist/types/FrontendAPI.js.map +1 -1
  50. 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>
@@ -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>
@@ -1,9 +1,9 @@
1
1
  <template >
2
- <div class="relative" v-if="threeDotsDropdownItems?.length || customActions?.length || (bulkActions?.some((action: AdminForthBulkActionCommon) => action.showInThreeDotsDropdown))">
2
+ <div class="relative" v-if="threeDotsDropdownItems?.length || customActions?.length || (bulkActions?.some((action: AdminForthBulkActionFront) => action.showInThreeDotsDropdown))">
3
3
  <button
4
4
  ref="buttonTriggerRef"
5
5
  @click="toggleDropdownVisibility"
6
- class="flex items-center py-2 px-2 text-sm font-medium text-lightThreeDotsMenuIconDots focus:outline-none bg-lightThreeDotsMenuIconBackground rounded border border-lightThreeDotsMenuIconBackgroundBorder hover:bg-lightThreeDotsMenuIconBackgroundHover hover:text-lightThreeDotsMenuIconDotsHover focus:z-10 focus:ring-4 focus:ring-lightThreeDotsMenuIconFocus dark:focus:ring-darkThreeDotsMenuIconFocus dark:bg-darkThreeDotsMenuIconBackground dark:text-darkThreeDotsMenuIconDots dark:border-darkThreeDotsMenuIconBackgroundBorder dark:hover:text-darkThreeDotsMenuIconDotsHover dark:hover:bg-darkThreeDotsMenuIconBackgroundHover rounded-default"
6
+ class="flex transition-all items-center af-button-shadow py-2.5 px-2.5 text-sm font-medium text-lightThreeDotsMenuIconDots focus:outline-none bg-lightThreeDotsMenuIconBackground rounded border border-lightThreeDotsMenuIconBackgroundBorder hover:bg-lightThreeDotsMenuIconBackgroundHover hover:text-lightThreeDotsMenuIconDotsHover focus:z-10 focus:ring-4 focus:ring-lightThreeDotsMenuIconFocus dark:focus:ring-darkThreeDotsMenuIconFocus dark:bg-darkThreeDotsMenuIconBackground dark:text-darkThreeDotsMenuIconDots dark:border-darkThreeDotsMenuIconBackgroundBorder dark:hover:text-darkThreeDotsMenuIconDotsHover dark:hover:bg-darkThreeDotsMenuIconBackgroundHover rounded-default"
7
7
  >
8
8
  <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 4 15">
9
9
  <path d="M3.5 1.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm0 6.041a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm0 5.959a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z"/>
@@ -46,8 +46,9 @@
46
46
  <li v-for="action in customActions" :key="action.id">
47
47
  <div class="wrapper">
48
48
  <component
49
- :is="(action.customComponent && getCustomComponent(action.customComponent)) || CallActionWrapper"
50
- :meta="action.customComponent?.meta"
49
+ v-if="action.customComponent"
50
+ :is="(action.customComponent && getCustomComponent(formatComponent(action.customComponent))) || CallActionWrapper"
51
+ :meta="formatComponent(action.customComponent).meta"
51
52
  @callAction="(payload? : Object) => handleActionClick(action, payload)"
52
53
  >
53
54
  <a @click.prevent class="block px-4 py-2 hover:text-lightThreeDotsMenuBodyTextHover hover:bg-lightThreeDotsMenuBodyBackgroundHover dark:hover:bg-darkThreeDotsMenuBodyBackgroundHover dark:hover:text-darkThreeDotsMenuBodyTextHover">
@@ -88,14 +89,14 @@
88
89
 
89
90
 
90
91
  <script setup lang="ts">
91
- import { getCustomComponent, getIcon } from '@/utils';
92
+ import { getCustomComponent, getIcon, formatComponent, executeCustomAction } from '@/utils';
92
93
  import { useCoreStore } from '@/stores/core';
93
94
  import { useAdminforth } from '@/adminforth';
94
95
  import { callAdminForthApi } from '@/utils';
95
96
  import { useRoute, useRouter } from 'vue-router';
96
97
  import CallActionWrapper from '@/components/CallActionWrapper.vue'
97
98
  import { ref, type ComponentPublicInstance, onMounted, onUnmounted } from 'vue';
98
- import type { AdminForthBulkActionCommon, AdminForthComponentDeclarationFull } from '@/types/Common';
99
+ import type { AdminForthActionFront, AdminForthBulkActionFront, AdminForthComponentDeclarationFull } from '@/types/Common';
99
100
  import type { AdminForthActionInput } from '@/types/Back';
100
101
 
101
102
  const { list, alert} = useAdminforth();
@@ -109,8 +110,8 @@ const buttonTriggerRef = ref<HTMLElement | null>(null);
109
110
 
110
111
  const props = defineProps({
111
112
  threeDotsDropdownItems: Array<AdminForthComponentDeclarationFull>,
112
- customActions: Array<AdminForthActionInput>,
113
- bulkActions: Array<AdminForthBulkActionCommon>,
113
+ customActions: Array<AdminForthActionFront>,
114
+ bulkActions: Array<AdminForthBulkActionFront>,
114
115
  checkboxes: Array,
115
116
  updateList: {
116
117
  type: Function,
@@ -130,55 +131,32 @@ function setComponentRef(el: ComponentPublicInstance | null, index: number) {
130
131
 
131
132
  async function handleActionClick(action: AdminForthActionInput, payload: any) {
132
133
  list.closeThreeDotsDropdown();
133
-
134
- const actionId = action.id;
135
- const data = await callAdminForthApi({
136
- path: '/start_custom_action',
137
- method: 'POST',
138
- body: {
139
- resourceId: route.params.resourceId,
140
- actionId: actionId,
141
- recordId: route.params.primaryKey,
142
- extra: payload || {},
143
- }
144
- });
134
+ await executeCustomAction({
135
+ actionId: action.id,
136
+ resourceId: route.params.resourceId as string,
137
+ recordId: route.params.primaryKey as string,
138
+ extra: payload || {},
139
+ onSuccess: async (data: any) => {
140
+ await coreStore.fetchRecord({
141
+ resourceId: route.params.resourceId as string,
142
+ primaryKey: route.params.primaryKey as string,
143
+ source: 'show',
144
+ });
145
145
 
146
- if (data?.redirectUrl) {
147
- // Check if the URL should open in a new tab
148
- if (data.redirectUrl.includes('target=_blank')) {
149
- window.open(data.redirectUrl.replace('&target=_blank', '').replace('?target=_blank', ''), '_blank');
150
- } else {
151
- // Navigate within the app
152
- if (data.redirectUrl.startsWith('http')) {
153
- window.location.href = data.redirectUrl;
154
- } else {
155
- router.push(data.redirectUrl);
146
+ if (data.successMessage) {
147
+ alert({
148
+ message: data.successMessage,
149
+ variant: 'success'
150
+ });
156
151
  }
157
- }
158
- return;
159
- }
160
-
161
- if (data?.ok) {
162
- await coreStore.fetchRecord({
163
- resourceId: route.params.resourceId as string,
164
- primaryKey: route.params.primaryKey as string,
165
- source: 'show',
166
- });
167
-
168
- if (data.successMessage) {
152
+ },
153
+ onError: (error: string) => {
169
154
  alert({
170
- message: data.successMessage,
171
- variant: 'success'
155
+ message: error,
156
+ variant: 'danger'
172
157
  });
173
158
  }
174
- }
175
-
176
- if (data?.error) {
177
- alert({
178
- message: data.error,
179
- variant: 'danger'
180
- });
181
- }
159
+ });
182
160
  }
183
161
 
184
162
  function startBulkAction(actionId: string) {
@@ -51,6 +51,6 @@ export function initI18n(app: ReturnType<typeof createApp>) {
51
51
  },
52
52
  });
53
53
  app.use(i18n);
54
- i18nInstance = i18n
54
+ i18nInstance = i18n as typeof i18nInstance
55
55
  return i18n
56
56
  }
@@ -4,9 +4,11 @@ import { callAdminForthApi } from '@/utils';
4
4
  import websocket from '@/websocket';
5
5
  import { useAdminforth } from '@/adminforth';
6
6
 
7
- import type { AdminForthResourceCommon, AdminForthResourceColumnCommon, GetBaseConfigResponse, ResourceVeryShort, AdminUser, UserData, AdminForthConfigMenuItem, AdminForthConfigForFrontend } from '@/types/Common';
7
+ import type { AdminForthResourceCommon, AdminForthResourceColumnCommon, GetBaseConfigResponse, ResourceVeryShort, AdminUser, UserData, AdminForthConfigMenuItem, AdminForthConfigForFrontend, AdminForthResourceFrontend } from '@/types/Common';
8
8
  import type { Ref } from 'vue'
9
9
 
10
+
11
+
10
12
  export const useCoreStore = defineStore('core', () => {
11
13
  const { alert } = useAdminforth();
12
14
  const resourceById: Ref<Record<string, ResourceVeryShort>> = ref({});
@@ -15,7 +17,7 @@ export const useCoreStore = defineStore('core', () => {
15
17
  const menu: Ref<AdminForthConfigMenuItem[]> = ref([]);
16
18
  const config: Ref<AdminForthConfigForFrontend | null> = ref(null);
17
19
  const record: Ref<any | null> = ref({});
18
- const resource: Ref<AdminForthResourceCommon | null> = ref(null);
20
+ const resource: Ref<AdminForthResourceFrontend | null> = ref(null);
19
21
  const userData: Ref<UserData | null> = ref(null);
20
22
  const isResourceFetching = ref(false);
21
23
  const isInternetError = ref(false);