adminforth 1.0.10 → 1.0.13
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/auth.js +1 -1
- package/dataConnectors/sqlite.js +2 -3
- package/index.js +28 -26
- package/package.json +1 -1
- package/spa/index.html +1 -1
- package/spa/src/App.vue +3 -3
- package/spa/src/components/CustomDatePicker.vue +161 -0
- package/spa/src/components/CustomDateRangePicker.vue +22 -31
- package/spa/src/components/Dropdown.vue +1 -1
- package/spa/src/components/Filters.vue +25 -9
- package/spa/src/components/MenuLink.vue +1 -1
- package/spa/src/components/ResourceForm.vue +7 -0
- package/spa/src/stores/core.ts +12 -8
- package/spa/src/views/EditView.vue +10 -2
- package/spa/src/views/ListView.vue +55 -42
- package/spa/src/views/ShowView.vue +1 -2
package/auth.js
CHANGED
package/dataConnectors/sqlite.js
CHANGED
|
@@ -235,12 +235,11 @@ class SQLiteConnector {
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
async updateRecord({ resource, recordId, record, newValues }) {
|
|
238
|
-
const
|
|
239
|
-
const placeholders = Object.keys(newValues).map(() => '?').join(', ');
|
|
238
|
+
const columnsWithPlaceholders = Object.keys(newValues).map((col) => `${col} = ?`);
|
|
240
239
|
const values = [...Object.values(newValues), recordId];
|
|
241
240
|
|
|
242
241
|
this.db.prepare(
|
|
243
|
-
`UPDATE ${resource.table} SET ${
|
|
242
|
+
`UPDATE ${resource.table} SET ${columnsWithPlaceholders} WHERE ${this.getPrimaryKey(resource)} = ?`
|
|
244
243
|
).run(values);
|
|
245
244
|
}
|
|
246
245
|
|
package/index.js
CHANGED
|
@@ -370,6 +370,7 @@ class AdminForth {
|
|
|
370
370
|
handler: async ({input, adminUser, cookies}) => {
|
|
371
371
|
const cookieParsed = this.auth.verify(cookies['adminforth_jwt']);
|
|
372
372
|
let username = ''
|
|
373
|
+
let userFullName = ''
|
|
373
374
|
if (cookieParsed['pk'] == null) {
|
|
374
375
|
username = this.config.rootUser.username;
|
|
375
376
|
} else {
|
|
@@ -387,12 +388,15 @@ class AdminForth {
|
|
|
387
388
|
return { error: 'Unauthorized' };
|
|
388
389
|
}
|
|
389
390
|
username = user.data[0][this.config.auth.usernameField];
|
|
391
|
+
userFullName = user.data[0][this.config.auth.userFullName];
|
|
390
392
|
}
|
|
391
393
|
|
|
394
|
+
const userData = {
|
|
395
|
+
[this.config.auth.usernameField]: username,
|
|
396
|
+
[this.config.auth.userFullName]: userFullName
|
|
397
|
+
};
|
|
392
398
|
return {
|
|
393
|
-
user:
|
|
394
|
-
[this.config.auth.usernameField]: username
|
|
395
|
-
},
|
|
399
|
+
user: userData,
|
|
396
400
|
resources: this.config.resources.map((res) => ({
|
|
397
401
|
resourceId: res.resourceId,
|
|
398
402
|
label: res.label,
|
|
@@ -522,6 +526,20 @@ class AdminForth {
|
|
|
522
526
|
if (!resource) {
|
|
523
527
|
return { error: `Resource '${body['resourceId']}' not found` };
|
|
524
528
|
}
|
|
529
|
+
|
|
530
|
+
const record = body['record'];
|
|
531
|
+
// execute hook if needed
|
|
532
|
+
if (resource.hooks?.create?.beforeSave) {
|
|
533
|
+
const resp = await resource.hooks?.create?.beforeSave({ resource, record, adminUser });
|
|
534
|
+
if (!resp || (!resp.ok && !resp.error)) {
|
|
535
|
+
throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (resp.error) {
|
|
539
|
+
return { error: resp.error };
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
525
543
|
for (const column of resource.columns) {
|
|
526
544
|
if (column.fillOnCreate) {
|
|
527
545
|
if (body['record'][column.name] === undefined) {
|
|
@@ -547,31 +565,15 @@ class AdminForth {
|
|
|
547
565
|
}
|
|
548
566
|
}
|
|
549
567
|
}
|
|
550
|
-
const connector = this.connectors[resource.dataSource];
|
|
551
|
-
|
|
552
|
-
const record = body['record'];
|
|
553
|
-
|
|
554
|
-
// execute hook if needed
|
|
555
|
-
if (resource.hooks?.create?.beforeSave) {
|
|
556
|
-
const resp = await resource.hooks?.create?.beforeSave({ resource, record, adminUser });
|
|
557
|
-
if (!resp || (!resp.ok && !resp.error)) {
|
|
558
|
-
throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
if (resp.error) {
|
|
562
|
-
return { error: resp.error };
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
568
|
|
|
566
569
|
// remove virtual columns from record
|
|
567
570
|
for (const column of resource.columns.filter((col) => col.virtual)) {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
+
if (record[column.name]) {
|
|
572
|
+
delete record[column.name];
|
|
573
|
+
}
|
|
571
574
|
}
|
|
572
|
-
|
|
575
|
+
const connector = this.connectors[resource.dataSource];
|
|
573
576
|
await connector.createRecord({ resource, record });
|
|
574
|
-
|
|
575
577
|
// execute hook if needed
|
|
576
578
|
if (resource.hooks?.create?.afterSave) {
|
|
577
579
|
const resp = await resource.hooks?.create?.afterSave({ resource, record, adminUser });
|
|
@@ -593,7 +595,7 @@ class AdminForth {
|
|
|
593
595
|
noAuth: true, // TODO
|
|
594
596
|
method: 'POST',
|
|
595
597
|
path: '/update_record',
|
|
596
|
-
handler: async ({ body }) => {
|
|
598
|
+
handler: async ({ body, adminUser }) => {
|
|
597
599
|
console.log('update_record', body);
|
|
598
600
|
const resource = this.config.resources.find((res) => res.resourceId == body['resourceId']);
|
|
599
601
|
if (!resource) {
|
|
@@ -607,6 +609,7 @@ class AdminForth {
|
|
|
607
609
|
const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
|
|
608
610
|
return { error: `Record with ${primaryKeyColumn.name} ${recordId} not found` };
|
|
609
611
|
}
|
|
612
|
+
const record = body['record'];
|
|
610
613
|
|
|
611
614
|
// execute hook if needed
|
|
612
615
|
if (resource.hooks?.edit?.beforeSave) {
|
|
@@ -621,8 +624,7 @@ class AdminForth {
|
|
|
621
624
|
}
|
|
622
625
|
|
|
623
626
|
const newValues = {};
|
|
624
|
-
const
|
|
625
|
-
for (const col of resource.columns) {
|
|
627
|
+
for (const col of resource.columns.filter((col) => !col.virtual)) {
|
|
626
628
|
if (record[col.name] !== oldRecord[col.name]) {
|
|
627
629
|
newValues[col.name] = connector.setFieldValue(col, record[col.name]);
|
|
628
630
|
}
|
package/package.json
CHANGED
package/spa/index.html
CHANGED
package/spa/src/App.vue
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
<div class="flex items-center ms-3">
|
|
26
26
|
<div>
|
|
27
|
-
<button type="button" class="flex text-sm bg-gray-100 rounded-full focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600" aria-expanded="false" data-dropdown-toggle="dropdown-user">
|
|
27
|
+
<button type="button" class="flex text-sm bg-gray-100 rounded-full focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600 dark:bg-gray-700" aria-expanded="false" data-dropdown-toggle="dropdown-user">
|
|
28
28
|
<span class="sr-only">Open user menu</span>
|
|
29
29
|
<svg class="w-8 h-8 text-gray-400 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
|
|
30
30
|
<path fill-rule="evenodd" d="M12 20a7.966 7.966 0 0 1-5.002-1.756l.002.001v-.683c0-1.794 1.492-3.25 3.333-3.25h3.334c1.84 0 3.333 1.456 3.333 3.25v.683A7.966 7.966 0 0 1 12 20ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10c0 5.5-4.44 9.963-9.932 10h-.138C6.438 21.962 2 17.5 2 12Zm10-5c-1.84 0-3.333 1.455-3.333 3.25S10.159 13.5 12 13.5c1.84 0 3.333-1.455 3.333-3.25S13.841 7 12 7Z" clip-rule="evenodd"/>
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
<aside
|
|
71
71
|
v-if="loggedIn"
|
|
72
72
|
|
|
73
|
-
id="logo-sidebar" class="fixed top-0 left-0 z-
|
|
73
|
+
id="logo-sidebar" class="fixed top-0 left-0 z-10 w-64 h-screen pt-20 transition-transform -translate-x-full bg-white border-r border-gray-200 sm:translate-x-0 dark:bg-gray-800 dark:border-gray-700" aria-label="Sidebar">
|
|
74
74
|
<div class="h-full px-3 pb-4 overflow-y-auto bg-white dark:bg-gray-800">
|
|
75
75
|
<ul class="space-y-2 font-medium">
|
|
76
76
|
<template v-for="(item, i) in coreStore.menu" :key="`menu-${i}`">
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
</div>
|
|
107
107
|
</aside>
|
|
108
108
|
|
|
109
|
-
<div class="p-4 sm:ml-64" v-if="loggedIn">
|
|
109
|
+
<div class="p-4 sm:ml-64 dark:bg-gray-800" v-if="loggedIn">
|
|
110
110
|
<div class="p-0 dark:border-gray-700 mt-14">
|
|
111
111
|
<RouterView/>
|
|
112
112
|
</div>
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div class="mx-auto grid grid-cols-2 gap-4 mb-2">
|
|
4
|
+
<div>
|
|
5
|
+
<label for="start-time" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ label }}</label>
|
|
6
|
+
|
|
7
|
+
<div class="relative">
|
|
8
|
+
<div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5">
|
|
9
|
+
<IconCalendar class="w-4 h-4 text-gray-500 dark:text-gray-400"/>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<input ref="datepickerStartEl" type="text"
|
|
13
|
+
class="bg-gray-50 border leading-none 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"
|
|
14
|
+
placeholder="Start date">
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div>
|
|
20
|
+
<div class="mx-auto grid grid-cols-2 gap-4 mb-2" :class="{hidden: !showTimeInputs}">
|
|
21
|
+
<div>
|
|
22
|
+
<div class="relative">
|
|
23
|
+
<div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5 pointer-events-none">
|
|
24
|
+
<IconTime class="w-4 h-4 text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-700"/>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<input v-model="startTime" type="time" id="start-time" onfocus="this.showPicker()" onclick="this.showPicker()" step="1"
|
|
28
|
+
class="bg-gray-50 border leading-none 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"
|
|
29
|
+
value="00:00" required/>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<button type="button"
|
|
35
|
+
class="text-blue-700 dark:text-blue-500 text-base font-medium hover:underline p-0 inline-flex items-center mb-2"
|
|
36
|
+
@click="toggleTimeInputs">{{ showTimeInputs ? 'Hide time' : 'Show time' }}
|
|
37
|
+
<svg class="w-8 h-8 ms-0.5" :class="{'rotate-180': showTimeInputs}" aria-hidden="true"
|
|
38
|
+
xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
|
39
|
+
fill="none" viewBox="0 0 24 24">
|
|
40
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
41
|
+
d="m8 10 4 4 4-4"/>
|
|
42
|
+
</svg>
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
47
|
+
<script setup>
|
|
48
|
+
import {ref, computed, onMounted, watch, onBeforeUnmount} from 'vue';
|
|
49
|
+
import dayjs from 'dayjs';
|
|
50
|
+
import utc from 'dayjs/plugin/utc';
|
|
51
|
+
|
|
52
|
+
import {useCoreStore} from '@/stores/core';
|
|
53
|
+
|
|
54
|
+
import Datepicker from "flowbite-datepicker/Datepicker";
|
|
55
|
+
import IconCalendar from "@/components/icons/IconCalendar.vue";
|
|
56
|
+
import IconTime from "@/components/icons/IconTime.vue";
|
|
57
|
+
|
|
58
|
+
const coreStore = useCoreStore();
|
|
59
|
+
dayjs.extend(utc)
|
|
60
|
+
|
|
61
|
+
const props = defineProps({
|
|
62
|
+
valueStart: {
|
|
63
|
+
default: undefined
|
|
64
|
+
},
|
|
65
|
+
column: {
|
|
66
|
+
type: Object,
|
|
67
|
+
},
|
|
68
|
+
label: {
|
|
69
|
+
type: String,
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const emit = defineEmits(['update:valueStart']);
|
|
74
|
+
|
|
75
|
+
const datepickerStartEl = ref();
|
|
76
|
+
|
|
77
|
+
const showTimeInputs = ref(false);
|
|
78
|
+
|
|
79
|
+
const startDate = ref('');
|
|
80
|
+
|
|
81
|
+
const startTime = ref('');
|
|
82
|
+
|
|
83
|
+
const datepickerObject = ref('')
|
|
84
|
+
|
|
85
|
+
const start = computed(() => {
|
|
86
|
+
if (!startDate.value) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let date = dayjs(startDate.value);
|
|
91
|
+
|
|
92
|
+
if (startTime.value) {
|
|
93
|
+
date = addTimeToDate(formatTime(startTime.value), date)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return date.utc().toISOString();
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
function updateFromProps() {
|
|
100
|
+
if (props.valueStart === undefined) {
|
|
101
|
+
datepickerStartEl.value.value = '';
|
|
102
|
+
startTime.value = '';
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
datepickerObject.value.setDate(dayjs(props.valueStart).format('DD MMM YYYY'));
|
|
106
|
+
startTime.value = dayjs(props.valueStart).format('HH:mm:ss')
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
onMounted(() => {
|
|
111
|
+
updateFromProps();
|
|
112
|
+
|
|
113
|
+
watch(() => [props.valueStart], (value) => {
|
|
114
|
+
updateFromProps();
|
|
115
|
+
});
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
watch(start, () => {
|
|
119
|
+
//console.log('⚡ emit', start.value)
|
|
120
|
+
emit('update:valueStart', start.value)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
function initDatepickers() {
|
|
124
|
+
datepickerObject.value = new Datepicker(datepickerStartEl.value, { format: 'dd M yyyy' });
|
|
125
|
+
|
|
126
|
+
addChangeDateListener();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function addChangeDateListener() {
|
|
130
|
+
datepickerStartEl.value.addEventListener('changeDate', setStartDate)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function removeChangeDateListener() {
|
|
134
|
+
datepickerStartEl.value.removeEventListener('changeDate', setStartDate);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function setStartDate(event) {
|
|
138
|
+
startDate.value = event.detail.date
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function formatTime(time) {
|
|
142
|
+
return time.split(':').map(Number).length === 2 ? time + ':00' : time;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function addTimeToDate(time, date) {
|
|
146
|
+
const [hours, minutes, seconds] = time.split(':').map(Number)
|
|
147
|
+
return date.hour(hours).minute(minutes).second(seconds)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const toggleTimeInputs = () => {
|
|
151
|
+
showTimeInputs.value = !showTimeInputs.value
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
onMounted(() => {
|
|
155
|
+
initDatepickers();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
onBeforeUnmount(() => {
|
|
159
|
+
removeChangeDateListener();
|
|
160
|
+
})
|
|
161
|
+
</script>
|
|
@@ -1,33 +1,24 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
3
|
<div class="mx-auto grid grid-cols-2 gap-4 mb-2">
|
|
4
|
-
<div>
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
<div class="relative">
|
|
9
|
-
<div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5">
|
|
10
|
-
<IconCalendar class="w-4 h-4 text-gray-500 dark:text-gray-400"/>
|
|
11
|
-
</div>
|
|
12
|
-
|
|
13
|
-
<input ref="datepickerStartEl" type="text"
|
|
14
|
-
class="bg-gray-50 border leading-none 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-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
15
|
-
placeholder="Start date">
|
|
4
|
+
<div class="relative">
|
|
5
|
+
<div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5 pointer-events-none">
|
|
6
|
+
<IconCalendar class="w-4 h-4 text-gray-500 dark:text-gray-400"/>
|
|
16
7
|
</div>
|
|
17
|
-
</div>
|
|
18
|
-
|
|
19
|
-
<div>
|
|
20
|
-
<label for="start-time" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">End date:</label>
|
|
21
8
|
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
9
|
+
<input ref="datepickerStartEl" type="text"
|
|
10
|
+
class="bg-gray-50 border leading-none 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"
|
|
11
|
+
placeholder="From">
|
|
12
|
+
</div>
|
|
26
13
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
14
|
+
<div class="relative">
|
|
15
|
+
<div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5 pointer-events-none">
|
|
16
|
+
<IconCalendar class="w-4 h-4 text-gray-500 dark:text-gray-400"/>
|
|
30
17
|
</div>
|
|
18
|
+
|
|
19
|
+
<input ref="datepickerEndEl" type="text"
|
|
20
|
+
class="bg-gray-50 border leading-none 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"
|
|
21
|
+
placeholder="To">
|
|
31
22
|
</div>
|
|
32
23
|
</div>
|
|
33
24
|
|
|
@@ -35,25 +26,25 @@
|
|
|
35
26
|
<div class="mx-auto grid grid-cols-2 gap-4 mb-2" :class="{hidden: !showTimeInputs}">
|
|
36
27
|
<div>
|
|
37
28
|
<div class="relative">
|
|
38
|
-
<div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5
|
|
39
|
-
<IconTime class="w-4 h-4 text-gray-500 dark:text-gray-400 bg-white"/>
|
|
29
|
+
<div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5">
|
|
30
|
+
<IconTime class="w-4 h-4 text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-700"/>
|
|
40
31
|
</div>
|
|
41
32
|
|
|
42
33
|
<input v-model="startTime" type="time" id="start-time"
|
|
43
|
-
class="bg-gray-50 border leading-none 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-
|
|
44
|
-
|
|
34
|
+
class="bg-gray-50 border leading-none 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"
|
|
35
|
+
value="00:00" required/>
|
|
45
36
|
</div>
|
|
46
37
|
</div>
|
|
47
38
|
|
|
48
39
|
<div>
|
|
49
40
|
<div class="relative">
|
|
50
41
|
<div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5 pointer-events-none">
|
|
51
|
-
<IconTime class="w-4 h-4 text-gray-500 dark:text-gray-400 bg-white"/>
|
|
42
|
+
<IconTime class="w-4 h-4 text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-700"/>
|
|
52
43
|
</div>
|
|
53
44
|
|
|
54
45
|
<input v-model="endTime" type="time" id="end-time"
|
|
55
|
-
class="bg-gray-50 border leading-none 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-
|
|
56
|
-
|
|
46
|
+
class="bg-gray-50 border leading-none 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"
|
|
47
|
+
value="00:00" required/>
|
|
57
48
|
</div>
|
|
58
49
|
</div>
|
|
59
50
|
</div>
|
|
@@ -61,7 +52,7 @@
|
|
|
61
52
|
<button type="button"
|
|
62
53
|
class="text-blue-700 dark:text-blue-500 text-base font-medium hover:underline p-0 inline-flex items-center mb-2"
|
|
63
54
|
@click="toggleTimeInputs">{{ showTimeInputs ? 'Hide time' : 'Show time' }}
|
|
64
|
-
<svg class="w-8 h-8 ms-0.5" :class="{'rotate-180': showTimeInputs}" aria-hidden="true"
|
|
55
|
+
<svg class="w-8 h-8 ms-0.5 relative top-px" :class="{'rotate-180': showTimeInputs}" aria-hidden="true"
|
|
65
56
|
xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
|
66
57
|
fill="none" viewBox="0 0 24 24">
|
|
67
58
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
type="text"
|
|
6
6
|
v-model="search"
|
|
7
7
|
@focus="showDropdown = true"
|
|
8
|
-
class="block w-full pl-3 pr-10 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 sm:text-sm transition duration-150 ease-in-out"
|
|
8
|
+
class="block w-full pl-3 pr-10 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 sm:text-sm transition duration-150 ease-in-out 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"
|
|
9
9
|
:placeholder="selectedItems.length ? '' : 'Select...'"
|
|
10
10
|
/>
|
|
11
11
|
<div class="absolute inset-y-0 left-2 flex items-center pr-2 flex-wrap">
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<!-- drawer component -->
|
|
3
|
-
<div id="drawer-navigation"
|
|
3
|
+
<div id="drawer-navigation"
|
|
4
|
+
|
|
5
|
+
class="fixed top-14 right-0 z-50 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl"
|
|
6
|
+
|
|
7
|
+
:class="show ? 'top-0 transform-none' : ''"
|
|
4
8
|
tabindex="-1" aria-labelledby="drawer-navigation-label"
|
|
5
9
|
:style="{ height: `calc(100vh - 3.5rem)` }"
|
|
6
10
|
>
|
|
7
11
|
<h5 id="drawer-navigation-label" class="text-base font-semibold text-gray-500 uppercase dark:text-gray-400">
|
|
8
12
|
Filters
|
|
9
13
|
|
|
10
|
-
<button type="button"
|
|
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" >
|
|
11
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>
|
|
12
16
|
<span class="sr-only">Close menu</span>
|
|
13
17
|
</button>
|
|
@@ -16,7 +20,7 @@
|
|
|
16
20
|
<div class="py-4 ">
|
|
17
21
|
<ul class="space-y-3 font-medium">
|
|
18
22
|
<li v-for="c in columnsWithFilter" :key="c">
|
|
19
|
-
{{ c.label }}
|
|
23
|
+
<p class="dark:text-gray-400">{{ c.label }}</p>
|
|
20
24
|
|
|
21
25
|
<Dropdown
|
|
22
26
|
v-if="c.type === 'boolean'"
|
|
@@ -35,7 +39,7 @@
|
|
|
35
39
|
|
|
36
40
|
<input
|
|
37
41
|
v-else-if="[ 'string', 'text' ].includes(c.type)"
|
|
38
|
-
type="text" class="w-full py-1 px-2 border border-gray-300 rounded-md"
|
|
42
|
+
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"
|
|
39
43
|
placeholder="Search"
|
|
40
44
|
@input="setFilterItem({ column: c, operator: 'ilike', value: $event.target.value || undefined })"
|
|
41
45
|
:value="getFilterItem({ column: c, operator: 'ilike' })"
|
|
@@ -62,14 +66,14 @@
|
|
|
62
66
|
<input
|
|
63
67
|
type="number" aria-describedby="helper-text-explanation"
|
|
64
68
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-20 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
65
|
-
placeholder="
|
|
69
|
+
placeholder="From"
|
|
66
70
|
@input="setFilterItem({ column: c, operator: 'gte', value: $event.target.value || undefined })"
|
|
67
71
|
:value="getFilterItem({ column: c, operator: 'gte' })"
|
|
68
72
|
>
|
|
69
73
|
<input
|
|
70
74
|
type="number" aria-describedby="helper-text-explanation"
|
|
71
75
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-20 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
72
|
-
placeholder="
|
|
76
|
+
placeholder="To"
|
|
73
77
|
@input="setFilterItem({ column: c, operator: 'lte', value: $event.target.value || undefined})"
|
|
74
78
|
:value="getFilterItem({ column: c, operator: 'lte' })"
|
|
75
79
|
>
|
|
@@ -88,22 +92,34 @@
|
|
|
88
92
|
|
|
89
93
|
</div>
|
|
90
94
|
</div>
|
|
95
|
+
|
|
96
|
+
<div v-if="show" drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"
|
|
97
|
+
@click="$emit('hide')">
|
|
98
|
+
</div>
|
|
91
99
|
</template>
|
|
92
100
|
|
|
93
101
|
<script setup>
|
|
94
|
-
import {
|
|
102
|
+
import { watch, computed } from 'vue'
|
|
95
103
|
import Dropdown from '@/components/Dropdown.vue';
|
|
96
104
|
import CustomDateRangePicker from '@/components/CustomDateRangePicker.vue';
|
|
97
105
|
|
|
98
106
|
// props: columns
|
|
99
107
|
// add support for v-model:filers
|
|
100
|
-
const props = defineProps(['columns', 'filters']);
|
|
101
|
-
const emits = defineEmits(['update:filters']);
|
|
108
|
+
const props = defineProps(['columns', 'filters', 'show', 'columnsMinMax']);
|
|
109
|
+
const emits = defineEmits(['update:filters', 'hide']);
|
|
102
110
|
|
|
103
111
|
const columnsWithFilter = computed(
|
|
104
112
|
() => props.columns?.filter(column => column.showIn.includes('filter')) || []
|
|
105
113
|
);
|
|
106
114
|
|
|
115
|
+
// sync 'body' class 'overflow-hidden' with show prop show
|
|
116
|
+
watch(() => props.show, (show) => {
|
|
117
|
+
if (show) {
|
|
118
|
+
document.body.classList.add('overflow-hidden');
|
|
119
|
+
} else {
|
|
120
|
+
document.body.classList.remove('overflow-hidden');
|
|
121
|
+
}
|
|
122
|
+
});
|
|
107
123
|
|
|
108
124
|
// filters is a array of objects
|
|
109
125
|
// {
|
|
@@ -57,6 +57,12 @@
|
|
|
57
57
|
:value="currentValues[column.name]"
|
|
58
58
|
@input="setCurrentValue(column.name, $event.target.value)"
|
|
59
59
|
>
|
|
60
|
+
<CustomDatePicker
|
|
61
|
+
v-else-if="['datetime'].includes(column.type)"
|
|
62
|
+
:column="column"
|
|
63
|
+
:valueStart="currentValues[column.name]"
|
|
64
|
+
@update:valueStart="setCurrentValue(column.name, $event)"
|
|
65
|
+
/>
|
|
60
66
|
<input
|
|
61
67
|
v-else-if="['decimal', 'float'].includes(column.type)"
|
|
62
68
|
type="number"
|
|
@@ -121,6 +127,7 @@ import Dropdown from '@/components/Dropdown.vue';
|
|
|
121
127
|
import { IconExclamationCircleSolid } from '@iconify-prerendered/vue-flowbite';
|
|
122
128
|
import { initFlowbite } from 'flowbite'
|
|
123
129
|
import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
|
|
130
|
+
import CustomDatePicker from "@/components/CustomDatePicker.vue";
|
|
124
131
|
|
|
125
132
|
const props = defineProps({
|
|
126
133
|
loading: Boolean,
|
package/spa/src/stores/core.ts
CHANGED
|
@@ -42,14 +42,18 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
42
42
|
user.value = resp.user;
|
|
43
43
|
|
|
44
44
|
// find homepage:true in menu recuresively
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
if (import.meta.env.DEV){
|
|
46
|
+
return
|
|
47
|
+
} else {
|
|
48
|
+
const homepage = await findHomepage(menu.value);
|
|
49
|
+
if (homepage) {
|
|
50
|
+
if (homepage.resourceId) {
|
|
51
|
+
// redirect to homepage
|
|
52
|
+
router.push({ name: 'resource-list', params: { resourceId: homepage.resourceId } });
|
|
53
|
+
} else {
|
|
54
|
+
// redirect to path
|
|
55
|
+
router.push(homepage.path);
|
|
56
|
+
}
|
|
53
57
|
}
|
|
54
58
|
}
|
|
55
59
|
}
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<script setup>
|
|
38
38
|
|
|
39
39
|
import { ref, computed, onMounted } from 'vue';
|
|
40
|
-
import { useRoute } from 'vue-router';
|
|
40
|
+
import { useRoute, useRouter } from 'vue-router';
|
|
41
41
|
import { useCoreStore } from '@/stores/core';
|
|
42
42
|
import ResourceForm from '@/components/ResourceForm.vue';
|
|
43
43
|
import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
@@ -51,6 +51,7 @@ const isValid = ref(false);
|
|
|
51
51
|
const validating = ref(false);
|
|
52
52
|
|
|
53
53
|
const route = useRoute();
|
|
54
|
+
const router = useRouter();
|
|
54
55
|
|
|
55
56
|
const loading = ref(false);
|
|
56
57
|
|
|
@@ -78,6 +79,13 @@ onMounted(async () => {
|
|
|
78
79
|
});
|
|
79
80
|
|
|
80
81
|
async function saveRecord() {
|
|
82
|
+
if (!isValid.value) {
|
|
83
|
+
validating.value = true;
|
|
84
|
+
return;
|
|
85
|
+
} else {
|
|
86
|
+
validating.value = false;
|
|
87
|
+
}
|
|
88
|
+
|
|
81
89
|
saving.value = true;
|
|
82
90
|
await callAdminForthApi({
|
|
83
91
|
method: 'POST',
|
|
@@ -89,7 +97,7 @@ async function saveRecord() {
|
|
|
89
97
|
},
|
|
90
98
|
});
|
|
91
99
|
saving.value = false;
|
|
92
|
-
router.push({ name: 'resource
|
|
100
|
+
router.push({ name: 'resource-show', params: { resourceId: route.params.resourceId, primaryKey: coreStore.record[coreStore.primaryKey] } });
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
</script>
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="relative">
|
|
3
|
-
<
|
|
3
|
+
<Teleport to="body">
|
|
4
|
+
<Filters
|
|
5
|
+
:columns="coreStore.resourceColumns"
|
|
6
|
+
v-model:filters="filters"
|
|
7
|
+
:columnsMinMax="columnsMinMax" :show="filtersShow"
|
|
8
|
+
@hide="filtersShow = false"
|
|
9
|
+
/>
|
|
10
|
+
</Teleport>
|
|
4
11
|
|
|
5
12
|
<BreadcrumbsWithButtons>
|
|
6
13
|
<button @click="()=>{checkboxes = []}"
|
|
@@ -12,7 +19,7 @@
|
|
|
12
19
|
<IconBanOutline class="w-5 h-5 " />
|
|
13
20
|
<div :id="`tooltip-remove-all`"
|
|
14
21
|
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">
|
|
15
|
-
Remove
|
|
22
|
+
Remove selection
|
|
16
23
|
<div class="tooltip-arrow" data-popper-arrow></div>
|
|
17
24
|
</div>
|
|
18
25
|
</button>
|
|
@@ -34,7 +41,7 @@
|
|
|
34
41
|
</RouterLink>
|
|
35
42
|
<button
|
|
36
43
|
class="flex gap-1 items-center py-1 px-3 me-2 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
|
37
|
-
|
|
44
|
+
@click="()=>{filtersShow = !filtersShow}"
|
|
38
45
|
>
|
|
39
46
|
<IconFilterOutline class="w-4 h-4 me-2" />
|
|
40
47
|
Filter
|
|
@@ -46,49 +53,32 @@
|
|
|
46
53
|
|
|
47
54
|
|
|
48
55
|
</BreadcrumbsWithButtons>
|
|
56
|
+
|
|
49
57
|
<!-- table -->
|
|
50
58
|
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
|
|
51
59
|
|
|
52
60
|
|
|
53
61
|
|
|
54
62
|
<!-- skelet loader -->
|
|
55
|
-
<div v-if="!coreStore.resourceColumns"
|
|
63
|
+
<div role="status" v-if="!coreStore.resourceColumns"
|
|
56
64
|
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">
|
|
57
|
-
<div class="flex items-center justify-between">
|
|
58
|
-
<div>
|
|
59
|
-
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
|
|
60
|
-
<div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
|
|
61
|
-
</div>
|
|
62
|
-
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
|
|
63
|
-
</div>
|
|
64
|
-
<div class="flex items-center justify-between pt-4">
|
|
65
|
-
<div>
|
|
66
|
-
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
|
|
67
|
-
<div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
|
|
68
|
-
</div>
|
|
69
|
-
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
|
|
70
|
-
</div>
|
|
71
|
-
<div class="flex items-center justify-between pt-4">
|
|
72
|
-
<div>
|
|
73
|
-
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
|
|
74
|
-
<div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
|
|
75
|
-
</div>
|
|
76
|
-
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
|
|
77
|
-
</div>
|
|
78
|
-
<div class="flex items-center justify-between pt-4">
|
|
65
|
+
<div class="flex items-center justify-between h-16">
|
|
79
66
|
<div>
|
|
80
67
|
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
|
|
81
68
|
<div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
|
|
82
69
|
</div>
|
|
83
70
|
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
|
|
84
71
|
</div>
|
|
85
|
-
<div
|
|
72
|
+
<div class="flex items-center justify-between h-16 pt-4" v-for="i in new Array(10)" >
|
|
86
73
|
<div>
|
|
87
74
|
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
|
|
88
75
|
<div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
|
|
89
76
|
</div>
|
|
90
77
|
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
|
|
91
78
|
</div>
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
92
82
|
<span class="sr-only">Loading...</span>
|
|
93
83
|
</div>
|
|
94
84
|
|
|
@@ -105,13 +95,16 @@
|
|
|
105
95
|
|
|
106
96
|
|
|
107
97
|
<th v-for="c in columnsListed" scope="col" class="px-6 py-3">
|
|
108
|
-
<div class="flex items-center">
|
|
98
|
+
<div @click="onSortButtonClick(c.name)" class="flex items-center">
|
|
109
99
|
{{ c.label }}
|
|
110
|
-
|
|
111
|
-
|
|
100
|
+
|
|
101
|
+
<div :style = "{'color':ascArr.includes(c.name)?'green':descArr.includes(c.name)?'red':'currentColor'}" ><svg class="w-3 h-3 ms-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
|
|
102
|
+
fill='currentColor'
|
|
103
|
+
|
|
104
|
+
viewBox="0 0 24 24">
|
|
112
105
|
<path
|
|
113
106
|
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" />
|
|
114
|
-
</svg></
|
|
107
|
+
</svg></div>
|
|
115
108
|
</div>
|
|
116
109
|
</th>
|
|
117
110
|
|
|
@@ -227,22 +220,14 @@
|
|
|
227
220
|
Delete
|
|
228
221
|
<div class="tooltip-arrow" data-popper-arrow></div>
|
|
229
222
|
</div>
|
|
230
|
-
|
|
231
|
-
|
|
232
223
|
</td>
|
|
233
224
|
</tr>
|
|
234
|
-
|
|
235
|
-
|
|
236
225
|
</tbody>
|
|
237
226
|
</table>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
227
|
</div>
|
|
242
|
-
|
|
243
228
|
<!-- pagination -->
|
|
244
229
|
<div class="flex flex-col items-center mt-4 xs:flex-row xs:justify-between xs:items-center"
|
|
245
|
-
v-if="rows && totalRows > 0"
|
|
230
|
+
v-if="rows && totalRows >= pageSize && totalRows > 0"
|
|
246
231
|
>
|
|
247
232
|
<!-- Help text -->
|
|
248
233
|
<span class="text-sm text-gray-700 dark:text-gray-400">
|
|
@@ -315,6 +300,8 @@ import {
|
|
|
315
300
|
|
|
316
301
|
import Filters from '@/components/Filters.vue';
|
|
317
302
|
|
|
303
|
+
const filtersShow = ref(false);
|
|
304
|
+
|
|
318
305
|
|
|
319
306
|
const coreStore = useCoreStore();
|
|
320
307
|
const modalStore = useModalStore();
|
|
@@ -348,11 +335,37 @@ const allFromThisPageChecked = computed(() => {
|
|
|
348
335
|
if (!rows.value) return false;
|
|
349
336
|
return rows.value.every((r) => checkboxes.value.includes(r.id));
|
|
350
337
|
});
|
|
338
|
+
const ascArr = computed(() => sort.value.filter((s) => s.direction === 'asc').map((s) => s.field));
|
|
339
|
+
const descArr = computed(() => sort.value.filter((s) => s.direction === 'desc').map((s) => s.field));
|
|
351
340
|
|
|
352
|
-
watch([page
|
|
341
|
+
watch([page], async () => {
|
|
353
342
|
await init();
|
|
354
343
|
});
|
|
355
344
|
|
|
345
|
+
watch([filters], async () => {
|
|
346
|
+
page.value = 1;
|
|
347
|
+
await getList();
|
|
348
|
+
}, { deep: true });
|
|
349
|
+
|
|
350
|
+
watch([sort], async () => {
|
|
351
|
+
await init();
|
|
352
|
+
}, { deep: true });
|
|
353
|
+
|
|
354
|
+
function onSortButtonClick(field) {
|
|
355
|
+
const sortIndex = sort.value.findIndex((s) => s.field === field);
|
|
356
|
+
console.log('sortIndex', sortIndex);
|
|
357
|
+
if (sortIndex === -1) {
|
|
358
|
+
sort.value = [{ field, direction: 'asc' }, ...sort.value];
|
|
359
|
+
} else {
|
|
360
|
+
const sortField = sort.value[sortIndex];
|
|
361
|
+
if (sortField.direction === 'asc') {
|
|
362
|
+
sort.value[sortIndex].direction = 'desc';
|
|
363
|
+
} else {
|
|
364
|
+
sort.value.splice(sortIndex, 1);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
356
369
|
async function getList() {
|
|
357
370
|
rows.value = null;
|
|
358
371
|
const data = await callAdminForthApi({
|
|
@@ -366,7 +379,6 @@ async function getList() {
|
|
|
366
379
|
sort: sort.value,
|
|
367
380
|
}
|
|
368
381
|
});
|
|
369
|
-
console.log('coreStore.resourceColumns', coreStore.resourceColumns);
|
|
370
382
|
rows.value = data.data?.map(row => {
|
|
371
383
|
row._primaryKeyValue = row[coreStore.resourceColumns.find(c => c.primaryKey).name];
|
|
372
384
|
return row;
|
|
@@ -459,6 +471,7 @@ onMounted(async () => {
|
|
|
459
471
|
watch(() => route.params.resourceId, async () => {
|
|
460
472
|
filters.value = [];
|
|
461
473
|
checkboxes.value = [];
|
|
474
|
+
sort.value = [];
|
|
462
475
|
await init();
|
|
463
476
|
});
|
|
464
477
|
|
|
@@ -41,8 +41,7 @@
|
|
|
41
41
|
</tr>
|
|
42
42
|
</thead>
|
|
43
43
|
<tbody>
|
|
44
|
-
<tr v-for="column in coreStore.resourceColumns?.filter(c => c.showIn.includes('show'))
|
|
45
|
-
" :key="column.name"
|
|
44
|
+
<tr v-for="column in coreStore.resourceColumns?.filter(c => c.showIn.includes('show'))" :key="column.name"
|
|
46
45
|
class="odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 border-b dark:border-gray-700"
|
|
47
46
|
>
|
|
48
47
|
<td class="px-6 py-4 whitespace-nowrap">
|