adminforth 1.1.31 → 1.1.33
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 +9 -0
- package/dist/modules/codeInjector.js +19 -7
- package/dist/modules/styles.js +2 -2
- package/dist/spa/index.html +4 -4
- package/dist/spa/package-lock.json +206 -5
- package/dist/spa/package.json +8 -2
- package/dist/spa/src/App.vue +171 -72
- package/dist/spa/src/assets/logo.svg +19 -1
- package/dist/spa/src/components/AcceptModal.vue +3 -10
- package/dist/spa/src/components/BreadcrumbsWithButtons.vue +1 -1
- package/dist/spa/src/components/CustomDatePicker.vue +19 -6
- package/dist/spa/src/components/CustomDateRangePicker.vue +14 -5
- package/dist/spa/src/components/Dropdown.vue +19 -5
- package/dist/spa/src/components/Filters.vue +91 -37
- package/dist/spa/src/components/MenuLink.vue +2 -2
- package/dist/spa/src/components/ResourceForm.vue +163 -103
- package/dist/spa/src/components/ValueRenderer.vue +22 -6
- package/dist/spa/src/main.ts +1 -1
- package/dist/spa/src/router/index.ts +30 -19
- package/dist/spa/src/stores/core.ts +65 -59
- package/dist/spa/src/stores/modal.ts +13 -3
- package/dist/spa/src/utils.ts +65 -8
- package/dist/spa/src/views/CreateView.vue +76 -16
- package/dist/spa/src/views/EditView.vue +76 -14
- package/dist/spa/src/views/ListView.vue +88 -360
- package/dist/spa/src/views/LoginView.vue +17 -5
- package/dist/spa/src/views/ResourceParent.vue +1 -1
- package/dist/spa/src/views/ShowView.vue +114 -19
- package/dist/spa/tailwind.config.js +5 -2
- package/dist/spa/vite.config.ts +6 -0
- package/index.ts +11 -0
- package/modules/codeInjector.ts +19 -1
- package/modules/styles.ts +2 -2
- package/package.json +1 -1
- package/spa/index.html +1 -1
- package/spa/src/App.vue +3 -2
- package/spa/src/components/ResourceForm.vue +1 -1
- package/types/AdminForthConfig.ts +15 -0
- package/dist/plugins/AccessControl/index.js +0 -13
- package/dist/plugins/AccessControl/types.js +0 -1
- package/dist/plugins/plugins/ForeignInlineListPlugin/custom/InlineList.vue +0 -248
- package/dist/spa/public/favicon.ico +0 -0
- package/dist/spa/spa/.eslintrc.cjs +0 -14
- package/dist/spa/spa/.vscode/extensions.json +0 -6
- package/dist/spa/spa/README.md +0 -39
- package/dist/spa/spa/env.d.ts +0 -1
- package/dist/spa/spa/index.html +0 -23
- package/dist/spa/spa/package-lock.json +0 -4359
- package/dist/spa/spa/package.json +0 -47
- package/dist/spa/spa/postcss.config.js +0 -6
- package/dist/spa/spa/public/favicon.ico +0 -0
- package/dist/spa/spa/src/App.vue +0 -270
- package/dist/spa/spa/src/assets/base.css +0 -0
- package/dist/spa/spa/src/assets/logo.svg +0 -19
- package/dist/spa/spa/src/components/AcceptModal.vue +0 -45
- package/dist/spa/spa/src/components/Breadcrumbs.vue +0 -40
- package/dist/spa/spa/src/components/BreadcrumbsWithButtons.vue +0 -26
- package/dist/spa/spa/src/components/CustomDatePicker.vue +0 -174
- package/dist/spa/spa/src/components/CustomDateRangePicker.vue +0 -218
- package/dist/spa/spa/src/components/Dropdown.vue +0 -168
- package/dist/spa/spa/src/components/Filters.vue +0 -211
- package/dist/spa/spa/src/components/HelloWorld.vue +0 -17
- package/dist/spa/spa/src/components/MenuLink.vue +0 -24
- package/dist/spa/spa/src/components/ResourceForm.vue +0 -265
- package/dist/spa/spa/src/components/ShowTableItem.vue +0 -14
- package/dist/spa/spa/src/components/SingleSkeletLoader.vue +0 -13
- package/dist/spa/spa/src/components/ValueRenderer.vue +0 -60
- package/dist/spa/spa/src/components/icons/IconCalendar.vue +0 -5
- package/dist/spa/spa/src/components/icons/IconCommunity.vue +0 -7
- package/dist/spa/spa/src/components/icons/IconDocumentation.vue +0 -7
- package/dist/spa/spa/src/components/icons/IconEcosystem.vue +0 -7
- package/dist/spa/spa/src/components/icons/IconSupport.vue +0 -7
- package/dist/spa/spa/src/components/icons/IconTime.vue +0 -5
- package/dist/spa/spa/src/components/icons/IconTooling.vue +0 -19
- package/dist/spa/spa/src/index.scss +0 -26
- package/dist/spa/spa/src/main.ts +0 -18
- package/dist/spa/spa/src/router/index.ts +0 -64
- package/dist/spa/spa/src/stores/core.ts +0 -144
- package/dist/spa/spa/src/stores/modal.ts +0 -48
- package/dist/spa/spa/src/utils.ts +0 -101
- package/dist/spa/spa/src/views/CreateView.vue +0 -163
- package/dist/spa/spa/src/views/EditView.vue +0 -165
- package/dist/spa/spa/src/views/HomeView.vue +0 -8
- package/dist/spa/spa/src/views/ListView.vue +0 -257
- package/dist/spa/spa/src/views/LoginView.vue +0 -134
- package/dist/spa/spa/src/views/ResourceParent.vue +0 -18
- package/dist/spa/spa/src/views/ShowView.vue +0 -188
- package/dist/spa/spa/tailwind.config.js +0 -15
- package/dist/spa/spa/tsconfig.app.json +0 -14
- package/dist/spa/spa/tsconfig.json +0 -11
- package/dist/spa/spa/tsconfig.node.json +0 -19
- package/dist/spa/spa/vite.config.ts +0 -49
- package/dist/spa/src/views/HomeView.vue +0 -8
- package/dist/types.js +0 -30
- /package/dist/spa/{spa/public → public/assets}/favicon.png +0 -0
- /package/dist/spa/{spa/src → src}/components/CustomRangePicker.vue +0 -0
- /package/dist/spa/{spa/src → src}/components/ResourceListTable.vue +0 -0
- /package/dist/spa/{spa/src → src}/components/Toast.vue +0 -0
- /package/dist/spa/{spa/src → src}/composables/useFrontendApi.ts +0 -0
- /package/dist/spa/{spa/src → src}/composables/useStores.ts +0 -0
- /package/dist/spa/{spa/src → src}/spa_types/core.ts +0 -0
- /package/dist/spa/{spa/src → src}/stores/filters.ts +0 -0
- /package/dist/spa/{spa/src → src}/stores/toast.ts +0 -0
- /package/dist/spa/{spa/src → src}/stores/user.ts +0 -0
- /package/spa/public/{favicon.png → assets/favicon.png} +0 -0
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
<!-- drawer component -->
|
|
3
3
|
<div id="drawer-navigation"
|
|
4
4
|
|
|
5
|
-
class="fixed
|
|
5
|
+
class="fixed right-0 z-50 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"
|
|
9
|
-
:style="{ height: `calc(100vh
|
|
9
|
+
:style="{ height: `calc(100vh ` }"
|
|
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,11 +22,17 @@
|
|
|
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="filtersStore.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
|
-
:modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
35
|
+
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
30
36
|
/>
|
|
31
37
|
|
|
32
38
|
<Dropdown
|
|
@@ -34,7 +40,7 @@
|
|
|
34
40
|
:options="c.enum"
|
|
35
41
|
:allowCustom="c.allowCustom"
|
|
36
42
|
@update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
|
|
37
|
-
:modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
43
|
+
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
38
44
|
/>
|
|
39
45
|
|
|
40
46
|
<input
|
|
@@ -48,9 +54,9 @@
|
|
|
48
54
|
<CustomDateRangePicker
|
|
49
55
|
v-else-if="['datetime'].includes(c.type)"
|
|
50
56
|
:column="c"
|
|
51
|
-
:valueStart="filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
|
|
57
|
+
:valueStart="filtersStore.filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
|
|
52
58
|
@update:valueStart="setFilterItem({ column: c, operator: 'gte', value: $event || undefined })"
|
|
53
|
-
:valueEnd="filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
|
|
59
|
+
:valueEnd="filtersStore.filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
|
|
54
60
|
@update:valueEnd="setFilterItem({ column: c, operator: 'lte', value: $event || undefined })"
|
|
55
61
|
/>
|
|
56
62
|
|
|
@@ -62,22 +68,32 @@
|
|
|
62
68
|
:value="getFilterItem({ column: c, operator: 'ilike' })"
|
|
63
69
|
>
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
71
|
+
<CustomRangePicker
|
|
72
|
+
v-else-if="['integer', 'decimal', 'float'].includes(c.type) && c.allowMinMaxQuery"
|
|
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
|
+
/>
|
|
80
|
+
|
|
81
|
+
<div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
|
|
82
|
+
<input
|
|
83
|
+
type="number" aria-describedby="helper-text-explanation"
|
|
84
|
+
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"
|
|
85
|
+
placeholder="From"
|
|
86
|
+
@input="setFilterItem({ column: c, operator: 'gte', value: $event.target.value || undefined })"
|
|
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>
|
|
81
97
|
|
|
82
98
|
</li>
|
|
83
99
|
</ul>
|
|
@@ -85,33 +101,71 @@
|
|
|
85
101
|
|
|
86
102
|
<div class="flex justify-end gap-2">
|
|
87
103
|
<button
|
|
88
|
-
:disabled="!filters.length"
|
|
104
|
+
:disabled="!filtersStore.filters.length"
|
|
89
105
|
type="button"
|
|
90
|
-
class="flex items-center py-1 px-3
|
|
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"
|
|
91
107
|
@click="clear">Clear all</button>
|
|
92
108
|
|
|
93
109
|
</div>
|
|
94
110
|
</div>
|
|
95
111
|
|
|
96
|
-
<div v-if="show"
|
|
112
|
+
<div v-if="show" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"
|
|
97
113
|
@click="$emit('hide')">
|
|
98
114
|
</div>
|
|
99
115
|
</template>
|
|
100
116
|
|
|
101
117
|
<script setup>
|
|
102
|
-
import { watch, computed } from 'vue'
|
|
118
|
+
import { watch, computed, ref, onMounted } from 'vue'
|
|
103
119
|
import Dropdown from '@/components/Dropdown.vue';
|
|
104
120
|
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
|
+
|
|
105
129
|
|
|
106
130
|
// props: columns
|
|
107
131
|
// add support for v-model:filers
|
|
108
132
|
const props = defineProps(['columns', 'filters', 'show', 'columnsMinMax']);
|
|
109
133
|
const emits = defineEmits(['update:filters', 'hide']);
|
|
110
134
|
|
|
111
|
-
const
|
|
135
|
+
const router = useRouter();
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
const columnsWithFilter = computed(
|
|
112
139
|
() => props.columns?.filter(column => column.showIn.includes('filter')) || []
|
|
113
140
|
);
|
|
114
141
|
|
|
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
|
+
|
|
115
169
|
// sync 'body' class 'overflow-hidden' with show prop show
|
|
116
170
|
watch(() => props.show, (show) => {
|
|
117
171
|
if (show) {
|
|
@@ -130,28 +184,28 @@ watch(() => props.show, (show) => {
|
|
|
130
184
|
|
|
131
185
|
function setFilterItem({ column, operator, value }) {
|
|
132
186
|
|
|
133
|
-
const index =
|
|
187
|
+
const index = filtersStore.filters.findIndex(f => f.field === column.name && f.operator === operator);
|
|
134
188
|
if (value === undefined) {
|
|
135
189
|
if (index !== -1) {
|
|
136
|
-
|
|
190
|
+
filtersStore.filters.splice(index, 1);
|
|
137
191
|
}
|
|
138
|
-
emits('update:filters', [...
|
|
192
|
+
emits('update:filters', [...filtersStore.filters]);
|
|
139
193
|
return;
|
|
140
194
|
} else {
|
|
141
195
|
if (index === -1) {
|
|
142
|
-
|
|
196
|
+
filtersStore.setFilter({ field: column.name, value, operator });
|
|
143
197
|
} else {
|
|
144
|
-
|
|
198
|
+
filtersStore.setFilters([...filtersStore.filters.slice(0, index), { field: column.name, value, operator }, ...filtersStore.filters.slice(index + 1)])
|
|
145
199
|
}
|
|
146
200
|
}
|
|
147
|
-
emits('update:filters', [...
|
|
201
|
+
emits('update:filters', [...filtersStore.filters]);
|
|
148
202
|
}
|
|
149
203
|
|
|
150
204
|
function getFilterItem({ column, operator }) {
|
|
151
|
-
return
|
|
205
|
+
return filtersStore.filters.find(f => f.field === column.name && f.operator === operator)?.value || '';
|
|
152
206
|
}
|
|
153
207
|
|
|
154
208
|
async function clear() {
|
|
155
|
-
|
|
209
|
+
filtersStore.clearFilters();
|
|
156
210
|
}
|
|
157
211
|
</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-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
|
}"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
2
|
+
<div class="rounded-default">
|
|
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 rounded-default"
|
|
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-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,114 @@
|
|
|
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 "> <!--align-top-->
|
|
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
|
+
<Dropdown
|
|
48
|
+
single
|
|
49
|
+
v-if="column.foreignResource"
|
|
50
|
+
:options="columnOptions[column.name] || []"
|
|
51
|
+
:placeholder = "columnOptions[column.name]?.length || 'There are no options available'"
|
|
52
|
+
:modelValue="currentValues[column.name]"
|
|
53
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
54
|
+
></Dropdown>
|
|
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
|
+
auto-hide
|
|
83
|
+
@update:valueStart="setCurrentValue(column.name, $event)"
|
|
84
|
+
/>
|
|
85
|
+
<input
|
|
86
|
+
v-else-if="['decimal', 'float'].includes(column.type)"
|
|
87
|
+
type="number"
|
|
88
|
+
step="0.1"
|
|
89
|
+
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"
|
|
90
|
+
placeholder="0.0"
|
|
91
|
+
:value="currentValues[column.name]"
|
|
92
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
93
|
+
/>
|
|
94
|
+
<textarea
|
|
95
|
+
v-else-if="['text'].includes(column.type)"
|
|
96
|
+
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"
|
|
97
|
+
placeholder="Text"
|
|
98
|
+
:value="currentValues[column.name]"
|
|
99
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
100
|
+
>
|
|
101
|
+
</textarea>
|
|
102
|
+
<input
|
|
103
|
+
v-else
|
|
104
|
+
:type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
|
|
105
|
+
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"
|
|
106
|
+
placeholder="Text"
|
|
107
|
+
:value="currentValues[column.name]"
|
|
108
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
109
|
+
autocomplete="false"
|
|
110
|
+
data-lpignore="true"
|
|
111
|
+
readonly
|
|
112
|
+
onfocus="this.removeAttribute('readonly');"
|
|
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>
|
|
112
129
|
</tr>
|
|
113
130
|
|
|
114
131
|
</tbody>
|
|
@@ -121,19 +138,24 @@
|
|
|
121
138
|
|
|
122
139
|
<script setup>
|
|
123
140
|
|
|
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
141
|
import CustomDatePicker from "@/components/CustomDatePicker.vue";
|
|
142
|
+
import Dropdown from '@/components/Dropdown.vue';
|
|
143
|
+
import { useCoreStore } from '@/stores/core';
|
|
144
|
+
import { callAdminForthApi } from '@/utils';
|
|
145
|
+
import { IconExclamationCircleSolid, IconEyeSlashSolid, IconEyeSolid } from '@iconify-prerendered/vue-flowbite';
|
|
146
|
+
import { computedAsync } from '@vueuse/core';
|
|
147
|
+
import { initFlowbite } from 'flowbite';
|
|
148
|
+
import { computed, onMounted, ref, watch } from 'vue';
|
|
149
|
+
import { useRouter } from 'vue-router';
|
|
150
|
+
|
|
151
|
+
const router = useRouter();
|
|
131
152
|
|
|
132
153
|
const props = defineProps({
|
|
133
154
|
loading: Boolean,
|
|
134
|
-
|
|
155
|
+
resource: Object,
|
|
135
156
|
record: Object,
|
|
136
157
|
validating: Boolean,
|
|
158
|
+
customComponentsPerColumn: Object,
|
|
137
159
|
});
|
|
138
160
|
|
|
139
161
|
|
|
@@ -145,6 +167,7 @@ const emit = defineEmits(['update:record', 'update:isValid']);
|
|
|
145
167
|
|
|
146
168
|
const currentValues = ref({});
|
|
147
169
|
|
|
170
|
+
|
|
148
171
|
const columnError = (column) => {
|
|
149
172
|
const val = computed(() => {
|
|
150
173
|
if ( column.required[mode.value] && (currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '') ) {
|
|
@@ -166,14 +189,28 @@ const columnError = (column) => {
|
|
|
166
189
|
return `This field must be less than ${column.maxValue}`;
|
|
167
190
|
}
|
|
168
191
|
}
|
|
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
|
+
}
|
|
169
208
|
return null;
|
|
170
209
|
});
|
|
171
210
|
return val.value;
|
|
172
211
|
};
|
|
173
212
|
|
|
174
213
|
|
|
175
|
-
|
|
176
|
-
|
|
177
214
|
const setCurrentValue = (key, value) => {
|
|
178
215
|
currentValues.value[key] = value;
|
|
179
216
|
|
|
@@ -185,13 +222,36 @@ onMounted(() => {
|
|
|
185
222
|
currentValues.value[key] = props.record[key];
|
|
186
223
|
});
|
|
187
224
|
initFlowbite();
|
|
225
|
+
|
|
226
|
+
|
|
188
227
|
});
|
|
189
228
|
|
|
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
|
+
|
|
190
250
|
const coreStore = useCoreStore();
|
|
191
251
|
|
|
192
252
|
const editableColumns = computed(() => {
|
|
193
253
|
const mode = props.record ? 'edit' : 'create';
|
|
194
|
-
return props.
|
|
254
|
+
return props.resource?.columns?.filter(column => column.showIn.includes(mode));
|
|
195
255
|
});
|
|
196
256
|
|
|
197
257
|
const isValid = computed(() => {
|
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
|
|
4
|
+
<span v-if="column.foreignResource">
|
|
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>
|
|
5
16
|
<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>
|
|
6
17
|
</span>
|
|
7
18
|
<span v-else-if="column.enum">
|
|
8
|
-
{{ column.enum.find(e => e.value ===
|
|
19
|
+
{{ checkEmptyValues(column.enum.find(e => e.value === record[column.name])?.label,route.meta.type) }}
|
|
9
20
|
</span>
|
|
10
21
|
<span v-else-if="column.type === 'datetime'">
|
|
11
|
-
{{ formatDate(
|
|
22
|
+
{{ checkEmptyValues(formatDate(record[column.name]),route.meta.type) }}
|
|
12
23
|
|
|
13
24
|
</span>
|
|
14
25
|
<span v-else>
|
|
15
|
-
{{
|
|
26
|
+
{{ checkEmptyValues(record[column.name],route.meta.type) }}
|
|
16
27
|
</span>
|
|
17
28
|
</div>
|
|
18
29
|
</template>
|
|
@@ -23,17 +34,22 @@
|
|
|
23
34
|
import dayjs from 'dayjs';
|
|
24
35
|
import utc from 'dayjs/plugin/utc';
|
|
25
36
|
import timezone from 'dayjs/plugin/timezone';
|
|
37
|
+
import {checkEmptyValues} from '@/utils';
|
|
38
|
+
import { useRoute, useRouter } from 'vue-router';
|
|
39
|
+
|
|
26
40
|
|
|
27
41
|
import { useCoreStore } from '@/stores/core';
|
|
28
42
|
|
|
29
43
|
const coreStore = useCoreStore();
|
|
44
|
+
const route = useRoute();
|
|
45
|
+
|
|
30
46
|
|
|
31
47
|
dayjs.extend(utc);
|
|
32
48
|
dayjs.extend(timezone);
|
|
33
49
|
|
|
34
50
|
const props = defineProps({
|
|
35
51
|
column: Object,
|
|
36
|
-
|
|
52
|
+
record: Object
|
|
37
53
|
});
|
|
38
54
|
|
|
39
55
|
|