adminforth 1.2.86 → 1.2.88
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/restApi.js +9 -0
- package/dist/spa/.vscode/extensions.json +6 -0
- package/dist/spa/index.html +4 -4
- package/dist/spa/package-lock.json +6 -421
- package/dist/spa/package.json +2 -10
- package/dist/spa/public/favicon.ico +0 -0
- package/dist/spa/spa/.eslintrc.cjs +14 -0
- package/dist/spa/spa/.vscode/extensions.json +6 -0
- package/dist/spa/spa/README.md +39 -0
- package/dist/spa/spa/env.d.ts +1 -0
- package/dist/spa/spa/index.html +23 -0
- package/dist/spa/spa/package-lock.json +4573 -0
- package/dist/spa/spa/package.json +49 -0
- package/dist/spa/spa/postcss.config.js +6 -0
- package/dist/spa/spa/public/favicon.ico +0 -0
- package/dist/spa/spa/src/App.vue +314 -0
- package/dist/spa/spa/src/assets/base.css +0 -0
- package/dist/spa/spa/src/assets/logo.svg +19 -0
- package/dist/spa/spa/src/components/AcceptModal.vue +45 -0
- package/dist/spa/spa/src/components/Breadcrumbs.vue +40 -0
- package/dist/spa/spa/src/components/BreadcrumbsWithButtons.vue +26 -0
- package/dist/spa/spa/src/components/CustomDatePicker.vue +174 -0
- package/dist/spa/spa/src/components/CustomDateRangePicker.vue +218 -0
- package/dist/spa/spa/src/components/Dropdown.vue +168 -0
- package/dist/spa/spa/src/components/Filters.vue +222 -0
- package/dist/spa/spa/src/components/HelloWorld.vue +17 -0
- package/dist/spa/spa/src/components/MenuLink.vue +24 -0
- package/dist/spa/spa/src/components/ResourceForm.vue +289 -0
- package/dist/spa/{src → spa/src}/components/ResourceListTable.vue +3 -6
- package/dist/spa/spa/src/components/SingleSkeletLoader.vue +13 -0
- package/dist/spa/{src → spa/src}/components/Toast.vue +3 -9
- package/dist/spa/spa/src/components/ValueRenderer.vue +66 -0
- package/dist/spa/spa/src/components/icons/IconCalendar.vue +5 -0
- package/dist/spa/spa/src/components/icons/IconCommunity.vue +7 -0
- package/dist/spa/spa/src/components/icons/IconDocumentation.vue +7 -0
- package/dist/spa/spa/src/components/icons/IconEcosystem.vue +7 -0
- package/dist/spa/spa/src/components/icons/IconSupport.vue +7 -0
- package/dist/spa/spa/src/components/icons/IconTime.vue +5 -0
- package/dist/spa/spa/src/components/icons/IconTooling.vue +19 -0
- package/dist/spa/spa/src/index.scss +27 -0
- package/dist/spa/spa/src/main.ts +18 -0
- package/dist/spa/spa/src/router/index.ts +63 -0
- package/dist/spa/spa/src/stores/core.ts +144 -0
- package/dist/spa/spa/src/stores/modal.ts +48 -0
- package/dist/spa/spa/src/utils.ts +103 -0
- package/dist/spa/spa/src/views/CreateView.vue +156 -0
- package/dist/spa/spa/src/views/EditView.vue +157 -0
- package/dist/spa/spa/src/views/HomeView.vue +8 -0
- package/dist/spa/spa/src/views/ListView.vue +258 -0
- package/dist/spa/spa/src/views/LoginView.vue +143 -0
- package/dist/spa/spa/src/views/ResourceParent.vue +17 -0
- package/dist/spa/spa/src/views/ShowView.vue +184 -0
- package/dist/spa/spa/tailwind.config.js +17 -0
- package/dist/spa/spa/tsconfig.app.json +14 -0
- package/dist/spa/spa/tsconfig.json +11 -0
- package/dist/spa/spa/tsconfig.node.json +19 -0
- package/dist/spa/spa/vite.config.ts +56 -0
- package/dist/spa/src/App.vue +94 -236
- package/dist/spa/src/assets/base.css +0 -2
- package/dist/spa/src/assets/logo.svg +1 -19
- package/dist/spa/src/components/AcceptModal.vue +10 -3
- package/dist/spa/src/components/Breadcrumbs.vue +2 -3
- package/dist/spa/src/components/BreadcrumbsWithButtons.vue +1 -1
- package/dist/spa/src/components/CustomDatePicker.vue +10 -25
- 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 +38 -103
- package/dist/spa/src/components/MenuLink.vue +3 -3
- package/dist/spa/src/components/ResourceForm.vue +116 -205
- package/dist/spa/src/components/ValueRenderer.vue +8 -30
- package/dist/spa/src/index.scss +1 -2
- package/dist/spa/src/main.ts +2 -2
- package/dist/spa/src/router/index.ts +19 -29
- package/dist/spa/src/stores/core.ts +59 -65
- package/dist/spa/src/stores/modal.ts +3 -13
- package/dist/spa/src/utils.ts +10 -69
- package/dist/spa/src/views/CreateView.vue +16 -69
- package/dist/spa/src/views/EditView.vue +14 -68
- package/dist/spa/src/views/HomeView.vue +8 -0
- package/dist/spa/src/views/ListView.vue +371 -100
- package/dist/spa/src/views/LoginView.vue +29 -67
- package/dist/spa/src/views/ResourceParent.vue +2 -1
- package/dist/spa/src/views/ShowView.vue +25 -116
- package/dist/spa/tailwind.config.js +3 -8
- package/dist/spa/vite.config.ts +0 -13
- package/dist/types.js +30 -0
- package/modules/restApi.ts +14 -1
- package/package.json +1 -1
- package/spa/src/components/MenuLink.vue +3 -0
- package/types/AdminForthConfig.ts +6 -0
- package/dist/spa/src/types/AdminForthConfig.ts +0 -1477
- package/dist/spa/src/types/FrontendAPI.ts +0 -121
- /package/dist/spa/{public → spa/public}/assets/favicon.png +0 -0
- /package/dist/spa/{src → spa/src}/components/CustomRangePicker.vue +0 -0
- /package/dist/spa/{src → spa/src}/components/SkeleteLoader.vue +0 -0
- /package/dist/spa/{src → spa/src}/composables/useFrontendApi.ts +0 -0
- /package/dist/spa/{src → spa/src}/composables/useStores.ts +0 -0
- /package/dist/spa/{src → spa/src}/spa_types/core.ts +0 -0
- /package/dist/spa/{src → spa/src}/stores/filters.ts +0 -0
- /package/dist/spa/{src → spa/src}/stores/toast.ts +0 -0
- /package/dist/spa/{src → spa/src}/stores/user.ts +0 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- drawer component -->
|
|
3
|
+
<div id="drawer-navigation"
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
:class="show ? 'top-0 transform-none' : ''"
|
|
8
|
+
tabindex="-1" aria-labelledby="drawer-navigation-label"
|
|
9
|
+
:style="{ height: `calc(100vh ` }"
|
|
10
|
+
>
|
|
11
|
+
<h5 id="drawer-navigation-label" class="text-base font-semibold text-gray-500 uppercase dark:text-gray-400">
|
|
12
|
+
Filters
|
|
13
|
+
|
|
14
|
+
<button type="button" @click="$emit('hide')" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 absolute end-2.5 inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white" >
|
|
15
|
+
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
|
|
16
|
+
<span class="sr-only">Close menu</span>
|
|
17
|
+
</button>
|
|
18
|
+
</h5>
|
|
19
|
+
|
|
20
|
+
<div class="py-4 ">
|
|
21
|
+
<ul class="space-y-3 font-medium">
|
|
22
|
+
<li v-for="c in columnsWithFilter" :key="c">
|
|
23
|
+
<p class="dark:text-gray-400">{{ c.label }}</p>
|
|
24
|
+
|
|
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'"
|
|
33
|
+
:options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
|
|
34
|
+
@update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
|
|
35
|
+
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
36
|
+
/>
|
|
37
|
+
|
|
38
|
+
<Dropdown
|
|
39
|
+
v-else-if="c.enum"
|
|
40
|
+
:options="c.enum"
|
|
41
|
+
:allowCustom="c.allowCustom"
|
|
42
|
+
@update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
|
|
43
|
+
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
44
|
+
/>
|
|
45
|
+
|
|
46
|
+
<input
|
|
47
|
+
v-else-if="[ 'string', 'text' ].includes(c.type)"
|
|
48
|
+
type="text" class="w-full py-1 px-2 border border-gray-300 rounded-md 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"
|
|
49
|
+
placeholder="Search"
|
|
50
|
+
@input="setFilterItem({ column: c, operator: 'ilike', value: $event.target.value || undefined })"
|
|
51
|
+
:value="getFilterItem({ column: c, operator: 'ilike' })"
|
|
52
|
+
>
|
|
53
|
+
|
|
54
|
+
<CustomDateRangePicker
|
|
55
|
+
v-else-if="['datetime'].includes(c.type)"
|
|
56
|
+
:column="c"
|
|
57
|
+
:valueStart="filtersStore.filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
|
|
58
|
+
@update:valueStart="setFilterItem({ column: c, operator: 'gte', value: $event || undefined })"
|
|
59
|
+
:valueEnd="filtersStore.filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
|
|
60
|
+
@update:valueEnd="setFilterItem({ column: c, operator: 'lte', value: $event || undefined })"
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
<input
|
|
64
|
+
v-else-if="[ 'date', 'time' ].includes(c.type)"
|
|
65
|
+
type="text" class="w-full py-1 px-2 border border-gray-300 rounded-md"
|
|
66
|
+
placeholder="Search datetime"
|
|
67
|
+
@input="setFilterItem({ column: c, operator: 'ilike', value: $event.target.value || undefined })"
|
|
68
|
+
:value="getFilterItem({ column: c, operator: 'ilike' })"
|
|
69
|
+
>
|
|
70
|
+
|
|
71
|
+
<CustomRangePicker
|
|
72
|
+
v-else-if="['integer', 'decimal', 'float'].includes(c.type) && c.allowMinMaxQuery"
|
|
73
|
+
:min="getFilterMinValue(c.name)"
|
|
74
|
+
:max="getFilterMaxValue(c.name)"
|
|
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>
|
|
97
|
+
|
|
98
|
+
</li>
|
|
99
|
+
</ul>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div class="flex justify-end gap-2">
|
|
103
|
+
<button
|
|
104
|
+
:disabled="!filtersStore.filters.length"
|
|
105
|
+
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"
|
|
107
|
+
@click="clear">Clear all</button>
|
|
108
|
+
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div v-if="show" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"
|
|
113
|
+
@click="$emit('hide')">
|
|
114
|
+
</div>
|
|
115
|
+
</template>
|
|
116
|
+
|
|
117
|
+
<script setup>
|
|
118
|
+
import { watch, computed, ref, onMounted } from 'vue'
|
|
119
|
+
import Dropdown from '@/components/Dropdown.vue';
|
|
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
|
+
|
|
129
|
+
|
|
130
|
+
// props: columns
|
|
131
|
+
// add support for v-model:filers
|
|
132
|
+
const props = defineProps(['columns', 'filters', 'show', 'columnsMinMax']);
|
|
133
|
+
const emits = defineEmits(['update:filters', 'hide']);
|
|
134
|
+
|
|
135
|
+
const router = useRouter();
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
const columnsWithFilter = computed(
|
|
139
|
+
() => props.columns?.filter(column => column.showIn.includes('filter')) || []
|
|
140
|
+
);
|
|
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
|
+
|
|
169
|
+
// sync 'body' class 'overflow-hidden' with show prop show
|
|
170
|
+
watch(() => props.show, (show) => {
|
|
171
|
+
if (show) {
|
|
172
|
+
document.body.classList.add('overflow-hidden');
|
|
173
|
+
} else {
|
|
174
|
+
document.body.classList.remove('overflow-hidden');
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// filters is a array of objects
|
|
179
|
+
// {
|
|
180
|
+
// field: 'name',
|
|
181
|
+
// value: 'John',
|
|
182
|
+
// operator: 'like'
|
|
183
|
+
// }
|
|
184
|
+
|
|
185
|
+
function setFilterItem({ column, operator, value }) {
|
|
186
|
+
|
|
187
|
+
const index = filtersStore.filters.findIndex(f => f.field === column.name && f.operator === operator);
|
|
188
|
+
if (value === undefined) {
|
|
189
|
+
if (index !== -1) {
|
|
190
|
+
filtersStore.filters.splice(index, 1);
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
if (index === -1) {
|
|
194
|
+
filtersStore.setFilter({ field: column.name, value, operator });
|
|
195
|
+
} else {
|
|
196
|
+
filtersStore.setFilters([...filtersStore.filters.slice(0, index), { field: column.name, value, operator }, ...filtersStore.filters.slice(index + 1)])
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
emits('update:filters', [...filtersStore.filters]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function getFilterItem({ column, operator }) {
|
|
203
|
+
return filtersStore.filters.find(f => f.field === column.name && f.operator === operator)?.value || '';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function clear() {
|
|
207
|
+
filtersStore.clearFilters();
|
|
208
|
+
emits('update:filters', [...filtersStore.filters]);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function getFilterMinValue(columnName) {
|
|
212
|
+
if(props.columnsMinMax && props.columnsMinMax[columnName]) {
|
|
213
|
+
return props.columnsMinMax[columnName]?.min
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function getFilterMaxValue(columnName) {
|
|
218
|
+
if(props.columnsMinMax && props.columnsMinMax[columnName]) {
|
|
219
|
+
return props.columnsMinMax[columnName]?.max
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
</script>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
msg: string
|
|
4
|
+
}>()
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<div class="greetings">
|
|
9
|
+
<h1 class="green">{{ msg }}</h1>
|
|
10
|
+
<h3>
|
|
11
|
+
You’ve successfully created a project with
|
|
12
|
+
</h3>
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<style scoped>
|
|
17
|
+
</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<RouterLink
|
|
3
|
+
:to="{name: item.resourceId ? 'resource-list' : item.path, params: item.resourceId ? { resourceId: item.resourceId }: {}}"
|
|
4
|
+
class="flex group items-center py-2 text-lightSidebarText dark:text-darkSidebarText rounded-default hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextHover active:bg-lightSidebarActive dark:active:bg-darkSidebarHover" role="menuitem"
|
|
5
|
+
:class="{
|
|
6
|
+
'px-4': isChild,
|
|
7
|
+
'px-2': !isChild,
|
|
8
|
+
'bg-lightSidebarItemActive dark:bg-darkSidebarItemActive': item.resourceId ?
|
|
9
|
+
($route.params.resourceId === item.resourceId && $route.name === 'resource-list') :
|
|
10
|
+
($route.name === item.path)
|
|
11
|
+
}"
|
|
12
|
+
>
|
|
13
|
+
<component v-if="item.icon" :is="getIcon(item.icon)" class="w-5 h-5 text-lightSidebarIcons dark:text-darkSidebarIcons transition duration-75 group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover" ></component>
|
|
14
|
+
<span class="ms-3">{{ item.label }}</span>
|
|
15
|
+
</RouterLink>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script setup lang="ts">
|
|
19
|
+
import { getIcon } from '@/utils';
|
|
20
|
+
|
|
21
|
+
const props = defineProps(['item', 'isChild']);
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
</script>
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="rounded-default">
|
|
3
|
+
<div
|
|
4
|
+
class="relative shadow-resourseFormShadow sm:rounded-lg dark:shadow-2xl dark:shadow-black rounded-default"
|
|
5
|
+
>
|
|
6
|
+
<form autocomplete="off" @submit.prevent>
|
|
7
|
+
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 ">
|
|
8
|
+
<thead class="text-xs text-gray-700 uppercase bg-lightFormHeading dark:bg-gray-700 dark:text-gray-400">
|
|
9
|
+
<tr>
|
|
10
|
+
<th scope="col" class="px-6 py-3">
|
|
11
|
+
Field
|
|
12
|
+
</th>
|
|
13
|
+
<th scope="col" class="px-6 py-3 w-4/6">
|
|
14
|
+
Value
|
|
15
|
+
</th>
|
|
16
|
+
</tr>
|
|
17
|
+
</thead>
|
|
18
|
+
<tbody>
|
|
19
|
+
<tr v-for="column, i in editableColumns" :key="column.name"
|
|
20
|
+
class="bg-ligftForm dark:bg-gray-800 border-b dark:border-gray-700"
|
|
21
|
+
>
|
|
22
|
+
<td class="px-6 py-4 whitespace-nowrap flex items-center"> <!--align-top-->
|
|
23
|
+
{{ column.label }}
|
|
24
|
+
<span :data-tooltip-target="`tooltip-show-${i}`" class="ml-1 relative inline-block">
|
|
25
|
+
<IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
|
|
26
|
+
:class="(columnError(column) && validating) ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'"
|
|
27
|
+
/>
|
|
28
|
+
</span>
|
|
29
|
+
<div :id="`tooltip-show-${i}`"
|
|
30
|
+
role="tooltip"
|
|
31
|
+
class="ml-1 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
|
+
<template v-if="column?.components?.[props.source]?.file">
|
|
38
|
+
<component
|
|
39
|
+
:is="getCustomComponent(column.components[props.source])"
|
|
40
|
+
:column="column"
|
|
41
|
+
:value="currentValues[column.name]"
|
|
42
|
+
@update:value="setCurrentValue(column.name, $event)"
|
|
43
|
+
:meta="column.components[props.source].meta"
|
|
44
|
+
:record="currentValues"
|
|
45
|
+
@update:inValidity="customComponentsInValidity[column.name] = $event"
|
|
46
|
+
@update:emptiness="customComponentsEmptiness[column.name] = $event"
|
|
47
|
+
/>
|
|
48
|
+
</template>
|
|
49
|
+
<template v-else>
|
|
50
|
+
<Dropdown
|
|
51
|
+
single
|
|
52
|
+
v-if="column.foreignResource"
|
|
53
|
+
:options="columnOptions[column.name] || []"
|
|
54
|
+
:placeholder = "columnOptions[column.name]?.length ?'Select...': 'There are no options available'"
|
|
55
|
+
:modelValue="currentValues[column.name]"
|
|
56
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
57
|
+
></Dropdown>
|
|
58
|
+
<Dropdown
|
|
59
|
+
single
|
|
60
|
+
v-else-if="column.enum"
|
|
61
|
+
:options="column.enum"
|
|
62
|
+
:modelValue="currentValues[column.name]"
|
|
63
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
64
|
+
/>
|
|
65
|
+
<Dropdown
|
|
66
|
+
single
|
|
67
|
+
v-else-if="column.type === 'boolean'"
|
|
68
|
+
:options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
|
|
69
|
+
:modelValue="currentValues[column.name]"
|
|
70
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
71
|
+
/>
|
|
72
|
+
<input
|
|
73
|
+
v-else-if="['integer'].includes(column.type)"
|
|
74
|
+
type="number"
|
|
75
|
+
step="1"
|
|
76
|
+
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"
|
|
77
|
+
placeholder="0"
|
|
78
|
+
:value="currentValues[column.name]"
|
|
79
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
80
|
+
>
|
|
81
|
+
<CustomDatePicker
|
|
82
|
+
v-else-if="['datetime'].includes(column.type)"
|
|
83
|
+
:column="column"
|
|
84
|
+
:valueStart="currentValues[column.name]"
|
|
85
|
+
auto-hide
|
|
86
|
+
@update:valueStart="setCurrentValue(column.name, $event)"
|
|
87
|
+
/>
|
|
88
|
+
<input
|
|
89
|
+
v-else-if="['decimal', 'float'].includes(column.type)"
|
|
90
|
+
type="number"
|
|
91
|
+
step="0.1"
|
|
92
|
+
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"
|
|
93
|
+
placeholder="0.0"
|
|
94
|
+
:value="currentValues[column.name]"
|
|
95
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
96
|
+
/>
|
|
97
|
+
<textarea
|
|
98
|
+
v-else-if="['text', 'richtext'].includes(column.type)"
|
|
99
|
+
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"
|
|
100
|
+
placeholder="Text"
|
|
101
|
+
:value="currentValues[column.name]"
|
|
102
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
103
|
+
>
|
|
104
|
+
</textarea>
|
|
105
|
+
<input
|
|
106
|
+
v-else
|
|
107
|
+
:type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
|
|
108
|
+
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"
|
|
109
|
+
placeholder="Text"
|
|
110
|
+
:value="currentValues[column.name]"
|
|
111
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
112
|
+
autocomplete="false"
|
|
113
|
+
data-lpignore="true"
|
|
114
|
+
readonly
|
|
115
|
+
onfocus="this.removeAttribute('readonly');"
|
|
116
|
+
>
|
|
117
|
+
|
|
118
|
+
<button
|
|
119
|
+
v-if="column.masked"
|
|
120
|
+
type="button"
|
|
121
|
+
@click="unmasked[column.name] = !unmasked[column.name]"
|
|
122
|
+
class="h-6 absolute inset-y-2 top-6 right-6 flex items-center pr-2 z-index-100 focus:outline-none"
|
|
123
|
+
>
|
|
124
|
+
<IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]" />
|
|
125
|
+
<IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
|
|
126
|
+
</button>
|
|
127
|
+
</template>
|
|
128
|
+
<div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
|
|
129
|
+
|
|
130
|
+
<div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
|
|
131
|
+
|
|
132
|
+
</td>
|
|
133
|
+
</tr>
|
|
134
|
+
|
|
135
|
+
</tbody>
|
|
136
|
+
</table>
|
|
137
|
+
</form>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
</template>
|
|
142
|
+
|
|
143
|
+
<script setup>
|
|
144
|
+
|
|
145
|
+
import CustomDatePicker from "@/components/CustomDatePicker.vue";
|
|
146
|
+
import Dropdown from '@/components/Dropdown.vue';
|
|
147
|
+
import { useCoreStore } from '@/stores/core';
|
|
148
|
+
import { callAdminForthApi, getCustomComponent } from '@/utils';
|
|
149
|
+
import { IconExclamationCircleSolid, IconEyeSlashSolid, IconEyeSolid } from '@iconify-prerendered/vue-flowbite';
|
|
150
|
+
import { computedAsync } from '@vueuse/core';
|
|
151
|
+
import { initFlowbite } from 'flowbite';
|
|
152
|
+
import { computed, onMounted, ref, watch } from 'vue';
|
|
153
|
+
import { useRouter } from 'vue-router';
|
|
154
|
+
|
|
155
|
+
const router = useRouter();
|
|
156
|
+
|
|
157
|
+
const props = defineProps({
|
|
158
|
+
loading: Boolean,
|
|
159
|
+
resource: Object,
|
|
160
|
+
record: Object,
|
|
161
|
+
validating: Boolean,
|
|
162
|
+
source: String,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const unmasked = ref({});
|
|
166
|
+
|
|
167
|
+
const mode = computed(() => props.record && Object.keys(props.record).length ? 'edit' : 'create');
|
|
168
|
+
|
|
169
|
+
const emit = defineEmits(['update:record', 'update:isValid']);
|
|
170
|
+
|
|
171
|
+
const currentValues = ref({});
|
|
172
|
+
|
|
173
|
+
const customComponentsInValidity = ref({});
|
|
174
|
+
const customComponentsEmptiness = ref({});
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
const columnError = (column) => {
|
|
178
|
+
const val = computed(() => {
|
|
179
|
+
if (customComponentsInValidity.value[column.name]) {
|
|
180
|
+
return customComponentsInValidity.value[column.name];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (
|
|
184
|
+
column.required[mode.value] &&
|
|
185
|
+
(currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '') &&
|
|
186
|
+
// if component is custum it might tell other criteria for emptiness by emitting 'update:emptiness'
|
|
187
|
+
// components which do not emit 'update:emptiness' will have undefined value in customComponentsEmptiness
|
|
188
|
+
(customComponentsEmptiness.value[column.name] !== false)
|
|
189
|
+
|
|
190
|
+
) {
|
|
191
|
+
return 'This field is required';
|
|
192
|
+
}
|
|
193
|
+
if ( column.type === 'string' || column.type === 'text' ) {
|
|
194
|
+
if ( column.maxLength && currentValues.value[column.name]?.length > column.maxLength ) {
|
|
195
|
+
return `This field must be shorter than ${column.maxLength} characters`;
|
|
196
|
+
}
|
|
197
|
+
if ( column.minLength && currentValues.value[column.name]?.length < column.minLength ) {
|
|
198
|
+
return `This field must be longer than ${column.minLength} characters`;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if ( ['integer', 'decimal', 'float'].includes(column.type) ) {
|
|
202
|
+
if ( column.minValue !== undefined
|
|
203
|
+
&& currentValues.value[column.name] !== null
|
|
204
|
+
&& currentValues.value[column.name] < column.minValue
|
|
205
|
+
) {
|
|
206
|
+
return `This field must be greater than ${column.minValue}`;
|
|
207
|
+
}
|
|
208
|
+
if ( column.maxValue !== undefined && currentValues.value[column.name] > column.maxValue ) {
|
|
209
|
+
return `This field must be less than ${column.maxValue}`;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if ( column.validation && column.validation.length ) {
|
|
213
|
+
const validationArray = column.validation;
|
|
214
|
+
for (let i = 0; i < validationArray.length; i++) {
|
|
215
|
+
if (validationArray[i].regExp) {
|
|
216
|
+
const regExp = new RegExp(validationArray[i].regExp);
|
|
217
|
+
let value = currentValues.value[column.name];
|
|
218
|
+
if (value === undefined || value === null) {
|
|
219
|
+
value = '';
|
|
220
|
+
}
|
|
221
|
+
if (!regExp.test(value)) {
|
|
222
|
+
return validationArray[i].message;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
});
|
|
230
|
+
return val.value;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
const setCurrentValue = (key, value) => {
|
|
235
|
+
const col = props.resource.columns.find((column) => column.name === key);
|
|
236
|
+
if (['integer', 'float'].includes(col.type) && (value || value === 0)) {
|
|
237
|
+
currentValues.value[key] = +value;
|
|
238
|
+
} else {
|
|
239
|
+
currentValues.value[key] = value;
|
|
240
|
+
}
|
|
241
|
+
currentValues.value = { ...currentValues.value };
|
|
242
|
+
console.log('3️⃣ setCurrentValue', key, value);
|
|
243
|
+
emit('update:record', currentValues.value);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
onMounted(() => {
|
|
247
|
+
Object.keys(props.record).forEach((key) => {
|
|
248
|
+
currentValues.value[key] = props.record[key];
|
|
249
|
+
});
|
|
250
|
+
initFlowbite();
|
|
251
|
+
emit('update:isValid', isValid.value);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const columnOptions = computedAsync(async () => {
|
|
255
|
+
return (await Promise.all(
|
|
256
|
+
Object.values(props.resource.columns).map(async (column) => {
|
|
257
|
+
if (column.foreignResource) {
|
|
258
|
+
const list = await callAdminForthApi({
|
|
259
|
+
method: 'POST',
|
|
260
|
+
path: `/get_resource_foreign_data`,
|
|
261
|
+
body: {
|
|
262
|
+
resourceId: router.currentRoute.value.params.resourceId,
|
|
263
|
+
column: column.name,
|
|
264
|
+
limit: 1000,
|
|
265
|
+
offset: 0,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
return { [column.name]: list.items };
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
)).reduce((acc, val) => Object.assign(acc, val), {})
|
|
272
|
+
|
|
273
|
+
}, {});
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
const editableColumns = computed(() => {
|
|
277
|
+
const mode = props.record ? 'edit' : 'create';
|
|
278
|
+
return props.resource?.columns?.filter(column => column.showIn.includes(mode));
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const isValid = computed(() => {
|
|
282
|
+
return editableColumns.value?.every(column => !columnError(column));
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
watch(() => isValid.value, (value) => {
|
|
286
|
+
emit('update:isValid', value);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
</script>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<!-- table -->
|
|
3
|
-
<div class="relative overflow-x-auto shadow-listTableShadow dark:shadow-
|
|
3
|
+
<div class="relative overflow-x-auto shadow-listTableShadow dark:shadow-black overflow-y-hidden rounded-default">
|
|
4
4
|
|
|
5
5
|
<!-- skelet loader -->
|
|
6
6
|
<div role="status" v-if="!resource || !resource.columns"
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
</thead>
|
|
56
56
|
<tbody>
|
|
57
57
|
<SkeleteLoader v-if="!rows" :columns="3" :rows="resource?.columns.length + 2"/>
|
|
58
|
-
<tr v-else-if="rows.length === 0" class="bg-lightListTable dark:bg-darkListTable dark:border-darkListTableBorder">
|
|
58
|
+
<tr v-else-if="rows.length === 0" class="bg-lightListTable border-b dark:bg-darkListTable dark:border-darkListTableBorder">
|
|
59
59
|
<td :colspan="resource?.columns.length + 2">
|
|
60
60
|
|
|
61
61
|
<div id="toast-simple"
|
|
@@ -69,9 +69,7 @@
|
|
|
69
69
|
</tr>
|
|
70
70
|
|
|
71
71
|
<tr @click="onClick($event,row)" v-else v-for="(row, rowI) in rows" :key="row.id"
|
|
72
|
-
class="bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover cursor-pointer"
|
|
73
|
-
:class="{'border-b': rowI !== rows.length - 1}"
|
|
74
|
-
>
|
|
72
|
+
class="bg-lightListTable border-b dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover cursor-pointer">
|
|
75
73
|
<td class="w-4 p-4 cursor-default" @click="(e)=>{e.stopPropagation()}">
|
|
76
74
|
<div class="flex items center ">
|
|
77
75
|
<input
|
|
@@ -377,7 +375,6 @@ async function deleteRecord(row) {
|
|
|
377
375
|
primaryKey: row._primaryKeyValue,
|
|
378
376
|
}
|
|
379
377
|
});
|
|
380
|
-
console.log('safsa', res)
|
|
381
378
|
if (!res.error){
|
|
382
379
|
emits('update:records', true)
|
|
383
380
|
showSuccesTost('Record deleted successfully')
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
role="status" class="max-w-sm animate-pulse"
|
|
4
|
+
>
|
|
5
|
+
<div class="h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4"></div>
|
|
6
|
+
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px] mb-2.5"></div>
|
|
7
|
+
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5"></div>
|
|
8
|
+
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[330px] mb-2.5"></div>
|
|
9
|
+
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[300px] mb-2.5"></div>
|
|
10
|
+
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px]"></div>
|
|
11
|
+
<span class="sr-only">Loading...</span>
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
<div id="toast-default" class="flex items-center w-full p-4 text-gray-500 rounded-lg shadow dark:text-gray-400 dark:bg-gray-800" role="alert"
|
|
5
|
-
:class="
|
|
6
|
-
{
|
|
7
|
-
'danger': 'bg-red-100',
|
|
8
|
-
}[toast.variant]
|
|
9
|
-
"
|
|
10
|
-
>
|
|
4
|
+
<div id="toast-default" class="flex items-center w-full p-4 text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800" role="alert">
|
|
11
5
|
<div v-if="toast.variant == 'info'" class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-blue-500 bg-blue-100 rounded-lg dark:bg-blue-800 dark:text-blue-200">
|
|
12
6
|
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
|
|
13
7
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.147 15.085a7.159 7.159 0 0 1-6.189 3.307A6.713 6.713 0 0 1 3.1 15.444c-2.679-4.513.287-8.737.888-9.548A4.373 4.373 0 0 0 5 1.608c1.287.953 6.445 3.218 5.537 10.5 1.5-1.122 2.706-3.01 2.853-6.14 1.433 1.049 3.993 5.395 1.757 9.117Z"/>
|
|
@@ -33,8 +27,8 @@
|
|
|
33
27
|
<span class="sr-only">Check icon</span>
|
|
34
28
|
</div>
|
|
35
29
|
|
|
36
|
-
<div class="ms-3 text-sm font-normal max-w-xs
|
|
37
|
-
<div class="ms-3 text-sm font-normal max-w-xs
|
|
30
|
+
<div class="ms-3 text-sm font-normal max-w-xs" v-if="toast.messageHtml" v-html="toast.messageHtml"></div>
|
|
31
|
+
<div class="ms-3 text-sm font-normal max-w-xs" v-else>{{toast.message}}</div>
|
|
38
32
|
<button @click="closeToast" type="button" class="ms-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" >
|
|
39
33
|
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
|
40
34
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|