adminforth 1.0.30 → 1.0.31
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/index.js +88 -25
- package/dist/modules/codeInjector.js +37 -15
- package/dist/spa/spa/package-lock.json +108 -1
- package/dist/spa/spa/package.json +4 -1
- package/dist/spa/spa/src/App.vue +6 -2
- package/dist/spa/spa/src/components/AcceptModal.vue +1 -1
- package/dist/spa/spa/src/components/CustomRangePicker.vue +143 -0
- package/dist/spa/spa/src/components/Dropdown.vue +1 -1
- package/dist/spa/spa/src/components/Filters.vue +54 -21
- package/dist/spa/spa/src/components/ResourceForm.vue +38 -4
- package/dist/spa/spa/src/router/index.ts +3 -1
- package/dist/spa/spa/src/stores/core.ts +1 -14
- package/dist/spa/spa/src/views/ListView.vue +4 -1
- package/dist/spa/spa/src/views/LoginView.vue +1 -1
- package/index.ts +65 -0
- package/modules/codeInjector.ts +45 -18
- package/package.json +1 -1
- package/spa/package-lock.json +108 -1
- package/spa/package.json +4 -1
- package/spa/src/App.vue +6 -2
- package/spa/src/components/AcceptModal.vue +1 -1
- package/spa/src/components/CustomRangePicker.vue +143 -0
- package/spa/src/components/Dropdown.vue +1 -1
- package/spa/src/components/Filters.vue +54 -21
- package/spa/src/components/ResourceForm.vue +38 -4
- package/spa/src/router/index.ts +3 -1
- package/spa/src/stores/core.ts +1 -14
- package/spa/src/views/ListView.vue +4 -1
- package/spa/src/views/LoginView.vue +1 -1
|
@@ -0,0 +1,143 @@
|
|
|
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: 0,
|
|
46
|
+
},
|
|
47
|
+
valueEnd: {
|
|
48
|
+
default: 10,
|
|
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 !== minFormatted.value || end.value !== maxFormatted.value;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const start = ref(minFormatted.value);
|
|
68
|
+
const end = ref(maxFormatted.value);
|
|
69
|
+
|
|
70
|
+
const sliderValue = ref([start.value, end.value]);
|
|
71
|
+
|
|
72
|
+
const updateFromSlider =
|
|
73
|
+
debounce((value: [number, number]) => {
|
|
74
|
+
start.value = value[0];
|
|
75
|
+
end.value = value[1];
|
|
76
|
+
}, 500);
|
|
77
|
+
|
|
78
|
+
function updateFromProps() {
|
|
79
|
+
if (props.valueStart || props.valueEnd) {
|
|
80
|
+
setFromProps(props.valueStart, props.valueEnd)
|
|
81
|
+
} else {
|
|
82
|
+
clear();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
onMounted(() => {
|
|
87
|
+
updateFromProps();
|
|
88
|
+
|
|
89
|
+
watch(() => [props.valueStart, props.valueEnd], (value) => {
|
|
90
|
+
updateFromProps();
|
|
91
|
+
});
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
watch(start, () => {
|
|
95
|
+
//console.log('⚡ emit', start.value)
|
|
96
|
+
emit('update:valueStart', start.value)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
watch(end, () => {
|
|
100
|
+
//console.log('⚡ emit', end.value)
|
|
101
|
+
emit('update:valueEnd', end.value);
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const clear = () => {
|
|
105
|
+
start.value = minFormatted.value;
|
|
106
|
+
end.value = maxFormatted.value;
|
|
107
|
+
sliderValue.value = [start.value, end.value]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function setFromProps(startValue: number, endValue: number) {
|
|
111
|
+
start.value = startValue ? startValue : minFormatted.value
|
|
112
|
+
end.value = endValue ? endValue : maxFormatted.value
|
|
113
|
+
sliderValue.value = [start.value, end.value]
|
|
114
|
+
}
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<style lang="scss" scoped>
|
|
118
|
+
.custom-slider {
|
|
119
|
+
&:deep(.vue-slider-rail) {
|
|
120
|
+
background-color: rgb(229 231 235);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
&:deep(.vue-slider-dot-handle) {
|
|
124
|
+
background-color: #1c64f2;
|
|
125
|
+
border: none;
|
|
126
|
+
box-shadow: none;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
&:deep(.vue-slider-dot-handle:hover) {
|
|
130
|
+
background-color: #1c64f2;
|
|
131
|
+
border: none;
|
|
132
|
+
box-shadow: none;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
&:deep(.vue-slider-process) {
|
|
136
|
+
background-color: rgb(225 239 254);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
&:deep(.vue-slider-process:hover) {
|
|
140
|
+
background-color: rgb(225 239 254);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
</style>
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
<IconCaretUpSolid v-else class="h-5 w-5 text-gray-400" />
|
|
35
35
|
</div>
|
|
36
36
|
</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">
|
|
37
|
+
<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
38
|
<div
|
|
39
39
|
v-for="item in filteredItems"
|
|
40
40
|
:key="item.value"
|
|
@@ -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>
|
|
@@ -99,19 +98,53 @@
|
|
|
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) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div>
|
|
3
3
|
|
|
4
4
|
<div
|
|
5
|
-
class="relative shadow-md sm:rounded-lg dark:shadow-2xl"
|
|
5
|
+
class="relative shadow-md 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">
|
|
@@ -36,7 +36,14 @@
|
|
|
36
36
|
<td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap relative">
|
|
37
37
|
<Dropdown
|
|
38
38
|
single
|
|
39
|
-
v-if="column.
|
|
39
|
+
v-if="column.foreignResource"
|
|
40
|
+
:options="columnOptions[column.name] || []"
|
|
41
|
+
:modelValue="currentValues[column.name]"
|
|
42
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
43
|
+
/>
|
|
44
|
+
<Dropdown
|
|
45
|
+
single
|
|
46
|
+
v-else-if="column.enum"
|
|
40
47
|
:options="column.enum"
|
|
41
48
|
:modelValue="currentValues[column.name]"
|
|
42
49
|
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
@@ -128,6 +135,11 @@ import { IconExclamationCircleSolid } from '@iconify-prerendered/vue-flowbite';
|
|
|
128
135
|
import { initFlowbite } from 'flowbite'
|
|
129
136
|
import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
|
|
130
137
|
import CustomDatePicker from "@/components/CustomDatePicker.vue";
|
|
138
|
+
import { callAdminForthApi } from '@/utils';
|
|
139
|
+
import { useRouter } from 'vue-router';
|
|
140
|
+
import { computedAsync } from '@vueuse/core';
|
|
141
|
+
|
|
142
|
+
const router = useRouter();
|
|
131
143
|
|
|
132
144
|
const props = defineProps({
|
|
133
145
|
loading: Boolean,
|
|
@@ -145,6 +157,7 @@ const emit = defineEmits(['update:record', 'update:isValid']);
|
|
|
145
157
|
|
|
146
158
|
const currentValues = ref({});
|
|
147
159
|
|
|
160
|
+
|
|
148
161
|
const columnError = (column) => {
|
|
149
162
|
const val = computed(() => {
|
|
150
163
|
if ( column.required[mode.value] && (currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '') ) {
|
|
@@ -172,8 +185,6 @@ const columnError = (column) => {
|
|
|
172
185
|
};
|
|
173
186
|
|
|
174
187
|
|
|
175
|
-
|
|
176
|
-
|
|
177
188
|
const setCurrentValue = (key, value) => {
|
|
178
189
|
currentValues.value[key] = value;
|
|
179
190
|
|
|
@@ -185,8 +196,31 @@ onMounted(() => {
|
|
|
185
196
|
currentValues.value[key] = props.record[key];
|
|
186
197
|
});
|
|
187
198
|
initFlowbite();
|
|
199
|
+
|
|
200
|
+
|
|
188
201
|
});
|
|
189
202
|
|
|
203
|
+
const columnOptions = computedAsync(async () => {
|
|
204
|
+
return (await Promise.all(
|
|
205
|
+
Object.values(props.resourceColumns).map(async (column) => {
|
|
206
|
+
if (column.foreignResource) {
|
|
207
|
+
const list = await callAdminForthApi({
|
|
208
|
+
method: 'POST',
|
|
209
|
+
path: `/get_resource_foreign_data`,
|
|
210
|
+
body: {
|
|
211
|
+
resourceId: router.currentRoute.value.params.resourceId,
|
|
212
|
+
column: column.name,
|
|
213
|
+
limit: 1000,
|
|
214
|
+
offset: 0,
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
return { [column.name]: list.items };
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
)).reduce((acc, val) => Object.assign(acc, val), {})
|
|
221
|
+
|
|
222
|
+
}, {});
|
|
223
|
+
|
|
190
224
|
const coreStore = useCoreStore();
|
|
191
225
|
|
|
192
226
|
const editableColumns = computed(() => {
|
package/spa/src/router/index.ts
CHANGED
package/spa/src/stores/core.ts
CHANGED
|
@@ -42,20 +42,7 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
42
42
|
user.value = resp.user;
|
|
43
43
|
|
|
44
44
|
// find homepage:true in menu recuresively
|
|
45
|
-
|
|
46
|
-
return
|
|
47
|
-
} else {
|
|
48
|
-
const homepage = await findHomepage(menu.value);
|
|
49
|
-
if (homepage) {
|
|
50
|
-
if (homepage.resourceId) {
|
|
51
|
-
// redirect to homepage
|
|
52
|
-
router.push({ name: 'resource-list', params: { resourceId: homepage.resourceId } });
|
|
53
|
-
} else {
|
|
54
|
-
// redirect to path
|
|
55
|
-
router.push(homepage.path);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
45
|
+
|
|
59
46
|
}
|
|
60
47
|
|
|
61
48
|
async function fetchRecord({ resourceId, primaryKey }) {
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
</BreadcrumbsWithButtons>
|
|
66
66
|
|
|
67
67
|
<!-- table -->
|
|
68
|
-
<div class="relative overflow-x-auto shadow-md sm:rounded-lg dark:
|
|
68
|
+
<div class="relative overflow-x-auto shadow-md dark:shadow-black sm:rounded-lg dark:s">
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
<!-- skelet loader -->
|
|
@@ -357,6 +357,9 @@ const totalRows = ref(0);
|
|
|
357
357
|
const allCheckedActions = ref([]);
|
|
358
358
|
const allowedActions = ref({});
|
|
359
359
|
|
|
360
|
+
onMounted(() => {
|
|
361
|
+
console.log('LISTVIEW mounted⌛⌛⌛⌛⌛⌛🔻🔻🔻');
|
|
362
|
+
});
|
|
360
363
|
|
|
361
364
|
const DEFAULT_PAGE_SIZE = 10;
|
|
362
365
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<div id="authentication-modal" tabindex="-1" class=" overflow-y-auto overflow-x-hidden z-50 min-w-[400px] justify-center items-center md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
|
12
12
|
<div class="relative p-4 w-full max-w-md max-h-full">
|
|
13
13
|
<!-- Modal content -->
|
|
14
|
-
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
|
14
|
+
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black" >
|
|
15
15
|
<!-- Modal header -->
|
|
16
16
|
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
|
|
17
17
|
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">
|