adminforth 1.0.10 → 1.0.11

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.
@@ -235,12 +235,11 @@ class SQLiteConnector {
235
235
  }
236
236
 
237
237
  async updateRecord({ resource, recordId, record, newValues }) {
238
- const columns = Object.keys(newValues).map((col) => col).join(', ');
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 ${columns} = ${placeholders} WHERE ${this.getPrimaryKey(resource)} = ?`
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,
@@ -593,7 +597,7 @@ class AdminForth {
593
597
  noAuth: true, // TODO
594
598
  method: 'POST',
595
599
  path: '/update_record',
596
- handler: async ({ body }) => {
600
+ handler: async ({ body, adminUser }) => {
597
601
  console.log('update_record', body);
598
602
  const resource = this.config.resources.find((res) => res.resourceId == body['resourceId']);
599
603
  if (!resource) {
@@ -607,6 +611,7 @@ class AdminForth {
607
611
  const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
608
612
  return { error: `Record with ${primaryKeyColumn.name} ${recordId} not found` };
609
613
  }
614
+ const record = body['record'];
610
615
 
611
616
  // execute hook if needed
612
617
  if (resource.hooks?.edit?.beforeSave) {
@@ -621,8 +626,7 @@ class AdminForth {
621
626
  }
622
627
 
623
628
  const newValues = {};
624
- const record = body['record'];
625
- for (const col of resource.columns) {
629
+ for (const col of resource.columns.filter((col) => !col.virtual)) {
626
630
  if (record[col.name] !== oldRecord[col.name]) {
627
631
  newValues[col.name] = connector.setFieldValue(col, record[col.name]);
628
632
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/spa/index.html CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  </head>
19
19
  <body>
20
- <div id="app"></div>
20
+ <div id="app" class="min-h-screen dark:bg-gray-800"></div>
21
21
  <script type="module" src="/src/main.ts"></script>
22
22
  </body>
23
23
  </html>
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-40 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">
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>
@@ -2,7 +2,7 @@
2
2
  <div>
3
3
  <div class="mx-auto grid grid-cols-2 gap-4 mb-2">
4
4
  <div>
5
- <label for="start-time" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Start
5
+ <label for="start-time" class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400">Start
6
6
  date:</label>
7
7
 
8
8
  <div class="relative">
@@ -11,13 +11,13 @@
11
11
  </div>
12
12
 
13
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"
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-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
15
15
  placeholder="Start date">
16
16
  </div>
17
17
  </div>
18
18
 
19
19
  <div>
20
- <label for="start-time" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">End date:</label>
20
+ <label for="start-time" class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400">End date:</label>
21
21
 
22
22
  <div class="relative">
23
23
  <div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5">
@@ -25,7 +25,7 @@
25
25
  </div>
26
26
 
27
27
  <input ref="datepickerEndEl" type="text"
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-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
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
29
  placeholder="End date">
30
30
  </div>
31
31
  </div>
@@ -36,24 +36,24 @@
36
36
  <div>
37
37
  <div class="relative">
38
38
  <div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5 pointer-events-none">
39
- <IconTime class="w-4 h-4 text-gray-500 dark:text-gray-400 bg-white"/>
39
+ <IconTime class="w-4 h-4 text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-700"/>
40
40
  </div>
41
41
 
42
42
  <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-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
44
- min="09:00" max="18:00" value="00:00" required/>
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-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
44
+ value="00:00" required/>
45
45
  </div>
46
46
  </div>
47
47
 
48
48
  <div>
49
49
  <div class="relative">
50
50
  <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"/>
51
+ <IconTime class="w-4 h-4 text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-700"/>
52
52
  </div>
53
53
 
54
54
  <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-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
56
- min="09:00" max="18:00" value="00:00" required/>
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-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
56
+ value="00:00" required/>
57
57
  </div>
58
58
  </div>
59
59
  </div>
@@ -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" 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"
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" data-drawer-hide="drawer-navigation" aria-controls="drawer-navigation" 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" >
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="from"
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="to"
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 { ref, computed } from 'vue'
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']);
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
  // {
@@ -5,7 +5,7 @@
5
5
  :class="{
6
6
  'px-4': isChild,
7
7
  'px-2': !isChild,
8
- 'bg-blue-100': item.resourceId ?
8
+ 'bg-blue-100 dark:bg-gray-700': item.resourceId ?
9
9
  ($route.params.resourceId === item.resourceId && $route.name === 'resource-list') :
10
10
  ($route.name === item.path)
11
11
  }"
@@ -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,
@@ -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
- const homepage = await findHomepage(menu.value);
46
- if (homepage) {
47
- if (homepage.resourceId) {
48
- // redirect to homepage
49
- router.push({ name: 'resource-list', params: { resourceId: homepage.resourceId } });
50
- } else {
51
- // redirect to path
52
- router.push(homepage.path);
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
 
@@ -89,7 +90,7 @@ async function saveRecord() {
89
90
  },
90
91
  });
91
92
  saving.value = false;
92
- router.push({ name: 'resource.show', params: { resourceId: route.params.resourceId, primaryKey: coreStore.record[coreStore.primaryKey] } });
93
+ router.push({ name: 'resource-show', params: { resourceId: route.params.resourceId, primaryKey: coreStore.record[coreStore.primaryKey] } });
93
94
  }
94
95
 
95
96
  </script>
@@ -1,6 +1,13 @@
1
1
  <template>
2
2
  <div class="relative">
3
- <Filters :columns="coreStore.resourceColumns" v-model:filters="filters" :columnsMinMax="columnsMinMax" />
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 selection
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
- data-drawer-target="drawer-navigation" data-drawer-show="drawer-navigation" aria-controls="drawer-navigation" data-drawer-placement="right"
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" role="status"
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 class="flex items-center justify-between pt-4">
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
- <a href="#"><svg class="w-3 h-3 ms-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
111
- fill="currentColor" viewBox="0 0 24 24">
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></a>
107
+ </svg></div>
115
108
  </div>
116
109
  </th>
117
110
 
@@ -227,19 +220,11 @@
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
230
  v-if="rows && totalRows > 0"
@@ -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, filters, sort], async () => {
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({
@@ -459,6 +472,7 @@ onMounted(async () => {
459
472
  watch(() => route.params.resourceId, async () => {
460
473
  filters.value = [];
461
474
  checkboxes.value = [];
475
+ sort.value = [];
462
476
  await init();
463
477
  });
464
478
 
@@ -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">