adminforth 1.0.11 → 1.0.15

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 CHANGED
@@ -28,7 +28,7 @@ class AdminForthAuth {
28
28
  }
29
29
 
30
30
  // issue JWT token
31
- const expiresIn = process.env.ADMINFORTH_AUTH_EXPIRESIN || '1h';
31
+ const expiresIn = process.env.ADMINFORTH_AUTH_EXPIRESIN || '24h';
32
32
  return jwt.sign(payload, secret, { expiresIn });
33
33
  }
34
34
 
package/index.js CHANGED
@@ -7,6 +7,7 @@ import CodeInjector from './modules/codeInjector.js';
7
7
  import { guessLabelFromName } from './modules/utils.js';
8
8
  import ExpressServer from './servers/express.js';
9
9
  import {v1 as uuid} from 'uuid';
10
+ import fs from 'fs';
10
11
 
11
12
 
12
13
  import { AdminForthFilterOperators, AdminForthTypes } from './types.js';
@@ -63,6 +64,14 @@ class AdminForth {
63
64
  }
64
65
  }
65
66
 
67
+ if (!this.config.customization) {
68
+ this.config.customization = {};
69
+ }
70
+
71
+ if (!this.config.customization.customComponentsDir) {
72
+ this.config.customization.customComponentsDir = './custom';
73
+ }
74
+
66
75
 
67
76
  const errors = [];
68
77
  if (!this.config.baseUrl) {
@@ -176,6 +185,17 @@ class AdminForth {
176
185
  if (item.component && !item.path) {
177
186
  errors.push(`Menu item with component must have path : ${JSON.stringify(item)}`);
178
187
  }
188
+ // make sure component starts with @@
189
+ if (item.component) {
190
+ if (!item.component.startsWith('@@')) {
191
+ errors.push(`Menu item component must start with @@ : ${JSON.stringify(item)}`);
192
+ }
193
+
194
+ const path = item.component.replace('@@', this.config.customization.customComponentsDir);
195
+ if ( !fs.existsSync(path) ) {
196
+ errors.push(`Menu item component "${item.component.replace('@@', '')}" does not exist in "${this.config.customization.customComponentsDir}"`);
197
+ }
198
+ }
179
199
 
180
200
  if (item.homepage) {
181
201
  homepages++;
@@ -188,6 +208,7 @@ class AdminForth {
188
208
  }
189
209
  });
190
210
  };
211
+ browseMenu(this.config.menu);
191
212
 
192
213
  }
193
214
 
@@ -526,6 +547,20 @@ class AdminForth {
526
547
  if (!resource) {
527
548
  return { error: `Resource '${body['resourceId']}' not found` };
528
549
  }
550
+
551
+ const record = body['record'];
552
+ // execute hook if needed
553
+ if (resource.hooks?.create?.beforeSave) {
554
+ const resp = await resource.hooks?.create?.beforeSave({ resource, record, adminUser });
555
+ if (!resp || (!resp.ok && !resp.error)) {
556
+ throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
557
+ }
558
+
559
+ if (resp.error) {
560
+ return { error: resp.error };
561
+ }
562
+ }
563
+
529
564
  for (const column of resource.columns) {
530
565
  if (column.fillOnCreate) {
531
566
  if (body['record'][column.name] === undefined) {
@@ -551,31 +586,15 @@ class AdminForth {
551
586
  }
552
587
  }
553
588
  }
554
- const connector = this.connectors[resource.dataSource];
555
-
556
- const record = body['record'];
557
-
558
- // execute hook if needed
559
- if (resource.hooks?.create?.beforeSave) {
560
- const resp = await resource.hooks?.create?.beforeSave({ resource, record, adminUser });
561
- if (!resp || (!resp.ok && !resp.error)) {
562
- throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
563
- }
564
-
565
- if (resp.error) {
566
- return { error: resp.error };
567
- }
568
- }
569
589
 
570
590
  // remove virtual columns from record
571
591
  for (const column of resource.columns.filter((col) => col.virtual)) {
572
- if (record[column.name]) {
573
- delete record[column.name];
574
- }
592
+ if (record[column.name]) {
593
+ delete record[column.name];
594
+ }
575
595
  }
576
-
596
+ const connector = this.connectors[resource.dataSource];
577
597
  await connector.createRecord({ resource, record });
578
-
579
598
  // execute hook if needed
580
599
  if (resource.hooks?.create?.afterSave) {
581
600
  const resp = await resource.hooks?.create?.afterSave({ resource, record, adminUser });
@@ -87,7 +87,6 @@ class CodeInjector {
87
87
  await fs.promises.mkdir(spaTmpPath, { recursive: true });
88
88
  }
89
89
 
90
- const customFiles = [];
91
90
  const icons = [];
92
91
  let routes = '';
93
92
 
@@ -97,11 +96,10 @@ class CodeInjector {
97
96
  icons.push(item.icon);
98
97
  }
99
98
  if (item.component) {
100
- customFiles.push(item.component);
101
99
  routes += `{
102
100
  path: '${item.path}',
103
101
  name: '${item.path}',
104
- component: import('@/custom/${item.component}'),
102
+ component: import('${item.component}'),
105
103
  },\n`
106
104
  }
107
105
  if (item.children) {
@@ -111,10 +109,6 @@ class CodeInjector {
111
109
  };
112
110
  collectAssetsFromMenu(this.adminforth.config.menu);
113
111
 
114
- if (this.adminforth.config.customization?.vueUsesFile) {
115
- customFiles.push(this.adminforth.config.customization.vueUsesFile);
116
- }
117
-
118
112
  // create spa_tmp folder, or ignore if it exists
119
113
  try {
120
114
  await fs.promises.mkdir(spaTmpPath);
@@ -136,12 +130,12 @@ class CodeInjector {
136
130
  },
137
131
  });
138
132
 
139
- // copy custom files
140
- await Promise.all(customFiles.map(async (file) => {
141
- const src = path.join(file);
142
- const dest = path.join(spaTmpPath, 'src', 'custom', file);
143
- await fsExtra.copy(src, dest);
144
- }))
133
+ // copy whole custom directory
134
+ if (this.adminforth.config.customization?.customComponentsDir) {
135
+ await fsExtra.copy(this.adminforth.config.customization.customComponentsDir, path.join(spaTmpPath, 'src', 'custom'), {
136
+ recursive: true,
137
+ });
138
+ }
145
139
  }
146
140
 
147
141
  //collect all 'icon' fields from resources bulkActions
@@ -184,7 +178,7 @@ class CodeInjector {
184
178
  let imports = iconImports + '\n';
185
179
 
186
180
  if (this.adminforth.config.customization?.vueUsesFile) {
187
- imports += `import addCustomUses from '@/custom/${this.adminforth.config.customization.vueUsesFile}';\n`;
181
+ imports += `import addCustomUses from '${this.adminforth.config.customization.vueUsesFile}';\n`;
188
182
  }
189
183
 
190
184
  // inject that code into spa_tmp/src/App.vue
@@ -286,11 +280,61 @@ class CodeInjector {
286
280
  });
287
281
  }
288
282
 
283
+ async watchCustomComponentsForCopy() {
284
+ const customComponentsDir = this.adminforth.config.customization.customComponentsDir;
285
+
286
+ // check if folder exists
287
+ try {
288
+ await fs.promises.access(customComponentsDir, fs.constants.F_OK);
289
+ } catch (e) {
290
+ return;
291
+ }
292
+
293
+ // get all subdirs
294
+ const directories = [];
295
+ const collectDirectories = async (dir) => {
296
+ directories.push(dir);
297
+
298
+ const files = await fs.promises.readdir(dir, { withFileTypes: true });
299
+ for (const file of files) {
300
+ if (file.isDirectory()) {
301
+ await collectDirectories(path.join(dir, file.name));
302
+ }
303
+ }
304
+ };
305
+
306
+ await collectDirectories(customComponentsDir);
307
+
308
+ const watcher = filewatcher();
309
+ directories.forEach((dir) => {
310
+ watcher.add(dir);
311
+ });
312
+
313
+ watcher.on(
314
+ 'change',
315
+ async (file) => {
316
+ // copy one file
317
+ // TODO: non optimal, copy only changed file, test on both nested and parent dir
318
+ await fsExtra.copy(this.adminforth.config.customization.customComponentsDir, path.join(spaTmpPath, 'src', 'custom'), {
319
+ recursive: true,
320
+ });
321
+ }
322
+ )
323
+ process.on('exit', () => {
324
+ watcher.removeAll();
325
+ });
326
+ }
327
+
289
328
  async bundleNow({hotReload = false, verbose = false}) {
290
329
  this.adminforth.config.runningHotReload = hotReload;
291
330
 
292
331
  await this.prepareSources({ verbose });
293
- await this.watchForReprepare();
332
+
333
+ if (hotReload) {
334
+ await this.watchForReprepare();
335
+ await this.watchCustomComponentsForCopy();
336
+ }
337
+
294
338
  console.log('AdminForth bundling');
295
339
 
296
340
  const cwd = CodeInjector.SPA_TMP_PATH;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.0.11",
3
+ "version": "1.0.15",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/spa/src/App.vue CHANGED
@@ -32,7 +32,7 @@
32
32
 
33
33
  </button>
34
34
  </div>
35
- <div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded shadow dark:bg-gray-700 dark:divide-gray-600" id="dropdown-user">
35
+ <div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded shadow dark:bg-gray-700 dark:divide-gray-600 dark:shadow-xl" id="dropdown-user">
36
36
  <div class="px-4 py-3" role="none">
37
37
  <p class="text-sm text-gray-900 dark:text-white" role="none" v-if="coreStore.userFullname">
38
38
  {{ coreStore.userFullname }}
@@ -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
- <label for="start-time" class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400">Start
6
- date:</label>
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-700 dark:border-gray-600 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-gray-400">End date:</label>
21
8
 
22
- <div class="relative">
23
- <div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5">
24
- <IconCalendar class="w-4 h-4 text-gray-500 dark:text-gray-400"/>
25
- </div>
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
- <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-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
29
- placeholder="End date">
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,7 +26,7 @@
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 pointer-events-none">
29
+ <div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5">
39
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
 
@@ -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"
@@ -2,7 +2,7 @@
2
2
  <!-- drawer component -->
3
3
  <div id="drawer-navigation"
4
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"
5
+ class="fixed top-14 right-0 z-40 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl"
6
6
 
7
7
  :class="show ? 'top-0 transform-none' : ''"
8
8
  tabindex="-1" aria-labelledby="drawer-navigation-label"
@@ -105,7 +105,7 @@ import CustomDateRangePicker from '@/components/CustomDateRangePicker.vue';
105
105
 
106
106
  // props: columns
107
107
  // add support for v-model:filers
108
- const props = defineProps(['columns', 'filters', 'show']);
108
+ const props = defineProps(['columns', 'filters', 'show', 'columnsMinMax']);
109
109
  const emits = defineEmits(['update:filters', 'hide']);
110
110
 
111
111
  const columnsWithFilter = computed(
@@ -2,7 +2,7 @@
2
2
  <div>
3
3
 
4
4
  <div
5
- class="relative shadow-md sm:rounded-lg"
5
+ class="relative shadow-md sm:rounded-lg dark:shadow-2xl"
6
6
  >
7
7
  <form autocomplete="off" @submit.prevent>
8
8
  <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
@@ -72,7 +72,6 @@ export const useCoreStore = defineStore('core', () => {
72
72
  }
73
73
 
74
74
  async function fetchColumns({ resourceId }) {
75
- console.log('fetchColumns 1', resourceId, resourceColumnsId.value);
76
75
  if (resourceColumnsId.value === resourceId && resourceColumns.value) {
77
76
  // already fetched
78
77
  return;
@@ -79,6 +79,13 @@ onMounted(async () => {
79
79
  });
80
80
 
81
81
  async function saveRecord() {
82
+ if (!isValid.value) {
83
+ validating.value = true;
84
+ return;
85
+ } else {
86
+ validating.value = false;
87
+ }
88
+
82
89
  saving.value = true;
83
90
  await callAdminForthApi({
84
91
  method: 'POST',
@@ -1,67 +1,76 @@
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"
4
+ <Filters
5
+ :columns="coreStore.resourceColumns"
6
+ v-model:filters="filters"
7
+ :columnsMinMax="columnsMinMax" :show="filtersShow"
8
8
  @hide="filtersShow = false"
9
9
  />
10
10
  </Teleport>
11
11
 
12
12
  <BreadcrumbsWithButtons>
13
- <button @click="()=>{checkboxes = []}"
13
+ <button
14
+ @click="()=>{checkboxes = []}"
14
15
  v-if="checkboxes.length"
15
- :data-tooltip-target="`tooltip-remove-all`"
16
+ data-tooltip-target="tooltip-remove-all"
16
17
  data-tooltip-placement="bottom"
17
18
  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"
18
- >
19
- <IconBanOutline class="w-5 h-5 " />
20
- <div :id="`tooltip-remove-all`"
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">
22
- Remove selection
23
- <div class="tooltip-arrow" data-popper-arrow></div>
24
- </div>
19
+ >
20
+ <IconBanOutline class="w-5 h-5 "/>
21
+
22
+ <div id="tooltip-remove-all" role="tooltip"
23
+ 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">
24
+ Remove selection
25
+ <div class="tooltip-arrow" data-popper-arrow></div>
26
+ </div>
25
27
  </button>
26
- <button v-if="checkboxes.length" v-for="(action,i) in allCheckedActions" :key="action.id" @click="startBulkAction(action.id)"
28
+
29
+ <button
30
+ v-if="checkboxes.length" v-for="(action,i) in allCheckedActions" :key="action.id"
31
+ @click="startBulkAction(action.id)"
27
32
  class="flex gap-1 items-center py-1 px-3 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"
28
33
  :class="{'bg-red-100 text-red-800 border-red-400 dark:bg-red-700 dark:text-red-400 dark:border-red-400':action.state==='danger', 'bg-green-100 text-green-800 border-green-400 dark:bg-green-700 dark:text-green-400 dark:border-green-400':action.state==='success',
29
34
  'bg-blue-100 text-blue-800 border-blue-400 dark:bg-blue-700 dark:text-blue-400 dark:border-blue-400':action.state==='active',
30
35
  }"
31
- >
32
- <component v-if="action.icon" :is="getIcon(action.icon)" class="w-5 h-5 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white" ></component>
36
+ >
37
+ <component
38
+ v-if="action.icon"
39
+ :is="getIcon(action.icon)"
40
+ class="w-5 h-5 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white"></component>
33
41
 
34
42
  {{ `${action.label} (${checkboxes.length})` }}
35
43
  </button>
36
- <RouterLink :to="{ name: 'resource-create', params: { resourceId: $route.params.resourceId } }"
44
+
45
+ <RouterLink
46
+ :to="{ name: 'resource-create', params: { resourceId: $route.params.resourceId } }"
37
47
  class="flex items-center py-1 px-3 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"
38
48
  >
39
- <IconPlusOutline class="w-4 h-4 me-2" />
40
- Create
49
+ <IconPlusOutline class="w-4 h-4 me-2"/>
50
+ Create
41
51
  </RouterLink>
42
- <button
52
+
53
+ <button
43
54
  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"
44
55
  @click="()=>{filtersShow = !filtersShow}"
45
56
  >
46
- <IconFilterOutline class="w-4 h-4 me-2" />
47
- Filter
48
- <span class="bg-red-100 text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400"
49
- v-if="filters.length">
57
+ <IconFilterOutline class="w-4 h-4 me-2"/>
58
+ Filter
59
+ <span
60
+ class="bg-red-100 text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400"
61
+ v-if="filters.length">
50
62
  {{ filters.length }}
51
63
  </span>
52
64
  </button>
53
-
54
-
55
65
  </BreadcrumbsWithButtons>
56
66
 
57
67
  <!-- table -->
58
- <div class="relative overflow-x-auto shadow-md sm:rounded-lg">
68
+ <div class="relative overflow-x-auto shadow-md sm:rounded-lg dark:shadow-2xl">
59
69
 
60
-
61
70
 
62
71
  <!-- skelet loader -->
63
- <div role="status" v-if="!coreStore.resourceColumns"
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">
72
+ <div role="status" v-if="!coreStore.resourceColumns"
73
+ 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">
65
74
  <div class="flex items-center justify-between h-16">
66
75
  <div>
67
76
  <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
@@ -69,234 +78,262 @@
69
78
  </div>
70
79
  <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
71
80
  </div>
72
- <div class="flex items-center justify-between h-16 pt-4" v-for="i in new Array(10)" >
81
+
82
+ <div class="flex items-center justify-between h-16 pt-4" v-for="i in new Array(10)">
73
83
  <div>
74
84
  <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
75
85
  <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
76
86
  </div>
77
87
  <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
78
88
  </div>
79
-
80
-
81
-
82
89
  <span class="sr-only">Loading...</span>
83
90
  </div>
84
91
 
85
92
  <table v-else class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
86
93
  <thead class="text-xs text-gray-700 bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
87
- <tr>
88
- <th scope="col" class="p-4">
89
- <div v-if="rows && rows.length" class="flex items-center">
90
- <input id="checkbox-all-search" type="checkbox" :checked="allFromThisPageChecked" @change="selectAll()"
91
- 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">
92
- <label for="checkbox-all-search" class="sr-only">checkbox</label>
93
- </div>
94
- </th>
95
-
96
-
97
- <th v-for="c in columnsListed" scope="col" class="px-6 py-3">
98
- <div @click="onSortButtonClick(c.name)" class="flex items-center">
99
- {{ c.label }}
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
-
94
+ <tr>
95
+ <th scope="col" class="p-4">
96
+ <div v-if="rows && rows.length" class="flex items-center">
97
+ <input id="checkbox-all-search" type="checkbox" :checked="allFromThisPageChecked" @change="selectAll()"
98
+ 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">
99
+ <label for="checkbox-all-search" class="sr-only">checkbox</label>
100
+ </div>
101
+ </th>
102
+
103
+ <th v-for="c in columnsListed" scope="col" class="px-6 py-3">
104
+ <div @click="() => c.sortable && onSortButtonClick(c.name)" class="flex items-center cursor-pointer">
105
+ {{ c.label }}
106
+
107
+ <div v-if="c.sortable"
108
+ :style="{ 'color':ascArr.includes(c.name)?'green':descArr.includes(c.name)?'red':'currentColor'}">
109
+ <svg v-if="ascArr.includes(c.name) || descArr.includes(c.name)" class="w-3 h-3 ms-1.5"
110
+ :class="{'rotate-180':descArr.includes(c.name)}" viewBox="0 0 24 24">
111
+ <path
112
+ 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"/>
113
+ </svg>
114
+ <svg v-else class="w-3 h-3 ms-1.5 opacity-30" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
115
+ fill='currentColor'
104
116
  viewBox="0 0 24 24">
105
- <path
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" />
107
- </svg></div>
117
+ <path
118
+ 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"/>
119
+ </svg>
108
120
  </div>
109
- </th>
110
-
111
- <th scope="col" class="px-6 py-3">
112
- Actions
113
- </th>
121
+ </div>
122
+ </th>
114
123
 
115
- </tr>
124
+ <th scope="col" class="px-6 py-3">
125
+ Actions
126
+ </th>
127
+ </tr>
116
128
  </thead>
117
129
  <tbody>
118
- <tr v-if="!rows" class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
119
- <td :colspan="coreStore.resourceColumns.length + 2">
120
-
121
- <div role="status"
122
- 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">
123
- <div class="flex items-center justify-between">
124
- <div>
125
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
126
- <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
127
- </div>
128
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
130
+ <tr v-if="!rows" class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
131
+ <td :colspan="coreStore.resourceColumns.length + 2">
132
+
133
+ <div role="status"
134
+ 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">
135
+ <div class="flex items-center justify-between">
136
+ <div>
137
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
138
+ <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
129
139
  </div>
130
- <div class="flex items-center justify-between pt-4">
131
- <div>
132
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
133
- <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
134
- </div>
135
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
140
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
141
+ </div>
142
+ <div class="flex items-center justify-between pt-4">
143
+ <div>
144
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
145
+ <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
136
146
  </div>
137
- <div class="flex items-center justify-between pt-4">
138
- <div>
139
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
140
- <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
141
- </div>
142
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
147
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
148
+ </div>
149
+ <div class="flex items-center justify-between pt-4">
150
+ <div>
151
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
152
+ <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
143
153
  </div>
144
- <div class="flex items-center justify-between pt-4">
145
- <div>
146
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
147
- <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
148
- </div>
149
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
154
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
155
+ </div>
156
+ <div class="flex items-center justify-between pt-4">
157
+ <div>
158
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
159
+ <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
150
160
  </div>
151
- <div class="flex items-center justify-between pt-4">
152
- <div>
153
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
154
- <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
155
- </div>
156
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
161
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
162
+ </div>
163
+ <div class="flex items-center justify-between pt-4">
164
+ <div>
165
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
166
+ <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
157
167
  </div>
158
- <span class="sr-only">Loading...</span>
168
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
159
169
  </div>
160
- </td>
161
- </tr>
162
- <tr v-else-if="rows.length === 0" class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
163
- <td :colspan="coreStore.resourceColumns.length + 2">
164
-
165
- <div id="toast-simple" class=" mx-auto my-5 flex items-center w-full max-w-xs p-4 space-x-4 rtl:space-x-reverse text-gray-500 bg-white divide-x rtl:divide-x-reverse divide-gray-200 dark:text-gray-400 dark:divide-gray-700 space-x dark:bg-gray-800" role="alert">
166
- <IconInboxOutline class="w-6 h-6 text-gray-500 dark:text-gray-400" />
167
- <div class="ps-4 text-sm font-normal">No items here yet</div>
170
+ <span class="sr-only">Loading...</span>
171
+ </div>
172
+ </td>
173
+ </tr>
174
+ <tr v-else-if="rows.length === 0" class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
175
+ <td :colspan="coreStore.resourceColumns.length + 2">
176
+
177
+ <div id="toast-simple"
178
+ class=" mx-auto my-5 flex items-center w-full max-w-xs p-4 space-x-4 rtl:space-x-reverse text-gray-500 bg-white divide-x rtl:divide-x-reverse divide-gray-200 dark:text-gray-400 dark:divide-gray-700 space-x dark:bg-gray-800"
179
+ role="alert">
180
+ <IconInboxOutline class="w-6 h-6 text-gray-500 dark:text-gray-400"/>
181
+ <div class="ps-4 text-sm font-normal">No items here yet</div>
168
182
  </div>
169
183
 
170
- </td>
171
- </tr>
184
+ </td>
185
+ </tr>
172
186
 
173
- <tr v-else v-for="(row, rowI) in rows" :key="row.id"
187
+ <tr v-else v-for="(row, rowI) in rows" :key="row.id"
174
188
  class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600">
175
- <td class="w-4 p-4">
176
- <div class="flex items center">
177
- <input id="checkbox-table-search-1" type="checkbox" :checked="checkboxes.includes(row.id)" @change="(e)=>{addToCheckedValues(row.id)}"
178
- 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">
179
- <label for="checkbox-table-search-1" class="sr-only">checkbox</label>
180
- </div>
181
- </td>
182
- <td v-for="c in columnsListed" class="px-6 py-4">
183
- <ValueRenderer :column="c" :row="row" />
184
- </td>
185
- <td class="flex items-center px-6 py-4">
186
-
187
- <RouterLink :to="{ name: 'resource-show', params: { resourceId: $route.params.resourceId, primaryKey: row._primaryKeyValue } }"
188
- class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
189
- :data-tooltip-target="`tooltip-show-${rowI}`"
190
- >
191
- <IconEyeSolid class="w-5 h-5 me-2" />
192
- </RouterLink>
193
- <div :id="`tooltip-show-${rowI}`"
194
- 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">
195
- Show item
196
- <div class="tooltip-arrow" data-popper-arrow></div>
197
- </div>
189
+ <td class="w-4 p-4">
190
+ <div class="flex items center">
191
+ <input
192
+ id="checkbox-table-search-1"
193
+ type="checkbox"
194
+ :checked="checkboxes.includes(row.id)"
195
+ @change="(e)=>{addToCheckedValues(row.id)}"
196
+ 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">
197
+ <label for="checkbox-table-search-1" class="sr-only">checkbox</label>
198
+ </div>
199
+ </td>
200
+ <td v-for="c in columnsListed" class="px-6 py-4">
201
+ <ValueRenderer :column="c" :row="row"/>
202
+ </td>
203
+ <td class="flex items-center px-6 py-4">
204
+
205
+ <RouterLink
206
+ :to="{ name: 'resource-show', params: { resourceId: $route.params.resourceId, primaryKey: row._primaryKeyValue } }"
207
+ class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
208
+ :data-tooltip-target="`tooltip-show-${rowI}`"
209
+ >
210
+ <IconEyeSolid class="w-5 h-5 me-2"/>
211
+ </RouterLink>
212
+
213
+ <div :id="`tooltip-show-${rowI}`"
214
+ role="tooltip"
215
+ 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">
216
+ Show item
217
+ <div class="tooltip-arrow" data-popper-arrow></div>
218
+ </div>
198
219
 
199
- <RouterLink :to="{ name: 'resource-edit', params: { resourceId: $route.params.resourceId, primaryKey: row._primaryKeyValue } }"
200
- class="font-medium text-blue-600 dark:text-blue-500 hover:underline ms-3"
201
- :data-tooltip-target="`tooltip-edit-${rowI}`"
202
- >
203
- <IconPenSolid class="w-5 h-5 me-2" />
204
- </RouterLink>
205
- <div :id="`tooltip-edit-${rowI}`"
206
- 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">
207
- Edit
208
- <div class="tooltip-arrow" data-popper-arrow></div>
209
- </div>
220
+ <RouterLink
221
+ :to="{ name: 'resource-edit', params: { resourceId: $route.params.resourceId, primaryKey: row._primaryKeyValue } }"
222
+ class="font-medium text-blue-600 dark:text-blue-500 hover:underline ms-3"
223
+ :data-tooltip-target="`tooltip-edit-${rowI}`"
224
+ >
225
+ <IconPenSolid class="w-5 h-5 me-2"/>
226
+ </RouterLink>
227
+
228
+ <div :id="`tooltip-edit-${rowI}`"
229
+ role="tooltip"
230
+ 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">
231
+ Edit
232
+ <div class="tooltip-arrow" data-popper-arrow></div>
233
+ </div>
210
234
 
211
- <button v-if = "allowDelete"
212
- class="font-medium text-red-600 dark:text-red-500 hover:underline ms-3"
213
- :data-tooltip-target="`tooltip-delete-${rowI}`"
214
- @click="showDeleteModal(row)"
215
- >
216
- <IconTrashBinSolid class="w-5 h-5 me-2" />
217
- </button>
218
- <div :id="`tooltip-delete-${rowI}`"
219
- 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">
220
- Delete
221
- <div class="tooltip-arrow" data-popper-arrow></div>
222
- </div>
223
- </td>
224
- </tr>
235
+ <button v-if="allowDelete"
236
+ class="font-medium text-red-600 dark:text-red-500 hover:underline ms-3"
237
+ :data-tooltip-target="`tooltip-delete-${rowI}`"
238
+ @click="showDeleteModal(row)"
239
+ >
240
+ <IconTrashBinSolid class="w-5 h-5 me-2"/>
241
+ </button>
242
+
243
+ <div :id="`tooltip-delete-${rowI}`"
244
+ role="tooltip"
245
+ 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">
246
+ Delete
247
+ <div class="tooltip-arrow" data-popper-arrow></div>
248
+ </div>
249
+ </td>
250
+ </tr>
225
251
  </tbody>
226
252
  </table>
227
253
  </div>
228
254
  <!-- pagination -->
229
255
  <div class="flex flex-col items-center mt-4 xs:flex-row xs:justify-between xs:items-center"
230
- v-if="rows && totalRows > 0"
256
+ v-if="rows && totalRows >= pageSize && totalRows > 0"
231
257
  >
232
- <!-- Help text -->
233
- <span class="text-sm text-gray-700 dark:text-gray-400">
258
+ <!-- Help text -->
259
+ <span class="text-sm text-gray-700 dark:text-gray-400">
234
260
  Showing <span class="font-semibold text-gray-900 dark:text-white">
235
261
  {{ (page - 1) * pageSize + 1 }}
236
262
  </span> to <span class="font-semibold text-gray-900 dark:text-white">
237
263
  {{ Math.min(page * pageSize, totalRows) }}
238
264
  </span> of <span class="font-semibold text-gray-900 dark:text-white">{{
239
- totalRows
240
- }}</span> Entries
265
+ totalRows
266
+ }}</span> Entries
241
267
  </span>
242
- <div class="inline-flex mt-2 xs:mt-0">
243
- <!-- Buttons -->
244
- <button
245
- class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-r-0 rounded-s border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50"
246
- @click="page--" :disabled="page <= 1">
247
- <svg class="w-3.5 h-3.5 me-2 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10">
248
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5H1m0 0 4 4M1 5l4-4"/>
249
- </svg>
250
- Prev
251
- </button>
252
- <button
253
- class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-r-0 border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50"
254
- @click="page = 1" :disabled="page <= 1">
255
- <!-- <IconChevronDoubleLeftOutline class="w-4 h-4" /> -->
256
- 1
257
- </button>
258
- <input type="text" class="w-10 py-1.5 px-3 text-sm text-center text-gray-700 border border-gray-300 dark:border-gray-700 dark:text-gray-400 dark:bg-gray-800 z-10"
259
- v-model="page" />
260
-
261
- <button
262
- class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-l-0 border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50"
263
- @click="page = totalPages" :disabled="page >= totalPages">
264
- {{ totalPages }}
265
- <!-- <IconChevronDoubleRightOutline class="w-4 h-4" /> -->
266
- </button>
267
- <button
268
- class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-l-0 rounded-e border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50"
269
-
270
- @click="page++" :disabled="page >= totalPages">
271
- Next
272
- <svg class="w-3.5 h-3.5 ms-2 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10">
273
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/>
274
- </svg>
275
- </button>
276
- </div>
268
+ <div class="inline-flex mt-2 xs:mt-0">
269
+ <!-- Buttons -->
270
+ <button
271
+ class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-r-0 rounded-s border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50"
272
+ @click="page--" :disabled="page <= 1">
273
+ <svg class="w-3.5 h-3.5 me-2 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
274
+ viewBox="0 0 14 10">
275
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
276
+ d="M13 5H1m0 0 4 4M1 5l4-4"/>
277
+ </svg>
278
+ Prev
279
+ </button>
280
+ <button
281
+ class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-r-0 border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50"
282
+ @click="page = 1" :disabled="page <= 1">
283
+ <!-- <IconChevronDoubleLeftOutline class="w-4 h-4" /> -->
284
+ 1
285
+ </button>
286
+ <input type="text"
287
+ class="w-10 py-1.5 px-3 text-sm text-center text-gray-700 border border-gray-300 dark:border-gray-700 dark:text-gray-400 dark:bg-gray-800 z-10"
288
+ v-model="page"/>
289
+
290
+ <button
291
+ class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-l-0 border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50"
292
+ @click="page = totalPages" :disabled="page >= totalPages">
293
+ {{ totalPages }}
294
+ <!-- <IconChevronDoubleRightOutline class="w-4 h-4" /> -->
295
+ </button>
296
+ <button
297
+ class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-l-0 rounded-e border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50"
298
+
299
+ @click="page++" :disabled="page >= totalPages">
300
+ Next
301
+ <svg class="w-3.5 h-3.5 ms-2 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
302
+ viewBox="0 0 14 10">
303
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
304
+ d="M1 5h12m0 0L9 1m4 4L9 9"/>
305
+ </svg>
306
+ </button>
307
+ </div>
277
308
  </div>
278
309
  </div>
279
310
  </template>
280
311
 
281
312
  <script setup>
282
- import { ref, onMounted, watch, computed } from 'vue';
283
- import { callAdminForthApi,getIcon } from '@/utils';
284
- import { useRoute } from 'vue-router';
285
- import { useCoreStore } from '@/stores/core';
286
- import { useModalStore } from '@/stores/modal';
313
+ import {ref, onMounted, watch, computed} from 'vue';
314
+ import {callAdminForthApi, getIcon} from '@/utils';
315
+ import {useRoute} from 'vue-router';
316
+ import {useCoreStore} from '@/stores/core';
317
+ import {useModalStore} from '@/stores/modal';
287
318
  import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
288
- import { initFlowbite } from 'flowbite'
319
+ import {initFlowbite} from 'flowbite'
289
320
 
290
321
  import ValueRenderer from '@/components/ValueRenderer.vue';
291
322
 
292
- import { IconChevronDoubleLeftOutline, IconChevronDoubleRightOutline, IconInboxFullSolid, IconInboxOutline, IconPlusOutline } from '@iconify-prerendered/vue-flowbite';
323
+ import {
324
+ IconChevronDoubleLeftOutline,
325
+ IconChevronDoubleRightOutline,
326
+ IconInboxFullSolid,
327
+ IconInboxOutline,
328
+ IconPlusOutline
329
+ } from '@iconify-prerendered/vue-flowbite';
293
330
 
294
- import {
295
- IconEyeSolid,
331
+ import {
332
+ IconEyeSolid,
296
333
  IconTrashBinSolid,
297
334
  IconPenSolid,
298
- IconFilterOutline,IconBanOutline
299
- } from '@iconify-prerendered/vue-flowbite';
335
+ IconFilterOutline, IconBanOutline
336
+ } from '@iconify-prerendered/vue-flowbite';
300
337
 
301
338
  import Filters from '@/components/Filters.vue';
302
339
 
@@ -313,6 +350,7 @@ const page = ref(1);
313
350
  const filters = ref([]);
314
351
  const columnsMinMax = ref({});
315
352
  const sort = ref([]);
353
+ const fetchStatus = ref({pending: false, error: null, success: false});
316
354
 
317
355
  const rows = ref(null);
318
356
  const totalRows = ref(0);
@@ -325,7 +363,13 @@ const columnsListed = computed(() => coreStore.resourceColumns?.filter(c => c.sh
325
363
 
326
364
  async function selectAll(value) {
327
365
  console.log('select all');
328
- rows.value.forEach((r)=>{if(!checkboxes.value.includes(r.id)){checkboxes.value.push(r.id)}else{checkboxes.value = checkboxes.value.filter((item) => item !== r.id)}});
366
+ rows.value.forEach((r) => {
367
+ if (!checkboxes.value.includes(r.id)) {
368
+ checkboxes.value.push(r.id)
369
+ } else {
370
+ checkboxes.value = checkboxes.value.filter((item) => item !== r.id)
371
+ }
372
+ });
329
373
  // checkboxes.value = rows.value.map((v) => v.id);
330
374
  }
331
375
 
@@ -345,17 +389,18 @@ watch([page], async () => {
345
389
  watch([filters], async () => {
346
390
  page.value = 1;
347
391
  await getList();
348
- }, { deep: true });
392
+ }, {deep: true});
349
393
 
350
394
  watch([sort], async () => {
351
395
  await init();
352
- }, { deep: true });
396
+ }, {deep: true});
353
397
 
354
398
  function onSortButtonClick(field) {
399
+ if (fetchStatus.value.pending) return;
355
400
  const sortIndex = sort.value.findIndex((s) => s.field === field);
356
401
  console.log('sortIndex', sortIndex);
357
402
  if (sortIndex === -1) {
358
- sort.value = [{ field, direction: 'asc' }, ...sort.value];
403
+ sort.value = [{field, direction: 'asc'}, ...sort.value];
359
404
  } else {
360
405
  const sortField = sort.value[sortIndex];
361
406
  if (sortField.direction === 'asc') {
@@ -368,6 +413,7 @@ function onSortButtonClick(field) {
368
413
 
369
414
  async function getList() {
370
415
  rows.value = null;
416
+ fetchStatus.value.pending = true;
371
417
  const data = await callAdminForthApi({
372
418
  path: '/get_resource_data',
373
419
  method: 'POST',
@@ -379,13 +425,13 @@ async function getList() {
379
425
  sort: sort.value,
380
426
  }
381
427
  });
382
- console.log('coreStore.resourceColumns', coreStore.resourceColumns);
428
+ fetchStatus.value.pending = false;
383
429
  rows.value = data.data?.map(row => {
384
430
  row._primaryKeyValue = row[coreStore.resourceColumns.find(c => c.primaryKey).name];
385
431
  return row;
386
432
  });
387
433
  totalRows.value = data.total;
388
- allCheckedActions.value = data.options?.bulkActions||[];
434
+ allCheckedActions.value = data.options?.bulkActions || [];
389
435
  allowDelete.value = data.options?.allowDelete;
390
436
 
391
437
 
@@ -396,7 +442,7 @@ async function getList() {
396
442
  }
397
443
 
398
444
 
399
- function showDeleteModal (row){
445
+ function showDeleteModal(row) {
400
446
  if (!coreStore.config?.deleteConfirmation) {
401
447
  return deleteRecord(row);
402
448
  }
@@ -405,11 +451,13 @@ function showDeleteModal (row){
405
451
  acceptText: 'Delete',
406
452
  cancelText: 'Cancel',
407
453
  });
408
- modalStore.setOnAcceptFunction(()=>{return deleteRecord(row)})
454
+ modalStore.setOnAcceptFunction(() => {
455
+ return deleteRecord(row)
456
+ })
409
457
  console.log('row', row);
410
458
  modalStore.togleModal();
411
459
 
412
- }
460
+ }
413
461
 
414
462
  async function deleteRecord(row) {
415
463
  await callAdminForthApi({
@@ -418,32 +466,32 @@ async function deleteRecord(row) {
418
466
  body: {
419
467
  resourceId: route.params.resourceId,
420
468
  primaryKey: row._primaryKeyValue,
421
- recordId:row.id
469
+ recordId: row.id
422
470
  }
423
471
  });
424
472
  await getList();
425
473
  modalStore.resetmodalState()
426
474
  }
427
475
 
428
- async function startBulkAction(actionId){
429
- const data =await callAdminForthApi({
476
+ async function startBulkAction(actionId) {
477
+ const data = await callAdminForthApi({
430
478
  path: '/start_bulk_action',
431
479
  method: 'POST',
432
480
  body: {
433
481
  resourceId: route.params.resourceId,
434
482
  actionId: actionId,
435
483
  recordIds: checkboxes.value
436
-
484
+
437
485
  }
438
486
  });
439
- if (data?.status === 'success'){
487
+ if (data?.status === 'success') {
440
488
  checkboxes.value = [];
441
489
  }
442
490
  await getList();
443
491
  }
444
492
 
445
- function addToCheckedValues(id){
446
- if (checkboxes.value.includes(id)){
493
+ function addToCheckedValues(id) {
494
+ if (checkboxes.value.includes(id)) {
447
495
  checkboxes.value = checkboxes.value.filter((item) => item !== id);
448
496
  } else {
449
497
  checkboxes.value.push(id);
@@ -464,9 +512,10 @@ async function init() {
464
512
  }
465
513
  });
466
514
  }
515
+
467
516
  onMounted(async () => {
468
517
  await init();
469
- });
518
+ });
470
519
 
471
520
  // on route param change
472
521
  watch(() => route.params.resourceId, async () => {
@@ -36,7 +36,8 @@ export default defineConfig({
36
36
  ],
37
37
  resolve: {
38
38
  alias: {
39
- '@': fileURLToPath(new URL('./src', import.meta.url))
39
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
40
+ '@@': fileURLToPath(new URL('./src/custom', import.meta.url)),
40
41
  }
41
42
  }
42
43
  })