adminforth 1.7.0 → 1.8.0
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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +57 -34
- package/dist/index.js.map +1 -1
- package/dist/modules/configValidator.d.ts.map +1 -1
- package/dist/modules/configValidator.js +31 -2
- package/dist/modules/configValidator.js.map +1 -1
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +3 -0
- package/dist/modules/restApi.js.map +1 -1
- package/dist/spa/src/App.vue +5 -2
- package/dist/spa/src/components/ColumnValueInput.vue +175 -0
- package/dist/spa/src/components/CustomDatePicker.vue +11 -3
- package/dist/spa/src/components/GroupsTable.vue +50 -128
- package/dist/spa/src/components/MenuLink.vue +1 -1
- package/dist/spa/src/components/ResourceForm.vue +88 -42
- package/dist/spa/src/components/ValueRenderer.vue +40 -0
- package/dist/spa/src/types/Back.ts +2 -0
- package/dist/spa/src/types/Common.ts +19 -0
- package/dist/spa/src/types/FrontendAPI.ts +1 -1
- package/dist/spa/src/views/ListView.vue +5 -1
- package/dist/types/Back.d.ts +2 -0
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +18 -0
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js.map +1 -1
- package/dist/types/FrontendAPI.d.ts +1 -1
- package/dist/types/FrontendAPI.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/spa/src/App.vue
CHANGED
|
@@ -209,9 +209,12 @@
|
|
|
209
209
|
drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-20">
|
|
210
210
|
</div>
|
|
211
211
|
|
|
212
|
-
|
|
212
|
+
<component
|
|
213
|
+
v-for="c in coreStore?.config?.globalInjections?.everyPageBottom || []"
|
|
214
|
+
:is="getCustomComponent(c)"
|
|
215
|
+
:meta="c.meta"
|
|
216
|
+
/>
|
|
213
217
|
</div>
|
|
214
|
-
|
|
215
218
|
</template>
|
|
216
219
|
|
|
217
220
|
<style lang="scss" scoped>
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex">
|
|
3
|
+
<component
|
|
4
|
+
v-if="column?.components?.[props.source]?.file"
|
|
5
|
+
:is="getCustomComponent(column.components[props.source])"
|
|
6
|
+
:column="column"
|
|
7
|
+
:value="value"
|
|
8
|
+
@update:value="$emit('update:modelValue', $event)"
|
|
9
|
+
:meta="column.components[props.source].meta"
|
|
10
|
+
:record="currentValues"
|
|
11
|
+
@update:inValidity="$emit('update:inValidity', $event)"
|
|
12
|
+
@update:emptiness="$emit('update:emptiness', $event)"
|
|
13
|
+
/>
|
|
14
|
+
<Select
|
|
15
|
+
v-else-if="column.foreignResource"
|
|
16
|
+
ref="input"
|
|
17
|
+
class="w-full"
|
|
18
|
+
:options="columnOptions[column.name] || []"
|
|
19
|
+
:placeholder = "columnOptions[column.name]?.length ?$t('Select...'): $t('There are no options available')"
|
|
20
|
+
:modelValue="value"
|
|
21
|
+
:readonly="column.editReadonly && source === 'edit'"
|
|
22
|
+
@update:modelValue="$emit('update:modelValue', $event)"
|
|
23
|
+
/>
|
|
24
|
+
<Select
|
|
25
|
+
v-else-if="column.enum"
|
|
26
|
+
ref="input"
|
|
27
|
+
class="w-full"
|
|
28
|
+
:options="column.enum"
|
|
29
|
+
:modelValue="value"
|
|
30
|
+
:readonly="column.editReadonly && source === 'edit'"
|
|
31
|
+
@update:modelValue="$emit('update:modelValue', $event)"
|
|
32
|
+
/>
|
|
33
|
+
<Select
|
|
34
|
+
v-else-if="(type || column.type) === 'boolean'"
|
|
35
|
+
ref="input"
|
|
36
|
+
class="w-full"
|
|
37
|
+
:options="getBooleanOptions(column)"
|
|
38
|
+
:modelValue="value"
|
|
39
|
+
:readonly="column.editReadonly && source === 'edit'"
|
|
40
|
+
@update:modelValue="$emit('update:modelValue', $event)"
|
|
41
|
+
/>
|
|
42
|
+
<input
|
|
43
|
+
v-else-if="['integer'].includes(type || column.type)"
|
|
44
|
+
ref="input"
|
|
45
|
+
type="number"
|
|
46
|
+
step="1"
|
|
47
|
+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-40 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white focus:ring-lightPrimary focus:border-lightPrimary dark:focus:ring-darkPrimary dark:focus:border-darkPrimary"
|
|
48
|
+
placeholder="0"
|
|
49
|
+
:readonly="column.editReadonly && source === 'edit'"
|
|
50
|
+
:value="value"
|
|
51
|
+
@input="$emit('update:modelValue', $event.target.value)"
|
|
52
|
+
>
|
|
53
|
+
<CustomDatePicker
|
|
54
|
+
v-else-if="['datetime'].includes(type || column.type)"
|
|
55
|
+
ref="input"
|
|
56
|
+
:column="column"
|
|
57
|
+
:valueStart="value"
|
|
58
|
+
auto-hide
|
|
59
|
+
@update:valueStart="$emit('update:modelValue', $event)"
|
|
60
|
+
:readonly="column.editReadonly && source === 'edit'"
|
|
61
|
+
/>
|
|
62
|
+
<input
|
|
63
|
+
v-else-if="['decimal', 'float'].includes(type || column.type)"
|
|
64
|
+
ref="input"
|
|
65
|
+
type="number"
|
|
66
|
+
step="0.1"
|
|
67
|
+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-40 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white focus:ring-lightPrimary focus:border-lightPrimary dark:focus:ring-darkPrimary dark:focus:border-darkPrimary"
|
|
68
|
+
placeholder="0.0"
|
|
69
|
+
:value="value"
|
|
70
|
+
@input="$emit('update:modelValue', $event.target.value)"
|
|
71
|
+
:readonly="column.editReadonly && source === 'edit'"
|
|
72
|
+
/>
|
|
73
|
+
<textarea
|
|
74
|
+
v-else-if="['text', 'richtext'].includes(type || column.type)"
|
|
75
|
+
ref="input"
|
|
76
|
+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white focus:ring-lightPrimary focus:border-lightPrimary dark:focus:ring-darkPrimary dark:focus:border-darkPrimary"
|
|
77
|
+
:placeholder="$t('Text')"
|
|
78
|
+
:value="value"
|
|
79
|
+
@input="$emit('update:modelValue', $event.target.value)"
|
|
80
|
+
:readonly="column.editReadonly && source === 'edit'"
|
|
81
|
+
/>
|
|
82
|
+
<textarea
|
|
83
|
+
v-else-if="['json'].includes(type || column.type)"
|
|
84
|
+
ref="input"
|
|
85
|
+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white focus:ring-lightPrimary focus:border-lightPrimary dark:focus:ring-darkPrimary dark:focus:border-darkPrimary"
|
|
86
|
+
:placeholder="$t('Text')"
|
|
87
|
+
:value="value"
|
|
88
|
+
@input="$emit('update:modelValue', $event.target.value)"
|
|
89
|
+
/>
|
|
90
|
+
<input
|
|
91
|
+
v-else
|
|
92
|
+
ref="input"
|
|
93
|
+
:type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
|
|
94
|
+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white focus:ring-lightPrimary focus:border-lightPrimary dark:focus:ring-darkPrimary dark:focus:border-darkPrimary"
|
|
95
|
+
:placeholder="$t('Text')"
|
|
96
|
+
:value="value"
|
|
97
|
+
@input="$emit('update:modelValue', $event.target.value)"
|
|
98
|
+
autocomplete="false"
|
|
99
|
+
data-lpignore="true"
|
|
100
|
+
readonly
|
|
101
|
+
@focus="onFocusHandler($event, column, source)"
|
|
102
|
+
>
|
|
103
|
+
|
|
104
|
+
<button
|
|
105
|
+
v-if="deletable"
|
|
106
|
+
type="button"
|
|
107
|
+
class="h-6 inset-y-2 right-0 flex items-center px-2 pt-4 z-index-100 focus:outline-none"
|
|
108
|
+
@click="$emit('delete')"
|
|
109
|
+
>
|
|
110
|
+
<IconTrashBinSolid class="w-6 h-6 text-gray-400"/>
|
|
111
|
+
</button>
|
|
112
|
+
<button
|
|
113
|
+
v-else-if="column.masked"
|
|
114
|
+
type="button"
|
|
115
|
+
@click="$emit('update:unmasked')"
|
|
116
|
+
class="h-6 inset-y-2 right-0 flex items-center px-2 pt-4 z-index-100 focus:outline-none"
|
|
117
|
+
>
|
|
118
|
+
<IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]"/>
|
|
119
|
+
<IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
</template>
|
|
123
|
+
|
|
124
|
+
<script setup lang="ts">
|
|
125
|
+
import { IconEyeSlashSolid, IconEyeSolid, IconTrashBinSolid } from '@iconify-prerendered/vue-flowbite';
|
|
126
|
+
import CustomDatePicker from "@/components/CustomDatePicker.vue";
|
|
127
|
+
import Select from '@/afcl/Select.vue';
|
|
128
|
+
import { ref } from 'vue';
|
|
129
|
+
import { getCustomComponent } from '@/utils';
|
|
130
|
+
import { useI18n } from 'vue-i18n';
|
|
131
|
+
|
|
132
|
+
const { t } = useI18n();
|
|
133
|
+
|
|
134
|
+
const props = defineProps<{
|
|
135
|
+
source: 'create' | 'edit',
|
|
136
|
+
column: any,
|
|
137
|
+
type: string,
|
|
138
|
+
value: any,
|
|
139
|
+
currentValues: any,
|
|
140
|
+
mode: string,
|
|
141
|
+
columnOptions: any,
|
|
142
|
+
unmasked: any,
|
|
143
|
+
deletable: boolean,
|
|
144
|
+
}>();
|
|
145
|
+
|
|
146
|
+
const input = ref(null);
|
|
147
|
+
|
|
148
|
+
const getBooleanOptions = (column: any) => {
|
|
149
|
+
const options: Array<{ label: string; value: boolean | null }> = [
|
|
150
|
+
{ label: t('Yes'), value: true },
|
|
151
|
+
{ label: t('No'), value: false },
|
|
152
|
+
];
|
|
153
|
+
if (!column.required[props.mode]) {
|
|
154
|
+
options.push({ label: t('Unset'), value: null });
|
|
155
|
+
}
|
|
156
|
+
return options;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
function onFocusHandler(event:FocusEvent, column:any, source:string, ) {
|
|
160
|
+
const focusedInput = event.target as HTMLInputElement;
|
|
161
|
+
if(!focusedInput) return;
|
|
162
|
+
if (column.editReadonly && source === 'edit') return;
|
|
163
|
+
else {
|
|
164
|
+
focusedInput.removeAttribute('readonly');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function focus() {
|
|
169
|
+
if (input.value?.focus) input.value?.focus();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
defineExpose({
|
|
173
|
+
focus,
|
|
174
|
+
});
|
|
175
|
+
</script>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div>
|
|
3
3
|
<div class="grid w-40 gap-4 mb-2">
|
|
4
4
|
<div>
|
|
5
|
-
<label for="start-time" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ label }}</label>
|
|
5
|
+
<label v-if="label" for="start-time" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ label }}</label>
|
|
6
6
|
|
|
7
7
|
<div class="relative">
|
|
8
8
|
<div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5 pointer-events-none">
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
</div>
|
|
11
11
|
|
|
12
12
|
<input ref="datepickerStartEl" type="text"
|
|
13
|
-
class="bg-gray-50 border
|
|
13
|
+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white focus:ring-lightPrimary focus:border-lightPrimary dark:focus:ring-darkPrimary dark:focus:border-darkPrimary"
|
|
14
14
|
:placeholder="$t('Select date')" :disabled="readonly" />
|
|
15
15
|
|
|
16
16
|
</div>
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
</div>
|
|
27
27
|
|
|
28
28
|
<input v-model="startTime" type="time" id="start-time" onfocus="this.showPicker()" onclick="this.showPicker()" step="1"
|
|
29
|
-
class="bg-gray-50 border
|
|
29
|
+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white focus:ring-lightPrimary focus:border-lightPrimary dark:focus:ring-darkPrimary dark:focus:border-darkPrimary"
|
|
30
30
|
value="00:00" :disabled="readonly" required/>
|
|
31
31
|
</div>
|
|
32
32
|
</div>
|
|
@@ -177,4 +177,12 @@ onBeforeUnmount(() => {
|
|
|
177
177
|
removeChangeDateListener();
|
|
178
178
|
destroyDatepickerElement();
|
|
179
179
|
});
|
|
180
|
+
|
|
181
|
+
function focus() {
|
|
182
|
+
datepickerStartEl.value?.focus();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
defineExpose({
|
|
186
|
+
focus,
|
|
187
|
+
});
|
|
180
188
|
</script>
|
|
@@ -38,115 +38,54 @@
|
|
|
38
38
|
</Tooltip>
|
|
39
39
|
</span>
|
|
40
40
|
</td>
|
|
41
|
-
<td
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
<td
|
|
42
|
+
class="px-6 py-4 whitespace-pre-wrap relative block md:table-cell"
|
|
43
|
+
:class="{'rounded-br-lg': i === group.columns.length - 1}"
|
|
44
|
+
>
|
|
45
|
+
<template v-if="column.isArray?.enabled">
|
|
46
|
+
<ColumnValueInput
|
|
47
|
+
v-for="(arrayItemValue, arrayItemIndex) in currentValues[column.name]"
|
|
48
|
+
:key="`${column.name}-${arrayItemIndex}`"
|
|
49
|
+
ref="arrayItemRefs"
|
|
50
|
+
:class="{'mt-2': arrayItemIndex}"
|
|
51
|
+
:source="source"
|
|
46
52
|
:column="column"
|
|
47
|
-
:
|
|
48
|
-
|
|
49
|
-
:
|
|
50
|
-
:
|
|
53
|
+
:type="column.isArray.itemType"
|
|
54
|
+
:value="arrayItemValue"
|
|
55
|
+
:currentValues="currentValues"
|
|
56
|
+
:mode="mode"
|
|
57
|
+
:columnOptions="columnOptions"
|
|
58
|
+
:deletable="!column.editReadonly"
|
|
59
|
+
@update:modelValue="setCurrentValue(column.name, $event, arrayItemIndex)"
|
|
60
|
+
@update:unmasked="unmasked[column.name] = !unmasked[column.name]"
|
|
51
61
|
@update:inValidity="customComponentsInValidity[column.name] = $event"
|
|
52
62
|
@update:emptiness="customComponentsEmptiness[column.name] = $event"
|
|
63
|
+
@delete="setCurrentValue(column.name, currentValues[column.name].filter((_, index) => index !== arrayItemIndex))"
|
|
53
64
|
/>
|
|
54
|
-
</template>
|
|
55
|
-
<template v-else>
|
|
56
|
-
<Select
|
|
57
|
-
class="w-full"
|
|
58
|
-
v-if="column.foreignResource"
|
|
59
|
-
:options="columnOptions[column.name] || []"
|
|
60
|
-
:placeholder = "columnOptions[column.name]?.length ?$t('Select...'): $t('There are no options available')"
|
|
61
|
-
:modelValue="currentValues[column.name]"
|
|
62
|
-
:readonly="column.editReadonly && source === 'edit'"
|
|
63
|
-
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
64
|
-
></Select>
|
|
65
|
-
<Select
|
|
66
|
-
class="w-full"
|
|
67
|
-
v-else-if="column.enum"
|
|
68
|
-
:options="column.enum"
|
|
69
|
-
:modelValue="currentValues[column.name]"
|
|
70
|
-
:readonly="column.editReadonly && source === 'edit'"
|
|
71
|
-
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
72
|
-
></Select>
|
|
73
|
-
<Select
|
|
74
|
-
class="w-full"
|
|
75
|
-
v-else-if="column.type === 'boolean'"
|
|
76
|
-
:options="getBooleanOptions(column)"
|
|
77
|
-
:modelValue="currentValues[column.name]"
|
|
78
|
-
:readonly="column.editReadonly && source === 'edit'"
|
|
79
|
-
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
80
|
-
></Select>
|
|
81
|
-
<input
|
|
82
|
-
v-else-if="['integer'].includes(column.type)"
|
|
83
|
-
type="number"
|
|
84
|
-
step="1"
|
|
85
|
-
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-40 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
86
|
-
placeholder="0"
|
|
87
|
-
:readonly="column.editReadonly && source === 'edit'"
|
|
88
|
-
:value="currentValues[column.name]"
|
|
89
|
-
@input="setCurrentValue(column.name, $event.target.value)"
|
|
90
|
-
>
|
|
91
|
-
<CustomDatePicker
|
|
92
|
-
v-else-if="['datetime'].includes(column.type)"
|
|
93
|
-
:column="column"
|
|
94
|
-
:valueStart="currentValues[column.name]"
|
|
95
|
-
auto-hide
|
|
96
|
-
@update:valueStart="setCurrentValue(column.name, $event)"
|
|
97
|
-
:readonly="column.editReadonly && source === 'edit'"
|
|
98
|
-
/>
|
|
99
|
-
<input
|
|
100
|
-
v-else-if="['decimal', 'float'].includes(column.type)"
|
|
101
|
-
type="number"
|
|
102
|
-
step="0.1"
|
|
103
|
-
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-40 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
104
|
-
placeholder="0.0"
|
|
105
|
-
:value="currentValues[column.name]"
|
|
106
|
-
@input="setCurrentValue(column.name, $event.target.value)"
|
|
107
|
-
:readonly="column.editReadonly && source === 'edit'"
|
|
108
|
-
/>
|
|
109
|
-
<textarea
|
|
110
|
-
v-else-if="['text', 'richtext'].includes(column.type)"
|
|
111
|
-
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
112
|
-
:placeholder="$t('Text')"
|
|
113
|
-
:value="currentValues[column.name]"
|
|
114
|
-
@input="setCurrentValue(column.name, $event.target.value)"
|
|
115
|
-
:readonly="column.editReadonly && source === 'edit'"
|
|
116
|
-
>
|
|
117
|
-
</textarea>
|
|
118
|
-
<textarea
|
|
119
|
-
v-else-if="['json'].includes(column.type)"
|
|
120
|
-
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
121
|
-
:placeholder="$t('Text')"
|
|
122
|
-
:value="currentValues[column.name]"
|
|
123
|
-
@input="setCurrentValue(column.name, $event.target.value)"
|
|
124
|
-
>
|
|
125
|
-
</textarea>
|
|
126
|
-
<input
|
|
127
|
-
v-else
|
|
128
|
-
:type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
|
|
129
|
-
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
130
|
-
:placeholder="$t('Text')"
|
|
131
|
-
:value="currentValues[column.name]"
|
|
132
|
-
@input="setCurrentValue(column.name, $event.target.value)"
|
|
133
|
-
autocomplete="false"
|
|
134
|
-
data-lpignore="true"
|
|
135
|
-
readonly
|
|
136
|
-
ref="readonlyInputs"
|
|
137
|
-
@focus="onFocusHandler($event, column, source)"
|
|
138
|
-
>
|
|
139
|
-
|
|
140
65
|
<button
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
66
|
+
v-if="!column.editReadonly"
|
|
67
|
+
@click="setCurrentValue(column.name, currentValues[column.name], currentValues[column.name].length); focusOnLastInput(column.name)"
|
|
68
|
+
class="flex items-center py-1 px-3 me-2 text-sm font-medium rounded-default text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
|
69
|
+
:class="{'mt-2': currentValues[column.name].length}"
|
|
145
70
|
>
|
|
146
|
-
|
|
147
|
-
|
|
71
|
+
<IconPlusOutline class="w-4 h-4 me-2"/>
|
|
72
|
+
{{ $t('Add') }}
|
|
148
73
|
</button>
|
|
149
74
|
</template>
|
|
75
|
+
<ColumnValueInput
|
|
76
|
+
v-else
|
|
77
|
+
:source="source"
|
|
78
|
+
:column="column"
|
|
79
|
+
:value="currentValues[column.name]"
|
|
80
|
+
:currentValues="currentValues"
|
|
81
|
+
:mode="mode"
|
|
82
|
+
:columnOptions="columnOptions"
|
|
83
|
+
:unmasked="unmasked"
|
|
84
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
85
|
+
@update:unmasked="unmasked[column.name] = !unmasked[column.name]"
|
|
86
|
+
@update:inValidity="customComponentsInValidity[column.name] = $event"
|
|
87
|
+
@update:emptiness="customComponentsEmptiness[column.name] = $event"
|
|
88
|
+
/>
|
|
150
89
|
<div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
|
|
151
90
|
<div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
|
|
152
91
|
</td>
|
|
@@ -157,12 +96,10 @@
|
|
|
157
96
|
</template>
|
|
158
97
|
|
|
159
98
|
<script setup lang="ts">
|
|
160
|
-
import { IconExclamationCircleSolid,
|
|
161
|
-
import
|
|
162
|
-
import Select from '@/afcl/Select.vue';
|
|
163
|
-
import { getCustomComponent } from '@/utils';
|
|
99
|
+
import { IconExclamationCircleSolid, IconPlusOutline } from '@iconify-prerendered/vue-flowbite';
|
|
100
|
+
import ColumnValueInput from "@/components/ColumnValueInput.vue";
|
|
164
101
|
import { Tooltip } from '@/afcl';
|
|
165
|
-
import { ref, computed, watch, type Ref } from 'vue';
|
|
102
|
+
import { ref, computed, watch, nextTick, type Ref } from 'vue';
|
|
166
103
|
import { useI18n } from 'vue-i18n';
|
|
167
104
|
|
|
168
105
|
const { t } = useI18n();
|
|
@@ -179,33 +116,18 @@
|
|
|
179
116
|
columnOptions: any,
|
|
180
117
|
}>();
|
|
181
118
|
|
|
119
|
+
const arrayItemRefs = ref([]);
|
|
120
|
+
|
|
182
121
|
const customComponentsInValidity: Ref<Record<string, boolean>> = ref({});
|
|
183
122
|
const customComponentsEmptiness: Ref<Record<string, boolean>> = ref({});
|
|
184
123
|
|
|
185
|
-
const getBooleanOptions = (column: any) => {
|
|
186
|
-
const options: Array<{ label: string; value: boolean | null }> = [
|
|
187
|
-
{ label: t('Yes'), value: true },
|
|
188
|
-
{ label: t('No'), value: false },
|
|
189
|
-
];
|
|
190
|
-
if (!column.required[props.mode]) {
|
|
191
|
-
options.push({ label: t('Unset'), value: null });
|
|
192
|
-
}
|
|
193
|
-
return options;
|
|
194
|
-
};
|
|
195
|
-
function onFocusHandler(event:FocusEvent, column:any, source:string, ) {
|
|
196
|
-
const focusedInput = event.target as HTMLInputElement;
|
|
197
|
-
if(!focusedInput) return;
|
|
198
|
-
if (column.editReadonly && source === 'edit') return;
|
|
199
|
-
else {
|
|
200
|
-
focusedInput.removeAttribute('readonly');
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
|
|
205
124
|
const emit = defineEmits(['update:customComponentsInValidity', 'update:customComponentsEmptiness']);
|
|
206
125
|
|
|
207
|
-
|
|
208
|
-
|
|
126
|
+
async function focusOnLastInput(column) {
|
|
127
|
+
// wait for element to register
|
|
128
|
+
await nextTick();
|
|
129
|
+
arrayItemRefs.value[arrayItemRefs.value.length - 1].focus();
|
|
130
|
+
}
|
|
209
131
|
|
|
210
132
|
watch(customComponentsInValidity, (newVal) => {
|
|
211
133
|
emit('update:customComponentsInValidity', newVal);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
}"
|
|
12
12
|
>
|
|
13
13
|
<component v-if="item.icon" :is="getIcon(item.icon)" class="w-5 h-5 text-lightSidebarIcons dark:text-darkSidebarIcons transition duration-75 group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover" ></component>
|
|
14
|
-
<span class="ms-3">{{ item.label }}</span>
|
|
14
|
+
<span class="text-ellipsis overflow-hidden ms-3">{{ item.label }}</span>
|
|
15
15
|
<span v-if="item.badge" class="inline-flex items-center justify-center h-3 py-3 px-1 ms-3 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
|
|
16
16
|
fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent min-w-[1.5rem] max-w-[3rem]"
|
|
17
17
|
>
|
|
@@ -101,7 +101,7 @@ const columnError = (column) => {
|
|
|
101
101
|
|
|
102
102
|
if (
|
|
103
103
|
column.required[mode.value] &&
|
|
104
|
-
(currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '') &&
|
|
104
|
+
(currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '' || (column.isArray?.enabled && !currentValues.value[column.name].length)) &&
|
|
105
105
|
// if component is custum it might tell other criteria for emptiness by emitting 'update:emptiness'
|
|
106
106
|
// components which do not emit 'update:emptiness' will have undefined value in customComponentsEmptiness
|
|
107
107
|
(customComponentsEmptiness.value[column.name] !== false)
|
|
@@ -109,68 +109,103 @@ const columnError = (column) => {
|
|
|
109
109
|
) {
|
|
110
110
|
return t('This field is required');
|
|
111
111
|
}
|
|
112
|
-
if (column.type === 'json' && currentValues.value[column.name]) {
|
|
112
|
+
if (column.type === 'json' && !column.isArray?.enabled && currentValues.value[column.name]) {
|
|
113
113
|
try {
|
|
114
114
|
JSON.parse(currentValues.value[column.name]);
|
|
115
115
|
} catch (e) {
|
|
116
116
|
return t('Invalid JSON');
|
|
117
117
|
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if ( column.minLength && currentValues.value[column.name]?.length < column.minLength ) {
|
|
125
|
-
// if column.required[mode.value] is false, then we check if the field is empty
|
|
126
|
-
let needToCheckEmpty = column.required[mode.value] || currentValues.value[column.name]?.length > 0;
|
|
127
|
-
if (!needToCheckEmpty) {
|
|
128
|
-
return null;
|
|
118
|
+
} else if (column.isArray?.enabled) {
|
|
119
|
+
if (!column.isArray.allowDuplicateItems) {
|
|
120
|
+
if (currentValues.value[column.name].filter((value, index, self) => self.indexOf(value) !== index).length > 0) {
|
|
121
|
+
return t('Array cannot contain duplicate items');
|
|
129
122
|
}
|
|
130
|
-
return t('This field must be longer than {minLength} characters', { minLength: column.minLength });
|
|
131
123
|
}
|
|
124
|
+
|
|
125
|
+
return currentValues.value[column.name] && currentValues.value[column.name].reduce((error, item) => {
|
|
126
|
+
return error || validateValue(column.isArray.itemType, item, column) ||
|
|
127
|
+
(item === null || !item.toString() ? t('Array cannot contain empty items') : null);
|
|
128
|
+
}, null);
|
|
129
|
+
} else {
|
|
130
|
+
return validateValue(column.type, currentValues.value[column.name], column);
|
|
132
131
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
132
|
+
|
|
133
|
+
});
|
|
134
|
+
return val.value;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const validateValue = (type, value, column) => {
|
|
138
|
+
if (type === 'string' || type === 'text') {
|
|
139
|
+
if (column.maxLength && value?.length > column.maxLength) {
|
|
140
|
+
return t('This field must be shorter than {maxLength} characters', { maxLength: column.maxLength });
|
|
143
141
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if
|
|
147
|
-
|
|
142
|
+
|
|
143
|
+
if (column.minLength && value?.length < column.minLength) {
|
|
144
|
+
// if column.required[mode.value] is false, then we check if the field is empty
|
|
145
|
+
let needToCheckEmpty = column.required[mode.value] || value?.length > 0;
|
|
146
|
+
if (!needToCheckEmpty) {
|
|
147
|
+
return null;
|
|
148
148
|
}
|
|
149
|
+
return t('This field must be longer than {minLength} characters', { minLength: column.minLength });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (['integer', 'decimal', 'float'].includes(type)) {
|
|
153
|
+
if (column.minValue !== undefined
|
|
154
|
+
&& value !== null
|
|
155
|
+
&& value < column.minValue
|
|
156
|
+
) {
|
|
157
|
+
return t('This field must be greater than {minValue}', { minValue: column.minValue });
|
|
158
|
+
}
|
|
159
|
+
if (column.maxValue !== undefined && value > column.maxValue) {
|
|
160
|
+
return t('This field must be less than {maxValue}', { maxValue: column.maxValue });
|
|
149
161
|
}
|
|
162
|
+
}
|
|
163
|
+
if (value && column.validation) {
|
|
164
|
+
const error = applyRegexValidation(value, column.validation);
|
|
165
|
+
if (error) {
|
|
166
|
+
return error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
150
169
|
|
|
151
|
-
|
|
152
|
-
});
|
|
153
|
-
return val.value;
|
|
170
|
+
return null;
|
|
154
171
|
};
|
|
155
172
|
|
|
156
173
|
|
|
157
|
-
const setCurrentValue = (key, value) => {
|
|
174
|
+
const setCurrentValue = (key, value, index=null) => {
|
|
158
175
|
const col = props.resource.columns.find((column) => column.name === key);
|
|
159
|
-
if
|
|
160
|
-
|
|
176
|
+
// if field is an array, we need to update the array or individual element
|
|
177
|
+
if (col.type === 'json' && col.isArray?.enabled) {
|
|
178
|
+
if (index === null) {
|
|
179
|
+
currentValues.value[key] = value;
|
|
180
|
+
} else if (index === currentValues.value[key].length) {
|
|
181
|
+
currentValues.value[key].push(null);
|
|
182
|
+
} else {
|
|
183
|
+
if (['integer', 'float'].includes(col.isArray.itemType) && (value || value === 0)) {
|
|
184
|
+
currentValues.value[key][index] = +value;
|
|
185
|
+
} else {
|
|
186
|
+
currentValues.value[key][index] = value;
|
|
187
|
+
}
|
|
188
|
+
if (['text', 'richtext', 'string'].includes(col.isArray.itemType) && col.enforceLowerCase) {
|
|
189
|
+
currentValues.value[key][index] = currentValues.value[key][index].toLowerCase();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
161
192
|
} else {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
193
|
+
if (['integer', 'float'].includes(col.type) && (value || value === 0)) {
|
|
194
|
+
currentValues.value[key] = +value;
|
|
195
|
+
} else {
|
|
196
|
+
currentValues.value[key] = value;
|
|
197
|
+
}
|
|
198
|
+
if (['text', 'richtext', 'string'].includes(col.type) && col.enforceLowerCase) {
|
|
199
|
+
currentValues.value[key] = currentValues.value[key].toLowerCase();
|
|
200
|
+
}
|
|
166
201
|
}
|
|
167
202
|
|
|
168
203
|
currentValues.value = { ...currentValues.value };
|
|
169
204
|
|
|
170
|
-
//json fields should transform to object
|
|
205
|
+
// json fields should transform to object
|
|
171
206
|
const up = {...currentValues.value};
|
|
172
207
|
props.resource.columns.forEach((column) => {
|
|
173
|
-
if (column.type === 'json' && up[column.name]) {
|
|
208
|
+
if (column.type === 'json' && !column.isArray?.enabled && up[column.name]) {
|
|
174
209
|
try {
|
|
175
210
|
up[column.name] = JSON.parse(up[column.name]);
|
|
176
211
|
} catch (e) {
|
|
@@ -185,8 +220,19 @@ onMounted(() => {
|
|
|
185
220
|
currentValues.value = Object.assign({}, props.record);
|
|
186
221
|
// json values should transform to string
|
|
187
222
|
props.resource.columns.forEach((column) => {
|
|
188
|
-
if (column.type === 'json'
|
|
189
|
-
|
|
223
|
+
if (column.type === 'json') {
|
|
224
|
+
if (column.isArray?.enabled) {
|
|
225
|
+
// if value is null or undefined, we should set it to empty array
|
|
226
|
+
if (!currentValues.value[column.name]) {
|
|
227
|
+
currentValues.value[column.name] = [];
|
|
228
|
+
} else {
|
|
229
|
+
// else copy array to prevent mutation
|
|
230
|
+
currentValues.value[column.name] = [...currentValues.value[column.name]];
|
|
231
|
+
}
|
|
232
|
+
} else if (currentValues.value[column.name]) {
|
|
233
|
+
currentValues.value[column.name] = JSON.stringify(currentValues.value[column.name], null, 2);
|
|
234
|
+
}
|
|
235
|
+
|
|
190
236
|
}
|
|
191
237
|
});
|
|
192
238
|
emit('update:isValid', isValid.value);
|