adminforth 1.1.27 → 1.1.29
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/modules/codeInjector.js +4 -4
- 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/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/modules/codeInjector.ts +4 -4
- package/package.json +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
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
<div class="mx-auto grid grid-cols-2 gap-4 mb-2" :class="{hidden: !showTimeInputs}">
|
|
27
27
|
<div>
|
|
28
28
|
<div class="relative">
|
|
29
|
-
<div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5">
|
|
29
|
+
<div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5 pointer-events-none">
|
|
30
30
|
<IconTime class="w-4 h-4 text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-700"/>
|
|
31
31
|
</div>
|
|
32
32
|
|
|
@@ -132,11 +132,11 @@ const end = computed(() => {
|
|
|
132
132
|
})
|
|
133
133
|
|
|
134
134
|
function updateFromProps() {
|
|
135
|
-
if (props.valueStart
|
|
135
|
+
if (!props.valueStart) {
|
|
136
136
|
datepickerStartEl.value.value = '';
|
|
137
137
|
startTime.value = '';
|
|
138
138
|
}
|
|
139
|
-
if (props.valueEnd
|
|
139
|
+
if (!props.valueEnd) {
|
|
140
140
|
datepickerEndEl.value.value = '';
|
|
141
141
|
endTime.value = '';
|
|
142
142
|
}
|
|
@@ -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
|
}"
|