adminforth 1.13.0-next.9 → 1.13.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 +1 -1
- package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
- package/dist/dataConnectors/baseConnector.js +11 -4
- package/dist/dataConnectors/baseConnector.js.map +1 -1
- package/dist/dataConnectors/clickhouse.d.ts +1 -1
- package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
- package/dist/dataConnectors/clickhouse.js +1 -0
- package/dist/dataConnectors/clickhouse.js.map +1 -1
- package/dist/dataConnectors/mongo.d.ts +1 -1
- package/dist/dataConnectors/mongo.d.ts.map +1 -1
- package/dist/dataConnectors/mongo.js +1 -0
- package/dist/dataConnectors/mongo.js.map +1 -1
- package/dist/dataConnectors/mysql.d.ts +1 -1
- package/dist/dataConnectors/mysql.d.ts.map +1 -1
- package/dist/dataConnectors/mysql.js +1 -0
- package/dist/dataConnectors/mysql.js.map +1 -1
- package/dist/dataConnectors/postgres.d.ts +1 -1
- package/dist/dataConnectors/postgres.d.ts.map +1 -1
- package/dist/dataConnectors/postgres.js +4 -2
- package/dist/dataConnectors/postgres.js.map +1 -1
- package/dist/dataConnectors/sqlite.d.ts +1 -1
- package/dist/dataConnectors/sqlite.d.ts.map +1 -1
- package/dist/dataConnectors/sqlite.js +1 -0
- 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 +2 -4
- 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 +55 -9
- package/dist/modules/configValidator.js.map +1 -1
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +81 -13
- package/dist/modules/restApi.js.map +1 -1
- package/dist/spa/src/afcl/Select.vue +27 -2
- package/dist/spa/src/components/ColumnValueInput.vue +8 -1
- package/dist/spa/src/components/ColumnValueInputWrapper.vue +75 -0
- package/dist/spa/src/components/GroupsTable.vue +15 -50
- package/dist/spa/src/components/ResourceForm.vue +18 -5
- package/dist/spa/src/components/ResourceListTable.vue +34 -6
- 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 +36 -5
- package/dist/spa/src/types/Common.ts +11 -1
- package/dist/spa/src/views/EditView.vue +6 -2
- package/dist/spa/src/views/ShowView.vue +17 -1
- package/dist/types/Back.d.ts +35 -4
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +11 -2
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js +1 -0
- package/dist/types/Common.js.map +1 -1
- package/package.json +1 -1
|
@@ -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);
|
|
@@ -18,15 +18,16 @@
|
|
|
18
18
|
<!-- table header -->
|
|
19
19
|
<tr class="t-header sticky z-10 top-0 text-xs bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-gray-400">
|
|
20
20
|
<td scope="col" class="p-4">
|
|
21
|
-
<div
|
|
21
|
+
<div class="flex items-center">
|
|
22
22
|
<input id="checkbox-all-search" type="checkbox" :checked="allFromThisPageChecked" @change="selectAll()"
|
|
23
|
-
|
|
23
|
+
:disabled="!rows || !rows.length"
|
|
24
|
+
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded
|
|
24
25
|
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
26
|
<label for="checkbox-all-search" class="sr-only">{{ $t('checkbox') }}</label>
|
|
26
27
|
</div>
|
|
27
28
|
</td>
|
|
28
29
|
|
|
29
|
-
<td v-for="c in columnsListed" scope="col" class="px-2 md:px-3 lg:px-6 py-3">
|
|
30
|
+
<td v-for="c in columnsListed" ref="headerRefs" scope="col" class="px-2 md:px-3 lg:px-6 py-3">
|
|
30
31
|
|
|
31
32
|
<div @click="(evt) => c.sortable && onSortButtonClick(evt, c.name)"
|
|
32
33
|
class="flex items-center " :class="{'cursor-pointer':c.sortable}">
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
:columns="resource?.columns.filter(c => c.showIn.list).length + 2"
|
|
66
67
|
:rows="rowHeights.length || 3"
|
|
67
68
|
:row-heights="rowHeights"
|
|
69
|
+
:column-widths="columnWidths"
|
|
68
70
|
/>
|
|
69
71
|
<tr v-else-if="rows.length === 0" class="bg-lightListTable dark:bg-darkListTable dark:border-darkListTableBorder">
|
|
70
72
|
<td :colspan="resource?.columns.length + 2">
|
|
@@ -178,7 +180,7 @@
|
|
|
178
180
|
<button
|
|
179
181
|
@click="startCustomAction(action.id, row)"
|
|
180
182
|
>
|
|
181
|
-
<component v-if="action.icon" :is="getIcon(action.icon)" class="w-5 h-5 text-lightPrimary dark:text-darkPrimary"></component>
|
|
183
|
+
<component v-if="action.icon" :is="getIcon(action.icon)" class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"></component>
|
|
182
184
|
</button>
|
|
183
185
|
<template v-slot:tooltip>
|
|
184
186
|
{{ action.name }}
|
|
@@ -376,10 +378,13 @@ watch(() => props.page, (newPage) => {
|
|
|
376
378
|
});
|
|
377
379
|
|
|
378
380
|
const rowRefs = useTemplateRef('rowRefs');
|
|
381
|
+
const headerRefs = useTemplateRef('headerRefs');
|
|
379
382
|
const rowHeights = ref([]);
|
|
383
|
+
const columnWidths = ref([]);
|
|
380
384
|
watch(() => props.rows, (newRows) => {
|
|
381
385
|
// rows are set to null when new records are loading
|
|
382
386
|
rowHeights.value = newRows || !rowRefs.value ? [] : rowRefs.value.map((el) => el.offsetHeight);
|
|
387
|
+
columnWidths.value = newRows || !headerRefs.value ? [] : [48, ...headerRefs.value.map((el) => el.offsetWidth)];
|
|
383
388
|
});
|
|
384
389
|
|
|
385
390
|
function addToCheckedValues(id) {
|
|
@@ -411,7 +416,7 @@ async function selectAll(value) {
|
|
|
411
416
|
const totalPages = computed(() => Math.ceil(props.totalRows / props.pageSize));
|
|
412
417
|
|
|
413
418
|
const allFromThisPageChecked = computed(() => {
|
|
414
|
-
if (!props.rows) return false;
|
|
419
|
+
if (!props.rows || !props.rows.length) return false;
|
|
415
420
|
return props.rows.every((r) => checkboxesInternal.value.includes(r._primaryKeyValue));
|
|
416
421
|
});
|
|
417
422
|
const ascArr = computed(() => sort.value.filter((s) => s.direction === 'asc').map((s) => s.field));
|
|
@@ -536,6 +541,20 @@ async function startCustomAction(actionId, row) {
|
|
|
536
541
|
|
|
537
542
|
actionLoadingStates.value[actionId] = false;
|
|
538
543
|
|
|
544
|
+
if (data?.redirectUrl) {
|
|
545
|
+
// Check if the URL should open in a new tab
|
|
546
|
+
if (data.redirectUrl.includes('target=_blank')) {
|
|
547
|
+
window.open(data.redirectUrl.replace('&target=_blank', '').replace('?target=_blank', ''), '_blank');
|
|
548
|
+
} else {
|
|
549
|
+
// Navigate within the app
|
|
550
|
+
if (data.redirectUrl.startsWith('http')) {
|
|
551
|
+
window.location.href = data.redirectUrl;
|
|
552
|
+
} else {
|
|
553
|
+
router.push(data.redirectUrl);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
539
558
|
if (data?.ok) {
|
|
540
559
|
emits('update:records', true);
|
|
541
560
|
|
|
@@ -553,4 +572,13 @@ async function startCustomAction(actionId, row) {
|
|
|
553
572
|
}
|
|
554
573
|
|
|
555
574
|
|
|
556
|
-
</script>
|
|
575
|
+
</script>
|
|
576
|
+
|
|
577
|
+
<style lang="scss" scoped>
|
|
578
|
+
input[type="checkbox"][disabled] {
|
|
579
|
+
@apply opacity-50;
|
|
580
|
+
}
|
|
581
|
+
input[type="checkbox"]:not([disabled]) {
|
|
582
|
+
@apply cursor-pointer;
|
|
583
|
+
}
|
|
584
|
+
</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({
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
|
-
<span
|
|
4
|
-
|
|
3
|
+
<span
|
|
4
|
+
v-if="column.foreignResource"
|
|
5
|
+
:class="{'flex flex-wrap': column.isArray?.enabled}"
|
|
6
|
+
@click="(e)=>{e.stopPropagation()}"
|
|
7
|
+
>
|
|
8
|
+
<span
|
|
9
|
+
v-if="record[column.name] && column.isArray?.enabled"
|
|
10
|
+
v-for="foreignResource in record[column.name]"
|
|
11
|
+
class="rounded-md m-0.5 bg-lightAnnouncementBG dark:bg-darkAnnouncementBG text-lightAnnouncementText dark:text-darkAnnouncementText py-0.5 px-2.5 text-sm"
|
|
12
|
+
>
|
|
13
|
+
<RouterLink
|
|
14
|
+
class="font-medium text-lightSidebarText dark:text-darkSidebarText hover:brightness-110 whitespace-nowrap"
|
|
15
|
+
:to="{ name: 'resource-show', params: { primaryKey: foreignResource.pk, resourceId: column.foreignResource.resourceId || column.foreignResource.polymorphicResources.find((pr) => pr.whenValue === record[column.foreignResource.polymorphicOn]).resourceId } }"
|
|
16
|
+
>
|
|
17
|
+
{{ foreignResource.label }}
|
|
18
|
+
</RouterLink>
|
|
19
|
+
</span>
|
|
20
|
+
<RouterLink v-else-if="record[column.name]" class="font-medium text-lightPrimary dark:text-darkPrimary hover:brightness-110 whitespace-nowrap"
|
|
5
21
|
:to="{ name: 'resource-show', params: { primaryKey: record[column.name].pk, resourceId: column.foreignResource.resourceId || column.foreignResource.polymorphicResources.find((pr) => pr.whenValue === record[column.foreignResource.polymorphicOn]).resourceId } }">
|
|
6
22
|
{{ record[column.name].label }}
|
|
7
23
|
</RouterLink>
|
|
@@ -206,9 +206,9 @@ export interface IAdminForthDataSourceConnector {
|
|
|
206
206
|
|
|
207
207
|
|
|
208
208
|
/**
|
|
209
|
-
* Used to create record in database.
|
|
209
|
+
* Used to create record in database. Should return value of primary key column of created record.
|
|
210
210
|
*/
|
|
211
|
-
createRecordOriginalValues({ resource, record }: { resource: AdminForthResource, record: any }): Promise<
|
|
211
|
+
createRecordOriginalValues({ resource, record }: { resource: AdminForthResource, record: any }): Promise<string>;
|
|
212
212
|
|
|
213
213
|
/**
|
|
214
214
|
* Update record in database. newValues might have not all fields in record, but only changed ones.
|
|
@@ -727,7 +727,12 @@ export interface AdminForthActionInput {
|
|
|
727
727
|
showButton?: boolean,
|
|
728
728
|
showThreeDotsMenu?: boolean,
|
|
729
729
|
};
|
|
730
|
-
|
|
730
|
+
allowed?: (params: {
|
|
731
|
+
adminUser: AdminUser;
|
|
732
|
+
standardAllowedActions: AllowedActions;
|
|
733
|
+
}) => boolean;
|
|
734
|
+
url?: string;
|
|
735
|
+
action?: (params: {
|
|
731
736
|
adminforth: IAdminForth;
|
|
732
737
|
resource: AdminForthResource;
|
|
733
738
|
recordId: string;
|
|
@@ -1146,8 +1151,6 @@ export interface ResourceOptionsInput extends Omit<AdminForthResourceCommon['opt
|
|
|
1146
1151
|
*/
|
|
1147
1152
|
bulkActions?: Array<AdminForthBulkAction>,
|
|
1148
1153
|
|
|
1149
|
-
actions?: Array<AdminForthActionInput>,
|
|
1150
|
-
|
|
1151
1154
|
/**
|
|
1152
1155
|
* Allowed actions for resource.
|
|
1153
1156
|
*
|
|
@@ -1165,10 +1168,38 @@ export interface ResourceOptionsInput extends Omit<AdminForthResourceCommon['opt
|
|
|
1165
1168
|
*
|
|
1166
1169
|
*/
|
|
1167
1170
|
allowedActions?: AllowedActionsInput,
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* Array of actions which will be displayed in the resource.
|
|
1174
|
+
*
|
|
1175
|
+
* Example:
|
|
1176
|
+
*
|
|
1177
|
+
* ```ts
|
|
1178
|
+
* actions: [
|
|
1179
|
+
* {
|
|
1180
|
+
* name: 'Auto submit',
|
|
1181
|
+
* allowed: ({ adminUser, standardAllowedActions }) => {
|
|
1182
|
+
* return adminUser.dbUser.role === 'superadmin';
|
|
1183
|
+
* },
|
|
1184
|
+
* action: ({ adminUser, resource, recordId, adminforth, extra, tr }) => {
|
|
1185
|
+
* console.log("auto submit", recordId, adminUser);
|
|
1186
|
+
* return { ok: true, successMessage: "Auto submitted" };
|
|
1187
|
+
* },
|
|
1188
|
+
* showIn: {
|
|
1189
|
+
* list: true,
|
|
1190
|
+
* showButton: true,
|
|
1191
|
+
* showThreeDotsMenu: true,
|
|
1192
|
+
* },
|
|
1193
|
+
* },
|
|
1194
|
+
* ]
|
|
1195
|
+
* ```
|
|
1196
|
+
*/
|
|
1197
|
+
actions?: Array<AdminForthActionInput>,
|
|
1168
1198
|
};
|
|
1169
1199
|
|
|
1170
1200
|
export interface ResourceOptions extends Omit<ResourceOptionsInput, 'allowedActions'> {
|
|
1171
1201
|
allowedActions: AllowedActions,
|
|
1202
|
+
actions?: Array<AdminForthActionInput>,
|
|
1172
1203
|
}
|
|
1173
1204
|
|
|
1174
1205
|
/**
|
|
@@ -50,6 +50,7 @@ export enum ActionCheckSource {
|
|
|
50
50
|
CreateRequest = 'createRequest',
|
|
51
51
|
DeleteRequest = 'deleteRequest',
|
|
52
52
|
BulkActionRequest = 'bulkActionRequest',
|
|
53
|
+
CustomActionRequest = 'customActionRequest',
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
export enum AllowedActionsEnum {
|
|
@@ -357,18 +358,22 @@ export interface AdminForthResourceInputCommon {
|
|
|
357
358
|
fieldGroups?: {
|
|
358
359
|
groupName: string;
|
|
359
360
|
columns: string[];
|
|
361
|
+
noTitle?: boolean;
|
|
360
362
|
}[];
|
|
361
363
|
createFieldGroups?: {
|
|
362
364
|
groupName: string;
|
|
363
365
|
columns: string[];
|
|
366
|
+
noTitle?: boolean;
|
|
364
367
|
}[];
|
|
365
368
|
editFieldGroups?: {
|
|
366
369
|
groupName: string;
|
|
367
370
|
columns: string[];
|
|
371
|
+
noTitle?: boolean;
|
|
368
372
|
}[];
|
|
369
373
|
showFieldGroups?: {
|
|
370
374
|
groupName: string;
|
|
371
375
|
columns: string[];
|
|
376
|
+
noTitle?: boolean;
|
|
372
377
|
}[];
|
|
373
378
|
|
|
374
379
|
/**
|
|
@@ -551,6 +556,11 @@ export interface AdminForthForeignResourceCommon {
|
|
|
551
556
|
unsetLabel?: string,
|
|
552
557
|
}
|
|
553
558
|
|
|
559
|
+
export type FillOnCreateFunction = (params: {
|
|
560
|
+
initialRecord: any,
|
|
561
|
+
adminUser: AdminUser,
|
|
562
|
+
}) => any;
|
|
563
|
+
|
|
554
564
|
/**
|
|
555
565
|
* Column describes one field in the table or collection in database.
|
|
556
566
|
*/
|
|
@@ -681,7 +691,7 @@ export interface AdminForthResourceColumnInputCommon {
|
|
|
681
691
|
/**
|
|
682
692
|
* Called on the backend when the record is saved to a database. Value returned by `fillOnCreate` will be saved to the database.
|
|
683
693
|
*/
|
|
684
|
-
fillOnCreate?:
|
|
694
|
+
fillOnCreate?: FillOnCreateFunction,
|
|
685
695
|
|
|
686
696
|
/**
|
|
687
697
|
* Single value that will be substituted in create form. User can change it before saving the record.
|
|
@@ -107,7 +107,11 @@ const editableRecord = computed(() => {
|
|
|
107
107
|
}
|
|
108
108
|
coreStore.resource.columns.forEach(column => {
|
|
109
109
|
if (column.foreignResource) {
|
|
110
|
-
|
|
110
|
+
if (column.isArray?.enabled) {
|
|
111
|
+
newRecord[column.name] = newRecord[column.name]?.map(fr => fr.pk);
|
|
112
|
+
} else {
|
|
113
|
+
newRecord[column.name] = newRecord[column.name]?.pk;
|
|
114
|
+
}
|
|
111
115
|
}
|
|
112
116
|
});
|
|
113
117
|
return newRecord;
|
|
@@ -172,7 +176,7 @@ async function saveRecord() {
|
|
|
172
176
|
});
|
|
173
177
|
}
|
|
174
178
|
saving.value = false;
|
|
175
|
-
router.push({ name: 'resource-show', params: { resourceId: route.params.resourceId, primaryKey:
|
|
179
|
+
router.push({ name: 'resource-show', params: { resourceId: route.params.resourceId, primaryKey: resp.recordId } });
|
|
176
180
|
}
|
|
177
181
|
|
|
178
182
|
</script>
|
|
@@ -83,11 +83,12 @@
|
|
|
83
83
|
:record="coreStore.record"
|
|
84
84
|
/>
|
|
85
85
|
</div>
|
|
86
|
-
<template v-else>
|
|
86
|
+
<template v-else>
|
|
87
87
|
<template v-for="group in groups" :key="group.groupName">
|
|
88
88
|
<ShowTable
|
|
89
89
|
:columns="group.columns"
|
|
90
90
|
:groupName="group.groupName"
|
|
91
|
+
:noTitle="group.noTitle"
|
|
91
92
|
:resource="coreStore.resource"
|
|
92
93
|
:record="coreStore.record"
|
|
93
94
|
/>
|
|
@@ -245,6 +246,21 @@ async function startCustomAction(actionId) {
|
|
|
245
246
|
|
|
246
247
|
actionLoadingStates.value[actionId] = false;
|
|
247
248
|
|
|
249
|
+
if (data?.redirectUrl) {
|
|
250
|
+
// Check if the URL should open in a new tab
|
|
251
|
+
if (data.redirectUrl.includes('target=_blank')) {
|
|
252
|
+
window.open(data.redirectUrl.replace('&target=_blank', '').replace('?target=_blank', ''), '_blank');
|
|
253
|
+
} else {
|
|
254
|
+
// Navigate within the app
|
|
255
|
+
if (data.redirectUrl.startsWith('http')) {
|
|
256
|
+
window.location.href = data.redirectUrl;
|
|
257
|
+
} else {
|
|
258
|
+
router.push(data.redirectUrl);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
248
264
|
if (data?.ok) {
|
|
249
265
|
await coreStore.fetchRecord({
|
|
250
266
|
resourceId: route.params.resourceId,
|
package/dist/types/Back.d.ts
CHANGED
|
@@ -169,12 +169,12 @@ export interface IAdminForthDataSourceConnector {
|
|
|
169
169
|
};
|
|
170
170
|
}>;
|
|
171
171
|
/**
|
|
172
|
-
* Used to create record in database.
|
|
172
|
+
* Used to create record in database. Should return value of primary key column of created record.
|
|
173
173
|
*/
|
|
174
174
|
createRecordOriginalValues({ resource, record }: {
|
|
175
175
|
resource: AdminForthResource;
|
|
176
176
|
record: any;
|
|
177
|
-
}): Promise<
|
|
177
|
+
}): Promise<string>;
|
|
178
178
|
/**
|
|
179
179
|
* Update record in database. newValues might have not all fields in record, but only changed ones.
|
|
180
180
|
* recordId is value of field which is marked as {@link AdminForthResourceColumn.primaryKey}
|
|
@@ -697,7 +697,12 @@ export interface AdminForthActionInput {
|
|
|
697
697
|
showButton?: boolean;
|
|
698
698
|
showThreeDotsMenu?: boolean;
|
|
699
699
|
};
|
|
700
|
-
|
|
700
|
+
allowed?: (params: {
|
|
701
|
+
adminUser: AdminUser;
|
|
702
|
+
standardAllowedActions: AllowedActions;
|
|
703
|
+
}) => boolean;
|
|
704
|
+
url?: string;
|
|
705
|
+
action?: (params: {
|
|
701
706
|
adminforth: IAdminForth;
|
|
702
707
|
resource: AdminForthResource;
|
|
703
708
|
recordId: string;
|
|
@@ -1025,7 +1030,6 @@ export interface ResourceOptionsInput extends Omit<AdminForthResourceCommon['opt
|
|
|
1025
1030
|
* using checkboxes.
|
|
1026
1031
|
*/
|
|
1027
1032
|
bulkActions?: Array<AdminForthBulkAction>;
|
|
1028
|
-
actions?: Array<AdminForthActionInput>;
|
|
1029
1033
|
/**
|
|
1030
1034
|
* Allowed actions for resource.
|
|
1031
1035
|
*
|
|
@@ -1043,9 +1047,36 @@ export interface ResourceOptionsInput extends Omit<AdminForthResourceCommon['opt
|
|
|
1043
1047
|
*
|
|
1044
1048
|
*/
|
|
1045
1049
|
allowedActions?: AllowedActionsInput;
|
|
1050
|
+
/**
|
|
1051
|
+
* Array of actions which will be displayed in the resource.
|
|
1052
|
+
*
|
|
1053
|
+
* Example:
|
|
1054
|
+
*
|
|
1055
|
+
* ```ts
|
|
1056
|
+
* actions: [
|
|
1057
|
+
* {
|
|
1058
|
+
* name: 'Auto submit',
|
|
1059
|
+
* allowed: ({ adminUser, standardAllowedActions }) => {
|
|
1060
|
+
* return adminUser.dbUser.role === 'superadmin';
|
|
1061
|
+
* },
|
|
1062
|
+
* action: ({ adminUser, resource, recordId, adminforth, extra, tr }) => {
|
|
1063
|
+
* console.log("auto submit", recordId, adminUser);
|
|
1064
|
+
* return { ok: true, successMessage: "Auto submitted" };
|
|
1065
|
+
* },
|
|
1066
|
+
* showIn: {
|
|
1067
|
+
* list: true,
|
|
1068
|
+
* showButton: true,
|
|
1069
|
+
* showThreeDotsMenu: true,
|
|
1070
|
+
* },
|
|
1071
|
+
* },
|
|
1072
|
+
* ]
|
|
1073
|
+
* ```
|
|
1074
|
+
*/
|
|
1075
|
+
actions?: Array<AdminForthActionInput>;
|
|
1046
1076
|
}
|
|
1047
1077
|
export interface ResourceOptions extends Omit<ResourceOptionsInput, 'allowedActions'> {
|
|
1048
1078
|
allowedActions: AllowedActions;
|
|
1079
|
+
actions?: Array<AdminForthActionInput>;
|
|
1049
1080
|
}
|
|
1050
1081
|
/**
|
|
1051
1082
|
* Resource describes one table or collection in database.
|