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.
@@ -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 leading-none 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"
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 leading-none 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"
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 class="px-6 py-4 whitespace-pre-wrap relative block md:table-cell rounded-br-lg "
42
- :class="{'rounded-br-lg': i === group.columns.length - 1}">
43
- <template v-if="column?.components?.[props.source]?.file">
44
- <component
45
- :is="getCustomComponent(column.components[props.source])"
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
- :value="currentValues[column.name]"
48
- @update:value="setCurrentValue(column.name, $event)"
49
- :meta="column.components[props.source].meta"
50
- :record="currentValues"
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
- v-if="column.masked"
142
- type="button"
143
- @click="unmasked[column.name] = !unmasked[column.name]"
144
- class="h-6 absolute inset-y-2 top-6 right-6 flex items-center pr-2 z-index-100 focus:outline-none"
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
- <IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]" />
147
- <IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
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, IconEyeSlashSolid, IconEyeSolid } from '@iconify-prerendered/vue-flowbite';
161
- import CustomDatePicker from "@/components/CustomDatePicker.vue";
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
- if ( column.type === 'string' || column.type === 'text' ) {
120
- if ( column.maxLength && currentValues.value[column.name]?.length > column.maxLength ) {
121
- return t('This field must be shorter than {maxLength} characters', { maxLength: column.maxLength });
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
- if ( ['integer', 'decimal', 'float'].includes(column.type) ) {
134
- if ( column.minValue !== undefined
135
- && currentValues.value[column.name] !== null
136
- && currentValues.value[column.name] < column.minValue
137
- ) {
138
- return t('This field must be greater than {minValue}', { minValue: column.minValue });
139
- }
140
- if ( column.maxValue !== undefined && currentValues.value[column.name] > column.maxValue ) {
141
- return t('This field must be less than {maxValue}', { maxValue: column.maxValue });
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
- if (currentValues.value[column.name] && column.validation) {
145
- const error = applyRegexValidation(currentValues.value[column.name], column.validation);
146
- if (error) {
147
- return error;
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
- return null;
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 (['integer', 'float'].includes(col.type) && (value || value === 0)) {
160
- currentValues.value[key] = +value;
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
- currentValues.value[key] = value;
163
- }
164
- if (['text', 'richtext', 'string'].includes(col.type) && col.enforceLowerCase) {
165
- currentValues.value[key] = currentValues.value[key].toLowerCase();
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' && currentValues.value[column.name]) {
189
- currentValues.value[column.name] = JSON.stringify(currentValues.value[column.name], null, 2);
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);