adminforth 1.7.0 → 1.9.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.
Files changed (62) hide show
  1. package/commands/createApp/templates/{users.ts.hbs → adminuser.ts.hbs} +2 -2
  2. package/commands/createApp/templates/index.ts.hbs +5 -5
  3. package/commands/createApp/utils.js +2 -2
  4. package/dist/dataConnectors/baseConnector.d.ts +3 -0
  5. package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  6. package/dist/dataConnectors/baseConnector.js +7 -0
  7. package/dist/dataConnectors/baseConnector.js.map +1 -1
  8. package/dist/dataConnectors/clickhouse.d.ts +1 -8
  9. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  10. package/dist/dataConnectors/clickhouse.js +4 -6
  11. package/dist/dataConnectors/clickhouse.js.map +1 -1
  12. package/dist/dataConnectors/mongo.d.ts +1 -5
  13. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  14. package/dist/dataConnectors/mongo.js +15 -13
  15. package/dist/dataConnectors/mongo.js.map +1 -1
  16. package/dist/dataConnectors/mysql.d.ts +68 -0
  17. package/dist/dataConnectors/mysql.d.ts.map +1 -0
  18. package/dist/dataConnectors/mysql.js +308 -0
  19. package/dist/dataConnectors/mysql.js.map +1 -0
  20. package/dist/dataConnectors/postgres.d.ts +1 -4
  21. package/dist/dataConnectors/postgres.d.ts.map +1 -1
  22. package/dist/dataConnectors/postgres.js +25 -24
  23. package/dist/dataConnectors/postgres.js.map +1 -1
  24. package/dist/dataConnectors/sqlite.d.ts +1 -4
  25. package/dist/dataConnectors/sqlite.d.ts.map +1 -1
  26. package/dist/dataConnectors/sqlite.js +13 -12
  27. package/dist/dataConnectors/sqlite.js.map +1 -1
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +68 -35
  31. package/dist/index.js.map +1 -1
  32. package/dist/modules/configValidator.d.ts.map +1 -1
  33. package/dist/modules/configValidator.js +58 -7
  34. package/dist/modules/configValidator.js.map +1 -1
  35. package/dist/modules/restApi.d.ts.map +1 -1
  36. package/dist/modules/restApi.js +5 -1
  37. package/dist/modules/restApi.js.map +1 -1
  38. package/dist/spa/src/App.vue +6 -3
  39. package/dist/spa/src/afcl/Dialog.vue +96 -0
  40. package/dist/spa/src/afcl/Input.vue +8 -6
  41. package/dist/spa/src/afcl/Select.vue +2 -3
  42. package/dist/spa/src/components/ColumnValueInput.vue +182 -0
  43. package/dist/spa/src/components/CustomDatePicker.vue +11 -3
  44. package/dist/spa/src/components/Filters.vue +5 -5
  45. package/dist/spa/src/components/GroupsTable.vue +52 -130
  46. package/dist/spa/src/components/MenuLink.vue +1 -1
  47. package/dist/spa/src/components/ResourceForm.vue +115 -56
  48. package/dist/spa/src/components/ValueRenderer.vue +40 -0
  49. package/dist/spa/src/types/Back.ts +20 -6
  50. package/dist/spa/src/types/Common.ts +25 -0
  51. package/dist/spa/src/types/FrontendAPI.ts +1 -1
  52. package/dist/spa/src/utils.ts +2 -1
  53. package/dist/spa/src/views/ListView.vue +5 -1
  54. package/dist/types/Back.d.ts +16 -8
  55. package/dist/types/Back.d.ts.map +1 -1
  56. package/dist/types/Back.js.map +1 -1
  57. package/dist/types/Common.d.ts +23 -0
  58. package/dist/types/Common.d.ts.map +1 -1
  59. package/dist/types/Common.js.map +1 -1
  60. package/dist/types/FrontendAPI.d.ts +1 -1
  61. package/dist/types/FrontendAPI.d.ts.map +1 -1
  62. package/package.json +2 -1
@@ -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,39 +116,24 @@
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
 
126
+ async function focusOnLastInput(column) {
127
+ // wait for element to register
128
+ await nextTick();
129
+ arrayItemRefs.value[arrayItemRefs.value.length - 1].focus();
130
+ }
207
131
 
208
-
209
-
210
- watch(customComponentsInValidity, (newVal) => {
132
+ watch(customComponentsInValidity.value, (newVal) => {
211
133
  emit('update:customComponentsInValidity', newVal);
212
134
  });
213
135
 
214
- watch(customComponentsEmptiness, (newVal) => {
136
+ watch(customComponentsEmptiness.value, (newVal) => {
215
137
  emit('update:customComponentsEmptiness', newVal);
216
138
  });
217
139
 
@@ -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
  >
@@ -15,8 +15,8 @@
15
15
  :validating="validating"
16
16
  :columnError="columnError"
17
17
  :setCurrentValue="setCurrentValue"
18
- @update:customComponentsInValidity="(data) => customComponentsInValidity.value = { ...customComponentsInValidity.value, ...data }"
19
- @update:customComponentsEmptiness="(data) => customComponentsEmptiness.value = { ...customComponentsEmptiness.value, ...data }"
18
+ @update:customComponentsInValidity="(data) => customComponentsInValidity = { ...customComponentsInValidity, ...data }"
19
+ @update:customComponentsEmptiness="(data) => customComponentsEmptiness = { ...customComponentsEmptiness, ...data }"
20
20
  />
21
21
  </div>
22
22
  <div v-else class="flex flex-col gap-4">
@@ -32,8 +32,8 @@
32
32
  :validating="validating"
33
33
  :columnError="columnError"
34
34
  :setCurrentValue="setCurrentValue"
35
- @update:customComponentsInValidity="(data) => customComponentsInValidity.value = { ...customComponentsInValidity.value, ...data }"
36
- @update:customComponentsEmptiness="(data) => customComponentsEmptiness.value = { ...customComponentsEmptiness.value, ...data }"
35
+ @update:customComponentsInValidity="(data) => customComponentsInValidity = { ...customComponentsInValidity, ...data }"
36
+ @update:customComponentsEmptiness="(data) => customComponentsEmptiness = { ...customComponentsEmptiness, ...data }"
37
37
  />
38
38
  </template>
39
39
  <div v-if="otherColumns.length > 0">
@@ -48,8 +48,8 @@
48
48
  :validating="validating"
49
49
  :columnError="columnError"
50
50
  :setCurrentValue="setCurrentValue"
51
- @update:customComponentsInValidity="(data) => customComponentsInValidity.value = { ...customComponentsInValidity.value, ...data }"
52
- @update:customComponentsEmptiness="(data) => customComponentsEmptiness.value = { ...customComponentsEmptiness.value, ...data }"
51
+ @update:customComponentsInValidity="(data) => customComponentsInValidity = { ...customComponentsInValidity, ...data }"
52
+ @update:customComponentsEmptiness="(data) => customComponentsEmptiness = { ...customComponentsEmptiness, ...data }"
53
53
  />
54
54
  </div>
55
55
  </div>
@@ -95,82 +95,127 @@ const columnError = (column) => {
95
95
  if (!currentValues.value) {
96
96
  return null;
97
97
  }
98
- if (customComponentsInValidity.value[column.name]) {
99
- return customComponentsInValidity.value[column.name];
98
+ if (customComponentsInValidity.value?.[column.name]) {
99
+ return customComponentsInValidity.value?.[column.name];
100
100
  }
101
+
102
+ if ( column.required[mode.value] ) {
103
+ const naturalEmptiness = currentValues.value[column.name] === undefined ||
104
+ currentValues.value[column.name] === null ||
105
+ currentValues.value[column.name] === '' ||
106
+ (column.isArray?.enabled && !currentValues.value[column.name].length);
101
107
 
102
- if (
103
- column.required[mode.value] &&
104
- (currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '') &&
108
+ const emitedEmptiness = customComponentsEmptiness.value?.[column.name];
105
109
  // if component is custum it might tell other criteria for emptiness by emitting 'update:emptiness'
106
110
  // components which do not emit 'update:emptiness' will have undefined value in customComponentsEmptiness
107
- (customComponentsEmptiness.value[column.name] !== false)
108
-
109
- ) {
110
- return t('This field is required');
111
+ let actualEmptiness;
112
+ if (emitedEmptiness !== undefined) {
113
+ actualEmptiness = emitedEmptiness;
114
+ } else {
115
+ actualEmptiness = naturalEmptiness;
116
+ }
117
+ if (actualEmptiness) {
118
+ return t('This field is required');
119
+ }
111
120
  }
112
- if (column.type === 'json' && currentValues.value[column.name]) {
121
+ if (column.type === 'json' && !column.isArray?.enabled && currentValues.value[column.name]) {
113
122
  try {
114
123
  JSON.parse(currentValues.value[column.name]);
115
124
  } catch (e) {
116
125
  return t('Invalid JSON');
117
126
  }
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;
127
+ } else if (column.isArray?.enabled) {
128
+ if (!column.isArray.allowDuplicateItems) {
129
+ if (currentValues.value[column.name].filter((value, index, self) => self.indexOf(value) !== index).length > 0) {
130
+ return t('Array cannot contain duplicate items');
129
131
  }
130
- return t('This field must be longer than {minLength} characters', { minLength: column.minLength });
131
132
  }
133
+
134
+ return currentValues.value[column.name] && currentValues.value[column.name].reduce((error, item) => {
135
+ return error || validateValue(column.isArray.itemType, item, column) ||
136
+ (item === null || !item.toString() ? t('Array cannot contain empty items') : null);
137
+ }, null);
138
+ } else {
139
+ return validateValue(column.type, currentValues.value[column.name], column);
132
140
  }
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
- }
141
+
142
+ return null;
143
+ });
144
+ return val.value;
145
+ };
146
+
147
+ const validateValue = (type, value, column) => {
148
+ if (type === 'string' || type === 'text') {
149
+ if (column.maxLength && value?.length > column.maxLength) {
150
+ return t('This field must be shorter than {maxLength} characters', { maxLength: column.maxLength });
143
151
  }
144
- if (currentValues.value[column.name] && column.validation) {
145
- const error = applyRegexValidation(currentValues.value[column.name], column.validation);
146
- if (error) {
147
- return error;
152
+
153
+ if (column.minLength && value?.length < column.minLength) {
154
+ // if column.required[mode.value] is false, then we check if the field is empty
155
+ let needToCheckEmpty = column.required[mode.value] || value?.length > 0;
156
+ if (!needToCheckEmpty) {
157
+ return null;
148
158
  }
159
+ return t('This field must be longer than {minLength} characters', { minLength: column.minLength });
160
+ }
161
+ }
162
+ if (['integer', 'decimal', 'float'].includes(type)) {
163
+ if (column.minValue !== undefined
164
+ && value !== null
165
+ && value < column.minValue
166
+ ) {
167
+ return t('This field must be greater than {minValue}', { minValue: column.minValue });
168
+ }
169
+ if (column.maxValue !== undefined && value > column.maxValue) {
170
+ return t('This field must be less than {maxValue}', { maxValue: column.maxValue });
149
171
  }
172
+ }
173
+ if (value && column.validation) {
174
+ const error = applyRegexValidation(value, column.validation);
175
+ if (error) {
176
+ return error;
177
+ }
178
+ }
150
179
 
151
- return null;
152
- });
153
- return val.value;
180
+ return null;
154
181
  };
155
182
 
156
183
 
157
- const setCurrentValue = (key, value) => {
184
+ const setCurrentValue = (key, value, index=null) => {
158
185
  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;
186
+ // if field is an array, we need to update the array or individual element
187
+ if (col.type === 'json' && col.isArray?.enabled) {
188
+ if (index === null) {
189
+ currentValues.value[key] = value;
190
+ } else if (index === currentValues.value[key].length) {
191
+ currentValues.value[key].push(null);
192
+ } else {
193
+ if (['integer', 'float'].includes(col.isArray.itemType) && (value || value === 0)) {
194
+ currentValues.value[key][index] = +value;
195
+ } else {
196
+ currentValues.value[key][index] = value;
197
+ }
198
+ if (['text', 'richtext', 'string'].includes(col.isArray.itemType) && col.enforceLowerCase) {
199
+ currentValues.value[key][index] = currentValues.value[key][index].toLowerCase();
200
+ }
201
+ }
161
202
  } 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();
203
+ if (['integer', 'float'].includes(col.type) && (value || value === 0)) {
204
+ currentValues.value[key] = +value;
205
+ } else {
206
+ currentValues.value[key] = value;
207
+ }
208
+ if (['text', 'richtext', 'string'].includes(col.type) && col.enforceLowerCase) {
209
+ currentValues.value[key] = currentValues.value[key].toLowerCase();
210
+ }
166
211
  }
167
212
 
168
213
  currentValues.value = { ...currentValues.value };
169
214
 
170
- //json fields should transform to object
215
+ // json fields should transform to object
171
216
  const up = {...currentValues.value};
172
217
  props.resource.columns.forEach((column) => {
173
- if (column.type === 'json' && up[column.name]) {
218
+ if (column.type === 'json' && !column.isArray?.enabled && up[column.name]) {
174
219
  try {
175
220
  up[column.name] = JSON.parse(up[column.name]);
176
221
  } catch (e) {
@@ -185,8 +230,19 @@ onMounted(() => {
185
230
  currentValues.value = Object.assign({}, props.record);
186
231
  // json values should transform to string
187
232
  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);
233
+ if (column.type === 'json') {
234
+ if (column.isArray?.enabled) {
235
+ // if value is null or undefined, we should set it to empty array
236
+ if (!currentValues.value[column.name]) {
237
+ currentValues.value[column.name] = [];
238
+ } else {
239
+ // else copy array to prevent mutation
240
+ currentValues.value[column.name] = [...currentValues.value[column.name]];
241
+ }
242
+ } else if (currentValues.value[column.name]) {
243
+ currentValues.value[column.name] = JSON.stringify(currentValues.value[column.name], null, 2);
244
+ }
245
+
190
246
  }
191
247
  });
192
248
  emit('update:isValid', isValid.value);
@@ -206,6 +262,9 @@ const columnOptions = computedAsync(async () => {
206
262
  offset: 0,
207
263
  },
208
264
  });
265
+
266
+ if (!column.required[props.source]) list.items.push({ value: null, label: t('Unset') });
267
+
209
268
  return { [column.name]: list.items };
210
269
  }
211
270
  })
@@ -14,6 +14,32 @@
14
14
  <span v-if="record[column.name]" class="bg-green-100 text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-green-400 border border-green-400">{{ $t('Yes') }}</span>
15
15
  <span v-else class="bg-red-100 text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400">{{ $t('No') }}</span>
16
16
  </span>
17
+ <span
18
+ v-else-if="column.type === 'json' && column.isArray?.enabled"
19
+ class="flex flex-wrap"
20
+ >
21
+ <template v-for="(arrayItem, arrayItemIndex) in record[column.name]">
22
+ <span
23
+ v-if="column.isArray.itemType === 'boolean' && arrayItem"
24
+ :key="`${column.name}-${arrayItemIndex}`"
25
+ class="bg-green-100 text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-green-400 border border-green-400">
26
+ {{ $t('Yes') }}
27
+ </span>
28
+ <span
29
+ v-else-if="column.isArray.itemType === 'boolean'"
30
+ :key="`${column.name}-${arrayItemIndex}`"
31
+ class="bg-red-100 text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400">
32
+ {{ $t('No') }}
33
+ </span>
34
+ <span
35
+ v-else
36
+ :key="`${column.name}-${arrayItemIndex}`"
37
+ class="rounded-md m-0.5 bg-lightAnnouncementBG dark:bg-darkAnnouncementBG text-lightAnnouncementText dark:text-darkAnnouncementText py-0.5 px-2.5 text-sm"
38
+ >
39
+ {{ checkEmptyValues(getArrayItemDisplayValue(arrayItem, column), route.meta.type) }}
40
+ </span>
41
+ </template>
42
+ </span>
17
43
  <span v-else-if="column.enum">
18
44
  {{ checkEmptyValues(column.enum.find(e => e.value === record[column.name])?.label || record[column.name], route.meta.type) }}
19
45
  </span>
@@ -102,6 +128,20 @@ function formatTime(time: string) {
102
128
  if (!time) return '';
103
129
  return dayjs(`0000-00-00 ${time}`).format(coreStore.config?.timeFormat || 'HH:mm:ss');
104
130
  }
131
+
132
+ function getArrayItemDisplayValue(value, column) {
133
+ if (column.isArray?.itemType === 'datetime') {
134
+ return formatDateTime(value);
135
+ } else if (column.isArray?.itemType === 'date') {
136
+ return formatDate(value);
137
+ } else if (column.isArray?.itemType === 'time') {
138
+ return formatTime(value);
139
+ } else if (column.enum) {
140
+ return column.enum.find(e => e.value === value)?.label || value;
141
+ }
142
+
143
+ return value;
144
+ }
105
145
  </script>
106
146
 
107
147
  <style lang="scss">
@@ -118,6 +118,14 @@ export interface IAdminForthSort {
118
118
  }
119
119
 
120
120
  export interface IAdminForthDataSourceConnector {
121
+
122
+ client: any;
123
+
124
+ /**
125
+ * Function to setup client connection to database.
126
+ * @param url URL to database. Examples: clickhouse://demo:demo@localhost:8125/demo
127
+ */
128
+ setupClient(url: string): Promise<void>;
121
129
 
122
130
  /**
123
131
  * Optional.
@@ -250,7 +258,7 @@ export interface IAdminForthDataSourceConnectorBase extends IAdminForthDataSourc
250
258
 
251
259
 
252
260
  export interface IAdminForthDataSourceConnectorConstructor {
253
- new ({ url }: { url: string }): IAdminForthDataSourceConnectorBase;
261
+ new (): IAdminForthDataSourceConnectorBase;
254
262
  }
255
263
 
256
264
  export interface IAdminForthAuth {
@@ -708,6 +716,7 @@ interface AdminForthInputConfigCustomization {
708
716
  userMenu?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
709
717
  header?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
710
718
  sidebar?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
719
+ everyPageBottom?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
711
720
  }
712
721
  }
713
722
 
@@ -984,6 +993,7 @@ export interface AdminForthConfigCustomization extends Omit<AdminForthInputConfi
984
993
  userMenu: Array<AdminForthComponentDeclarationFull>,
985
994
  header: Array<AdminForthComponentDeclarationFull>,
986
995
  sidebar: Array<AdminForthComponentDeclarationFull>,
996
+ everyPageBottom: Array<AdminForthComponentDeclarationFull>,
987
997
  },
988
998
  }
989
999
 
@@ -1322,14 +1332,18 @@ export interface AdminForthForeignResource extends AdminForthForeignResourceComm
1322
1332
  },
1323
1333
  }
1324
1334
 
1325
- /**
1326
- * Object which describes on what pages should column be displayed on.
1327
- */
1328
- export type ShowInInput = {
1335
+ export type ShowInModernInput = {
1329
1336
  [key in AdminForthResourcePages]?: AllowedActionValue
1330
1337
  } & {
1331
1338
  all?: AllowedActionValue;
1332
- } & Array<AdminForthResourcePages | keyof typeof AdminForthResourcePages>;
1339
+ }
1340
+
1341
+ export type ShowInLegacyInput = Array<AdminForthResourcePages | keyof typeof AdminForthResourcePages>;
1342
+
1343
+ /**
1344
+ * Object which describes on what pages should column be displayed on.
1345
+ */
1346
+ export type ShowInInput = ShowInModernInput | ShowInLegacyInput;
1333
1347
 
1334
1348
  export type ShowIn = {
1335
1349
  [key in AdminForthResourcePages]: AllowedActionValue
@@ -568,6 +568,24 @@ export interface AdminForthResourceColumnInputCommon {
568
568
  */
569
569
  type?: AdminForthDataTypes,
570
570
 
571
+ /**
572
+ * Defines whether column is array and what type of items it contains.
573
+ * AdminForth will use this information to render proper input fields in the admin panel with control buttons to add and remove items.
574
+ * If enabled, requires column type to be JSON.
575
+ * Cannot be used with masked columns, columns with foreignResource or primary key columns.
576
+ */
577
+ isArray?: {
578
+ enabled: boolean,
579
+ /**
580
+ * Type of items in array. Cannot be JSON or RICHTEXT.
581
+ */
582
+ itemType: AdminForthDataTypes,
583
+ /**
584
+ * If enabled, AdminForth will allow to add items with the same value.
585
+ */
586
+ allowDuplicateItems?: boolean,
587
+ },
588
+
571
589
  /**
572
590
  * An optional configuration object for extra settings.
573
591
  */
@@ -597,6 +615,12 @@ export interface AdminForthResourceColumnInputCommon {
597
615
  */
598
616
  required?: boolean | { create?: boolean, edit?: boolean },
599
617
 
618
+ /**
619
+ * Prefix and suffix for input field on create and edit pages.
620
+ */
621
+ inputPrefix?: string,
622
+ inputSuffix?: string,
623
+
600
624
  /**
601
625
  * Whether AdminForth will show editing note near the field in edit/create form.
602
626
  */
@@ -948,6 +972,7 @@ export interface AdminForthConfigForFrontend {
948
972
  userMenu: Array<AdminForthComponentDeclarationFull>,
949
973
  header: Array<AdminForthComponentDeclarationFull>,
950
974
  sidebar: Array<AdminForthComponentDeclarationFull>,
975
+ everyPageBottom: Array<AdminForthComponentDeclarationFull>,
951
976
  }
952
977
  }
953
978
 
@@ -38,7 +38,7 @@ export interface FrontendAPIInterface {
38
38
  * @param params - The parameters of the dialog
39
39
  * @returns A promise that resolves when the user confirms the dialog
40
40
  */
41
- confirm(params:ConfirmParams ): Promise<void>;
41
+ confirm(params: ConfirmParams): Promise<boolean>;
42
42
 
43
43
  /**
44
44
  * Show an alert
@@ -116,7 +116,8 @@ export function initThreeDotsDropdown() {
116
116
  // this resource has three dots dropdown
117
117
  const dd = new Dropdown(
118
118
  threeDotsDropdown,
119
- document.querySelector('[data-dropdown-toggle="listThreeDotsDropdown"]') as HTMLElement,
119
+ document.querySelector('[data-dropdown-toggle="listThreeDotsDropdown"]') as HTMLElement,
120
+ { placement: 'bottom-end' }
120
121
  );
121
122
  adminforth.list.closeThreeDotsDropdown = () => {
122
123
  dd.hide();