adminforth 1.1.31 → 1.1.32
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/dist/plugins/AccessControl/index.js +56 -3
- package/dist/spa/index.html +2 -2
- package/dist/spa/package-lock.json +189 -1
- package/dist/spa/package.json +6 -1
- package/dist/spa/spa/src/App.vue +2 -0
- package/dist/spa/spa/src/components/ResourceForm.vue +1 -1
- package/dist/spa/src/App.vue +76 -57
- package/dist/spa/src/components/AcceptModal.vue +1 -1
- package/dist/spa/src/components/CustomDatePicker.vue +3 -3
- package/dist/spa/src/components/CustomDateRangePicker.vue +3 -3
- package/dist/spa/src/components/CustomRangePicker.vue +148 -0
- package/dist/spa/src/components/Dropdown.vue +13 -4
- package/dist/spa/src/components/Filters.vue +55 -22
- package/dist/spa/src/components/MenuLink.vue +2 -2
- package/dist/spa/src/components/ResourceForm.vue +157 -99
- package/dist/spa/src/components/ValueRenderer.vue +11 -1
- package/dist/spa/src/router/index.ts +3 -2
- package/dist/spa/src/stores/core.ts +22 -33
- package/dist/spa/src/utils.ts +5 -3
- package/dist/spa/src/views/CreateView.vue +15 -7
- package/dist/spa/src/views/EditView.vue +36 -8
- package/dist/spa/src/views/ListView.vue +35 -23
- package/dist/spa/src/views/LoginView.vue +1 -1
- package/dist/spa/src/views/ResourceParent.vue +1 -1
- package/dist/spa/src/views/ShowView.vue +20 -9
- package/dist/spa/tailwind.config.js +5 -2
- package/package.json +1 -1
- package/spa/src/App.vue +2 -0
- package/spa/src/components/ResourceForm.vue +1 -1
- package/dist/plugins/plugins/ForeignInlineListPlugin/custom/InlineList.vue +0 -248
- package/dist/spa/spa/src/components/ShowTableItem.vue +0 -14
- package/dist/spa/spa/src/views/HomeView.vue +0 -8
- package/dist/spa/src/views/HomeView.vue +0 -8
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-wrap gap-2">
|
|
3
|
+
<input
|
|
4
|
+
type="number" aria-describedby="helper-text-explanation"
|
|
5
|
+
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-20 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"
|
|
6
|
+
placeholder="From"
|
|
7
|
+
v-model="start"
|
|
8
|
+
>
|
|
9
|
+
|
|
10
|
+
<input
|
|
11
|
+
type="number" aria-describedby="helper-text-explanation"
|
|
12
|
+
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-20 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"
|
|
13
|
+
placeholder="To"
|
|
14
|
+
v-model="end"
|
|
15
|
+
>
|
|
16
|
+
|
|
17
|
+
<button
|
|
18
|
+
v-if="isChanged"
|
|
19
|
+
type="button"
|
|
20
|
+
class="flex items-center p-0.5 ml-auto px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 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 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
21
|
+
@click="clear">Clear
|
|
22
|
+
</button>
|
|
23
|
+
|
|
24
|
+
<div class="w-full">
|
|
25
|
+
<vue-slider
|
|
26
|
+
class="custom-slider"
|
|
27
|
+
:dot-size="20"
|
|
28
|
+
height="7.99px"
|
|
29
|
+
:min="minFormatted"
|
|
30
|
+
:max="maxFormatted"
|
|
31
|
+
v-model="sliderValue"
|
|
32
|
+
@update:model-value="updateFromSlider($event)"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
<script setup lang="ts">
|
|
38
|
+
import VueSlider from 'vue-slider-component';
|
|
39
|
+
import 'vue-slider-component/theme/antd.css'
|
|
40
|
+
import {computed, onMounted, ref, watch} from "vue";
|
|
41
|
+
import debounce from 'debounce'
|
|
42
|
+
|
|
43
|
+
const props = defineProps({
|
|
44
|
+
valueStart: {
|
|
45
|
+
default: '',
|
|
46
|
+
},
|
|
47
|
+
valueEnd: {
|
|
48
|
+
default: '',
|
|
49
|
+
},
|
|
50
|
+
min: {
|
|
51
|
+
default: 0,
|
|
52
|
+
},
|
|
53
|
+
max: {
|
|
54
|
+
default: 10,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const emit = defineEmits(['update:valueStart', 'update:valueEnd']);
|
|
59
|
+
|
|
60
|
+
const minFormatted = computed(() => Math.floor(props.min));
|
|
61
|
+
const maxFormatted = computed(() => Math.ceil(props.max));
|
|
62
|
+
|
|
63
|
+
const isChanged = computed(() => {
|
|
64
|
+
return start.value && start.value !== minFormatted.value || end.value && end.value !== maxFormatted.value;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const start = ref(props.valueStart);
|
|
68
|
+
const end = ref(props.valueEnd);
|
|
69
|
+
|
|
70
|
+
const sliderValue = ref([minFormatted.value, maxFormatted.value]);
|
|
71
|
+
|
|
72
|
+
const updateFromSlider =
|
|
73
|
+
debounce((value: [number, number]) => {
|
|
74
|
+
start.value = value[0] === minFormatted.value ? '': value[0];
|
|
75
|
+
end.value = value[1] === maxFormatted.value ? '': value[1];
|
|
76
|
+
}, 500);
|
|
77
|
+
|
|
78
|
+
onMounted(() => {
|
|
79
|
+
updateStartFromProps();
|
|
80
|
+
updateEndFromProps();
|
|
81
|
+
|
|
82
|
+
watch(() => props.valueStart, (value) => {
|
|
83
|
+
updateStartFromProps();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
watch(() => props.valueEnd, (value) => {
|
|
87
|
+
updateEndFromProps();
|
|
88
|
+
});
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
function updateStartFromProps() {
|
|
92
|
+
start.value = props.valueStart;
|
|
93
|
+
setSliderValues(start.value, end.value)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function updateEndFromProps() {
|
|
97
|
+
end.value = props.valueEnd;
|
|
98
|
+
setSliderValues(start.value, end.value)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
watch(start, () => {
|
|
102
|
+
console.log('⚡ emit', start.value)
|
|
103
|
+
emit('update:valueStart', start.value)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
watch(end, () => {
|
|
107
|
+
console.log('⚡ emit', end.value)
|
|
108
|
+
emit('update:valueEnd', end.value);
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const clear = () => {
|
|
112
|
+
start.value = ''
|
|
113
|
+
end.value = ''
|
|
114
|
+
setSliderValues('', '')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function setSliderValues(start, end) {
|
|
118
|
+
sliderValue.value = [start || minFormatted.value, end || maxFormatted.value];
|
|
119
|
+
}
|
|
120
|
+
</script>
|
|
121
|
+
|
|
122
|
+
<style lang="scss" scoped>
|
|
123
|
+
.custom-slider {
|
|
124
|
+
&:deep(.vue-slider-rail) {
|
|
125
|
+
background-color: rgb(229 231 235);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
&:deep(.vue-slider-dot-handle) {
|
|
129
|
+
background-color: #1c64f2;
|
|
130
|
+
border: none;
|
|
131
|
+
box-shadow: none;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
&:deep(.vue-slider-dot-handle:hover) {
|
|
135
|
+
background-color: #1c64f2;
|
|
136
|
+
border: none;
|
|
137
|
+
box-shadow: none;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
&:deep(.vue-slider-process) {
|
|
141
|
+
background-color: rgb(225 239 254);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
&:deep(.vue-slider-process:hover) {
|
|
145
|
+
background-color: rgb(225 239 254);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
</style>
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
:placeholder="selectedItems.length ? '' : 'Select...'"
|
|
10
10
|
/>
|
|
11
11
|
<div class="absolute inset-y-0 left-2 flex items-center pr-2 flex-wrap">
|
|
12
|
-
|
|
12
|
+
{{ }}
|
|
13
|
+
<div v-for="item in selectedItems" :key="item?.name" class="bg-blue-100 text-blue-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300">
|
|
13
14
|
<span>{{ item.label }}</span>
|
|
14
15
|
<button
|
|
15
16
|
type="button"
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
<IconCaretUpSolid v-else class="h-5 w-5 text-gray-400" />
|
|
35
36
|
</div>
|
|
36
37
|
</div>
|
|
37
|
-
<div v-if="showDropdown" class="absolute z-10 mt-1 w-full bg-white shadow-lg rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
|
38
|
+
<div v-if="showDropdown" class="absolute z-10 mt-1 w-full bg-white shadow-lg dark:shadow-black rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
|
38
39
|
<div
|
|
39
40
|
v-for="item in filteredItems"
|
|
40
41
|
:key="item.value"
|
|
@@ -76,10 +77,14 @@ const showDropdown = ref(false);
|
|
|
76
77
|
const selectedItems = ref([]);
|
|
77
78
|
|
|
78
79
|
function updateFromProps() {
|
|
79
|
-
console.log('⚡ updateFromProps', props.modelValue)
|
|
80
80
|
if (props.modelValue !== undefined) {
|
|
81
81
|
if (props.single) {
|
|
82
|
-
|
|
82
|
+
const el = props.options.find(item => item.value === props.modelValue);
|
|
83
|
+
if (el) {
|
|
84
|
+
selectedItems.value = [el];
|
|
85
|
+
} else {
|
|
86
|
+
selectedItems.value = [];
|
|
87
|
+
}
|
|
83
88
|
} else {
|
|
84
89
|
selectedItems.value = props.options.filter(item => props.modelValue.includes(item.value));
|
|
85
90
|
}
|
|
@@ -93,6 +98,10 @@ onMounted(() => {
|
|
|
93
98
|
updateFromProps();
|
|
94
99
|
});
|
|
95
100
|
|
|
101
|
+
watch(() => props.options, () => {
|
|
102
|
+
updateFromProps();
|
|
103
|
+
});
|
|
104
|
+
|
|
96
105
|
addClickListener();
|
|
97
106
|
|
|
98
107
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<!-- drawer component -->
|
|
3
3
|
<div id="drawer-navigation"
|
|
4
4
|
|
|
5
|
-
class="fixed top-14 right-0 z-40 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl"
|
|
5
|
+
class="fixed top-14 right-0 z-40 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl dark:shadow-black"
|
|
6
6
|
|
|
7
7
|
:class="show ? 'top-0 transform-none' : ''"
|
|
8
8
|
tabindex="-1" aria-labelledby="drawer-navigation-label"
|
|
@@ -22,8 +22,14 @@
|
|
|
22
22
|
<li v-for="c in columnsWithFilter" :key="c">
|
|
23
23
|
<p class="dark:text-gray-400">{{ c.label }}</p>
|
|
24
24
|
|
|
25
|
-
<Dropdown
|
|
26
|
-
v-if="c.
|
|
25
|
+
<Dropdown
|
|
26
|
+
v-if="c.foreignResource"
|
|
27
|
+
:options="columnOptions[c.name] || []"
|
|
28
|
+
@update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
|
|
29
|
+
:modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
30
|
+
/>
|
|
31
|
+
<Dropdown
|
|
32
|
+
v-else-if="c.type === 'boolean'"
|
|
27
33
|
:options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
|
|
28
34
|
@update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
|
|
29
35
|
:modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
@@ -62,22 +68,15 @@
|
|
|
62
68
|
:value="getFilterItem({ column: c, operator: 'ilike' })"
|
|
63
69
|
>
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
type="number" aria-describedby="helper-text-explanation"
|
|
75
|
-
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-20 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"
|
|
76
|
-
placeholder="To"
|
|
77
|
-
@input="setFilterItem({ column: c, operator: 'lte', value: $event.target.value || undefined})"
|
|
78
|
-
:value="getFilterItem({ column: c, operator: 'lte' })"
|
|
79
|
-
>
|
|
80
|
-
</div>
|
|
71
|
+
<CustomRangePicker
|
|
72
|
+
v-else-if="['integer', 'decimal', 'float'].includes(c.type)"
|
|
73
|
+
:min="c.min"
|
|
74
|
+
:max="c.max"
|
|
75
|
+
:valueStart="getFilterItem({ column: c, operator: 'gte' })"
|
|
76
|
+
@update:valueStart="setFilterItem({ column: c, operator: 'gte', value: $event || undefined })"
|
|
77
|
+
:valueEnd="getFilterItem({ column: c, operator: 'lte' })"
|
|
78
|
+
@update:valueEnd="setFilterItem({ column: c, operator: 'lte', value: $event || undefined })"
|
|
79
|
+
/>
|
|
81
80
|
|
|
82
81
|
</li>
|
|
83
82
|
</ul>
|
|
@@ -93,25 +92,59 @@
|
|
|
93
92
|
</div>
|
|
94
93
|
</div>
|
|
95
94
|
|
|
96
|
-
<div v-if="show" drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-
|
|
95
|
+
<div v-if="show" drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-50"
|
|
97
96
|
@click="$emit('hide')">
|
|
98
97
|
</div>
|
|
99
98
|
</template>
|
|
100
99
|
|
|
101
100
|
<script setup>
|
|
102
|
-
import { watch, computed } from 'vue'
|
|
101
|
+
import { watch, computed, ref, onMounted } from 'vue'
|
|
103
102
|
import Dropdown from '@/components/Dropdown.vue';
|
|
104
103
|
import CustomDateRangePicker from '@/components/CustomDateRangePicker.vue';
|
|
104
|
+
import { callAdminForthApi } from '@/utils';
|
|
105
|
+
import { useRouter } from 'vue-router';
|
|
106
|
+
import { computedAsync } from '@vueuse/core'
|
|
107
|
+
import CustomRangePicker from "@/components/CustomRangePicker.vue";
|
|
105
108
|
|
|
106
109
|
// props: columns
|
|
107
110
|
// add support for v-model:filers
|
|
108
111
|
const props = defineProps(['columns', 'filters', 'show', 'columnsMinMax']);
|
|
109
112
|
const emits = defineEmits(['update:filters', 'hide']);
|
|
110
113
|
|
|
111
|
-
const
|
|
114
|
+
const router = useRouter();
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
const columnsWithFilter = computed(
|
|
112
118
|
() => props.columns?.filter(column => column.showIn.includes('filter')) || []
|
|
113
119
|
);
|
|
114
120
|
|
|
121
|
+
const columnOptions = computedAsync(async () => {
|
|
122
|
+
const ret = {};
|
|
123
|
+
if (!props.columns) {
|
|
124
|
+
return ret;
|
|
125
|
+
}
|
|
126
|
+
await Promise.all(
|
|
127
|
+
Object.values(props.columns).map(async (column) => {
|
|
128
|
+
if (column.foreignResource) {
|
|
129
|
+
const list = await callAdminForthApi({
|
|
130
|
+
method: 'POST',
|
|
131
|
+
path: `/get_resource_foreign_data`,
|
|
132
|
+
body: {
|
|
133
|
+
resourceId: router.currentRoute.value.params.resourceId,
|
|
134
|
+
column: column.name,
|
|
135
|
+
limit: 1000,
|
|
136
|
+
offset: 0,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
ret[column.name] = list.items;
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return ret;
|
|
145
|
+
}, {});
|
|
146
|
+
|
|
147
|
+
|
|
115
148
|
// sync 'body' class 'overflow-hidden' with show prop show
|
|
116
149
|
watch(() => props.show, (show) => {
|
|
117
150
|
if (show) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<RouterLink
|
|
3
3
|
:to="{name: item.resourceId ? 'resource-list' : item.path, params: item.resourceId ? { resourceId: item.resourceId }: {}}"
|
|
4
|
-
class="flex items-center py-2 text-
|
|
4
|
+
class="flex items-center py-2 text-nav-menu-text rounded-default hover:bg-nav-menu-bg-hover active:bg-nav-menu-bg-active" role="menuitem"
|
|
5
5
|
:class="{
|
|
6
6
|
'px-4': isChild,
|
|
7
7
|
'px-2': !isChild,
|
|
8
|
-
'bg-
|
|
8
|
+
'bg-nav-menu-bg-active dark:bg-gray-700': item.resourceId ?
|
|
9
9
|
($route.params.resourceId === item.resourceId && $route.name === 'resource-list') :
|
|
10
10
|
($route.name === item.path)
|
|
11
11
|
}"
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
<div>
|
|
3
3
|
|
|
4
4
|
<div
|
|
5
|
-
class="relative shadow-
|
|
5
|
+
class="relative shadow-resourse-form-shadow sm:rounded-lg dark:shadow-2xl dark:shadow-black"
|
|
6
6
|
>
|
|
7
7
|
<form autocomplete="off" @submit.prevent>
|
|
8
8
|
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
|
9
|
-
<thead class="text-xs text-gray-700 uppercase bg-
|
|
9
|
+
<thead class="text-xs text-gray-700 uppercase bg-form-view-heading dark:bg-gray-700 dark:text-gray-400">
|
|
10
10
|
<tr>
|
|
11
11
|
<th scope="col" class="px-6 py-3">
|
|
12
12
|
Field
|
|
@@ -18,97 +18,116 @@
|
|
|
18
18
|
</thead>
|
|
19
19
|
<tbody>
|
|
20
20
|
<tr v-for="column, i in editableColumns" :key="column.name"
|
|
21
|
-
class="
|
|
21
|
+
class="bg-form-view-bg dark:bg-gray-800 border-b dark:border-gray-700"
|
|
22
22
|
>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
<!-- if column is in customComponentsPerColumn, use this component. If not, use this code -->
|
|
24
|
+
<template v-if="customComponentsPerColumn[column.name]">
|
|
25
|
+
<component
|
|
26
|
+
:is="customComponentsPerColumn[column.name]"
|
|
27
|
+
:column="column"
|
|
28
|
+
:value="currentValues[column.name]"
|
|
29
|
+
@update:value="setCurrentValue(column.name, $event)"
|
|
28
30
|
/>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
31
|
+
</template>
|
|
32
|
+
<template v-else>
|
|
33
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
34
|
+
{{ column.label }}
|
|
35
|
+
<span :data-tooltip-target="`tooltip-show-${i}`" class="relative inline-block">
|
|
36
|
+
<IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
|
|
37
|
+
:class="(columnError(column) && validating) ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'"
|
|
38
|
+
/>
|
|
39
|
+
</span>
|
|
40
|
+
<div :id="`tooltip-show-${i}`"
|
|
41
|
+
role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
|
|
42
|
+
Required field
|
|
43
|
+
<div class="tooltip-arrow" data-popper-arrow></div>
|
|
44
|
+
</div>
|
|
45
|
+
</td>
|
|
46
|
+
<td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap relative">
|
|
47
|
+
|
|
48
|
+
<Dropdown
|
|
49
|
+
single
|
|
50
|
+
v-if="column.foreignResource"
|
|
51
|
+
:options="columnOptions[column.name] || []"
|
|
52
|
+
:modelValue="currentValues[column.name]"
|
|
53
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
54
|
+
/>
|
|
55
|
+
<Dropdown
|
|
56
|
+
single
|
|
57
|
+
v-else-if="column.enum"
|
|
58
|
+
:options="column.enum"
|
|
59
|
+
:modelValue="currentValues[column.name]"
|
|
60
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
61
|
+
/>
|
|
62
|
+
<Dropdown
|
|
63
|
+
single
|
|
64
|
+
v-else-if="column.type === 'boolean'"
|
|
65
|
+
:options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
|
|
66
|
+
:modelValue="currentValues[column.name]"
|
|
67
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
68
|
+
/>
|
|
69
|
+
<input
|
|
70
|
+
v-else-if="['integer'].includes(column.type)"
|
|
71
|
+
type="number"
|
|
72
|
+
step="1"
|
|
73
|
+
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"
|
|
74
|
+
placeholder="0"
|
|
75
|
+
:value="currentValues[column.name]"
|
|
76
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
77
|
+
>
|
|
78
|
+
<CustomDatePicker
|
|
79
|
+
v-else-if="['datetime'].includes(column.type)"
|
|
80
|
+
:column="column"
|
|
81
|
+
:valueStart="currentValues[column.name]"
|
|
82
|
+
@update:valueStart="setCurrentValue(column.name, $event)"
|
|
83
|
+
/>
|
|
84
|
+
<input
|
|
85
|
+
v-else-if="['decimal', 'float'].includes(column.type)"
|
|
86
|
+
type="number"
|
|
87
|
+
step="0.1"
|
|
88
|
+
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"
|
|
89
|
+
placeholder="0.0"
|
|
90
|
+
:value="currentValues[column.name]"
|
|
91
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
92
|
+
/>
|
|
93
|
+
<textarea
|
|
94
|
+
v-else-if="['text'].includes(column.type)"
|
|
95
|
+
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"
|
|
96
|
+
placeholder="Text"
|
|
97
|
+
:value="currentValues[column.name]"
|
|
98
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
99
|
+
>
|
|
100
|
+
</textarea>
|
|
101
|
+
<input
|
|
102
|
+
v-else
|
|
103
|
+
:type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
|
|
104
|
+
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"
|
|
105
|
+
placeholder="Text"
|
|
106
|
+
:value="currentValues[column.name]"
|
|
107
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
108
|
+
autocomplete="false"
|
|
109
|
+
data-lpignore="true"
|
|
110
|
+
readonly
|
|
111
|
+
onfocus="this.removeAttribute('readonly');"
|
|
112
|
+
>
|
|
113
|
+
|
|
114
|
+
<button
|
|
115
|
+
v-if="column.masked"
|
|
116
|
+
type="button"
|
|
117
|
+
@click="unmasked[column.name] = !unmasked[column.name]"
|
|
118
|
+
class="h-6 absolute inset-y-2 top-6 right-6 flex items-center pr-2 z-index-100 focus:outline-none"
|
|
119
|
+
>
|
|
120
|
+
<IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]" />
|
|
121
|
+
<IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
|
|
122
|
+
</button>
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
<div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
|
|
127
|
+
|
|
128
|
+
<div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
|
|
129
|
+
</td>
|
|
130
|
+
</template>
|
|
112
131
|
</tr>
|
|
113
132
|
|
|
114
133
|
</tbody>
|
|
@@ -121,19 +140,24 @@
|
|
|
121
140
|
|
|
122
141
|
<script setup>
|
|
123
142
|
|
|
124
|
-
import { ref, computed, onMounted, watch } from 'vue';
|
|
125
|
-
import { useCoreStore } from '@/stores/core';
|
|
126
|
-
import Dropdown from '@/components/Dropdown.vue';
|
|
127
|
-
import { IconExclamationCircleSolid } from '@iconify-prerendered/vue-flowbite';
|
|
128
|
-
import { initFlowbite } from 'flowbite'
|
|
129
|
-
import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
|
|
130
143
|
import CustomDatePicker from "@/components/CustomDatePicker.vue";
|
|
144
|
+
import Dropdown from '@/components/Dropdown.vue';
|
|
145
|
+
import { useCoreStore } from '@/stores/core';
|
|
146
|
+
import { callAdminForthApi } from '@/utils';
|
|
147
|
+
import { IconExclamationCircleSolid, IconEyeSlashSolid, IconEyeSolid } from '@iconify-prerendered/vue-flowbite';
|
|
148
|
+
import { computedAsync } from '@vueuse/core';
|
|
149
|
+
import { initFlowbite } from 'flowbite';
|
|
150
|
+
import { computed, onMounted, ref, watch } from 'vue';
|
|
151
|
+
import { useRouter } from 'vue-router';
|
|
152
|
+
|
|
153
|
+
const router = useRouter();
|
|
131
154
|
|
|
132
155
|
const props = defineProps({
|
|
133
156
|
loading: Boolean,
|
|
134
157
|
resourceColumns: Object,
|
|
135
158
|
record: Object,
|
|
136
159
|
validating: Boolean,
|
|
160
|
+
customComponentsPerColumn: Object,
|
|
137
161
|
});
|
|
138
162
|
|
|
139
163
|
|
|
@@ -145,6 +169,7 @@ const emit = defineEmits(['update:record', 'update:isValid']);
|
|
|
145
169
|
|
|
146
170
|
const currentValues = ref({});
|
|
147
171
|
|
|
172
|
+
|
|
148
173
|
const columnError = (column) => {
|
|
149
174
|
const val = computed(() => {
|
|
150
175
|
if ( column.required[mode.value] && (currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '') ) {
|
|
@@ -166,14 +191,24 @@ const columnError = (column) => {
|
|
|
166
191
|
return `This field must be less than ${column.maxValue}`;
|
|
167
192
|
}
|
|
168
193
|
}
|
|
194
|
+
if ( column.validation && column.validation.length ){
|
|
195
|
+
const validationArray = column.validation;
|
|
196
|
+
for (let i = 0; i < validationArray.length; i++) {
|
|
197
|
+
if (validationArray[i].regExp) {
|
|
198
|
+
const regExp = new RegExp(validationArray[i].regExp);
|
|
199
|
+
if (!regExp.test(currentValues.value[column.name])) {
|
|
200
|
+
return validationArray[i].message;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
}
|
|
169
206
|
return null;
|
|
170
207
|
});
|
|
171
208
|
return val.value;
|
|
172
209
|
};
|
|
173
210
|
|
|
174
211
|
|
|
175
|
-
|
|
176
|
-
|
|
177
212
|
const setCurrentValue = (key, value) => {
|
|
178
213
|
currentValues.value[key] = value;
|
|
179
214
|
|
|
@@ -185,8 +220,31 @@ onMounted(() => {
|
|
|
185
220
|
currentValues.value[key] = props.record[key];
|
|
186
221
|
});
|
|
187
222
|
initFlowbite();
|
|
223
|
+
|
|
224
|
+
|
|
188
225
|
});
|
|
189
226
|
|
|
227
|
+
const columnOptions = computedAsync(async () => {
|
|
228
|
+
return (await Promise.all(
|
|
229
|
+
Object.values(props.resourceColumns).map(async (column) => {
|
|
230
|
+
if (column.foreignResource) {
|
|
231
|
+
const list = await callAdminForthApi({
|
|
232
|
+
method: 'POST',
|
|
233
|
+
path: `/get_resource_foreign_data`,
|
|
234
|
+
body: {
|
|
235
|
+
resourceId: router.currentRoute.value.params.resourceId,
|
|
236
|
+
column: column.name,
|
|
237
|
+
limit: 1000,
|
|
238
|
+
offset: 0,
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
return { [column.name]: list.items };
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
)).reduce((acc, val) => Object.assign(acc, val), {})
|
|
245
|
+
|
|
246
|
+
}, {});
|
|
247
|
+
|
|
190
248
|
const coreStore = useCoreStore();
|
|
191
249
|
|
|
192
250
|
const editableColumns = computed(() => {
|