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.
- package/commands/createApp/templates/{users.ts.hbs → adminuser.ts.hbs} +2 -2
- package/commands/createApp/templates/index.ts.hbs +5 -5
- package/commands/createApp/utils.js +2 -2
- package/dist/dataConnectors/baseConnector.d.ts +3 -0
- package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
- package/dist/dataConnectors/baseConnector.js +7 -0
- package/dist/dataConnectors/baseConnector.js.map +1 -1
- package/dist/dataConnectors/clickhouse.d.ts +1 -8
- package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
- package/dist/dataConnectors/clickhouse.js +4 -6
- package/dist/dataConnectors/clickhouse.js.map +1 -1
- package/dist/dataConnectors/mongo.d.ts +1 -5
- package/dist/dataConnectors/mongo.d.ts.map +1 -1
- package/dist/dataConnectors/mongo.js +15 -13
- package/dist/dataConnectors/mongo.js.map +1 -1
- package/dist/dataConnectors/mysql.d.ts +68 -0
- package/dist/dataConnectors/mysql.d.ts.map +1 -0
- package/dist/dataConnectors/mysql.js +308 -0
- package/dist/dataConnectors/mysql.js.map +1 -0
- package/dist/dataConnectors/postgres.d.ts +1 -4
- package/dist/dataConnectors/postgres.d.ts.map +1 -1
- package/dist/dataConnectors/postgres.js +25 -24
- package/dist/dataConnectors/postgres.js.map +1 -1
- package/dist/dataConnectors/sqlite.d.ts +1 -4
- package/dist/dataConnectors/sqlite.d.ts.map +1 -1
- package/dist/dataConnectors/sqlite.js +13 -12
- package/dist/dataConnectors/sqlite.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/configValidator.d.ts.map +1 -1
- package/dist/modules/configValidator.js +27 -5
- package/dist/modules/configValidator.js.map +1 -1
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +5 -4
- package/dist/modules/restApi.js.map +1 -1
- package/dist/spa/src/App.vue +1 -1
- package/dist/spa/src/afcl/Dialog.vue +99 -0
- package/dist/spa/src/afcl/Input.vue +16 -6
- package/dist/spa/src/afcl/Select.vue +2 -3
- package/dist/spa/src/afcl/index.ts +1 -0
- package/dist/spa/src/components/ColumnValueInput.vue +44 -26
- package/dist/spa/src/components/CustomDatePicker.vue +2 -1
- package/dist/spa/src/components/CustomDateRangePicker.vue +3 -4
- package/dist/spa/src/components/Filters.vue +5 -5
- package/dist/spa/src/components/GroupsTable.vue +2 -2
- package/dist/spa/src/components/ResourceForm.vue +28 -15
- package/dist/spa/src/types/Back.ts +18 -6
- package/dist/spa/src/types/Common.ts +44 -3
- package/dist/spa/src/utils.ts +2 -1
- package/dist/spa/src/views/LoginView.vue +13 -2
- package/dist/types/Back.d.ts +14 -8
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +43 -3
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js.map +1 -1
- package/package.json +2 -1
package/dist/spa/src/App.vue
CHANGED
|
@@ -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-
|
|
7
|
-
<slot name="prefix"
|
|
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"
|
|
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
|
|
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
|
-
<
|
|
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="
|
|
49
|
+
class="w-40"
|
|
48
50
|
placeholder="0"
|
|
51
|
+
:prefix="column.inputPrefix"
|
|
52
|
+
:suffix="column.inputSuffix"
|
|
49
53
|
:readonly="column.editReadonly && source === 'edit'"
|
|
50
|
-
:
|
|
51
|
-
@
|
|
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
|
-
<
|
|
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="
|
|
71
|
+
class="w-40"
|
|
68
72
|
placeholder="0.0"
|
|
69
|
-
:
|
|
70
|
-
|
|
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
|
-
<
|
|
96
|
+
<Input
|
|
91
97
|
v-else
|
|
92
98
|
ref="input"
|
|
93
99
|
:type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
|
|
94
|
-
class="
|
|
100
|
+
class="w-full"
|
|
95
101
|
:placeholder="$t('Text')"
|
|
96
|
-
:
|
|
97
|
-
|
|
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 =
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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="[
|
|
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="[
|
|
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
|
|
19
|
-
@update:customComponentsEmptiness="(data) => customComponentsEmptiness
|
|
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
|
|
36
|
-
@update:customComponentsEmptiness="(data) => customComponentsEmptiness
|
|
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
|
|
52
|
-
@update:customComponentsEmptiness="(data) => customComponentsEmptiness
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 (
|
|
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
|
-
}
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
package/dist/spa/src/utils.ts
CHANGED
|
@@ -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();
|