adminforth 1.3.23 → 1.3.24

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.
@@ -28,7 +28,7 @@ export default class ConfigValidator {
28
28
  }
29
29
  return [];
30
30
  }
31
- validateComponent(component, errors, ignoreExistsCheck = false) {
31
+ validateComponent(component, errors) {
32
32
  if (!component) {
33
33
  return component;
34
34
  }
@@ -39,6 +39,12 @@ export default class ConfigValidator {
39
39
  else {
40
40
  obj = component;
41
41
  }
42
+ let ignoreExistsCheck = false;
43
+ if (this.adminforth.codeInjector.allComponentNames.hasOwnProperty(component.file)) {
44
+ // not obvious, but if we are in this if, it means that this is plugin component
45
+ // if component is plugin component, we don't need to check if it exists in users folder
46
+ ignoreExistsCheck = true;
47
+ }
42
48
  if (!ignoreExistsCheck) {
43
49
  errors.push(...this.checkCustomFileExists(obj.file));
44
50
  }
@@ -98,10 +104,7 @@ export default class ConfigValidator {
98
104
  }
99
105
  if (this.config.customization.customPages) {
100
106
  this.config.customization.customPages.forEach((page, i) => {
101
- // validate component if it's not plugin injection
102
- if (this.adminforth.codeInjector.allComponentNames.hasOwnProperty(page.component)) {
103
- const validatedPage = this.validateComponent(page.component, errors, true);
104
- }
107
+ this.validateComponent(page.component, errors);
105
108
  });
106
109
  }
107
110
  else {
@@ -298,9 +301,14 @@ export default class ConfigValidator {
298
301
  });
299
302
  res.options.bulkActions = bulkActions;
300
303
  // if pageInjection is a string, make array with one element. Also check file exists
301
- const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom'];
304
+ const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems'];
305
+ const possiblePages = ['list', 'show', 'create', 'edit'];
302
306
  if (res.options.pageInjections) {
303
307
  Object.entries(res.options.pageInjections).map(([key, value]) => {
308
+ if (!possiblePages.includes(key)) {
309
+ const similar = suggestIfTypo(possiblePages, key);
310
+ errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${key}", allowed keys are ${possiblePages.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
311
+ }
304
312
  Object.entries(value).map(([injection, target]) => {
305
313
  if (possibleInjections.includes(injection)) {
306
314
  if (!Array.isArray(res.options.pageInjections[key][injection])) {
@@ -312,7 +320,8 @@ export default class ConfigValidator {
312
320
  });
313
321
  }
314
322
  else {
315
- errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')}`);
323
+ const similar = suggestIfTypo(possibleInjections, injection);
324
+ errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')} ${similar ? `Did you mean "${similar}"?` : ''}`);
316
325
  }
317
326
  });
318
327
  });
@@ -417,13 +426,7 @@ export default class ConfigValidator {
417
426
  for (const column of resource.columns) {
418
427
  if (column.components) {
419
428
  for (const [key, comp] of Object.entries(column.components)) {
420
- let ignoreExistsCheck = false;
421
- if (this.adminforth.codeInjector.allComponentNames[comp.file]) {
422
- // not obvious, but if we are in this if, it means that this is plugin component
423
- // and there is no sense to check if it exists in users folder
424
- ignoreExistsCheck = true;
425
- }
426
- column.components[key] = this.validateComponent(comp, errors, ignoreExistsCheck);
429
+ column.components[key] = this.validateComponent(comp, errors);
427
430
  }
428
431
  }
429
432
  }
@@ -32,7 +32,8 @@ export default class ConfigValidator implements IConfigValidator {
32
32
  return [];
33
33
  }
34
34
 
35
- validateComponent(component: AdminForthComponentDeclaration, errors: Array<string>, ignoreExistsCheck: boolean = false): AdminForthComponentDeclaration {
35
+ validateComponent(component: AdminForthComponentDeclaration, errors: Array<string>): AdminForthComponentDeclaration {
36
+
36
37
  if (!component) {
37
38
  return component;
38
39
  }
@@ -42,6 +43,18 @@ export default class ConfigValidator implements IConfigValidator {
42
43
  } else {
43
44
  obj = component;
44
45
  }
46
+
47
+ let ignoreExistsCheck = false;
48
+ if (
49
+ this.adminforth.codeInjector.allComponentNames.hasOwnProperty(
50
+ (component as AdminForthComponentDeclarationFull).file)
51
+ ) {
52
+ // not obvious, but if we are in this if, it means that this is plugin component
53
+ // if component is plugin component, we don't need to check if it exists in users folder
54
+ ignoreExistsCheck = true;
55
+ }
56
+
57
+
45
58
  if (!ignoreExistsCheck) {
46
59
  errors.push(...this.checkCustomFileExists(obj.file));
47
60
  }
@@ -109,10 +122,7 @@ export default class ConfigValidator implements IConfigValidator {
109
122
 
110
123
  if (this.config.customization.customPages) {
111
124
  this.config.customization.customPages.forEach((page, i) => {
112
- // validate component if it's not plugin injection
113
- if (this.adminforth.codeInjector.allComponentNames.hasOwnProperty(page.component as PropertyKey)) {
114
- const validatedPage = this.validateComponent(page.component, errors, true);
115
- }
125
+ this.validateComponent(page.component, errors);
116
126
  });
117
127
  } else {
118
128
  this.config.customization.customPages = [];
@@ -341,9 +351,16 @@ export default class ConfigValidator implements IConfigValidator {
341
351
  res.options.bulkActions = bulkActions;
342
352
 
343
353
  // if pageInjection is a string, make array with one element. Also check file exists
344
- const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom'];
354
+ const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems'];
355
+ const possiblePages = ['list', 'show', 'create', 'edit'];
356
+
345
357
  if (res.options.pageInjections) {
346
358
  Object.entries(res.options.pageInjections).map(([key, value]) => {
359
+ if (!possiblePages.includes(key)) {
360
+ const similar = suggestIfTypo(possiblePages, key);
361
+ errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${key}", allowed keys are ${possiblePages.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
362
+ }
363
+
347
364
  Object.entries(value).map(([injection, target]) => {
348
365
  if (possibleInjections.includes(injection)) {
349
366
  if (!Array.isArray(res.options.pageInjections[key][injection])) {
@@ -354,7 +371,8 @@ export default class ConfigValidator implements IConfigValidator {
354
371
  res.options.pageInjections[key][injection][i] = this.validateComponent(target, errors);
355
372
  });
356
373
  } else {
357
- errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')}`);
374
+ const similar = suggestIfTypo(possibleInjections, injection);
375
+ errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')} ${similar ? `Did you mean "${similar}"?` : ''}`);
358
376
  }
359
377
  });
360
378
 
@@ -477,13 +495,8 @@ export default class ConfigValidator implements IConfigValidator {
477
495
  if (column.components) {
478
496
 
479
497
  for (const [key, comp] of Object.entries(column.components as Record<string, AdminForthComponentDeclarationFull>)) {
480
- let ignoreExistsCheck = false;
481
- if (this.adminforth.codeInjector.allComponentNames[comp.file]) {
482
- // not obvious, but if we are in this if, it means that this is plugin component
483
- // and there is no sense to check if it exists in users folder
484
- ignoreExistsCheck = true;
485
- }
486
- column.components[key] = this.validateComponent(comp, errors, ignoreExistsCheck);
498
+
499
+ column.components[key] = this.validateComponent(comp, errors);
487
500
  }
488
501
  }
489
502
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.3.23",
3
+ "version": "1.3.24",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/spa/src/App.vue CHANGED
@@ -165,7 +165,7 @@
165
165
  </div>
166
166
  </div>
167
167
  <AcceptModal />
168
- <div v-if="toastStore.toasts.length>0" class="fixed bottom-5 right-5 flex gap-1 flex-col-reverse">
168
+ <div v-if="toastStore.toasts.length>0" class="fixed bottom-5 right-5 flex gap-1 flex-col-reverse z-50">
169
169
  <transition-group
170
170
  name="fade"
171
171
  tag="div"
@@ -1,7 +1,8 @@
1
1
  <template>
2
2
 
3
3
 
4
- <div id="toast-default" class="flex items-center w-full p-4 text-gray-500 rounded-lg shadow dark:text-gray-400 dark:bg-gray-800" role="alert"
4
+ <div id="toast-default" class="flex items-center w-full p-4 text-gray-500 rounded-lg shadow dark:text-gray-400 dark:bg-gray-800"
5
+ role="alert"
5
6
  :class="
6
7
  {
7
8
  'danger': 'bg-red-100',
@@ -1,6 +1,6 @@
1
1
  import { createRouter, createWebHistory } from 'vue-router';
2
2
  import ResourceParent from '@/views/ResourceParent.vue';
3
- import { useFiltersStore } from '@/stores/filters';
3
+
4
4
  /* IMPORTANT:ADMINFORTH ROUTES IMPORTS */
5
5
 
6
6
  const router = createRouter({
@@ -47,6 +47,10 @@ export const useCoreStore = defineStore('core', () => {
47
47
  if (!resource.value) {
48
48
  throw new Error('Columns not fetched yet');
49
49
  }
50
+ const col = resource.value.columns.find((col: AdminForthResourceColumn) => col.primaryKey);
51
+ if (!col) {
52
+ throw new Error(`Primary key not found in resource ${resourceId}`);
53
+ }
50
54
 
51
55
  const respData = await callAdminForthApi({
52
56
  path: '/get_resource_data',
@@ -56,7 +60,7 @@ export const useCoreStore = defineStore('core', () => {
56
60
  resourceId: resourceId,
57
61
  filters: [
58
62
  {
59
- field: resource.value.columns.find((col: AdminForthResourceColumn) => col.primaryKey).name,
63
+ field: col.name,
60
64
  operator: 'eq',
61
65
  value: primaryKey
62
66
  }
@@ -3,7 +3,14 @@ import { defineStore } from 'pinia';
3
3
 
4
4
  export const useFiltersStore = defineStore('filters', () => {
5
5
  const filters: Ref<any[]> = ref([]);
6
-
6
+ const sort: Ref<any> = ref({});
7
+
8
+ const setSort = (s: any) => {
9
+ sort.value = s;
10
+ }
11
+ const getSort = () => {
12
+ return sort.value;
13
+ }
7
14
  const setFilter = (filter: any) => {
8
15
  filters.value.push(filter);
9
16
  }
@@ -16,5 +23,5 @@ export const useFiltersStore = defineStore('filters', () => {
16
23
  const clearFilters = () => {
17
24
  filters.value = [];
18
25
  }
19
- return {setFilter, getFilters, clearFilters, filters, setFilters}
26
+ return {setFilter, getFilters, clearFilters, filters, setFilters, setSort, getSort}
20
27
  })
@@ -72,6 +72,33 @@
72
72
  {{ filtersStore.filters.length }}
73
73
  </span>
74
74
  </button>
75
+
76
+ <template v-if="coreStore.resourceOptions?.pageInjections?.list?.threeDotsDropdownItems">
77
+ <button id="dropdownMenuIconButton"
78
+ data-dropdown-toggle="dropdownDots"
79
+ class="flex items-center py-2 px-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary 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 rounded-default"
80
+ >
81
+ <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 4 15">
82
+ <path d="M3.5 1.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm0 6.041a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm0 5.959a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z"/>
83
+ </svg>
84
+ </button>
85
+
86
+ <!-- Dropdown menu -->
87
+ <div id="dropdownDots" class="z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700 dark:divide-gray-600">
88
+ <ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownMenuIconButton">
89
+ <li v-for="item in coreStore.resourceOptions?.pageInjections?.list?.threeDotsDropdownItems" :key="`dropdown-item-${item.label}`">
90
+ <a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
91
+ <component :is="getCustomComponent(item)"
92
+ :meta="item.meta"
93
+ :resource="coreStore.resource"
94
+ :adminUser="coreStore.adminUser"
95
+ />
96
+ </a>
97
+ </li>
98
+ </ul>
99
+ </div>
100
+ </template>
101
+
75
102
  </BreadcrumbsWithButtons>
76
103
 
77
104
  <component
@@ -138,6 +165,11 @@ const page = ref(1);
138
165
  const columnsMinMax = ref({});
139
166
  const sort = ref([]);
140
167
 
168
+ watch(() => sort, async (to, from) => {
169
+ // in store sort might be needed for plugins
170
+ filtersStore.setSort(sort.value);
171
+ }, {deep: true});
172
+
141
173
  const rows = ref(null);
142
174
  const totalRows = ref(0);
143
175
  const checkboxes = ref([]);
@@ -224,6 +256,9 @@ async function init() {
224
256
  resourceId: route.params.resourceId
225
257
  });
226
258
 
259
+ initFlowbite();
260
+
261
+
227
262
  // !!! clear filters should be in same tick with sort assignment so that watch can catch it
228
263
  filtersStore.clearFilters();
229
264
  if (coreStore.resource.options?.defaultSort) {
@@ -1043,6 +1043,7 @@ export type AdminForthResource = {
1043
1043
  beforeBreadcrumbs?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
1044
1044
  afterBreadcrumbs?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
1045
1045
  bottom?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
1046
+ threeDotsDropdownItems?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
1046
1047
  },
1047
1048
 
1048
1049
  /**