adminforth 1.1.50 → 1.1.52
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/dataConnectors/postgres.ts +1 -1
- package/dist/dataConnectors/postgres.js +1 -1
- package/dist/spa/index.html +4 -4
- package/dist/spa/package-lock.json +5 -206
- package/dist/spa/package.json +2 -8
- package/dist/spa/public/favicon.ico +0 -0
- package/dist/spa/spa/public/favicon.ico +0 -0
- package/dist/spa/spa/src/App.vue +2 -2
- package/dist/spa/spa/src/views/HomeView.vue +8 -0
- package/dist/spa/spa/src/views/ResourceParent.vue +2 -1
- package/dist/spa/src/App.vue +72 -171
- package/dist/spa/src/assets/logo.svg +1 -19
- package/dist/spa/src/components/AcceptModal.vue +10 -3
- package/dist/spa/src/components/BreadcrumbsWithButtons.vue +1 -1
- package/dist/spa/src/components/CustomDatePicker.vue +6 -19
- package/dist/spa/src/components/CustomDateRangePicker.vue +5 -14
- package/dist/spa/src/components/Dropdown.vue +5 -19
- package/dist/spa/src/components/Filters.vue +37 -91
- package/dist/spa/src/components/MenuLink.vue +2 -2
- package/dist/spa/src/components/ResourceForm.vue +103 -163
- package/dist/spa/src/components/ValueRenderer.vue +6 -22
- package/dist/spa/src/main.ts +1 -1
- package/dist/spa/src/router/index.ts +19 -30
- package/dist/spa/src/stores/core.ts +59 -65
- package/dist/spa/src/stores/modal.ts +3 -13
- package/dist/spa/src/utils.ts +8 -65
- package/dist/spa/src/views/CreateView.vue +16 -76
- package/dist/spa/src/views/EditView.vue +14 -76
- package/dist/spa/src/views/HomeView.vue +8 -0
- package/dist/spa/src/views/ListView.vue +360 -88
- package/dist/spa/src/views/LoginView.vue +5 -17
- package/dist/spa/src/views/ResourceParent.vue +1 -1
- package/dist/spa/src/views/ShowView.vue +19 -114
- package/dist/spa/tailwind.config.js +2 -5
- package/dist/spa/vite.config.ts +0 -6
- package/dist/types.js +30 -0
- package/package.json +1 -1
- package/spa/src/App.vue +2 -2
- package/spa/src/views/ResourceParent.vue +2 -1
- package/dist/spa/public/assets/favicon.png +0 -0
- package/dist/spa/src/components/CustomRangePicker.vue +0 -148
- package/dist/spa/src/components/ResourceListTable.vue +0 -419
- package/dist/spa/src/components/Toast.vue +0 -66
- package/dist/spa/src/composables/useFrontendApi.ts +0 -26
- package/dist/spa/src/composables/useStores.ts +0 -113
- package/dist/spa/src/spa_types/core.ts +0 -51
- package/dist/spa/src/stores/filters.ts +0 -22
- package/dist/spa/src/stores/toast.ts +0 -15
- package/dist/spa/src/stores/user.ts +0 -54
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
<!-- drawer component -->
|
|
3
3
|
<div id="drawer-navigation"
|
|
4
4
|
|
|
5
|
-
class="fixed
|
|
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"
|
|
6
6
|
|
|
7
7
|
:class="show ? 'top-0 transform-none' : ''"
|
|
8
8
|
tabindex="-1" aria-labelledby="drawer-navigation-label"
|
|
9
|
-
:style="{ height: `calc(100vh ` }"
|
|
9
|
+
:style="{ height: `calc(100vh - 3.5rem)` }"
|
|
10
10
|
>
|
|
11
11
|
<h5 id="drawer-navigation-label" class="text-base font-semibold text-gray-500 uppercase dark:text-gray-400">
|
|
12
12
|
Filters
|
|
@@ -22,17 +22,11 @@
|
|
|
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.
|
|
27
|
-
:options="columnOptions[c.name] || []"
|
|
28
|
-
@update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
|
|
29
|
-
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
30
|
-
/>
|
|
31
|
-
<Dropdown
|
|
32
|
-
v-else-if="c.type === 'boolean'"
|
|
25
|
+
<Dropdown
|
|
26
|
+
v-if="c.type === 'boolean'"
|
|
33
27
|
:options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
|
|
34
28
|
@update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
|
|
35
|
-
:modelValue="
|
|
29
|
+
:modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
36
30
|
/>
|
|
37
31
|
|
|
38
32
|
<Dropdown
|
|
@@ -40,7 +34,7 @@
|
|
|
40
34
|
:options="c.enum"
|
|
41
35
|
:allowCustom="c.allowCustom"
|
|
42
36
|
@update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
|
|
43
|
-
:modelValue="
|
|
37
|
+
:modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
44
38
|
/>
|
|
45
39
|
|
|
46
40
|
<input
|
|
@@ -54,9 +48,9 @@
|
|
|
54
48
|
<CustomDateRangePicker
|
|
55
49
|
v-else-if="['datetime'].includes(c.type)"
|
|
56
50
|
:column="c"
|
|
57
|
-
:valueStart="
|
|
51
|
+
:valueStart="filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
|
|
58
52
|
@update:valueStart="setFilterItem({ column: c, operator: 'gte', value: $event || undefined })"
|
|
59
|
-
:valueEnd="
|
|
53
|
+
:valueEnd="filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
|
|
60
54
|
@update:valueEnd="setFilterItem({ column: c, operator: 'lte', value: $event || undefined })"
|
|
61
55
|
/>
|
|
62
56
|
|
|
@@ -68,32 +62,22 @@
|
|
|
68
62
|
:value="getFilterItem({ column: c, operator: 'ilike' })"
|
|
69
63
|
>
|
|
70
64
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
:value="getFilterItem({ column: c, operator: 'gte' })"
|
|
88
|
-
>
|
|
89
|
-
<input
|
|
90
|
-
type="number" aria-describedby="helper-text-explanation"
|
|
91
|
-
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"
|
|
92
|
-
placeholder="To"
|
|
93
|
-
@input="setFilterItem({ column: c, operator: 'lte', value: $event.target.value || undefined})"
|
|
94
|
-
:value="getFilterItem({ column: c, operator: 'lte' })"
|
|
95
|
-
>
|
|
96
|
-
</div>
|
|
65
|
+
<div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
|
|
66
|
+
<input
|
|
67
|
+
type="number" aria-describedby="helper-text-explanation"
|
|
68
|
+
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"
|
|
69
|
+
placeholder="From"
|
|
70
|
+
@input="setFilterItem({ column: c, operator: 'gte', value: $event.target.value || undefined })"
|
|
71
|
+
:value="getFilterItem({ column: c, operator: 'gte' })"
|
|
72
|
+
>
|
|
73
|
+
<input
|
|
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>
|
|
97
81
|
|
|
98
82
|
</li>
|
|
99
83
|
</ul>
|
|
@@ -101,71 +85,33 @@
|
|
|
101
85
|
|
|
102
86
|
<div class="flex justify-end gap-2">
|
|
103
87
|
<button
|
|
104
|
-
:disabled="!
|
|
88
|
+
:disabled="!filters.length"
|
|
105
89
|
type="button"
|
|
106
|
-
class="flex items-center py-1 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"
|
|
90
|
+
class="flex items-center py-1 px-3 mb-2 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"
|
|
107
91
|
@click="clear">Clear all</button>
|
|
108
92
|
|
|
109
93
|
</div>
|
|
110
94
|
</div>
|
|
111
95
|
|
|
112
|
-
<div v-if="show"
|
|
96
|
+
<div v-if="show" drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"
|
|
113
97
|
@click="$emit('hide')">
|
|
114
98
|
</div>
|
|
115
99
|
</template>
|
|
116
100
|
|
|
117
101
|
<script setup>
|
|
118
|
-
import { watch, computed
|
|
102
|
+
import { watch, computed } from 'vue'
|
|
119
103
|
import Dropdown from '@/components/Dropdown.vue';
|
|
120
104
|
import CustomDateRangePicker from '@/components/CustomDateRangePicker.vue';
|
|
121
|
-
import { callAdminForthApi } from '@/utils';
|
|
122
|
-
import { useRouter } from 'vue-router';
|
|
123
|
-
import { computedAsync } from '@vueuse/core'
|
|
124
|
-
import CustomRangePicker from "@/components/CustomRangePicker.vue";
|
|
125
|
-
import { useFiltersStore } from '@/stores/filters';
|
|
126
|
-
|
|
127
|
-
const filtersStore = useFiltersStore();
|
|
128
|
-
|
|
129
105
|
|
|
130
106
|
// props: columns
|
|
131
107
|
// add support for v-model:filers
|
|
132
108
|
const props = defineProps(['columns', 'filters', 'show', 'columnsMinMax']);
|
|
133
109
|
const emits = defineEmits(['update:filters', 'hide']);
|
|
134
110
|
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const columnsWithFilter = computed(
|
|
111
|
+
const columnsWithFilter = computed(
|
|
139
112
|
() => props.columns?.filter(column => column.showIn.includes('filter')) || []
|
|
140
113
|
);
|
|
141
114
|
|
|
142
|
-
const columnOptions = computedAsync(async () => {
|
|
143
|
-
const ret = {};
|
|
144
|
-
if (!props.columns) {
|
|
145
|
-
return ret;
|
|
146
|
-
}
|
|
147
|
-
await Promise.all(
|
|
148
|
-
Object.values(props.columns).map(async (column) => {
|
|
149
|
-
if (column.foreignResource) {
|
|
150
|
-
const list = await callAdminForthApi({
|
|
151
|
-
method: 'POST',
|
|
152
|
-
path: `/get_resource_foreign_data`,
|
|
153
|
-
body: {
|
|
154
|
-
resourceId: router.currentRoute.value.params.resourceId,
|
|
155
|
-
column: column.name,
|
|
156
|
-
limit: 1000,
|
|
157
|
-
offset: 0,
|
|
158
|
-
},
|
|
159
|
-
});
|
|
160
|
-
ret[column.name] = list.items;
|
|
161
|
-
}
|
|
162
|
-
})
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
return ret;
|
|
166
|
-
}, {});
|
|
167
|
-
|
|
168
|
-
|
|
169
115
|
// sync 'body' class 'overflow-hidden' with show prop show
|
|
170
116
|
watch(() => props.show, (show) => {
|
|
171
117
|
if (show) {
|
|
@@ -184,28 +130,28 @@ watch(() => props.show, (show) => {
|
|
|
184
130
|
|
|
185
131
|
function setFilterItem({ column, operator, value }) {
|
|
186
132
|
|
|
187
|
-
const index =
|
|
133
|
+
const index = props.filters.findIndex(f => f.field === column.name && f.operator === operator);
|
|
188
134
|
if (value === undefined) {
|
|
189
135
|
if (index !== -1) {
|
|
190
|
-
|
|
136
|
+
props.filters.splice(index, 1);
|
|
191
137
|
}
|
|
192
|
-
emits('update:filters', [...
|
|
138
|
+
emits('update:filters', [...props.filters]);
|
|
193
139
|
return;
|
|
194
140
|
} else {
|
|
195
141
|
if (index === -1) {
|
|
196
|
-
|
|
142
|
+
props.filters.push({ field: column.name, value, operator });
|
|
197
143
|
} else {
|
|
198
|
-
|
|
144
|
+
props.filters[index].value = value;
|
|
199
145
|
}
|
|
200
146
|
}
|
|
201
|
-
emits('update:filters', [...
|
|
147
|
+
emits('update:filters', [...props.filters]);
|
|
202
148
|
}
|
|
203
149
|
|
|
204
150
|
function getFilterItem({ column, operator }) {
|
|
205
|
-
return
|
|
151
|
+
return props.filters.find(f => f.field === column.name && f.operator === operator)?.value || '';
|
|
206
152
|
}
|
|
207
153
|
|
|
208
154
|
async function clear() {
|
|
209
|
-
|
|
155
|
+
emits('update:filters', []);
|
|
210
156
|
}
|
|
211
157
|
</script>
|
|
@@ -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-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white" role="menuitem"
|
|
5
5
|
:class="{
|
|
6
6
|
'px-4': isChild,
|
|
7
7
|
'px-2': !isChild,
|
|
8
|
-
'bg-
|
|
8
|
+
'bg-blue-100 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
|
}"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div>
|
|
3
3
|
|
|
4
4
|
<div
|
|
5
|
-
class="relative shadow-
|
|
5
|
+
class="relative shadow-md sm:rounded-lg dark:shadow-2xl"
|
|
6
6
|
>
|
|
7
7
|
<form autocomplete="off" @submit.prevent>
|
|
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-
|
|
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-gray-50 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,114 +18,97 @@
|
|
|
18
18
|
</thead>
|
|
19
19
|
<tbody>
|
|
20
20
|
<tr v-for="column, i in editableColumns" :key="column.name"
|
|
21
|
-
class="bg-
|
|
21
|
+
class="odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 border-b dark:border-gray-700"
|
|
22
22
|
>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
:value="currentValues[column.name]"
|
|
29
|
-
@update:value="setCurrentValue(column.name, $event)"
|
|
23
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
24
|
+
{{ column.label }}
|
|
25
|
+
<span :data-tooltip-target="`tooltip-show-${i}`" class="relative inline-block">
|
|
26
|
+
<IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
|
|
27
|
+
:class="(columnError(column) && validating) ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'"
|
|
30
28
|
/>
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
<button
|
|
116
|
-
v-if="column.masked"
|
|
117
|
-
type="button"
|
|
118
|
-
@click="unmasked[column.name] = !unmasked[column.name]"
|
|
119
|
-
class="h-6 absolute inset-y-2 top-6 right-6 flex items-center pr-2 z-index-100 focus:outline-none"
|
|
120
|
-
>
|
|
121
|
-
<IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]" />
|
|
122
|
-
<IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
|
|
123
|
-
</button>
|
|
124
|
-
<div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
|
|
125
|
-
|
|
126
|
-
<div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
|
|
127
|
-
</td>
|
|
128
|
-
</template>
|
|
29
|
+
</span>
|
|
30
|
+
<div :id="`tooltip-show-${i}`"
|
|
31
|
+
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">
|
|
32
|
+
Required field
|
|
33
|
+
<div class="tooltip-arrow" data-popper-arrow></div>
|
|
34
|
+
</div>
|
|
35
|
+
</td>
|
|
36
|
+
<td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap relative">
|
|
37
|
+
<Dropdown
|
|
38
|
+
single
|
|
39
|
+
v-if="column.enum"
|
|
40
|
+
:options="column.enum"
|
|
41
|
+
:modelValue="currentValues[column.name]"
|
|
42
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
43
|
+
/>
|
|
44
|
+
<Dropdown
|
|
45
|
+
single
|
|
46
|
+
v-else-if="column.type === 'boolean'"
|
|
47
|
+
:options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
|
|
48
|
+
:modelValue="currentValues[column.name]"
|
|
49
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
50
|
+
/>
|
|
51
|
+
<input
|
|
52
|
+
v-else-if="['integer'].includes(column.type)"
|
|
53
|
+
type="number"
|
|
54
|
+
step="1"
|
|
55
|
+
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"
|
|
56
|
+
placeholder="0"
|
|
57
|
+
:value="currentValues[column.name]"
|
|
58
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
59
|
+
>
|
|
60
|
+
<CustomDatePicker
|
|
61
|
+
v-else-if="['datetime'].includes(column.type)"
|
|
62
|
+
:column="column"
|
|
63
|
+
:valueStart="currentValues[column.name]"
|
|
64
|
+
@update:valueStart="setCurrentValue(column.name, $event)"
|
|
65
|
+
/>
|
|
66
|
+
<input
|
|
67
|
+
v-else-if="['decimal', 'float'].includes(column.type)"
|
|
68
|
+
type="number"
|
|
69
|
+
step="0.1"
|
|
70
|
+
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"
|
|
71
|
+
placeholder="0.0"
|
|
72
|
+
:value="currentValues[column.name]"
|
|
73
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
74
|
+
/>
|
|
75
|
+
<textarea
|
|
76
|
+
v-else-if="['text'].includes(column.type)"
|
|
77
|
+
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"
|
|
78
|
+
placeholder="Text"
|
|
79
|
+
:value="currentValues[column.name]"
|
|
80
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
81
|
+
>
|
|
82
|
+
</textarea>
|
|
83
|
+
<input
|
|
84
|
+
v-else
|
|
85
|
+
:type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
|
|
86
|
+
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"
|
|
87
|
+
placeholder="Text"
|
|
88
|
+
:value="currentValues[column.name]"
|
|
89
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
90
|
+
autocomplete="false"
|
|
91
|
+
data-lpignore="true"
|
|
92
|
+
readonly
|
|
93
|
+
onfocus="this.removeAttribute('readonly');"
|
|
94
|
+
>
|
|
95
|
+
|
|
96
|
+
<button
|
|
97
|
+
v-if="column.masked"
|
|
98
|
+
type="button"
|
|
99
|
+
@click="unmasked[column.name] = !unmasked[column.name]"
|
|
100
|
+
class="h-6 absolute inset-y-2 top-6 right-6 flex items-center pr-2 z-index-100 focus:outline-none"
|
|
101
|
+
>
|
|
102
|
+
<IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]" />
|
|
103
|
+
<IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
|
|
104
|
+
</button>
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
<div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
|
|
109
|
+
|
|
110
|
+
<div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
|
|
111
|
+
</td>
|
|
129
112
|
</tr>
|
|
130
113
|
|
|
131
114
|
</tbody>
|
|
@@ -138,24 +121,19 @@
|
|
|
138
121
|
|
|
139
122
|
<script setup>
|
|
140
123
|
|
|
141
|
-
import
|
|
142
|
-
import Dropdown from '@/components/Dropdown.vue';
|
|
124
|
+
import { ref, computed, onMounted, watch } from 'vue';
|
|
143
125
|
import { useCoreStore } from '@/stores/core';
|
|
144
|
-
import
|
|
145
|
-
import { IconExclamationCircleSolid
|
|
146
|
-
import {
|
|
147
|
-
import {
|
|
148
|
-
import
|
|
149
|
-
import { useRouter } from 'vue-router';
|
|
150
|
-
|
|
151
|
-
const router = useRouter();
|
|
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
|
+
import CustomDatePicker from "@/components/CustomDatePicker.vue";
|
|
152
131
|
|
|
153
132
|
const props = defineProps({
|
|
154
133
|
loading: Boolean,
|
|
155
|
-
|
|
134
|
+
resourceColumns: Object,
|
|
156
135
|
record: Object,
|
|
157
136
|
validating: Boolean,
|
|
158
|
-
customComponentsPerColumn: Object,
|
|
159
137
|
});
|
|
160
138
|
|
|
161
139
|
|
|
@@ -167,7 +145,6 @@ const emit = defineEmits(['update:record', 'update:isValid']);
|
|
|
167
145
|
|
|
168
146
|
const currentValues = ref({});
|
|
169
147
|
|
|
170
|
-
|
|
171
148
|
const columnError = (column) => {
|
|
172
149
|
const val = computed(() => {
|
|
173
150
|
if ( column.required[mode.value] && (currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '') ) {
|
|
@@ -189,28 +166,14 @@ const columnError = (column) => {
|
|
|
189
166
|
return `This field must be less than ${column.maxValue}`;
|
|
190
167
|
}
|
|
191
168
|
}
|
|
192
|
-
if ( column.validation && column.validation.length ){
|
|
193
|
-
const validationArray = column.validation;
|
|
194
|
-
for (let i = 0; i < validationArray.length; i++) {
|
|
195
|
-
if (validationArray[i].regExp) {
|
|
196
|
-
const regExp = new RegExp(validationArray[i].regExp);
|
|
197
|
-
let value = currentValues.value[column.name];
|
|
198
|
-
if (value === undefined || value === null) {
|
|
199
|
-
value = '';
|
|
200
|
-
}
|
|
201
|
-
if (!regExp.test(value)) {
|
|
202
|
-
return validationArray[i].message;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
}
|
|
208
169
|
return null;
|
|
209
170
|
});
|
|
210
171
|
return val.value;
|
|
211
172
|
};
|
|
212
173
|
|
|
213
174
|
|
|
175
|
+
|
|
176
|
+
|
|
214
177
|
const setCurrentValue = (key, value) => {
|
|
215
178
|
currentValues.value[key] = value;
|
|
216
179
|
|
|
@@ -222,36 +185,13 @@ onMounted(() => {
|
|
|
222
185
|
currentValues.value[key] = props.record[key];
|
|
223
186
|
});
|
|
224
187
|
initFlowbite();
|
|
225
|
-
|
|
226
|
-
|
|
227
188
|
});
|
|
228
189
|
|
|
229
|
-
const columnOptions = computedAsync(async () => {
|
|
230
|
-
return (await Promise.all(
|
|
231
|
-
Object.values(props.resource.columns).map(async (column) => {
|
|
232
|
-
if (column.foreignResource) {
|
|
233
|
-
const list = await callAdminForthApi({
|
|
234
|
-
method: 'POST',
|
|
235
|
-
path: `/get_resource_foreign_data`,
|
|
236
|
-
body: {
|
|
237
|
-
resourceId: router.currentRoute.value.params.resourceId,
|
|
238
|
-
column: column.name,
|
|
239
|
-
limit: 1000,
|
|
240
|
-
offset: 0,
|
|
241
|
-
},
|
|
242
|
-
});
|
|
243
|
-
return { [column.name]: list.items };
|
|
244
|
-
}
|
|
245
|
-
})
|
|
246
|
-
)).reduce((acc, val) => Object.assign(acc, val), {})
|
|
247
|
-
|
|
248
|
-
}, {});
|
|
249
|
-
|
|
250
190
|
const coreStore = useCoreStore();
|
|
251
191
|
|
|
252
192
|
const editableColumns = computed(() => {
|
|
253
193
|
const mode = props.record ? 'edit' : 'create';
|
|
254
|
-
return props.
|
|
194
|
+
return props.resourceColumns?.filter(column => column.showIn.includes(mode));
|
|
255
195
|
});
|
|
256
196
|
|
|
257
197
|
const isValid = computed(() => {
|
|
@@ -1,29 +1,18 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<RouterLink v-if="record[column.name]" class="font-medium text-blue-600 dark:text-blue-500 hover:brightness-110"
|
|
6
|
-
:to="{ name: 'resource-show', params: { resourceId: column.foreignResource.resourceId, primaryKey: record[column.name].pk } }">
|
|
7
|
-
{{ record[column.name].label }}
|
|
8
|
-
</RouterLink>
|
|
9
|
-
<div v-else>
|
|
10
|
-
<span class="text-gray-400">-</span>
|
|
11
|
-
</div>
|
|
12
|
-
</span>
|
|
13
|
-
|
|
14
|
-
<span v-else-if="column.type === 'boolean'">
|
|
15
|
-
<span v-if="record[column.name]" class="bg-green-100 text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-green-400 border border-green-400">Yes</span>
|
|
3
|
+
<span v-if="column.type === 'boolean'">
|
|
4
|
+
<span v-if="row[column.name]" class="bg-green-100 text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-green-400 border border-green-400">Yes</span>
|
|
16
5
|
<span v-else class="bg-red-100 text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400">No</span>
|
|
17
6
|
</span>
|
|
18
7
|
<span v-else-if="column.enum">
|
|
19
|
-
{{
|
|
8
|
+
{{ column.enum.find(e => e.value === row[column.name])?.label }}
|
|
20
9
|
</span>
|
|
21
10
|
<span v-else-if="column.type === 'datetime'">
|
|
22
|
-
{{
|
|
11
|
+
{{ formatDate(row[column.name]) }}
|
|
23
12
|
|
|
24
13
|
</span>
|
|
25
14
|
<span v-else>
|
|
26
|
-
{{
|
|
15
|
+
{{ row[column.name] }}
|
|
27
16
|
</span>
|
|
28
17
|
</div>
|
|
29
18
|
</template>
|
|
@@ -34,22 +23,17 @@
|
|
|
34
23
|
import dayjs from 'dayjs';
|
|
35
24
|
import utc from 'dayjs/plugin/utc';
|
|
36
25
|
import timezone from 'dayjs/plugin/timezone';
|
|
37
|
-
import {checkEmptyValues} from '@/utils';
|
|
38
|
-
import { useRoute, useRouter } from 'vue-router';
|
|
39
|
-
|
|
40
26
|
|
|
41
27
|
import { useCoreStore } from '@/stores/core';
|
|
42
28
|
|
|
43
29
|
const coreStore = useCoreStore();
|
|
44
|
-
const route = useRoute();
|
|
45
|
-
|
|
46
30
|
|
|
47
31
|
dayjs.extend(utc);
|
|
48
32
|
dayjs.extend(timezone);
|
|
49
33
|
|
|
50
34
|
const props = defineProps({
|
|
51
35
|
column: Object,
|
|
52
|
-
|
|
36
|
+
row: Object
|
|
53
37
|
});
|
|
54
38
|
|
|
55
39
|
|