adminforth 1.2.56 → 1.2.58
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/spa/index.html +1 -1
- package/dist/spa/package-lock.json +215 -1
- package/dist/spa/package.json +2 -0
- package/dist/spa/spa/src/types/AdminForthConfig.ts +1477 -0
- package/dist/spa/spa/src/types/FrontendAPI.ts +121 -0
- package/dist/spa/src/App.vue +176 -133
- package/dist/spa/src/assets/base.css +2 -0
- package/dist/spa/src/components/Breadcrumbs.vue +3 -2
- package/dist/spa/src/components/CustomDatePicker.vue +6 -4
- package/dist/spa/src/components/CustomRangePicker.vue +11 -7
- package/dist/spa/src/components/Filters.vue +16 -5
- package/dist/spa/src/components/MenuLink.vue +3 -3
- package/dist/spa/src/components/ResourceForm.vue +140 -111
- package/dist/spa/src/components/ResourceListTable.vue +76 -101
- package/dist/spa/src/components/SkeleteLoader.vue +23 -0
- package/dist/spa/src/components/Toast.vue +18 -7
- package/dist/spa/src/components/ValueRenderer.vue +10 -4
- package/dist/spa/src/composables/useStores.ts +25 -11
- package/dist/spa/src/index.scss +2 -1
- package/dist/spa/src/main.ts +1 -1
- package/dist/spa/src/router/index.ts +0 -1
- package/dist/spa/src/stores/user.ts +21 -3
- package/dist/spa/src/types/AdminForthConfig.ts +1477 -0
- package/dist/spa/src/types/FrontendAPI.ts +121 -0
- package/dist/spa/src/utils.ts +6 -4
- package/dist/spa/src/views/CreateView.vue +2 -9
- package/dist/spa/src/views/EditView.vue +1 -9
- package/dist/spa/src/views/ListView.vue +17 -16
- package/dist/spa/src/views/LoginView.vue +57 -31
- package/dist/spa/src/views/ResourceParent.vue +1 -2
- package/dist/spa/src/views/ShowView.vue +17 -21
- package/dist/spa/tailwind.config.js +3 -1
- package/dist/spa/vite.config.ts +7 -0
- package/package.json +3 -4
|
@@ -1,42 +1,29 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<!-- table -->
|
|
3
|
-
<div class="relative overflow-x-auto shadow-
|
|
3
|
+
<div class="relative overflow-x-auto shadow-listTableShadow dark:shadow-darkListTableShadow overflow-y-hidden rounded-default">
|
|
4
4
|
|
|
5
5
|
<!-- skelet loader -->
|
|
6
6
|
<div role="status" v-if="!resource || !resource.columns"
|
|
7
7
|
class="max-w p-4 space-y-4 divide-y divide-gray-200 rounded shadow animate-pulse dark:divide-gray-700 md:p-6 dark:border-gray-700">
|
|
8
|
-
|
|
9
|
-
<div>
|
|
10
|
-
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
|
|
11
|
-
<div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
|
|
12
|
-
</div>
|
|
13
|
-
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
|
|
14
|
-
</div>
|
|
15
|
-
|
|
16
|
-
<div class="flex items-center justify-between h-16 pt-4" v-for="i in new Array(10)">
|
|
17
|
-
<div>
|
|
18
|
-
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
|
|
19
|
-
<div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
|
|
20
|
-
</div>
|
|
21
|
-
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
|
|
22
|
-
</div>
|
|
23
|
-
<span class="sr-only">Loading...</span>
|
|
8
|
+
|
|
24
9
|
</div>
|
|
25
10
|
|
|
26
11
|
<table v-else class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 rounded-default">
|
|
27
|
-
<thead class="text-xs text-view-table-heading-text bg-
|
|
12
|
+
<thead class="text-xs text-view-table-heading-text bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-gray-400">
|
|
28
13
|
<tr>
|
|
29
14
|
<th scope="col" class="p-4">
|
|
30
15
|
<div v-if="rows && rows.length" class="flex items-center">
|
|
31
|
-
<input id="checkbox-all-search" type="checkbox" :checked="allFromThisPageChecked" @change="selectAll()"
|
|
32
|
-
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
|
16
|
+
<input id="checkbox-all-search" type="checkbox" :checked="allFromThisPageChecked" @change="selectAll()"
|
|
17
|
+
class="w-4 h-4 cursor-pointer text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
|
33
18
|
<label for="checkbox-all-search" class="sr-only">checkbox</label>
|
|
34
19
|
</div>
|
|
35
20
|
</th>
|
|
36
21
|
|
|
37
22
|
<th v-for="c in columnsListed" scope="col" class="px-6 py-3">
|
|
23
|
+
|
|
38
24
|
<div @click="() => c.sortable && onSortButtonClick(c.name)" class="flex items-center " :class="{'cursor-pointer':c.sortable}">
|
|
39
25
|
{{ c.label }}
|
|
26
|
+
|
|
40
27
|
|
|
41
28
|
<div v-if="c.sortable"
|
|
42
29
|
:style="{ 'color':ascArr.includes(c.name)?'green':descArr.includes(c.name)?'red':'currentColor'}">
|
|
@@ -52,6 +39,12 @@
|
|
|
52
39
|
d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z"/>
|
|
53
40
|
</svg>
|
|
54
41
|
</div>
|
|
42
|
+
<span
|
|
43
|
+
class="bg-red-100 text-red-800 text-xs font-medium me-1 px-1 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400"
|
|
44
|
+
v-if="sort.findIndex((s) => s.field === c.name) !== -1 && sort?.length > 1">
|
|
45
|
+
{{ sort.findIndex((s) => s.field === c.name) + 1 }}
|
|
46
|
+
</span>
|
|
47
|
+
|
|
55
48
|
</div>
|
|
56
49
|
</th>
|
|
57
50
|
|
|
@@ -61,51 +54,8 @@
|
|
|
61
54
|
</tr>
|
|
62
55
|
</thead>
|
|
63
56
|
<tbody>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
<div role="status"
|
|
68
|
-
class="max-w p-4 space-y-4 divide-y divide-gray-200 rounded animate-pulse dark:divide-gray-700 md:p-6 dark:border-gray-700">
|
|
69
|
-
<div class="flex items-center justify-between">
|
|
70
|
-
<div>
|
|
71
|
-
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
|
|
72
|
-
<div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
|
|
73
|
-
</div>
|
|
74
|
-
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
|
|
75
|
-
</div>
|
|
76
|
-
<div class="flex items-center justify-between pt-4">
|
|
77
|
-
<div>
|
|
78
|
-
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
|
|
79
|
-
<div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
|
|
80
|
-
</div>
|
|
81
|
-
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
|
|
82
|
-
</div>
|
|
83
|
-
<div class="flex items-center justify-between pt-4">
|
|
84
|
-
<div>
|
|
85
|
-
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
|
|
86
|
-
<div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
|
|
87
|
-
</div>
|
|
88
|
-
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
|
|
89
|
-
</div>
|
|
90
|
-
<div class="flex items-center justify-between pt-4">
|
|
91
|
-
<div>
|
|
92
|
-
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
|
|
93
|
-
<div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
|
|
94
|
-
</div>
|
|
95
|
-
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
|
|
96
|
-
</div>
|
|
97
|
-
<div class="flex items-center justify-between pt-4">
|
|
98
|
-
<div>
|
|
99
|
-
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
|
|
100
|
-
<div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
|
|
101
|
-
</div>
|
|
102
|
-
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
|
|
103
|
-
</div>
|
|
104
|
-
<span class="sr-only">Loading...</span>
|
|
105
|
-
</div>
|
|
106
|
-
</td>
|
|
107
|
-
</tr>
|
|
108
|
-
<tr v-else-if="rows.length === 0" class="bg-list-view-table-bg border-b dark:bg-gray-800 dark:border-gray-700">
|
|
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">
|
|
109
59
|
<td :colspan="resource?.columns.length + 2">
|
|
110
60
|
|
|
111
61
|
<div id="toast-simple"
|
|
@@ -118,16 +68,19 @@
|
|
|
118
68
|
</td>
|
|
119
69
|
</tr>
|
|
120
70
|
|
|
121
|
-
<tr v-else v-for="(row, rowI) in rows" :key="row.id"
|
|
122
|
-
class="bg-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
>
|
|
75
|
+
<td class="w-4 p-4 cursor-default" @click="(e)=>{e.stopPropagation()}">
|
|
76
|
+
<div class="flex items center ">
|
|
125
77
|
<input
|
|
78
|
+
@click="(e)=>{e.stopPropagation()}"
|
|
126
79
|
id="checkbox-table-search-1"
|
|
127
80
|
type="checkbox"
|
|
128
81
|
:checked="checkboxesInternal.includes(row.id)"
|
|
129
82
|
@change="(e)=>{addToCheckedValues(row.id)}"
|
|
130
|
-
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
|
83
|
+
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 cursor-pointer">
|
|
131
84
|
<label for="checkbox-table-search-1" class="sr-only">checkbox</label>
|
|
132
85
|
</div>
|
|
133
86
|
</td>
|
|
@@ -135,14 +88,14 @@
|
|
|
135
88
|
<!-- if c.name in listComponentsPerColumn, render it. If not, render ValueRenderer -->
|
|
136
89
|
<component
|
|
137
90
|
:is="c?.components?.list ? getCustomComponent(c.components.list) : ValueRenderer"
|
|
138
|
-
:meta="c?.components?.list
|
|
91
|
+
:meta="c?.components?.list?.meta"
|
|
139
92
|
:column="c"
|
|
140
93
|
:record="row"
|
|
141
94
|
:adminUser="coreStore.adminUser"
|
|
142
95
|
:resource="resource"
|
|
143
96
|
/>
|
|
144
97
|
</td>
|
|
145
|
-
<td class=" items-center px-6 py-4">
|
|
98
|
+
<td class=" items-center px-6 py-4 cursor-default" @click="(e)=>{e.stopPropagation()}">
|
|
146
99
|
<div class="flex">
|
|
147
100
|
<RouterLink
|
|
148
101
|
v-if="resource.options?.allowedActions.show"
|
|
@@ -277,7 +230,8 @@ import { callAdminForthApi, getIcon } from '@/utils';
|
|
|
277
230
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
278
231
|
import { getCustomComponent } from '@/utils';
|
|
279
232
|
import { useCoreStore } from '@/stores/core';
|
|
280
|
-
import { showSuccesTost } from '@/composables/useFrontendApi';
|
|
233
|
+
import { showSuccesTost, showErrorTost } from '@/composables/useFrontendApi';
|
|
234
|
+
import SkeleteLoader from '@/components/SkeleteLoader.vue';
|
|
281
235
|
|
|
282
236
|
import {
|
|
283
237
|
IconInboxOutline,
|
|
@@ -291,6 +245,7 @@ IconFilterOutline,
|
|
|
291
245
|
IconPenSolid,
|
|
292
246
|
IconTrashBinSolid
|
|
293
247
|
} from '@iconify-prerendered/vue-flowbite';
|
|
248
|
+
import router from '@/router';
|
|
294
249
|
|
|
295
250
|
const coreStore = useCoreStore();
|
|
296
251
|
|
|
@@ -299,7 +254,8 @@ const props = defineProps([
|
|
|
299
254
|
'rows',
|
|
300
255
|
'totalRows',
|
|
301
256
|
'pageSize',
|
|
302
|
-
'checkboxes'
|
|
257
|
+
'checkboxes',
|
|
258
|
+
'sort'
|
|
303
259
|
])
|
|
304
260
|
|
|
305
261
|
// emits, update page
|
|
@@ -335,6 +291,10 @@ watch(() => props.checkboxes, (newCheckboxes) => {
|
|
|
335
291
|
checkboxesInternal.value = newCheckboxes;
|
|
336
292
|
});
|
|
337
293
|
|
|
294
|
+
watch(() => props.sort, (newSort) => {
|
|
295
|
+
sort.value = newSort;
|
|
296
|
+
});
|
|
297
|
+
|
|
338
298
|
function addToCheckedValues(id) {
|
|
339
299
|
if (checkboxesInternal.value.includes(id)) {
|
|
340
300
|
checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== id);
|
|
@@ -347,25 +307,25 @@ function addToCheckedValues(id) {
|
|
|
347
307
|
const columnsListed = computed(() => props.resource?.columns?.filter(c => c.showIn.includes('list')));
|
|
348
308
|
|
|
349
309
|
async function selectAll(value) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
checkboxesInternal.value.
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
|
|
310
|
+
if (!allFromThisPageChecked.value) {
|
|
311
|
+
props.rows.forEach((r) => {
|
|
312
|
+
if (!checkboxesInternal.value.includes(r.id)) {
|
|
313
|
+
checkboxesInternal.value.push(r.id)
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
} else {
|
|
317
|
+
props.rows.forEach((r) => {
|
|
318
|
+
checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== r.id);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
checkboxesInternal.value = [ ...checkboxesInternal.value ];
|
|
359
322
|
}
|
|
360
323
|
|
|
361
324
|
const totalPages = computed(() => Math.ceil(props.totalRows / props.pageSize));
|
|
362
325
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
326
|
const allFromThisPageChecked = computed(() => {
|
|
367
327
|
if (!props.rows) return false;
|
|
368
|
-
return props.rows.every((r) =>
|
|
328
|
+
return props.rows.every((r) => checkboxesInternal.value.includes(r.id));
|
|
369
329
|
});
|
|
370
330
|
const ascArr = computed(() => sort.value.filter((s) => s.direction === 'asc').map((s) => s.field));
|
|
371
331
|
const descArr = computed(() => sort.value.filter((s) => s.direction === 'desc').map((s) => s.field));
|
|
@@ -374,7 +334,7 @@ const descArr = computed(() => sort.value.filter((s) => s.direction === 'desc').
|
|
|
374
334
|
function onSortButtonClick(field) {
|
|
375
335
|
const sortIndex = sort.value.findIndex((s) => s.field === field);
|
|
376
336
|
if (sortIndex === -1) {
|
|
377
|
-
sort.value = [{field, direction: 'asc'}
|
|
337
|
+
sort.value = [...sort.value,{field, direction: 'asc'}];
|
|
378
338
|
} else {
|
|
379
339
|
const sortField = sort.value[sortIndex];
|
|
380
340
|
if (sortField.direction === 'asc') {
|
|
@@ -385,6 +345,22 @@ function onSortButtonClick(field) {
|
|
|
385
345
|
}
|
|
386
346
|
}
|
|
387
347
|
|
|
348
|
+
|
|
349
|
+
const clickTarget = ref(null);
|
|
350
|
+
async function onClick(e,row) {
|
|
351
|
+
if(clickTarget.value === e.target) return;
|
|
352
|
+
clickTarget.value = e.target;
|
|
353
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
354
|
+
if (window.getSelection().toString()) return;
|
|
355
|
+
else {router.push({
|
|
356
|
+
name: 'resource-show',
|
|
357
|
+
params: {
|
|
358
|
+
resourceId: props.resource.resourceId,
|
|
359
|
+
primaryKey: row._primaryKeyValue,
|
|
360
|
+
},
|
|
361
|
+
})}
|
|
362
|
+
}
|
|
363
|
+
|
|
388
364
|
async function deleteRecord(row) {
|
|
389
365
|
const data = await window.adminforth.confirm({
|
|
390
366
|
message: 'Are you sure you want to delete this item?',
|
|
@@ -394,26 +370,25 @@ async function deleteRecord(row) {
|
|
|
394
370
|
if (data) {
|
|
395
371
|
try {
|
|
396
372
|
const res = await callAdminForthApi({
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
373
|
+
path: '/delete_record',
|
|
374
|
+
method: 'POST',
|
|
375
|
+
body: {
|
|
376
|
+
resourceId: props.resource.resourceId,
|
|
377
|
+
primaryKey: row._primaryKeyValue,
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
console.log('safsa', res)
|
|
403
381
|
if (!res.error){
|
|
404
382
|
emits('update:records', true)
|
|
405
383
|
showSuccesTost('Record deleted successfully')
|
|
406
384
|
} else {
|
|
407
|
-
|
|
385
|
+
showErrorTost(res.error)
|
|
408
386
|
}
|
|
409
387
|
|
|
410
388
|
} catch (e) {
|
|
389
|
+
showErrorTost(`Something went wrong, please try again later`);
|
|
411
390
|
console.error(e);
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
391
|
+
};
|
|
392
|
+
}
|
|
416
393
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
394
|
</script>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<tr v-for="c in new Array(props.columns)" class="bg-lightListTable border-b dark:bg-darkListTable dark:border-darkListBorder">
|
|
3
|
+
<td v-for="r in new Array(props.rows)" class="items-center px-6 py-8 cursor-default" >
|
|
4
|
+
<div role="status" class="max-w-sm animate-pulse">
|
|
5
|
+
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px]"></div>
|
|
6
|
+
</div>
|
|
7
|
+
</td>
|
|
8
|
+
</tr>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script setup>
|
|
12
|
+
|
|
13
|
+
const props = defineProps({
|
|
14
|
+
columns:Number,
|
|
15
|
+
rows:Number
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<style lang="scss" scoped>
|
|
22
|
+
|
|
23
|
+
</style>
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
<div id="toast-default" class="flex items-center w-full
|
|
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
|
+
>
|
|
5
11
|
<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">
|
|
6
12
|
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
|
|
7
13
|
<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"/>
|
|
@@ -26,7 +32,9 @@
|
|
|
26
32
|
</svg>
|
|
27
33
|
<span class="sr-only">Check icon</span>
|
|
28
34
|
</div>
|
|
29
|
-
|
|
35
|
+
|
|
36
|
+
<div class="ms-3 text-sm font-normal max-w-xs pr-2" v-if="toast.messageHtml" v-html="toast.messageHtml"></div>
|
|
37
|
+
<div class="ms-3 text-sm font-normal max-w-xs pr-2" v-else>{{toast.message}}</div>
|
|
30
38
|
<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" >
|
|
31
39
|
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
|
32
40
|
<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"/>
|
|
@@ -43,10 +51,13 @@ import { useToastStore } from '@/stores/toast';
|
|
|
43
51
|
const toastStore = useToastStore();
|
|
44
52
|
const emit = defineEmits(['close']);
|
|
45
53
|
const props = defineProps<{
|
|
46
|
-
toast:{
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
toast: {
|
|
55
|
+
message?: string;
|
|
56
|
+
messageHtml?: string;
|
|
57
|
+
variant: string;
|
|
58
|
+
id: string;
|
|
59
|
+
timeout?: number|'unlimited';
|
|
60
|
+
}
|
|
50
61
|
}>();
|
|
51
62
|
function closeToast() {
|
|
52
63
|
emit('close');
|
|
@@ -55,7 +66,7 @@ function closeToast() {
|
|
|
55
66
|
onMounted(() => {
|
|
56
67
|
if (props.toast.timeout === 'unlimited') return;
|
|
57
68
|
else {
|
|
58
|
-
setTimeout(() => {emit('close');}, props.toast.timeout
|
|
69
|
+
setTimeout(() => {emit('close');}, (props.toast.timeout || 10) * 1e3 );
|
|
59
70
|
}
|
|
60
71
|
});
|
|
61
72
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
|
-
|
|
4
|
-
<span v-if="column.foreignResource">
|
|
3
|
+
<span @click="(e)=>{e.stopPropagation()}" v-if="column.foreignResource">
|
|
5
4
|
<RouterLink v-if="record[column.name]" class="font-medium text-blue-600 dark:text-blue-500 hover:brightness-110"
|
|
6
5
|
:to="{ name: 'resource-show', params: { resourceId: column.foreignResource.resourceId, primaryKey: record[column.name].pk } }">
|
|
7
6
|
{{ record[column.name].label }}
|
|
@@ -18,9 +17,11 @@
|
|
|
18
17
|
<span v-else-if="column.enum">
|
|
19
18
|
{{ checkEmptyValues(column.enum.find(e => e.value === record[column.name])?.label,route.meta.type) }}
|
|
20
19
|
</span>
|
|
21
|
-
<span v-else-if="column.type === 'datetime'">
|
|
20
|
+
<span v-else-if="column.type === 'datetime'" class="whitespace-nowrap">
|
|
22
21
|
{{ checkEmptyValues(formatDate(record[column.name]),route.meta.type) }}
|
|
23
|
-
|
|
22
|
+
</span>
|
|
23
|
+
<span v-else-if="column.type === 'richtext'">
|
|
24
|
+
<div v-html="protectAgainstXSS(record[column.name])"></div>
|
|
24
25
|
</span>
|
|
25
26
|
<span v-else>
|
|
26
27
|
{{ checkEmptyValues(record[column.name],route.meta.type) }}
|
|
@@ -36,6 +37,7 @@ import utc from 'dayjs/plugin/utc';
|
|
|
36
37
|
import timezone from 'dayjs/plugin/timezone';
|
|
37
38
|
import {checkEmptyValues} from '@/utils';
|
|
38
39
|
import { useRoute, useRouter } from 'vue-router';
|
|
40
|
+
import sanitizeHtml from 'sanitize-html';
|
|
39
41
|
|
|
40
42
|
|
|
41
43
|
import { useCoreStore } from '@/stores/core';
|
|
@@ -52,6 +54,10 @@ const props = defineProps({
|
|
|
52
54
|
record: Object
|
|
53
55
|
});
|
|
54
56
|
|
|
57
|
+
function protectAgainstXSS(value) {
|
|
58
|
+
return sanitizeHtml(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
55
61
|
|
|
56
62
|
function formatDate(date) {
|
|
57
63
|
if (!date) return '';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { FrontendAPIInterface, ConfirmParams, AlertParams,
|
|
1
|
+
import type { FrontendAPIInterface, ConfirmParams, AlertParams, } from '../types/FrontendAPI';
|
|
2
|
+
import type { AdminForthFilterOperators } from '@/types/AdminForthConfig';
|
|
2
3
|
import { useToastStore } from '../stores/toast';
|
|
3
4
|
import { useModalStore } from '../stores/modal';
|
|
4
5
|
import { useCoreStore } from '@/stores/core';
|
|
@@ -6,9 +7,20 @@ import { useFiltersStore } from '@/stores/filters';
|
|
|
6
7
|
import router from '@/router'
|
|
7
8
|
import type { AdminForthResourceColumn } from '@/types/AdminForthConfig';
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
type FilterParams = {
|
|
11
|
+
/**
|
|
12
|
+
* Field of resource to filter
|
|
13
|
+
*/
|
|
14
|
+
field: string;
|
|
15
|
+
/**
|
|
16
|
+
* Operator of filter
|
|
17
|
+
*/
|
|
18
|
+
operator: AdminForthFilterOperators;
|
|
19
|
+
/**
|
|
20
|
+
* Value of filter
|
|
21
|
+
*/
|
|
22
|
+
value: string | number | boolean ;
|
|
23
|
+
}
|
|
12
24
|
|
|
13
25
|
declare global {
|
|
14
26
|
interface Window {
|
|
@@ -23,7 +35,6 @@ declare global {
|
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
export class FrontendAPI implements FrontendAPIInterface {
|
|
26
|
-
private validOperators: Operator[] = ['lte', 'gte', 'in','ilike'];
|
|
27
38
|
private toastStore:any
|
|
28
39
|
private modalStore:any
|
|
29
40
|
private filtersStore:any
|
|
@@ -56,6 +67,7 @@ export class FrontendAPI implements FrontendAPIInterface {
|
|
|
56
67
|
alert(params: AlertParams): void {
|
|
57
68
|
this.toastStore.addToast({
|
|
58
69
|
message: params.message,
|
|
70
|
+
messageHtml: params.messageHtml,
|
|
59
71
|
variant: params.variant,
|
|
60
72
|
timeout: params.timeout
|
|
61
73
|
})
|
|
@@ -70,12 +82,8 @@ export class FrontendAPI implements FrontendAPIInterface {
|
|
|
70
82
|
const filterField = this.coreStore.resourceColumnsWithFilters.find((col: AdminForthResourceColumn) => col.name === filter.field)
|
|
71
83
|
if(!filterField){
|
|
72
84
|
throw new Error(`Field ${filter.field} is not available for filtering`)
|
|
73
|
-
}
|
|
74
|
-
if(filterField) {
|
|
75
|
-
if(!this.validOperators.includes(filter.operator)){
|
|
76
|
-
throw new Error(`Operator ${filter.operator} is not valid`)
|
|
77
|
-
}
|
|
78
85
|
}
|
|
86
|
+
|
|
79
87
|
}
|
|
80
88
|
return true
|
|
81
89
|
}
|
|
@@ -103,7 +111,13 @@ export class FrontendAPI implements FrontendAPIInterface {
|
|
|
103
111
|
if(index === -1) {
|
|
104
112
|
this.filtersStore.setFilter(filter)
|
|
105
113
|
} else {
|
|
106
|
-
|
|
114
|
+
const filters = [...this.filtersStore.filters];
|
|
115
|
+
if (filter.value === undefined) {
|
|
116
|
+
filters.splice(index, 1);
|
|
117
|
+
} else {
|
|
118
|
+
filters[index] = filter;
|
|
119
|
+
}
|
|
120
|
+
this.filtersStore.setFilters(filters);
|
|
107
121
|
}
|
|
108
122
|
}
|
|
109
123
|
}
|
package/dist/spa/src/index.scss
CHANGED
package/dist/spa/src/main.ts
CHANGED
|
@@ -12,7 +12,6 @@ const router = createRouter({
|
|
|
12
12
|
component: () => import('@/views/LoginView.vue'),
|
|
13
13
|
meta: { title: 'login' },
|
|
14
14
|
beforeEnter: async (to, from, next) => {
|
|
15
|
-
const userStore = useUserStore()
|
|
16
15
|
if(localStorage.getItem('isAuthorized') === 'true'){
|
|
17
16
|
next({name: 'home'})
|
|
18
17
|
} else {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import {ref} from 'vue';
|
|
2
|
-
import {defineStore} from 'pinia';
|
|
1
|
+
import { ref } from 'vue';
|
|
2
|
+
import { defineStore } from 'pinia';
|
|
3
3
|
import { callAdminForthApi } from '@/utils';
|
|
4
|
+
import { initFlowbite } from 'flowbite'
|
|
5
|
+
import { useRouter } from 'vue-router';
|
|
6
|
+
import { useCoreStore } from './core';
|
|
7
|
+
import router from '@/router';
|
|
8
|
+
|
|
9
|
+
|
|
4
10
|
|
|
5
11
|
export const useUserStore = defineStore('user', () => {
|
|
6
12
|
const isAuthorized = ref(false);
|
|
@@ -15,6 +21,17 @@ export const useUserStore = defineStore('user', () => {
|
|
|
15
21
|
localStorage.setItem('isAuthorized', 'false');
|
|
16
22
|
}
|
|
17
23
|
|
|
24
|
+
async function finishLogin() {
|
|
25
|
+
const coreStore = useCoreStore();
|
|
26
|
+
authorize(); // TODO not sure we need this approach with localStorage
|
|
27
|
+
await router.push('/');
|
|
28
|
+
await router.isReady();
|
|
29
|
+
await coreStore.fetchMenuAndResource();
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
initFlowbite();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
async function logout() {
|
|
19
36
|
await callAdminForthApi({
|
|
20
37
|
path: '/logout',
|
|
@@ -48,7 +65,8 @@ export const useUserStore = defineStore('user', () => {
|
|
|
48
65
|
isAuthorized,
|
|
49
66
|
authorize,
|
|
50
67
|
unauthorize,
|
|
51
|
-
logout
|
|
68
|
+
logout,
|
|
69
|
+
finishLogin
|
|
52
70
|
}
|
|
53
71
|
|
|
54
72
|
});
|