adminforth 1.13.0-next.9 → 1.14.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/adminuser.ts.hbs +9 -7
- package/commands/createPlugin/templates/package.json.hbs +1 -1
- package/dist/dataConnectors/baseConnector.d.ts +10 -14
- package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
- package/dist/dataConnectors/baseConnector.js +76 -30
- package/dist/dataConnectors/baseConnector.js.map +1 -1
- package/dist/dataConnectors/clickhouse.d.ts +14 -23
- package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
- package/dist/dataConnectors/clickhouse.js +68 -27
- package/dist/dataConnectors/clickhouse.js.map +1 -1
- package/dist/dataConnectors/mongo.d.ts +12 -16
- package/dist/dataConnectors/mongo.d.ts.map +1 -1
- package/dist/dataConnectors/mongo.js +15 -11
- package/dist/dataConnectors/mongo.js.map +1 -1
- package/dist/dataConnectors/mysql.d.ts +8 -13
- package/dist/dataConnectors/mysql.d.ts.map +1 -1
- package/dist/dataConnectors/mysql.js +45 -26
- package/dist/dataConnectors/mysql.js.map +1 -1
- package/dist/dataConnectors/postgres.d.ts +8 -13
- package/dist/dataConnectors/postgres.d.ts.map +1 -1
- package/dist/dataConnectors/postgres.js +45 -28
- package/dist/dataConnectors/postgres.js.map +1 -1
- package/dist/dataConnectors/sqlite.d.ts +7 -4
- package/dist/dataConnectors/sqlite.d.ts.map +1 -1
- package/dist/dataConnectors/sqlite.js +48 -22
- 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 +5 -7
- package/dist/index.js.map +1 -1
- package/dist/modules/configValidator.d.ts +4 -0
- package/dist/modules/configValidator.d.ts.map +1 -1
- package/dist/modules/configValidator.js +69 -9
- package/dist/modules/configValidator.js.map +1 -1
- package/dist/modules/operationalResource.d.ts +4 -4
- package/dist/modules/operationalResource.d.ts.map +1 -1
- package/dist/modules/operationalResource.js +16 -2
- package/dist/modules/operationalResource.js.map +1 -1
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +163 -57
- package/dist/modules/restApi.js.map +1 -1
- package/dist/spa/src/afcl/Dialog.vue +13 -0
- package/dist/spa/src/afcl/Select.vue +27 -2
- package/dist/spa/src/components/ColumnValueInput.vue +12 -5
- package/dist/spa/src/components/ColumnValueInputWrapper.vue +77 -0
- package/dist/spa/src/components/Filters.vue +16 -8
- package/dist/spa/src/components/GroupsTable.vue +15 -50
- package/dist/spa/src/components/ResourceForm.vue +19 -6
- package/dist/spa/src/components/ResourceListTable.vue +39 -7
- package/dist/spa/src/components/ShowTable.vue +11 -3
- package/dist/spa/src/components/SkeleteLoader.vue +11 -3
- package/dist/spa/src/components/ThreeDotsMenu.vue +17 -1
- package/dist/spa/src/components/ValueRenderer.vue +18 -2
- package/dist/spa/src/types/Back.ts +67 -23
- package/dist/spa/src/types/Common.ts +26 -2
- package/dist/spa/src/views/EditView.vue +6 -2
- package/dist/spa/src/views/ListView.vue +5 -2
- package/dist/spa/src/views/ShowView.vue +17 -1
- package/dist/types/Back.d.ts +59 -22
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js +6 -0
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +26 -4
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js +3 -0
- package/dist/types/Common.js.map +1 -1
- package/package.json +1 -1
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
</div>
|
|
35
35
|
<teleport to="body" v-if="teleportToBody && showDropdown">
|
|
36
36
|
<div ref="dropdownEl" :style="getDropdownPosition" :class="{'shadow-none': isTop}"
|
|
37
|
-
class="fixed z-
|
|
37
|
+
class="fixed z-[5] w-full bg-white shadow-lg dark:shadow-black dark:bg-gray-700
|
|
38
38
|
dark:border-gray-600 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm max-h-48">
|
|
39
39
|
<div
|
|
40
40
|
v-for="item in filteredItems"
|
|
@@ -202,6 +202,23 @@ watch(
|
|
|
202
202
|
}
|
|
203
203
|
);
|
|
204
204
|
|
|
205
|
+
const handleScroll = () => {
|
|
206
|
+
if (showDropdown.value && inputEl.value) {
|
|
207
|
+
const rect = inputEl.value.getBoundingClientRect();
|
|
208
|
+
const style = {
|
|
209
|
+
left: `${rect.left}px`,
|
|
210
|
+
top: isTop.value && dropdownHeight.value
|
|
211
|
+
? `${rect.top - dropdownHeight.value - 8}px`
|
|
212
|
+
: `${rect.bottom + 8}px`,
|
|
213
|
+
width: `${rect.width}px`
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
if (dropdownEl.value) {
|
|
217
|
+
Object.assign(dropdownEl.value.style, style);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
205
222
|
onMounted(() => {
|
|
206
223
|
updateFromProps();
|
|
207
224
|
|
|
@@ -214,7 +231,11 @@ onMounted(() => {
|
|
|
214
231
|
});
|
|
215
232
|
|
|
216
233
|
addClickListener();
|
|
217
|
-
|
|
234
|
+
|
|
235
|
+
// Add scroll listeners if teleportToBody is true
|
|
236
|
+
if (props.teleportToBody) {
|
|
237
|
+
window.addEventListener('scroll', handleScroll, true);
|
|
238
|
+
}
|
|
218
239
|
});
|
|
219
240
|
|
|
220
241
|
const filteredItems = computed(() => {
|
|
@@ -268,6 +289,10 @@ const toogleItem = (item) => {
|
|
|
268
289
|
|
|
269
290
|
onUnmounted(() => {
|
|
270
291
|
removeClickListener();
|
|
292
|
+
// Remove scroll listeners if teleportToBody is true
|
|
293
|
+
if (props.teleportToBody) {
|
|
294
|
+
window.removeEventListener('scroll', handleScroll, true);
|
|
295
|
+
}
|
|
271
296
|
});
|
|
272
297
|
|
|
273
298
|
const getDropdownPosition = computed(() => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="flex">
|
|
2
|
+
<div class="flex" :class="{ 'opacity-50' : column.editReadonly && source === 'edit' }">
|
|
3
3
|
<component
|
|
4
4
|
v-if="column?.components?.[props.source]?.file"
|
|
5
5
|
:is="getCustomComponent(column.components[props.source])"
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
<Select
|
|
17
17
|
v-else-if="column.foreignResource"
|
|
18
18
|
ref="input"
|
|
19
|
-
class="w-full"
|
|
19
|
+
class="w-full min-w-24"
|
|
20
20
|
:options="columnOptions[column.name] || []"
|
|
21
|
+
teleportToBody
|
|
21
22
|
:placeholder = "columnOptions[column.name]?.length ?$t('Select...'): $t('There are no options available')"
|
|
22
23
|
:modelValue="value"
|
|
23
24
|
:readonly="column.editReadonly && source === 'edit'"
|
|
@@ -26,8 +27,9 @@
|
|
|
26
27
|
<Select
|
|
27
28
|
v-else-if="column.enum"
|
|
28
29
|
ref="input"
|
|
29
|
-
class="w-full"
|
|
30
|
+
class="w-full min-w-24"
|
|
30
31
|
:options="column.enum"
|
|
32
|
+
teleportToBody
|
|
31
33
|
:modelValue="value"
|
|
32
34
|
:readonly="column.editReadonly && source === 'edit'"
|
|
33
35
|
@update:modelValue="$emit('update:modelValue', $event)"
|
|
@@ -35,8 +37,9 @@
|
|
|
35
37
|
<Select
|
|
36
38
|
v-else-if="(type || column.type) === 'boolean'"
|
|
37
39
|
ref="input"
|
|
38
|
-
class="w-full"
|
|
40
|
+
class="w-full min-w-24"
|
|
39
41
|
:options="getBooleanOptions(column)"
|
|
42
|
+
teleportToBody
|
|
40
43
|
:modelValue="value"
|
|
41
44
|
:readonly="column.editReadonly && source === 'edit'"
|
|
42
45
|
@update:modelValue="$emit('update:modelValue', $event)"
|
|
@@ -48,6 +51,8 @@
|
|
|
48
51
|
step="1"
|
|
49
52
|
class="w-40"
|
|
50
53
|
placeholder="0"
|
|
54
|
+
:min="![undefined, null].includes(column.minValue) ? column.minValue : ''"
|
|
55
|
+
:max="![undefined, null].includes(column.maxValue) ? column.maxValue : ''"
|
|
51
56
|
:prefix="column.inputPrefix"
|
|
52
57
|
:suffix="column.inputSuffix"
|
|
53
58
|
:readonly="column.editReadonly && source === 'edit'"
|
|
@@ -70,6 +75,8 @@
|
|
|
70
75
|
step="0.1"
|
|
71
76
|
class="w-40"
|
|
72
77
|
placeholder="0.0"
|
|
78
|
+
:min="![undefined, null].includes(column.minValue) ? column.minValue : ''"
|
|
79
|
+
:max="![undefined, null].includes(column.maxValue) ? column.maxValue : ''"
|
|
73
80
|
:prefix="column.inputPrefix"
|
|
74
81
|
:suffix="column.inputSuffix"
|
|
75
82
|
:modelValue="value"
|
|
@@ -168,7 +175,7 @@
|
|
|
168
175
|
{ label: t('Yes'), value: true },
|
|
169
176
|
{ label: t('No'), value: false },
|
|
170
177
|
];
|
|
171
|
-
if (!column.required[props.mode]) {
|
|
178
|
+
if (!column.required[props.mode] && !column.isArray?.enabled) {
|
|
172
179
|
options.push({ label: t('Unset'), value: null });
|
|
173
180
|
}
|
|
174
181
|
return options;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<template v-if="column.isArray?.enabled">
|
|
3
|
+
<div class="flex flex-col">
|
|
4
|
+
<ColumnValueInput
|
|
5
|
+
v-for="(arrayItemValue, arrayItemIndex) in currentValues[column.name]"
|
|
6
|
+
:key="`${column.name}-${arrayItemIndex}`"
|
|
7
|
+
ref="arrayItemRefs"
|
|
8
|
+
:class="{'mt-2': arrayItemIndex}"
|
|
9
|
+
:source="source"
|
|
10
|
+
:column="column"
|
|
11
|
+
:type="column.isArray.itemType"
|
|
12
|
+
:value="arrayItemValue"
|
|
13
|
+
:currentValues="currentValues"
|
|
14
|
+
:mode="mode"
|
|
15
|
+
:columnOptions="columnOptions"
|
|
16
|
+
:deletable="!column.editReadonly"
|
|
17
|
+
@update:modelValue="setCurrentValue(column.name, $event, arrayItemIndex)"
|
|
18
|
+
@update:unmasked="$emit('update:unmasked', column.name)"
|
|
19
|
+
@update:inValidity="$emit('update:inValidity', { name: column.name, value: $event })"
|
|
20
|
+
@update:emptiness="$emit('update:emptiness', { name: column.name, value: $event })"
|
|
21
|
+
@delete="setCurrentValue(column.name, currentValues[column.name].filter((_, index) => index !== arrayItemIndex))"
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="flex items-center">
|
|
25
|
+
<button
|
|
26
|
+
v-if="!column.editReadonly"
|
|
27
|
+
type="button"
|
|
28
|
+
@click="addArrayItem"
|
|
29
|
+
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"
|
|
30
|
+
:class="{'mt-2': currentValues[column.name].length}"
|
|
31
|
+
>
|
|
32
|
+
<IconPlusOutline class="w-4 h-4 me-2"/>
|
|
33
|
+
{{ $t('Add') }}
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
<ColumnValueInput
|
|
38
|
+
v-else
|
|
39
|
+
:source="source"
|
|
40
|
+
:column="column"
|
|
41
|
+
:value="currentValues[column.name]"
|
|
42
|
+
:currentValues="currentValues"
|
|
43
|
+
:mode="mode"
|
|
44
|
+
:columnOptions="columnOptions"
|
|
45
|
+
:unmasked="unmasked"
|
|
46
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
47
|
+
@update:unmasked="$emit('update:unmasked', column.name)"
|
|
48
|
+
@update:inValidity="$emit('update:inValidity', { name: column.name, value: $event })"
|
|
49
|
+
@update:emptiness="$emit('update:emptiness', { name: column.name, value: $event })"
|
|
50
|
+
/>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<script setup lang="ts">
|
|
54
|
+
import { IconPlusOutline } from '@iconify-prerendered/vue-flowbite';
|
|
55
|
+
import ColumnValueInput from "./ColumnValueInput.vue";
|
|
56
|
+
import { ref, nextTick } from 'vue';
|
|
57
|
+
|
|
58
|
+
const props = defineProps<{
|
|
59
|
+
source: 'create' | 'edit',
|
|
60
|
+
column: any,
|
|
61
|
+
currentValues: any,
|
|
62
|
+
mode: string,
|
|
63
|
+
columnOptions: any,
|
|
64
|
+
unmasked: any,
|
|
65
|
+
setCurrentValue: Function
|
|
66
|
+
}>();
|
|
67
|
+
|
|
68
|
+
const emit = defineEmits(['update:unmasked', 'update:inValidity', 'update:emptiness', 'focus-last-input']);
|
|
69
|
+
|
|
70
|
+
const arrayItemRefs = ref([]);
|
|
71
|
+
|
|
72
|
+
async function addArrayItem() {
|
|
73
|
+
props.setCurrentValue(props.column.name, props.currentValues[props.column.name], props.currentValues[props.column.name].length);
|
|
74
|
+
await nextTick();
|
|
75
|
+
arrayItemRefs.value[arrayItemRefs.value.length - 1].focus();
|
|
76
|
+
}
|
|
77
|
+
</script>
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
|
|
25
25
|
<Select
|
|
26
26
|
v-if="c.foreignResource"
|
|
27
|
-
multiple
|
|
27
|
+
:multiple="c.filterOptions.multiselect"
|
|
28
28
|
class="w-full"
|
|
29
29
|
:options="columnOptions[c.name] || []"
|
|
30
|
-
@update:modelValue="onFilterInput[c.name]({ column: c, operator: 'in', value: $event.length ? $event : undefined
|
|
31
|
-
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
30
|
+
@update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event || undefined })"
|
|
31
|
+
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value || []"
|
|
32
32
|
/>
|
|
33
33
|
<Select
|
|
34
34
|
multiple
|
|
@@ -44,13 +44,13 @@
|
|
|
44
44
|
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
45
45
|
/>
|
|
46
46
|
|
|
47
|
-
<Select
|
|
48
|
-
multiple
|
|
47
|
+
<Select
|
|
48
|
+
:multiple="c.filterOptions.multiselect"
|
|
49
49
|
class="w-full"
|
|
50
50
|
v-else-if="c.enum"
|
|
51
51
|
:options="c.enum"
|
|
52
|
-
@update:modelValue="onFilterInput[c.name]({ column: c, operator: 'in', value: $event.length ? $event : undefined })"
|
|
53
|
-
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
52
|
+
@update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event || undefined })"
|
|
53
|
+
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value || []"
|
|
54
54
|
/>
|
|
55
55
|
|
|
56
56
|
<Input
|
|
@@ -118,7 +118,8 @@
|
|
|
118
118
|
</template>
|
|
119
119
|
|
|
120
120
|
<script setup>
|
|
121
|
-
import { watch, computed } from 'vue'
|
|
121
|
+
import { watch, computed } from 'vue';
|
|
122
|
+
import { useI18n } from 'vue-i18n';
|
|
122
123
|
import CustomDateRangePicker from '@/components/CustomDateRangePicker.vue';
|
|
123
124
|
import { callAdminForthApi } from '@/utils';
|
|
124
125
|
import { useRouter } from 'vue-router';
|
|
@@ -130,6 +131,7 @@ import Select from '@/afcl/Select.vue';
|
|
|
130
131
|
import debounce from 'debounce';
|
|
131
132
|
|
|
132
133
|
const filtersStore = useFiltersStore();
|
|
134
|
+
const { t } = useI18n();
|
|
133
135
|
|
|
134
136
|
|
|
135
137
|
// props: columns
|
|
@@ -163,6 +165,12 @@ const columnOptions = computedAsync(async () => {
|
|
|
163
165
|
},
|
|
164
166
|
});
|
|
165
167
|
ret[column.name] = list.items;
|
|
168
|
+
if (!column.filterOptions.multiselect) {
|
|
169
|
+
ret[column.name].push({
|
|
170
|
+
label: t('Unset'),
|
|
171
|
+
value: '',
|
|
172
|
+
});
|
|
173
|
+
}
|
|
166
174
|
}
|
|
167
175
|
})
|
|
168
176
|
);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="rounded-lg shadow-resourseFormShadow dark:shadow-darkResourseFormShadow dark:shadow-2xl">
|
|
3
|
-
<div v-if="group.groupName" class="text-md font-semibold px-6 py-3 flex flex-1 items-center dark:border-gray-600 text-gray-700 bg-lightFormHeading dark:bg-gray-700 dark:text-gray-400 rounded-t-lg">
|
|
3
|
+
<div v-if="group.groupName && !group.noTitle" class="text-md font-semibold px-6 py-3 flex flex-1 items-center dark:border-gray-600 text-gray-700 bg-lightFormHeading dark:bg-gray-700 dark:text-gray-400 rounded-t-lg">
|
|
4
4
|
{{ group.groupName }}
|
|
5
5
|
</div>
|
|
6
6
|
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
|
7
|
-
<thead class="text-xs text-gray-700 uppercase dark:text-gray-400 bg-lightFormHeading dark:bg-gray-700 block md:table-row-group ">
|
|
7
|
+
<thead v-if="!allColumnsHaveCustomComponent" class="text-xs text-gray-700 uppercase dark:text-gray-400 bg-lightFormHeading dark:bg-gray-700 block md:table-row-group ">
|
|
8
8
|
<tr>
|
|
9
9
|
<th scope="col" :class="{'rounded-tl-lg': !group.groupName}" class="px-6 py-3 hidden md:w-52 md:table-cell">
|
|
10
10
|
{{ $t('Field') }}
|
|
@@ -42,50 +42,17 @@
|
|
|
42
42
|
class="px-6 py-4 whitespace-pre-wrap relative block md:table-cell"
|
|
43
43
|
:class="{'rounded-br-lg': i === group.columns.length - 1}"
|
|
44
44
|
>
|
|
45
|
-
<
|
|
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"
|
|
52
|
-
:column="column"
|
|
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]"
|
|
61
|
-
@update:inValidity="customComponentsInValidity[column.name] = $event"
|
|
62
|
-
@update:emptiness="customComponentsEmptiness[column.name] = $event"
|
|
63
|
-
@delete="setCurrentValue(column.name, currentValues[column.name].filter((_, index) => index !== arrayItemIndex))"
|
|
64
|
-
/>
|
|
65
|
-
<button
|
|
66
|
-
v-if="!column.editReadonly"
|
|
67
|
-
type="button"
|
|
68
|
-
@click="setCurrentValue(column.name, currentValues[column.name], currentValues[column.name].length); focusOnLastInput(column.name)"
|
|
69
|
-
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"
|
|
70
|
-
:class="{'mt-2': currentValues[column.name].length}"
|
|
71
|
-
>
|
|
72
|
-
<IconPlusOutline class="w-4 h-4 me-2"/>
|
|
73
|
-
{{ $t('Add') }}
|
|
74
|
-
</button>
|
|
75
|
-
</template>
|
|
76
|
-
<ColumnValueInput
|
|
77
|
-
v-else
|
|
45
|
+
<ColumnValueInputWrapper
|
|
78
46
|
:source="source"
|
|
79
47
|
:column="column"
|
|
80
|
-
:value="currentValues[column.name]"
|
|
81
48
|
:currentValues="currentValues"
|
|
82
49
|
:mode="mode"
|
|
83
50
|
:columnOptions="columnOptions"
|
|
84
51
|
:unmasked="unmasked"
|
|
85
|
-
|
|
86
|
-
@update:unmasked="unmasked[
|
|
87
|
-
@update:inValidity="customComponentsInValidity[
|
|
88
|
-
@update:emptiness="customComponentsEmptiness[
|
|
52
|
+
:setCurrentValue="setCurrentValue"
|
|
53
|
+
@update:unmasked="unmasked[$event] = !unmasked[$event]"
|
|
54
|
+
@update:inValidity="customComponentsInValidity[$event.name] = $event.value"
|
|
55
|
+
@update:emptiness="customComponentsEmptiness[$event.name] = $event.value"
|
|
89
56
|
/>
|
|
90
57
|
<div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
|
|
91
58
|
<div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
|
|
@@ -98,10 +65,10 @@
|
|
|
98
65
|
|
|
99
66
|
<script setup lang="ts">
|
|
100
67
|
import { IconExclamationCircleSolid, IconPlusOutline } from '@iconify-prerendered/vue-flowbite';
|
|
101
|
-
import ColumnValueInput from "@/components/ColumnValueInput.vue";
|
|
102
68
|
import { Tooltip } from '@/afcl';
|
|
103
69
|
import { ref, computed, watch, nextTick, type Ref } from 'vue';
|
|
104
70
|
import { useI18n } from 'vue-i18n';
|
|
71
|
+
import ColumnValueInputWrapper from "@/components/ColumnValueInputWrapper.vue";
|
|
105
72
|
|
|
106
73
|
const { t } = useI18n();
|
|
107
74
|
|
|
@@ -117,19 +84,17 @@
|
|
|
117
84
|
columnOptions: any,
|
|
118
85
|
}>();
|
|
119
86
|
|
|
120
|
-
const arrayItemRefs = ref([]);
|
|
121
|
-
|
|
122
87
|
const customComponentsInValidity: Ref<Record<string, boolean>> = ref({});
|
|
123
88
|
const customComponentsEmptiness: Ref<Record<string, boolean>> = ref({});
|
|
89
|
+
const allColumnsHaveCustomComponent = computed(() => {
|
|
90
|
+
return props.group.columns.every(column => {
|
|
91
|
+
const componentKey = `${props.source}Row` as keyof typeof column.components;
|
|
92
|
+
return column.components?.[componentKey];
|
|
93
|
+
});
|
|
94
|
+
});
|
|
124
95
|
|
|
125
96
|
const emit = defineEmits(['update:customComponentsInValidity', 'update:customComponentsEmptiness']);
|
|
126
97
|
|
|
127
|
-
async function focusOnLastInput(column) {
|
|
128
|
-
// wait for element to register
|
|
129
|
-
await nextTick();
|
|
130
|
-
arrayItemRefs.value[arrayItemRefs.value.length - 1].focus();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
98
|
watch(customComponentsInValidity.value, (newVal) => {
|
|
134
99
|
emit('update:customComponentsInValidity', newVal);
|
|
135
100
|
});
|
|
@@ -138,4 +103,4 @@
|
|
|
138
103
|
emit('update:customComponentsEmptiness', newVal);
|
|
139
104
|
});
|
|
140
105
|
|
|
141
|
-
</script>
|
|
106
|
+
</script>
|
|
@@ -190,8 +190,12 @@ const setCurrentValue = (key, value, index=null) => {
|
|
|
190
190
|
} else if (index === currentValues.value[key].length) {
|
|
191
191
|
currentValues.value[key].push(null);
|
|
192
192
|
} else {
|
|
193
|
-
if (['integer', 'float'].includes(col.isArray.itemType)
|
|
194
|
-
|
|
193
|
+
if (['integer', 'float', 'decimal'].includes(col.isArray.itemType)) {
|
|
194
|
+
if (value || value === 0) {
|
|
195
|
+
currentValues.value[key][index] = +value;
|
|
196
|
+
} else {
|
|
197
|
+
currentValues.value[key][index] = null;
|
|
198
|
+
}
|
|
195
199
|
} else {
|
|
196
200
|
currentValues.value[key][index] = value;
|
|
197
201
|
}
|
|
@@ -200,8 +204,12 @@ const setCurrentValue = (key, value, index=null) => {
|
|
|
200
204
|
}
|
|
201
205
|
}
|
|
202
206
|
} else {
|
|
203
|
-
if (['integer', 'float'].includes(col.type)
|
|
204
|
-
|
|
207
|
+
if (['integer', 'float', 'decimal'].includes(col.type)) {
|
|
208
|
+
if (value || value === 0) {
|
|
209
|
+
currentValues.value[key] = +value;
|
|
210
|
+
} else {
|
|
211
|
+
currentValues.value[key] = null;
|
|
212
|
+
}
|
|
205
213
|
} else {
|
|
206
214
|
currentValues.value[key] = value;
|
|
207
215
|
}
|
|
@@ -237,7 +245,12 @@ onMounted(() => {
|
|
|
237
245
|
currentValues.value[column.name] = [];
|
|
238
246
|
} else {
|
|
239
247
|
// else copy array to prevent mutation
|
|
240
|
-
currentValues.value[column.name]
|
|
248
|
+
if (Array.isArray(currentValues.value[column.name])) {
|
|
249
|
+
currentValues.value[column.name] = [...currentValues.value[column.name]];
|
|
250
|
+
} else {
|
|
251
|
+
// fallback for old data
|
|
252
|
+
currentValues.value[column.name] = [`${currentValues.value[column.name]}`];
|
|
253
|
+
}
|
|
241
254
|
}
|
|
242
255
|
} else if (currentValues.value[column.name]) {
|
|
243
256
|
currentValues.value[column.name] = JSON.stringify(currentValues.value[column.name], null, 2);
|
|
@@ -263,7 +276,7 @@ const columnOptions = computedAsync(async () => {
|
|
|
263
276
|
},
|
|
264
277
|
});
|
|
265
278
|
|
|
266
|
-
if (!column.required[props.source]) list.items.push({ value: null, label: column.foreignResource.unsetLabel });
|
|
279
|
+
if (!column.required[props.source] && !column.isArray?.enabled) list.items.push({ value: null, label: column.foreignResource.unsetLabel });
|
|
267
280
|
|
|
268
281
|
return { [column.name]: list.items };
|
|
269
282
|
}
|
|
@@ -14,19 +14,19 @@
|
|
|
14
14
|
<table v-else class=" w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 rounded-default">
|
|
15
15
|
|
|
16
16
|
<tbody>
|
|
17
|
-
|
|
18
17
|
<!-- table header -->
|
|
19
18
|
<tr class="t-header sticky z-10 top-0 text-xs bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-gray-400">
|
|
20
19
|
<td scope="col" class="p-4">
|
|
21
|
-
<div
|
|
20
|
+
<div class="flex items-center">
|
|
22
21
|
<input id="checkbox-all-search" type="checkbox" :checked="allFromThisPageChecked" @change="selectAll()"
|
|
23
|
-
|
|
22
|
+
:disabled="!rows || !rows.length"
|
|
23
|
+
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded
|
|
24
24
|
focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
|
25
25
|
<label for="checkbox-all-search" class="sr-only">{{ $t('checkbox') }}</label>
|
|
26
26
|
</div>
|
|
27
27
|
</td>
|
|
28
28
|
|
|
29
|
-
<td v-for="c in columnsListed" scope="col" class="px-2 md:px-3 lg:px-6 py-3">
|
|
29
|
+
<td v-for="c in columnsListed" ref="headerRefs" scope="col" class="px-2 md:px-3 lg:px-6 py-3">
|
|
30
30
|
|
|
31
31
|
<div @click="(evt) => c.sortable && onSortButtonClick(evt, c.name)"
|
|
32
32
|
class="flex items-center " :class="{'cursor-pointer':c.sortable}">
|
|
@@ -59,13 +59,18 @@
|
|
|
59
59
|
{{ $t('Actions') }}
|
|
60
60
|
</td>
|
|
61
61
|
</tr>
|
|
62
|
+
<tr v-for="c in tableBodyStartInjection" :key="c.id" class="align-top border-b border-lightListBorder dark:border-darkListTableBorder">
|
|
63
|
+
<component :is="getCustomComponent(c)" :meta="c.meta" :resource="resource" :adminUser="coreStore.adminUser" />
|
|
64
|
+
</tr>
|
|
62
65
|
<!-- table header end -->
|
|
63
66
|
<SkeleteLoader
|
|
64
67
|
v-if="!rows"
|
|
65
68
|
:columns="resource?.columns.filter(c => c.showIn.list).length + 2"
|
|
66
69
|
:rows="rowHeights.length || 3"
|
|
67
70
|
:row-heights="rowHeights"
|
|
71
|
+
:column-widths="columnWidths"
|
|
68
72
|
/>
|
|
73
|
+
|
|
69
74
|
<tr v-else-if="rows.length === 0" class="bg-lightListTable dark:bg-darkListTable dark:border-darkListTableBorder">
|
|
70
75
|
<td :colspan="resource?.columns.length + 2">
|
|
71
76
|
|
|
@@ -178,7 +183,7 @@
|
|
|
178
183
|
<button
|
|
179
184
|
@click="startCustomAction(action.id, row)"
|
|
180
185
|
>
|
|
181
|
-
<component v-if="action.icon" :is="getIcon(action.icon)" class="w-5 h-5 text-lightPrimary dark:text-darkPrimary"></component>
|
|
186
|
+
<component v-if="action.icon" :is="getIcon(action.icon)" class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"></component>
|
|
182
187
|
</button>
|
|
183
188
|
<template v-slot:tooltip>
|
|
184
189
|
{{ action.name }}
|
|
@@ -320,6 +325,7 @@ const props = defineProps<{
|
|
|
320
325
|
sort: any[],
|
|
321
326
|
noRoundings?: boolean,
|
|
322
327
|
customActionsInjection?: any[],
|
|
328
|
+
tableBodyStartInjection?: any[],
|
|
323
329
|
}>();
|
|
324
330
|
|
|
325
331
|
// emits, update page
|
|
@@ -376,10 +382,13 @@ watch(() => props.page, (newPage) => {
|
|
|
376
382
|
});
|
|
377
383
|
|
|
378
384
|
const rowRefs = useTemplateRef('rowRefs');
|
|
385
|
+
const headerRefs = useTemplateRef('headerRefs');
|
|
379
386
|
const rowHeights = ref([]);
|
|
387
|
+
const columnWidths = ref([]);
|
|
380
388
|
watch(() => props.rows, (newRows) => {
|
|
381
389
|
// rows are set to null when new records are loading
|
|
382
390
|
rowHeights.value = newRows || !rowRefs.value ? [] : rowRefs.value.map((el) => el.offsetHeight);
|
|
391
|
+
columnWidths.value = newRows || !headerRefs.value ? [] : [48, ...headerRefs.value.map((el) => el.offsetWidth)];
|
|
383
392
|
});
|
|
384
393
|
|
|
385
394
|
function addToCheckedValues(id) {
|
|
@@ -411,7 +420,7 @@ async function selectAll(value) {
|
|
|
411
420
|
const totalPages = computed(() => Math.ceil(props.totalRows / props.pageSize));
|
|
412
421
|
|
|
413
422
|
const allFromThisPageChecked = computed(() => {
|
|
414
|
-
if (!props.rows) return false;
|
|
423
|
+
if (!props.rows || !props.rows.length) return false;
|
|
415
424
|
return props.rows.every((r) => checkboxesInternal.value.includes(r._primaryKeyValue));
|
|
416
425
|
});
|
|
417
426
|
const ascArr = computed(() => sort.value.filter((s) => s.direction === 'asc').map((s) => s.field));
|
|
@@ -536,6 +545,20 @@ async function startCustomAction(actionId, row) {
|
|
|
536
545
|
|
|
537
546
|
actionLoadingStates.value[actionId] = false;
|
|
538
547
|
|
|
548
|
+
if (data?.redirectUrl) {
|
|
549
|
+
// Check if the URL should open in a new tab
|
|
550
|
+
if (data.redirectUrl.includes('target=_blank')) {
|
|
551
|
+
window.open(data.redirectUrl.replace('&target=_blank', '').replace('?target=_blank', ''), '_blank');
|
|
552
|
+
} else {
|
|
553
|
+
// Navigate within the app
|
|
554
|
+
if (data.redirectUrl.startsWith('http')) {
|
|
555
|
+
window.location.href = data.redirectUrl;
|
|
556
|
+
} else {
|
|
557
|
+
router.push(data.redirectUrl);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
539
562
|
if (data?.ok) {
|
|
540
563
|
emits('update:records', true);
|
|
541
564
|
|
|
@@ -553,4 +576,13 @@ async function startCustomAction(actionId, row) {
|
|
|
553
576
|
}
|
|
554
577
|
|
|
555
578
|
|
|
556
|
-
</script>
|
|
579
|
+
</script>
|
|
580
|
+
|
|
581
|
+
<style lang="scss" scoped>
|
|
582
|
+
input[type="checkbox"][disabled] {
|
|
583
|
+
@apply opacity-50;
|
|
584
|
+
}
|
|
585
|
+
input[type="checkbox"]:not([disabled]) {
|
|
586
|
+
@apply cursor-pointer;
|
|
587
|
+
}
|
|
588
|
+
</style>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="overflow-x-auto rounded-default shadow-resourseFormShadow dark:shadow-darkResourseFormShadow">
|
|
3
|
-
<div v-if="groupName"
|
|
3
|
+
<div v-if="groupName && !noTitle" class="text-md font-semibold px-6 py-3 flex flex-1 items-center dark:border-gray-600 text-gray-700 bg-lightFormHeading dark:bg-gray-700 dark:text-gray-400 rounded-t-lg">
|
|
4
4
|
{{ groupName }}
|
|
5
5
|
</div>
|
|
6
6
|
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 table-fixed">
|
|
7
|
-
<thead class="text-gray-700 dark:text-gray-400 bg-lightFormHeading dark:bg-gray-700 block md:table-row-group">
|
|
7
|
+
<thead v-if="!allColumnsHaveCustomComponent" class="text-gray-700 dark:text-gray-400 bg-lightFormHeading dark:bg-gray-700 block md:table-row-group">
|
|
8
8
|
<tr>
|
|
9
9
|
<th scope="col" class="px-6 py-3 text-xs uppercase hidden md:w-52 md:table-cell">
|
|
10
10
|
{{ $t('Field') }}
|
|
@@ -59,7 +59,8 @@
|
|
|
59
59
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
60
60
|
import { getCustomComponent } from '@/utils';
|
|
61
61
|
import { useCoreStore } from '@/stores/core';
|
|
62
|
-
|
|
62
|
+
import { computed } from 'vue';
|
|
63
|
+
const props = defineProps<{
|
|
63
64
|
columns: Array<{
|
|
64
65
|
name: string;
|
|
65
66
|
label: string;
|
|
@@ -74,10 +75,17 @@
|
|
|
74
75
|
};
|
|
75
76
|
};
|
|
76
77
|
}>;
|
|
78
|
+
source: string;
|
|
77
79
|
groupName?: string | null;
|
|
80
|
+
noTitle?: boolean;
|
|
78
81
|
resource: Record<string, any>;
|
|
79
82
|
record: Record<string, any>;
|
|
80
83
|
}>();
|
|
81
84
|
|
|
82
85
|
const coreStore = useCoreStore();
|
|
86
|
+
const allColumnsHaveCustomComponent = computed(() => {
|
|
87
|
+
return props.columns.every(column => {
|
|
88
|
+
return column.components?.showRow;
|
|
89
|
+
});
|
|
90
|
+
});
|
|
83
91
|
</script>
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<tr
|
|
3
3
|
v-for="(r, ri) in new Array(props.rows)"
|
|
4
|
-
class="bg-lightListTable
|
|
5
|
-
:
|
|
4
|
+
class="bg-lightListTable dark:bg-darkListTable dark:border-darkListBorder"
|
|
5
|
+
:class="{'border-b': ri !== props.rows - 1}"
|
|
6
|
+
:style="[`height: ${props.rowHeights[ri] !== undefined ? props.rowHeights[ri] : 52.5}px`]"
|
|
6
7
|
>
|
|
7
|
-
<td
|
|
8
|
+
<td
|
|
9
|
+
v-for="(c, ci) in new Array(props.columns)" class="items-center px-6 py-4 cursor-default"
|
|
10
|
+
:style="[props.columnWidths[ci] !== undefined
|
|
11
|
+
? `min-width: ${props.columnWidths[ci]}px; width: ${props.columnWidths[ci]}px;`
|
|
12
|
+
: '']"
|
|
13
|
+
>
|
|
8
14
|
<div role="status" class="max-w-sm animate-pulse">
|
|
9
15
|
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px]"></div>
|
|
10
16
|
</div>
|
|
@@ -18,8 +24,10 @@ const props = withDefaults(defineProps<{
|
|
|
18
24
|
columns: number;
|
|
19
25
|
rows: number;
|
|
20
26
|
rowHeights?: number[];
|
|
27
|
+
columnWidths?: number[];
|
|
21
28
|
}>(), {
|
|
22
29
|
rowHeights: [],
|
|
30
|
+
columnWidths: [],
|
|
23
31
|
});
|
|
24
32
|
|
|
25
33
|
</script>
|
|
@@ -46,10 +46,11 @@ import { getCustomComponent, getIcon } from '@/utils';
|
|
|
46
46
|
import { useCoreStore } from '@/stores/core';
|
|
47
47
|
import adminforth from '@/adminforth';
|
|
48
48
|
import { callAdminForthApi } from '@/utils';
|
|
49
|
-
import { useRoute } from 'vue-router';
|
|
49
|
+
import { useRoute, useRouter } from 'vue-router';
|
|
50
50
|
|
|
51
51
|
const route = useRoute();
|
|
52
52
|
const coreStore = useCoreStore();
|
|
53
|
+
const router = useRouter();
|
|
53
54
|
|
|
54
55
|
const props = defineProps({
|
|
55
56
|
threeDotsDropdownItems: Array,
|
|
@@ -69,6 +70,21 @@ async function handleActionClick(action) {
|
|
|
69
70
|
recordId: route.params.primaryKey
|
|
70
71
|
}
|
|
71
72
|
});
|
|
73
|
+
|
|
74
|
+
if (data?.redirectUrl) {
|
|
75
|
+
// Check if the URL should open in a new tab
|
|
76
|
+
if (data.redirectUrl.includes('target=_blank')) {
|
|
77
|
+
window.open(data.redirectUrl.replace('&target=_blank', '').replace('?target=_blank', ''), '_blank');
|
|
78
|
+
} else {
|
|
79
|
+
// Navigate within the app
|
|
80
|
+
if (data.redirectUrl.startsWith('http')) {
|
|
81
|
+
window.location.href = data.redirectUrl;
|
|
82
|
+
} else {
|
|
83
|
+
router.push(data.redirectUrl);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
72
88
|
|
|
73
89
|
if (data?.ok) {
|
|
74
90
|
await coreStore.fetchRecord({
|