adminforth 1.6.4 → 1.7.0-next.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/commands/createApp/templates/users.ts.hbs +14 -4
  2. package/dist/dataConnectors/baseConnector.d.ts +3 -0
  3. package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  4. package/dist/dataConnectors/baseConnector.js +8 -1
  5. package/dist/dataConnectors/baseConnector.js.map +1 -1
  6. package/dist/dataConnectors/clickhouse.d.ts +1 -8
  7. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  8. package/dist/dataConnectors/clickhouse.js +4 -6
  9. package/dist/dataConnectors/clickhouse.js.map +1 -1
  10. package/dist/dataConnectors/mongo.d.ts +1 -5
  11. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  12. package/dist/dataConnectors/mongo.js +15 -13
  13. package/dist/dataConnectors/mongo.js.map +1 -1
  14. package/dist/dataConnectors/mysql.d.ts +68 -0
  15. package/dist/dataConnectors/mysql.d.ts.map +1 -0
  16. package/dist/dataConnectors/mysql.js +308 -0
  17. package/dist/dataConnectors/mysql.js.map +1 -0
  18. package/dist/dataConnectors/postgres.d.ts +1 -4
  19. package/dist/dataConnectors/postgres.d.ts.map +1 -1
  20. package/dist/dataConnectors/postgres.js +25 -24
  21. package/dist/dataConnectors/postgres.js.map +1 -1
  22. package/dist/dataConnectors/sqlite.d.ts +1 -4
  23. package/dist/dataConnectors/sqlite.d.ts.map +1 -1
  24. package/dist/dataConnectors/sqlite.js +13 -12
  25. package/dist/dataConnectors/sqlite.js.map +1 -1
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +68 -35
  29. package/dist/index.js.map +1 -1
  30. package/dist/modules/configValidator.d.ts +3 -2
  31. package/dist/modules/configValidator.d.ts.map +1 -1
  32. package/dist/modules/configValidator.js +67 -15
  33. package/dist/modules/configValidator.js.map +1 -1
  34. package/dist/modules/restApi.d.ts.map +1 -1
  35. package/dist/modules/restApi.js +17 -3
  36. package/dist/modules/restApi.js.map +1 -1
  37. package/dist/spa/src/App.vue +6 -3
  38. package/dist/spa/src/components/ColumnValueInput.vue +175 -0
  39. package/dist/spa/src/components/CustomDatePicker.vue +11 -3
  40. package/dist/spa/src/components/Filters.vue +1 -1
  41. package/dist/spa/src/components/GroupsTable.vue +50 -128
  42. package/dist/spa/src/components/MenuLink.vue +1 -1
  43. package/dist/spa/src/components/ResourceForm.vue +89 -43
  44. package/dist/spa/src/components/ResourceListTable.vue +2 -2
  45. package/dist/spa/src/components/ValueRenderer.vue +40 -0
  46. package/dist/spa/src/stores/core.ts +1 -1
  47. package/dist/spa/src/types/Back.ts +39 -5
  48. package/dist/spa/src/types/Common.ts +47 -7
  49. package/dist/spa/src/types/FrontendAPI.ts +1 -1
  50. package/dist/spa/src/views/ListView.vue +5 -1
  51. package/dist/spa/src/views/ShowView.vue +3 -3
  52. package/dist/types/Back.d.ts +31 -7
  53. package/dist/types/Back.d.ts.map +1 -1
  54. package/dist/types/Back.js +1 -1
  55. package/dist/types/Back.js.map +1 -1
  56. package/dist/types/Common.d.ts +45 -6
  57. package/dist/types/Common.d.ts.map +1 -1
  58. package/dist/types/Common.js.map +1 -1
  59. package/dist/types/FrontendAPI.d.ts +1 -1
  60. package/dist/types/FrontendAPI.d.ts.map +1 -1
  61. 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,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);
@@ -215,7 +261,7 @@ const columnOptions = computedAsync(async () => {
215
261
 
216
262
 
217
263
  const editableColumns = computed(() => {
218
- return props.resource?.columns?.filter(column => column.showIn.includes(mode.value));
264
+ return props.resource?.columns?.filter(column => column.showIn[mode.value]);
219
265
  });
220
266
 
221
267
  const isValid = computed(() => {
@@ -62,7 +62,7 @@
62
62
  <!-- table header end -->
63
63
  <SkeleteLoader
64
64
  v-if="!rows"
65
- :columns="resource?.columns.filter(c => c.showIn.includes('list')).length + 2"
65
+ :columns="resource?.columns.filter(c => c.showIn.list).length + 2"
66
66
  :rows="3"
67
67
  />
68
68
  <tr v-else-if="rows.length === 0" class="bg-lightListTable dark:bg-darkListTable dark:border-darkListTableBorder">
@@ -356,7 +356,7 @@ function addToCheckedValues(id) {
356
356
  checkboxesInternal.value = [ ...checkboxesInternal.value ]
357
357
  }
358
358
 
359
- const columnsListed = computed(() => props.resource?.columns?.filter(c => c.showIn.includes('list')));
359
+ const columnsListed = computed(() => props.resource?.columns?.filter(c => c.showIn.list));
360
360
 
361
361
  async function selectAll(value) {
362
362
  if (!allFromThisPageChecked.value) {
@@ -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">
@@ -21,7 +21,7 @@ export const useCoreStore = defineStore('core', () => {
21
21
  if (!resource.value) {
22
22
  return [];
23
23
  }
24
- return resource.value.columns.filter((col: AdminForthResourceColumnCommon) => col.showIn?.includes('filter'));
24
+ return resource.value.columns.filter((col: AdminForthResourceColumnCommon) => col.showIn?.filter);
25
25
  })
26
26
 
27
27
  const resourceOptions: Ref<AdminForthResourceCommon['options'] | null> = ref(null);
@@ -11,7 +11,8 @@ import { ActionCheckSource, AdminForthFilterOperators, AdminForthSortDirections,
11
11
  AdminForthResourceInputCommon,
12
12
  AdminForthComponentDeclarationFull,
13
13
  AdminForthConfigMenuItem,
14
- AnnouncementBadgeResponse
14
+ AnnouncementBadgeResponse,
15
+ AdminForthResourcePages,
15
16
  } from './Common.js';
16
17
  import { AnyCnameRecord } from 'dns';
17
18
 
@@ -117,6 +118,14 @@ export interface IAdminForthSort {
117
118
  }
118
119
 
119
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>;
120
129
 
121
130
  /**
122
131
  * Optional.
@@ -249,7 +258,7 @@ export interface IAdminForthDataSourceConnectorBase extends IAdminForthDataSourc
249
258
 
250
259
 
251
260
  export interface IAdminForthDataSourceConnectorConstructor {
252
- new ({ url }: { url: string }): IAdminForthDataSourceConnectorBase;
261
+ new (): IAdminForthDataSourceConnectorBase;
253
262
  }
254
263
 
255
264
  export interface IAdminForthAuth {
@@ -707,6 +716,7 @@ interface AdminForthInputConfigCustomization {
707
716
  userMenu?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
708
717
  header?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
709
718
  sidebar?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
719
+ everyPageBottom?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
710
720
  }
711
721
  }
712
722
 
@@ -798,7 +808,7 @@ export interface AdminForthResourceInput extends Omit<AdminForthResourceInputCom
798
808
 
799
809
  options?: ResourceOptionsInput,
800
810
 
801
- columns: Array<AdminForthResourceColumn>,
811
+ columns: Array<AdminForthResourceColumnInput>,
802
812
 
803
813
  dataSourceColumns?: Array<AdminForthResourceColumn>,
804
814
  }
@@ -983,6 +993,7 @@ export interface AdminForthConfigCustomization extends Omit<AdminForthInputConfi
983
993
  userMenu: Array<AdminForthComponentDeclarationFull>,
984
994
  header: Array<AdminForthComponentDeclarationFull>,
985
995
  sidebar: Array<AdminForthComponentDeclarationFull>,
996
+ everyPageBottom: Array<AdminForthComponentDeclarationFull>,
986
997
  },
987
998
  }
988
999
 
@@ -1140,7 +1151,7 @@ export interface ResourceOptions extends Omit<ResourceOptionsInput, 'allowedActi
1140
1151
  * Resource describes one table or collection in database.
1141
1152
  * AdminForth generates set of pages for 'list', 'show', 'edit', 'create', 'filter' operations for each resource.
1142
1153
  */
1143
- export interface AdminForthResource extends Omit<AdminForthResourceInput, 'options'> {
1154
+ export interface AdminForthResource extends Omit<AdminForthResourceInput, 'options' | 'columns'> {
1144
1155
  /**
1145
1156
  * Array of plugins which will be used to modify resource configuration.
1146
1157
  *
@@ -1321,7 +1332,30 @@ export interface AdminForthForeignResource extends AdminForthForeignResourceComm
1321
1332
  },
1322
1333
  }
1323
1334
 
1324
- export interface AdminForthResourceColumn extends AdminForthResourceColumnCommon {
1335
+ export type ShowInModernInput = {
1336
+ [key in AdminForthResourcePages]?: AllowedActionValue
1337
+ } & {
1338
+ all?: AllowedActionValue;
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;
1347
+
1348
+ export type ShowIn = {
1349
+ [key in AdminForthResourcePages]: AllowedActionValue
1350
+ }
1351
+
1352
+ export interface AdminForthResourceColumnInput extends Omit<AdminForthResourceColumnCommon, 'showIn'> {
1353
+ showIn?: ShowInInput,
1354
+ foreignResource?: AdminForthForeignResource,
1355
+ }
1356
+
1357
+ export interface AdminForthResourceColumn extends Omit<AdminForthResourceColumnInput, 'showIn'> {
1358
+ showIn?: ShowIn,
1325
1359
  foreignResource?: AdminForthForeignResource,
1326
1360
  }
1327
1361