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.
- package/commands/createApp/templates/package.json.hbs +1 -1
- package/dist/modules/restApi.d.ts +1 -0
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +65 -3
- package/dist/modules/restApi.js.map +1 -1
- package/dist/modules/styles.js +2 -2
- package/dist/modules/styles.js.map +1 -1
- package/dist/servers/express.d.ts.map +1 -1
- package/dist/servers/express.js +7 -1
- package/dist/servers/express.js.map +1 -1
- package/dist/spa/package-lock.json +85 -7
- package/dist/spa/package.json +4 -1
- package/dist/spa/pnpm-lock.yaml +339 -299
- package/dist/spa/src/App.vue +1 -1
- package/dist/spa/src/adminforth.ts +17 -29
- package/dist/spa/src/afcl/Input.vue +1 -1
- package/dist/spa/src/afcl/Modal.vue +12 -1
- package/dist/spa/src/afcl/Select.vue +4 -2
- package/dist/spa/src/afcl/Table.vue +27 -13
- package/dist/spa/src/components/AcceptModal.vue +2 -0
- package/dist/spa/src/components/ColumnValueInputWrapper.vue +35 -4
- package/dist/spa/src/components/CustomRangePicker.vue +22 -67
- package/dist/spa/src/components/GroupsTable.vue +7 -4
- package/dist/spa/src/components/ListActionsThreeDots.vue +9 -8
- package/dist/spa/src/components/RangePicker.vue +236 -0
- package/dist/spa/src/components/ResourceForm.vue +100 -6
- package/dist/spa/src/components/ResourceListTable.vue +45 -70
- package/dist/spa/src/components/Sidebar.vue +1 -1
- package/dist/spa/src/components/ThreeDotsMenu.vue +54 -57
- package/dist/spa/src/i18n.ts +1 -1
- package/dist/spa/src/stores/core.ts +4 -2
- package/dist/spa/src/types/Back.ts +10 -3
- package/dist/spa/src/types/Common.ts +43 -8
- package/dist/spa/src/types/FrontendAPI.ts +6 -1
- package/dist/spa/src/types/adapters/StorageAdapter.ts +12 -0
- package/dist/spa/src/utils/createEditUtils.ts +65 -0
- package/dist/spa/src/utils/index.ts +2 -1
- package/dist/spa/src/utils/listUtils.ts +8 -2
- package/dist/spa/src/utils/utils.ts +192 -12
- package/dist/spa/src/utils.ts +2 -1
- package/dist/spa/src/views/CreateView.vue +32 -59
- package/dist/spa/src/views/EditView.vue +30 -47
- package/dist/spa/src/views/ListView.vue +119 -18
- package/dist/spa/src/views/LoginView.vue +13 -13
- package/dist/spa/src/views/ShowView.vue +67 -61
- package/dist/spa/tsconfig.app.json +1 -1
- package/dist/types/Back.d.ts +7 -4
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +43 -8
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js.map +1 -1
- package/dist/types/FrontendAPI.d.ts +13 -1
- package/dist/types/FrontendAPI.d.ts.map +1 -1
- package/dist/types/FrontendAPI.js.map +1 -1
- package/dist/types/adapters/StorageAdapter.d.ts +11 -0
- package/dist/types/adapters/StorageAdapter.d.ts.map +1 -1
- 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
|
-
:
|
|
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
|
-
:
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
:
|
|
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-
|
|
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-
|
|
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
|
|
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:
|
|
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?:
|
|
387
|
-
tableRowReplaceInjection?:
|
|
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
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
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
|
|
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>
|