adminforth 1.3.23 → 1.3.25

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,8 +32,15 @@ export default class OperationalResource {
32
32
  })).data[0] || null;
33
33
  });
34
34
  }
35
- list(filter, limit, offset, sort) {
36
- return __awaiter(this, void 0, void 0, function* () {
35
+ list(filter_1) {
36
+ return __awaiter(this, arguments, void 0, function* (filter, limit = null, offset = null, sort = []) {
37
+ // check if type of limit and offset is number
38
+ if (limit !== null && typeof limit !== 'number') {
39
+ throw new Error('Limit must be a number');
40
+ }
41
+ if (offset !== null && typeof offset !== 'number') {
42
+ throw new Error('Offset must be a number');
43
+ }
37
44
  let appliedLimit = limit;
38
45
  if (limit === null) {
39
46
  appliedLimit = 1000000000;
@@ -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
  }
@@ -36,10 +36,18 @@ export default class OperationalResource implements IOperationalResource {
36
36
 
37
37
  async list(
38
38
  filter: IAdminForthFilter | IAdminForthFilter[],
39
- limit: number | null,
40
- offset: number | null,
41
- sort: IAdminForthSort | IAdminForthSort[]
39
+ limit: number | null = null,
40
+ offset: number | null = null,
41
+ sort: IAdminForthSort | IAdminForthSort[] = []
42
42
  ): Promise<any[]> {
43
+ // check if type of limit and offset is number
44
+ if (limit !== null && typeof limit !== 'number') {
45
+ throw new Error('Limit must be a number');
46
+ }
47
+ if (offset !== null && typeof offset !== 'number') {
48
+ throw new Error('Offset must be a number');
49
+ }
50
+
43
51
  let appliedLimit = limit;
44
52
  if (limit === null) {
45
53
  appliedLimit = 1000000000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.3.23",
3
+ "version": "1.3.25",
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"
@@ -230,10 +230,8 @@
230
230
  <script setup>
231
231
 
232
232
 
233
- import { initFlowbite } from 'flowbite';
234
- import { computed, onMounted, ref, watch } from 'vue';
235
- import { useRoute } from 'vue-router';
236
- import { callAdminForthApi, getIcon } from '@/utils';
233
+ import { computed, ref, watch } from 'vue';
234
+ import { callAdminForthApi } from '@/utils';
237
235
 
238
236
  import ValueRenderer from '@/components/ValueRenderer.vue';
239
237
  import { getCustomComponent } from '@/utils';
@@ -0,0 +1,43 @@
1
+ <template >
2
+ <template v-if="threeDotsDropdownItems?.length">
3
+ <button
4
+ data-dropdown-toggle="listThreeDotsDropdown"
5
+ 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"
6
+ >
7
+ <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 4 15">
8
+ <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"/>
9
+ </svg>
10
+ </button>
11
+
12
+ <!-- Dropdown menu -->
13
+ <div
14
+ id="listThreeDotsDropdown"
15
+ class="z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700 dark:divide-gray-600">
16
+ <ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownMenuIconButton">
17
+ <li v-for="item in threeDotsDropdownItems" :key="`dropdown-item-${item.label}`">
18
+ <a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
19
+ <component :is="getCustomComponent(item)"
20
+ :meta="item.meta"
21
+ :resource="coreStore.resource"
22
+ :adminUser="coreStore.adminUser"
23
+ />
24
+ </a>
25
+ </li>
26
+ </ul>
27
+ </div>
28
+ </template>
29
+ </template>
30
+
31
+
32
+ <script setup lang="ts">
33
+
34
+ import { getCustomComponent } from '@/utils';
35
+ import { useCoreStore } from '@/stores/core'
36
+
37
+ const coreStore = useCoreStore()
38
+
39
+ const props = defineProps<{
40
+ threeDotsDropdownItems: any[]
41
+ }>()
42
+
43
+ </script>
@@ -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',
@@ -24,17 +24,11 @@ import type { AdminForthResourceColumn } from '@/types/AdminForthConfig';
24
24
 
25
25
  declare global {
26
26
  interface Window {
27
- adminforth: {
28
- confirm: (params: ConfirmParams) => Promise<void>;
29
- alert: (params: AlertParams) => void;
30
- setListFilter: (filter: any) => void;
31
- updateListFilter: (filter: any) => void;
32
- clearListFilters: () => void;
33
- };
27
+ adminforth: FrontendAPIInterface;
34
28
  }
35
29
  }
36
30
 
37
- export class FrontendAPI implements FrontendAPIInterface {
31
+ export class FrontendAPI {
38
32
  private toastStore:any
39
33
  private modalStore:any
40
34
  private filtersStore:any
@@ -45,14 +39,19 @@ export class FrontendAPI implements FrontendAPIInterface {
45
39
  }
46
40
  this.toastStore = useToastStore();
47
41
  this.modalStore = useModalStore();
48
- console.log(this.toastStore, this.modalStore,'init of adminforth frontend api')
42
+
49
43
  window.adminforth = {
50
44
  confirm: this.confirm.bind(this),
51
45
  alert: this.alert.bind(this),
52
- setListFilter: this.setListFilter.bind(this),
53
- updateListFilter: this.updateListFilter.bind(this),
54
- clearListFilters: this.clearListFilters.bind(this),
55
- }
46
+
47
+ list: {
48
+ refresh: () => {/* will be redefined in list*/},
49
+ closeThreeDotsDropdown: () => {/* will be redefined in list*/},
50
+ setFilter: () => this.setListFilter.bind(this),
51
+ updateFilter: () => this.updateListFilter.bind(this),
52
+ clearFilters: () => this.clearListFilters.bind(this),
53
+ }
54
+ };
56
55
  }
57
56
 
58
57
  confirm(params: ConfirmParams): Promise<void> {
@@ -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
  })
package/spa/src/utils.ts CHANGED
@@ -2,9 +2,10 @@ import { onMounted, ref, resolveComponent } from 'vue';
2
2
  import type { CoreConfig } from './spa_types/core';
3
3
 
4
4
  import router from "./router";
5
- import { useRouter } from 'vue-router';
6
5
  import { useCoreStore } from './stores/core';
7
6
  import { useUserStore } from './stores/user';
7
+ import { Dropdown } from 'flowbite';
8
+
8
9
 
9
10
  export async function callApi({path, method, body=undefined}: {
10
11
  path: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
@@ -100,4 +101,16 @@ export function checkAcessByAllowedActions(allowedActions:any, action:any ) {
100
101
  }
101
102
  }
102
103
 
103
-
104
+ export function initThreeDotsDropdown() {
105
+ const threeDotsDropdown: HTMLElement | null = document.querySelector('#listThreeDotsDropdown');
106
+ if (threeDotsDropdown) {
107
+ // this resource has three dots dropdown
108
+ const dd = new Dropdown(
109
+ threeDotsDropdown,
110
+ document.querySelector('[data-dropdown-toggle="listThreeDotsDropdown"]') as HTMLElement,
111
+ );
112
+ window.adminforth.list.closeThreeDotsDropdown = () => {
113
+ dd.hide();
114
+ }
115
+ }
116
+ }
@@ -29,6 +29,10 @@
29
29
  Save
30
30
  </button>
31
31
 
32
+ <ThreeDotsMenu
33
+ :threeDotsDropdownItems="coreStore.resourceOptions?.pageInjections?.create?.threeDotsDropdownItems"
34
+ ></ThreeDotsMenu>
35
+
32
36
  </BreadcrumbsWithButtons>
33
37
 
34
38
  <component
@@ -73,12 +77,13 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
73
77
  import ResourceForm from '@/components/ResourceForm.vue';
74
78
  import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
75
79
  import { useCoreStore } from '@/stores/core';
76
- import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions } from '@/utils';
80
+ import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown } from '@/utils';
77
81
  import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
78
82
  import { onMounted, ref, watch } from 'vue';
79
83
  import { useRoute, useRouter } from 'vue-router';
80
84
  import { computed } from 'vue';
81
85
  import { showErrorTost } from '@/composables/useFrontendApi';
86
+ import ThreeDotsMenu from '@/components/ThreeDotsMenu.vue';
82
87
 
83
88
 
84
89
  const isValid = ref(false);
@@ -116,6 +121,7 @@ onMounted(async () => {
116
121
  });
117
122
  loading.value = false;
118
123
  checkAcessByAllowedActions(coreStore.resourceOptions.allowedActions,'create');
124
+ initThreeDotsDropdown();
119
125
  });
120
126
 
121
127
  async function saveRecord() {
@@ -25,6 +25,10 @@
25
25
  Save
26
26
  </button>
27
27
 
28
+ <ThreeDotsMenu
29
+ :threeDotsDropdownItems="coreStore.resourceOptions?.pageInjections?.edit?.threeDotsDropdownItems"
30
+ ></ThreeDotsMenu>
31
+
28
32
  </BreadcrumbsWithButtons>
29
33
 
30
34
  <component
@@ -69,11 +73,13 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
69
73
  import ResourceForm from '@/components/ResourceForm.vue';
70
74
  import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
71
75
  import { useCoreStore } from '@/stores/core';
72
- import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions } from '@/utils';
76
+ import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown } from '@/utils';
73
77
  import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
74
78
  import { computed, onMounted, ref } from 'vue';
75
79
  import { useRoute, useRouter } from 'vue-router';
76
80
  import { showErrorTost } from '@/composables/useFrontendApi';
81
+ import ThreeDotsMenu from '@/components/ThreeDotsMenu.vue';
82
+
77
83
 
78
84
  const coreStore = useCoreStore();
79
85
 
@@ -107,12 +113,13 @@ const editableRecord = computed(() => {
107
113
  })
108
114
 
109
115
  onMounted(async () => {
110
-
111
116
  loading.value = true;
112
117
 
113
118
  await coreStore.fetchResourceFull({
114
119
  resourceId: route.params.resourceId
115
120
  });
121
+ initThreeDotsDropdown();
122
+
116
123
  await coreStore.fetchRecord({
117
124
  resourceId: route.params.resourceId,
118
125
  primaryKey: route.params.primaryKey,
@@ -72,6 +72,10 @@
72
72
  {{ filtersStore.filters.length }}
73
73
  </span>
74
74
  </button>
75
+
76
+ <ThreeDotsMenu
77
+ :threeDotsDropdownItems="coreStore.resourceOptions?.pageInjections?.list?.threeDotsDropdownItems"
78
+ ></ThreeDotsMenu>
75
79
  </BreadcrumbsWithButtons>
76
80
 
77
81
  <component
@@ -112,11 +116,12 @@ import ResourceListTable from '@/components/ResourceListTable.vue';
112
116
  import { useCoreStore } from '@/stores/core';
113
117
  import { useFiltersStore } from '@/stores/filters';
114
118
  import { callAdminForthApi, getIcon } from '@/utils';
115
- import { initFlowbite } from 'flowbite';
116
119
  import { computed, onMounted, ref, watch } from 'vue';
117
120
  import { useRoute } from 'vue-router';
118
121
  import { showErrorTost } from '@/composables/useFrontendApi'
119
- import { getCustomComponent } from '@/utils';
122
+ import { getCustomComponent, initThreeDotsDropdown } from '@/utils';
123
+ import { initFlowbite } from 'flowbite';
124
+ import ThreeDotsMenu from '@/components/ThreeDotsMenu.vue';
120
125
 
121
126
 
122
127
  import {
@@ -138,6 +143,11 @@ const page = ref(1);
138
143
  const columnsMinMax = ref({});
139
144
  const sort = ref([]);
140
145
 
146
+ watch(() => sort, async (to, from) => {
147
+ // in store sort might be needed for plugins
148
+ filtersStore.setSort(sort.value);
149
+ }, {deep: true});
150
+
141
151
  const rows = ref(null);
142
152
  const totalRows = ref(0);
143
153
  const checkboxes = ref([]);
@@ -148,9 +158,6 @@ const DEFAULT_PAGE_SIZE = 10;
148
158
  const pageSize = computed(() => coreStore.resource?.options?.listPageSize || DEFAULT_PAGE_SIZE);
149
159
 
150
160
 
151
-
152
-
153
-
154
161
  async function getList() {
155
162
  rows.value = null;
156
163
  const data = await callAdminForthApi({
@@ -224,6 +231,8 @@ async function init() {
224
231
  resourceId: route.params.resourceId
225
232
  });
226
233
 
234
+ initFlowbite();
235
+
227
236
  // !!! clear filters should be in same tick with sort assignment so that watch can catch it
228
237
  filtersStore.clearFilters();
229
238
  if (coreStore.resource.options?.defaultSort) {
@@ -234,8 +243,7 @@ async function init() {
234
243
  } else {
235
244
  sort.value = [];
236
245
  }
237
- console.log('↘️init fired');
238
- // await getList();
246
+ // await getList(); - Not needed here, watch will trigger it
239
247
  columnsMinMax.value = await callAdminForthApi({
240
248
  path: '/get_min_max_for_columns',
241
249
  method: 'POST',
@@ -246,19 +254,21 @@ async function init() {
246
254
  }
247
255
 
248
256
  watch([page, sort, () => filtersStore.filters], async () => {
249
- console.log('↘️watch fired getList');
250
257
  await getList();
251
258
  }, { deep: true });
252
259
 
260
+ window.adminforth.list.refresh = async () => {
261
+ await getList();
262
+ }
263
+
253
264
  watch(() => filtersStore.filters, async (to, from) => {
254
265
  page.value = 1;
255
266
  checkboxes.value = [];
256
267
  }, {deep: true});
257
268
 
258
269
  onMounted(async () => {
259
- console.log('🧱onMounted fired');
260
- initFlowbite();
261
270
  await init();
271
+ initThreeDotsDropdown();
262
272
  });
263
273
 
264
274
 
@@ -174,7 +174,7 @@ async function login() {
174
174
  body: {
175
175
  username,
176
176
  password,
177
- rememberMe: rememberInput.value.checked,
177
+ rememberMe: rememberInput.value?.checked,
178
178
  }
179
179
  });
180
180
  inProgress.value = false;
@@ -22,6 +22,10 @@
22
22
  <IconTrashBinSolid class="w-4 h-4" />
23
23
  Delete
24
24
  </button>
25
+
26
+ <ThreeDotsMenu
27
+ :threeDotsDropdownItems="coreStore.resourceOptions?.pageInjections?.show?.threeDotsDropdownItems"
28
+ ></ThreeDotsMenu>
25
29
  </BreadcrumbsWithButtons>
26
30
 
27
31
  <component
@@ -119,31 +123,27 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
119
123
 
120
124
  import ValueRenderer from '@/components/ValueRenderer.vue';
121
125
  import { useCoreStore } from '@/stores/core';
122
- import { useModalStore } from '@/stores/modal';
123
- import { getCustomComponent, checkAcessByAllowedActions } from '@/utils';
126
+ import { getCustomComponent, checkAcessByAllowedActions, initThreeDotsDropdown } from '@/utils';
124
127
  import { IconPenSolid, IconTrashBinSolid } from '@iconify-prerendered/vue-flowbite';
125
128
  import { onMounted, ref } from 'vue';
126
129
  import { useRoute,useRouter } from 'vue-router';
127
130
  import {callAdminForthApi} from '@/utils';
128
131
  import { showSuccesTost, showErrorTost } from '@/composables/useFrontendApi';
132
+ import ThreeDotsMenu from '@/components/ThreeDotsMenu.vue';
129
133
 
130
134
 
131
- const item = ref(null);
132
135
  const route = useRoute();
133
136
  const router = useRouter();
134
137
  const loading = ref(true);
135
138
 
136
- console.log(route.params,'showWiev');
137
-
138
-
139
139
  const coreStore = useCoreStore();
140
- const modalStore = useModalStore();
141
140
 
142
141
  onMounted(async () => {
143
142
  loading.value = true;
144
143
  await coreStore.fetchResourceFull({
145
144
  resourceId: route.params.resourceId
146
145
  });
146
+ initThreeDotsDropdown();
147
147
  await coreStore.fetchRecord({
148
148
  resourceId: route.params.resourceId,
149
149
  primaryKey: route.params.primaryKey,
@@ -176,9 +176,8 @@ async function deleteRecord(row) {
176
176
 
177
177
  } catch (e) {
178
178
  console.error(e);
179
- };
180
- }
181
-
179
+ };
180
+ }
182
181
 
183
182
  }
184
183
 
@@ -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
  /**
@@ -19,6 +19,7 @@ export interface FrontendAPIInterface {
19
19
  * @returns A promise that resolves when the user confirms the dialog
20
20
  */
21
21
  confirm(params:ConfirmParams ): Promise<void>;
22
+
22
23
  /**
23
24
  * Show an alert
24
25
  *
@@ -33,37 +34,53 @@ export interface FrontendAPIInterface {
33
34
  * @param params - The parameters of the alert
34
35
  */
35
36
  alert(params:AlertParams): void;
36
- /**
37
- * Add a filter to the list of filters.
38
- * Works only when user located on the list page.
39
- * Can be used to set filter from charts or other components in pageInjections.
40
- *
41
- * Example:
42
- *
43
- * ```ts
44
- * window.adminforth.updateListFilter({field: 'name', operator: 'ilike', value: 'john'})
45
- * ```
46
- *
47
- * @param filter - The filter to add
48
- */
49
- setListFilter(filter: any): void;
50
- /**
51
- * Update a filter in the list of filters
52
- *
53
- * Example:
54
- *
55
- * ```ts
56
- * window.adminforth.updateListFilter({field: 'name', operator: 'ilike', value: 'john'})
57
- * ```
58
- *
59
- * @param filter - The filter to update
60
- */
61
- updateListFilter(filter: any): void;
62
- /**
63
- * Clear all filters from the list
64
- */
65
- clearListFilters(): void;
66
37
 
38
+
39
+ list: {
40
+
41
+ /**
42
+ * Refresh the list
43
+ */
44
+ refresh(): void;
45
+
46
+ /**
47
+ * Close the three dots dropdown
48
+ */
49
+ closeThreeDotsDropdown(): void;
50
+
51
+ /**
52
+ * Set a filter in the list
53
+ * Works only when user located on the list page.
54
+ * Can be used to set filter from charts or other components in pageInjections.
55
+ *
56
+ * Example:
57
+ *
58
+ * ```ts
59
+ * window.adminforth.list.setFilter({field: 'name', operator: 'ilike', value: 'john'})
60
+ * ```
61
+ *
62
+ * @param filter - The filter to set
63
+ */
64
+ setFilter(filter: any): void;
65
+
66
+ /**
67
+ * Update a filter in the list
68
+ *
69
+ * Example:
70
+ *
71
+ * ```ts
72
+ * window.adminforth.list.updateFilter({field: 'name', operator: 'ilike', value: 'john'})
73
+ * ```
74
+ *
75
+ * @param filter - The filter to update
76
+ */
77
+ updateFilter(filter: any): void;
78
+
79
+ /**
80
+ * Clear all filters from the list
81
+ */
82
+ clearFilters(): void;
83
+ }
67
84
  }
68
85
 
69
86
  export type ConfirmParams = {