adminforth 1.8.0 → 1.9.1-next.1

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 (58) 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.map +1 -1
  29. package/dist/index.js +11 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/modules/configValidator.d.ts.map +1 -1
  32. package/dist/modules/configValidator.js +27 -5
  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 +5 -4
  36. package/dist/modules/restApi.js.map +1 -1
  37. package/dist/spa/src/App.vue +1 -1
  38. package/dist/spa/src/afcl/Dialog.vue +99 -0
  39. package/dist/spa/src/afcl/Input.vue +16 -6
  40. package/dist/spa/src/afcl/Select.vue +2 -3
  41. package/dist/spa/src/afcl/index.ts +1 -0
  42. package/dist/spa/src/components/ColumnValueInput.vue +44 -26
  43. package/dist/spa/src/components/CustomDatePicker.vue +2 -1
  44. package/dist/spa/src/components/CustomDateRangePicker.vue +3 -4
  45. package/dist/spa/src/components/Filters.vue +5 -5
  46. package/dist/spa/src/components/GroupsTable.vue +2 -2
  47. package/dist/spa/src/components/ResourceForm.vue +28 -15
  48. package/dist/spa/src/types/Back.ts +18 -6
  49. package/dist/spa/src/types/Common.ts +44 -3
  50. package/dist/spa/src/utils.ts +2 -1
  51. package/dist/spa/src/views/LoginView.vue +13 -2
  52. package/dist/types/Back.d.ts +14 -8
  53. package/dist/types/Back.d.ts.map +1 -1
  54. package/dist/types/Back.js.map +1 -1
  55. package/dist/types/Common.d.ts +43 -3
  56. package/dist/types/Common.d.ts.map +1 -1
  57. package/dist/types/Common.js.map +1 -1
  58. package/package.json +2 -1
@@ -100,7 +100,7 @@
100
100
 
101
101
  <component v-if="item.icon" :is="getIcon(item.icon)" class="w-5 h-5 text-lightSidebarIcons group-hover:text-lightSidebarIconsHover transition duration-75 dark:group-hover:text-darkSidebarIconsHover dark:text-darkSidebarIcons" ></component>
102
102
 
103
- <span class="flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">{{ item.label }}
103
+ <span class="text-ellipsis overflow-hidden flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">{{ item.label }}
104
104
 
105
105
  <span v-if="item.badge" class="inline-flex items-center justify-center w-3 h-3 p-3 ms-3 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
106
106
  fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent">
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <div
3
+ v-if="$slots.trigger"
4
+ @click="modal?.show()" class="inline-flex items-center cursor-pointer"
5
+ >
6
+ <slot name="trigger"></slot>
7
+ </div>
8
+ <div ref="modalEl" tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
9
+ <div v-bind="$attrs" class="relative p-4 max-w-2xl max-h-full" :class="$attrs.class?.includes('w-') ? '' : 'w-full'">
10
+ <!-- Modal content -->
11
+ <div class="relative bg-white rounded-lg shadow-sm dark:bg-gray-700">
12
+ <!-- Modal header -->
13
+ <div
14
+ v-if="header"
15
+ class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600 border-gray-200"
16
+ >
17
+ <h3 class="text-xl font-semibold text-gray-900 dark:text-white">
18
+ {{ header }}
19
+ </h3>
20
+ <button
21
+ v-if="headerCloseButton"
22
+ type="button"
23
+ class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
24
+ @click="modal?.hide()"
25
+ >
26
+ <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
27
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
28
+ </svg>
29
+ <span class="sr-only">Close modal</span>
30
+ </button>
31
+ </div>
32
+ <!-- Modal body -->
33
+ <div class="p-4 md:p-5 space-y-4 text-gray-700 dark:text-gray-400">
34
+ <slot></slot>
35
+ </div>
36
+ <!-- Modal footer -->
37
+ <div
38
+ v-if="buttons.length"
39
+ class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600"
40
+ >
41
+ <Button
42
+ v-for="(button, buttonIndex) in buttons"
43
+ :key="buttonIndex"
44
+ v-bind="button.options"
45
+ :class="{ 'ms-3': buttonIndex > 0 }"
46
+ @click="button.onclick(modal)"
47
+ >
48
+ {{ button.label }}
49
+ </Button>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ </template>
55
+
56
+ <script setup lang="ts">
57
+ import Button from "./Button.vue";
58
+ import { ref, onMounted, nextTick, onUnmounted, type Ref } from 'vue';
59
+ import { Modal } from 'flowbite';
60
+
61
+ const modalEl = ref(null);
62
+ const modal: Ref<Modal|null> = ref(null);
63
+
64
+ const props = defineProps({
65
+ header: {
66
+ type: String,
67
+ default: '',
68
+ },
69
+ headerCloseButton: {
70
+ type: Boolean,
71
+ default: true,
72
+ },
73
+ buttons: {
74
+ type: Array,
75
+ default: () => [{ label: 'Close', onclick: (dialog) => dialog.hide(), type: '' }],
76
+ },
77
+ clickToCloseOutside: {
78
+ type: Boolean,
79
+ default: true,
80
+ },
81
+ });
82
+
83
+ onMounted(async () => {
84
+ //await one tick when all is mounted
85
+ await nextTick();
86
+ modal.value = new Modal(
87
+ modalEl.value,
88
+ {
89
+ backdrop: props.clickToCloseOutside ? 'dynamic' : 'static',
90
+ },
91
+ );
92
+ })
93
+
94
+ onUnmounted(() => {
95
+ //destroy tooltip
96
+ modal.value?.destroy();
97
+ })
98
+
99
+ </script>
@@ -2,13 +2,14 @@
2
2
 
3
3
  <div class="flex z-0">
4
4
  <span
5
- v-if="$slots.prefix"
6
- class="inline-flex items-center px-3 text-sm text-gray-900 bg-gray-200 border border-s-0 border-gray-300 rounded-e-md dark:bg-gray-600 dark:text-gray-400 dark:border-gray-600">
7
- <slot name="prefix"></slot>
5
+ v-if="$slots.prefix || prefix"
6
+ class="inline-flex items-center px-3 text-sm text-gray-900 bg-gray-200 border border-s-0 border-gray-300 rounded-s-md dark:bg-gray-600 dark:text-gray-400 dark:border-gray-600">
7
+ <slot name="prefix">{{ prefix }}</slot>
8
8
  </span>
9
9
 
10
10
  <!-- translate needed for bumping ring above prefix without z-index -->
11
11
  <input
12
+ ref="input"
12
13
  v-bind="$attrs"
13
14
  :type="type"
14
15
  @input="$emit('update:modelValue', $event.target?.value)"
@@ -16,14 +17,14 @@
16
17
  aria-describedby="helper-text-explanation"
17
18
  class="inline-flex bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-0 focus:ring-lightPrimary focus:border-lightPrimary dark:focus:ring-darkPrimary dark:focus:border-darkPrimary
18
19
  blue-500 focus:border-blue-500 block w-20 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white translate-y-0"
19
- :class="{'rounded-l-md': !$slots.prefix, 'rounded-r-md': !$slots.suffix, 'w-full': fullWidth}"
20
+ :class="{'rounded-l-md': !$slots.prefix && !prefix, 'rounded-r-md': !$slots.suffix && !suffix, 'w-full': fullWidth}"
20
21
  >
21
22
 
22
23
 
23
24
  <span
24
- v-if="$slots.suffix"
25
+ v-if="$slots.suffix || suffix"
25
26
  class="inline-flex items-center px-3 text-sm text-gray-900 bg-gray-200 border border-s-0 border-gray-300 rounded-e-md dark:bg-gray-600 dark:text-gray-400 dark:border-gray-600 ">
26
- <slot name="suffix"></slot>
27
+ <slot name="suffix">{{ suffix }}</slot>
27
28
  </span>
28
29
 
29
30
  </div>
@@ -31,12 +32,21 @@
31
32
 
32
33
  <script setup lang="ts">
33
34
 
35
+ import { ref } from 'vue';
36
+
34
37
  const props = defineProps({
35
38
  type: String,
36
39
  fullWidth: Boolean,
37
40
  modelValue: String,
41
+ suffix: String,
42
+ prefix: String,
38
43
  })
39
44
 
45
+ const input = ref<HTMLInputElement | null>(null)
46
+
47
+ defineExpose({
48
+ focus: () => input.value?.focus(),
49
+ });
40
50
 
41
51
  </script>
42
52
 
@@ -213,7 +213,7 @@ const removeClickListener = () => {
213
213
 
214
214
  const toogleItem = (item) => {
215
215
  if (selectedItems.value.includes(item)) {
216
- selectedItems.value = selectedItems.value.filter(i => i !== item);
216
+ selectedItems.value = selectedItems.value.filter(i => i.value !== item.value);
217
217
  } else {
218
218
  if (!props.multiple) {
219
219
  selectedItems.value = [item];
@@ -228,8 +228,7 @@ const toogleItem = (item) => {
228
228
  search.value = '';
229
229
  }
230
230
 
231
- const list = selectedItems.value.map(item => item.value);
232
- const updValue = list.length ? list : null;
231
+ const updValue = selectedItems.value.map(item => item.value);
233
232
  let emitValue;
234
233
  if (!props.multiple) {
235
234
  emitValue = updValue ? updValue[0] : null;
@@ -16,5 +16,6 @@ export { default as Table } from './Table.vue';
16
16
  export { default as ProgressBar } from './ProgressBar.vue';
17
17
  export { default as Spinner } from './Spinner.vue';
18
18
  export { default as Skeleton } from './Skeleton.vue';
19
+ export { default as Dialog } from './Dialog.vue';
19
20
 
20
21
 
@@ -8,6 +8,8 @@
8
8
  @update:value="$emit('update:modelValue', $event)"
9
9
  :meta="column.components[props.source].meta"
10
10
  :record="currentValues"
11
+ :resource="coreStore.resource"
12
+ :adminUser="coreStore.adminUser"
11
13
  @update:inValidity="$emit('update:inValidity', $event)"
12
14
  @update:emptiness="$emit('update:emptiness', $event)"
13
15
  />
@@ -39,17 +41,19 @@
39
41
  :readonly="column.editReadonly && source === 'edit'"
40
42
  @update:modelValue="$emit('update:modelValue', $event)"
41
43
  />
42
- <input
44
+ <Input
43
45
  v-else-if="['integer'].includes(type || column.type)"
44
46
  ref="input"
45
- type="number"
47
+ type="number"
46
48
  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"
49
+ class="w-40"
48
50
  placeholder="0"
51
+ :prefix="column.inputPrefix"
52
+ :suffix="column.inputSuffix"
49
53
  :readonly="column.editReadonly && source === 'edit'"
50
- :value="value"
51
- @input="$emit('update:modelValue', $event.target.value)"
52
- >
54
+ :modelValue="value"
55
+ @update:modelValue="$emit('update:modelValue', $event)"
56
+ />
53
57
  <CustomDatePicker
54
58
  v-else-if="['datetime'].includes(type || column.type)"
55
59
  ref="input"
@@ -59,15 +63,17 @@
59
63
  @update:valueStart="$emit('update:modelValue', $event)"
60
64
  :readonly="column.editReadonly && source === 'edit'"
61
65
  />
62
- <input
66
+ <Input
63
67
  v-else-if="['decimal', 'float'].includes(type || column.type)"
64
68
  ref="input"
65
69
  type="number"
66
70
  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"
71
+ class="w-40"
68
72
  placeholder="0.0"
69
- :value="value"
70
- @input="$emit('update:modelValue', $event.target.value)"
73
+ :prefix="column.inputPrefix"
74
+ :suffix="column.inputSuffix"
75
+ :modelValue="value"
76
+ @update:modelValue="$emit('update:modelValue', $event)"
71
77
  :readonly="column.editReadonly && source === 'edit'"
72
78
  />
73
79
  <textarea
@@ -87,19 +93,21 @@
87
93
  :value="value"
88
94
  @input="$emit('update:modelValue', $event.target.value)"
89
95
  />
90
- <input
96
+ <Input
91
97
  v-else
92
98
  ref="input"
93
99
  :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"
100
+ class="w-full"
95
101
  :placeholder="$t('Text')"
96
- :value="value"
97
- @input="$emit('update:modelValue', $event.target.value)"
102
+ :prefix="column.inputPrefix"
103
+ :suffix="column.inputSuffix"
104
+ :modelValue="value"
105
+ @update:modelValue="$emit('update:modelValue', $event)"
98
106
  autocomplete="false"
99
107
  data-lpignore="true"
100
108
  readonly
101
109
  @focus="onFocusHandler($event, column, source)"
102
- >
110
+ />
103
111
 
104
112
  <button
105
113
  v-if="deletable"
@@ -125,23 +133,33 @@
125
133
  import { IconEyeSlashSolid, IconEyeSolid, IconTrashBinSolid } from '@iconify-prerendered/vue-flowbite';
126
134
  import CustomDatePicker from "@/components/CustomDatePicker.vue";
127
135
  import Select from '@/afcl/Select.vue';
136
+ import Input from '@/afcl/Input.vue';
128
137
  import { ref } from 'vue';
129
138
  import { getCustomComponent } from '@/utils';
130
139
  import { useI18n } from 'vue-i18n';
140
+ import { useCoreStore } from '@/stores/core';
141
+
142
+ const coreStore = useCoreStore();
131
143
 
132
144
  const { t } = useI18n();
133
145
 
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
- }>();
146
+ const props = withDefaults(
147
+ defineProps<{
148
+ source: 'create' | 'edit',
149
+ column: any,
150
+ type?: string,
151
+ value: any,
152
+ currentValues: any,
153
+ mode: string,
154
+ columnOptions: any,
155
+ unmasked: any,
156
+ deletable?: boolean,
157
+ }>(),
158
+ {
159
+ type: undefined,
160
+ deletable: false,
161
+ }
162
+ );
145
163
 
146
164
  const input = ref(null);
147
165
 
@@ -129,7 +129,8 @@ watch(start, () => {
129
129
  })
130
130
 
131
131
  function initDatepickers() {
132
- const options = {format: 'dd M yyyy'};
132
+ const LS_LANG_KEY = `afLanguage`;
133
+ const options = {format: 'dd M yyyy', language: localStorage.getItem(LS_LANG_KEY)};
133
134
 
134
135
  if (props.autoHide) {
135
136
  options.autohide = true;
@@ -184,10 +184,9 @@ watch(end, () => {
184
184
  })
185
185
 
186
186
  function initDatepickers() {
187
-
188
- datepickerStartObject.value = new Datepicker(datepickerStartEl.value, {format: 'dd M yyyy'});
189
-
190
- datepickerEndObject.value = new Datepicker(datepickerEndEl.value, {format: 'dd M yyyy'});
187
+ const LS_LANG_KEY = `afLanguage`;
188
+ datepickerStartObject.value = new Datepicker(datepickerStartEl.value, {format: 'dd M yyyy', language: localStorage.getItem(LS_LANG_KEY)});
189
+ datepickerEndObject.value = new Datepicker(datepickerEndEl.value, {format: 'dd M yyyy', language: localStorage.getItem(LS_LANG_KEY)});
191
190
  addChangeDateListener();
192
191
  }
193
192
 
@@ -27,7 +27,7 @@
27
27
  multiple
28
28
  class="w-full"
29
29
  :options="columnOptions[c.name] || []"
30
- @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event || undefined })"
30
+ @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event.length ? $event : undefined })"
31
31
  :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
32
32
  />
33
33
  <Select
@@ -40,7 +40,7 @@
40
40
  // if field is not required, undefined might be there, and user might want to filter by it
41
41
  ...(c.required ? [] : [ { label: $t('Unset'), value: undefined } ])
42
42
  ]"
43
- @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event || undefined })"
43
+ @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event.length ? $event : undefined })"
44
44
  :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
45
45
  />
46
46
 
@@ -49,12 +49,12 @@
49
49
  class="w-full"
50
50
  v-else-if="c.enum"
51
51
  :options="c.enum"
52
- @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event || undefined })"
52
+ @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event.length ? $event : undefined })"
53
53
  :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
54
54
  />
55
55
 
56
56
  <Input
57
- v-else-if="[ 'string', 'text' ].includes(c.type)"
57
+ v-else-if="['string', 'text', 'json', 'richtext', 'unknown'].includes(c.type)"
58
58
  type="text"
59
59
  full-width
60
60
  :placeholder="$t('Search')"
@@ -72,7 +72,7 @@
72
72
  />
73
73
 
74
74
  <Input
75
- v-else-if="[ 'date', 'time' ].includes(c.type)"
75
+ v-else-if="['date', 'time'].includes(c.type)"
76
76
  type="text"
77
77
  full-width
78
78
  :placeholder="$t('Search datetime')"
@@ -129,11 +129,11 @@
129
129
  arrayItemRefs.value[arrayItemRefs.value.length - 1].focus();
130
130
  }
131
131
 
132
- watch(customComponentsInValidity, (newVal) => {
132
+ watch(customComponentsInValidity.value, (newVal) => {
133
133
  emit('update:customComponentsInValidity', newVal);
134
134
  });
135
135
 
136
- watch(customComponentsEmptiness, (newVal) => {
136
+ watch(customComponentsEmptiness.value, (newVal) => {
137
137
  emit('update:customComponentsEmptiness', newVal);
138
138
  });
139
139
 
@@ -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,19 +95,28 @@ 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] === '' || (column.isArray?.enabled && !currentValues.value[column.name].length)) &&
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
121
  if (column.type === 'json' && !column.isArray?.enabled && currentValues.value[column.name]) {
113
122
  try {
@@ -130,6 +139,7 @@ const columnError = (column) => {
130
139
  return validateValue(column.type, currentValues.value[column.name], column);
131
140
  }
132
141
 
142
+ return null;
133
143
  });
134
144
  return val.value;
135
145
  };
@@ -252,6 +262,9 @@ const columnOptions = computedAsync(async () => {
252
262
  offset: 0,
253
263
  },
254
264
  });
265
+
266
+ if (!column.required[props.source]) list.items.push({ value: null, label: t('Unset') });
267
+
255
268
  return { [column.name]: list.items };
256
269
  }
257
270
  })
@@ -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 {
@@ -1324,14 +1332,18 @@ export interface AdminForthForeignResource extends AdminForthForeignResourceComm
1324
1332
  },
1325
1333
  }
1326
1334
 
1327
- /**
1328
- * Object which describes on what pages should column be displayed on.
1329
- */
1330
- export type ShowInInput = {
1335
+ export type ShowInModernInput = {
1331
1336
  [key in AdminForthResourcePages]?: AllowedActionValue
1332
1337
  } & {
1333
1338
  all?: AllowedActionValue;
1334
- } & 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;
1335
1347
 
1336
1348
  export type ShowIn = {
1337
1349
  [key in AdminForthResourcePages]: AllowedActionValue
@@ -615,6 +615,12 @@ export interface AdminForthResourceColumnInputCommon {
615
615
  */
616
616
  required?: boolean | { create?: boolean, edit?: boolean },
617
617
 
618
+ /**
619
+ * Prefix and suffix for input field on create and edit pages.
620
+ */
621
+ inputPrefix?: string,
622
+ inputSuffix?: string,
623
+
618
624
  /**
619
625
  * Whether AdminForth will show editing note near the field in edit/create form.
620
626
  */
@@ -626,11 +632,41 @@ export interface AdminForthResourceColumnInputCommon {
626
632
  editReadonly?: boolean,
627
633
 
628
634
  /**
629
- * On which AdminForth pages this field will be shown. By default all.
635
+ * Defines on which AdminForth pages this field will be shown. By default all.
630
636
  * Example: if you want to show field only in create and edit pages, set it to
631
637
  *
632
638
  * ```ts
633
- * showIn: { create: true, edit: true}
639
+ * showIn: { create: true, edit: true }
640
+ * ```
641
+ *
642
+ * If you wish show only in list view, set it to:
643
+ *
644
+ * ```ts
645
+ * showIn: { all: false, list: true }
646
+ * ```
647
+ *
648
+ * If you wish to hide only in list you can use:
649
+ *
650
+ *
651
+ * ```ts
652
+ * showIn: { all: true, list: false }
653
+ * ```
654
+ *
655
+ * or
656
+ *
657
+ * ```ts
658
+ * showIn: { list: false } // all: true is by default already
659
+ * ```
660
+ *
661
+ * Also might have callback which will be called with same syntax as allowedActions.
662
+ *
663
+ * ```ts
664
+ * showIn: {
665
+ * list: ({ resource, adminUser }) => {
666
+ * return adminUser.dbUser.role === 'superadmin';
667
+ * },
668
+ * show: true,
669
+ * }
634
670
  * ```
635
671
  *
636
672
  */
@@ -699,7 +735,12 @@ export interface AdminForthResourceColumnInputCommon {
699
735
  virtual?: boolean,
700
736
 
701
737
  /**
702
- * Whether AdminForth will show this field in list view.
738
+ * Allow AdminForth to execute SELECT min(column) and SELECT max(column) queries to get min and max values for this column.
739
+ * This would improve UX of filters by adding sliders for numeric columns.
740
+ *
741
+ * NOTE: By default is option is `false` to prevent performance issues on large tables.
742
+ * If you are going to set it to `true`, make sure you have a one-item index on this column (one index for each column which has it) or ensure your table will not have a large number of records.
743
+ *
703
744
  */
704
745
  allowMinMaxQuery?: boolean,
705
746
 
@@ -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();