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.
- 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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +68 -35
- package/dist/index.js.map +1 -1
- package/dist/modules/configValidator.d.ts.map +1 -1
- package/dist/modules/configValidator.js +58 -7
- package/dist/modules/configValidator.js.map +1 -1
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +5 -1
- package/dist/modules/restApi.js.map +1 -1
- package/dist/spa/src/App.vue +6 -3
- package/dist/spa/src/afcl/Dialog.vue +96 -0
- package/dist/spa/src/afcl/Input.vue +8 -6
- package/dist/spa/src/afcl/Select.vue +2 -3
- package/dist/spa/src/components/ColumnValueInput.vue +182 -0
- package/dist/spa/src/components/CustomDatePicker.vue +11 -3
- package/dist/spa/src/components/Filters.vue +5 -5
- package/dist/spa/src/components/GroupsTable.vue +52 -130
- package/dist/spa/src/components/MenuLink.vue +1 -1
- package/dist/spa/src/components/ResourceForm.vue +115 -56
- package/dist/spa/src/components/ValueRenderer.vue +40 -0
- package/dist/spa/src/types/Back.ts +20 -6
- package/dist/spa/src/types/Common.ts +25 -0
- package/dist/spa/src/types/FrontendAPI.ts +1 -1
- package/dist/spa/src/utils.ts +2 -1
- package/dist/spa/src/views/ListView.vue +5 -1
- package/dist/types/Back.d.ts +16 -8
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +23 -0
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js.map +1 -1
- package/dist/types/FrontendAPI.d.ts +1 -1
- package/dist/types/FrontendAPI.d.ts.map +1 -1
- package/package.json +2 -1
|
@@ -38,115 +38,54 @@
|
|
|
38
38
|
</Tooltip>
|
|
39
39
|
</span>
|
|
40
40
|
</td>
|
|
41
|
-
<td
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
:
|
|
48
|
-
|
|
49
|
-
:
|
|
50
|
-
:
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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,
|
|
161
|
-
import
|
|
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
|
|
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,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
|
-
|
|
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
|
-
|
|
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
|
-
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
if
|
|
147
|
-
|
|
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
|
-
|
|
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
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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'
|
|
189
|
-
|
|
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 (
|
|
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
|
-
}
|
|
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
|
|
41
|
+
confirm(params: ConfirmParams): Promise<boolean>;
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Show an alert
|
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();
|