adminforth 1.1.4 → 1.1.5

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.
Files changed (38) hide show
  1. package/dist/index.js +6 -0
  2. package/dist/modules/styles.js +7 -6
  3. package/dist/plugins/ForeignInlineListPlugin/index.js +32 -1
  4. package/dist/plugins/base.js +4 -0
  5. package/dist/spa/spa/src/App.vue +2 -2
  6. package/dist/spa/spa/src/components/BreadcrumbsWithButtons.vue +1 -1
  7. package/dist/spa/spa/src/components/Filters.vue +19 -15
  8. package/dist/spa/spa/src/components/ResourceForm.vue +12 -12
  9. package/dist/spa/spa/src/components/ResourceListTable.vue +29 -15
  10. package/dist/spa/spa/src/composables/useStores.ts +63 -3
  11. package/dist/spa/spa/src/stores/core.ts +7 -0
  12. package/dist/spa/spa/src/stores/filters.ts +22 -0
  13. package/dist/spa/spa/src/stores/modal.ts +0 -1
  14. package/dist/spa/spa/src/views/CreateView.vue +8 -3
  15. package/dist/spa/spa/src/views/EditView.vue +2 -2
  16. package/dist/spa/spa/src/views/ListView.vue +21 -21
  17. package/dist/spa/spa/src/views/ShowView.vue +3 -3
  18. package/index.ts +10 -0
  19. package/modules/styles.ts +7 -6
  20. package/package.json +1 -1
  21. package/plugins/ForeignInlineListPlugin/custom/InlineList.vue +105 -13
  22. package/plugins/ForeignInlineListPlugin/index.ts +32 -5
  23. package/plugins/base.ts +7 -0
  24. package/spa/src/App.vue +2 -2
  25. package/spa/src/components/BreadcrumbsWithButtons.vue +1 -1
  26. package/spa/src/components/Filters.vue +19 -15
  27. package/spa/src/components/ResourceForm.vue +12 -12
  28. package/spa/src/components/ResourceListTable.vue +29 -15
  29. package/spa/src/composables/useStores.ts +63 -3
  30. package/spa/src/stores/core.ts +7 -0
  31. package/spa/src/stores/filters.ts +22 -0
  32. package/spa/src/stores/modal.ts +0 -1
  33. package/spa/src/views/CreateView.vue +8 -3
  34. package/spa/src/views/EditView.vue +2 -2
  35. package/spa/src/views/ListView.vue +21 -21
  36. package/spa/src/views/ShowView.vue +3 -3
  37. package/types/AdminForthConfig.ts +32 -1
  38. package/types/FrontendAPI.ts +47 -0
@@ -30,18 +30,18 @@
30
30
  />
31
31
  </template>
32
32
  <template v-else>
33
- <td class="px-6 py-4 whitespace-nowrap">
34
- {{ column.label }}
35
- <span :data-tooltip-target="`tooltip-show-${i}`" class="relative inline-block">
36
- <IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
37
- :class="(columnError(column) && validating) ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'"
38
- />
39
- </span>
40
- <div :id="`tooltip-show-${i}`"
41
- 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">
42
- Required field
43
- <div class="tooltip-arrow" data-popper-arrow></div>
44
- </div>
33
+ <td class="px-6 py-4 whitespace-nowrap "> <!--align-top-->
34
+ {{ column.label }}
35
+ <span :data-tooltip-target="`tooltip-show-${i}`" class="relative inline-block">
36
+ <IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
37
+ :class="(columnError(column) && validating) ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'"
38
+ />
39
+ </span>
40
+ <div :id="`tooltip-show-${i}`"
41
+ 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">
42
+ Required field
43
+ <div class="tooltip-arrow" data-popper-arrow></div>
44
+ </div>
45
45
  </td>
46
46
  <td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap relative">
47
47
 
@@ -125,7 +125,7 @@
125
125
  <input
126
126
  id="checkbox-table-search-1"
127
127
  type="checkbox"
128
- :checked="checkboxes.includes(row.id)"
128
+ :checked="checkboxesInternal.includes(row.id)"
129
129
  @change="(e)=>{addToCheckedValues(row.id)}"
130
130
  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">
131
131
  <label for="checkbox-table-search-1" class="sr-only">checkbox</label>
@@ -161,10 +161,12 @@
161
161
  </div>
162
162
 
163
163
  <RouterLink v-if="resource.options?.allowedActions.edit"
164
- :to="{ name: 'resource-edit', params: {
165
- resourceId: resource.id,
166
- primaryKey: row._primaryKeyValue
167
- }
164
+ :to="{
165
+ name: 'resource-edit',
166
+ params: {
167
+ resourceId: resource.id,
168
+ primaryKey: row._primaryKeyValue
169
+ }
168
170
  }"
169
171
  class="font-medium text-blue-600 dark:text-blue-500 hover:underline ms-3"
170
172
  :data-tooltip-target="`tooltip-edit-${rowI}`"
@@ -239,6 +241,7 @@
239
241
  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"
240
242
  @click="page = totalPages" :disabled="page >= totalPages">
241
243
  {{ totalPages }}
244
+
242
245
  <!-- <IconChevronDoubleRightOutline class="w-4 h-4" /> -->
243
246
  </button>
244
247
  <button
@@ -253,6 +256,7 @@
253
256
  </svg>
254
257
  </button>
255
258
  </div>
259
+
256
260
  </div>
257
261
  </template>
258
262
 
@@ -287,6 +291,7 @@ const props = defineProps([
287
291
  'rows',
288
292
  'totalRows',
289
293
  'pageSize',
294
+ 'checkboxes'
290
295
  ])
291
296
 
292
297
  // emits, update page
@@ -297,7 +302,7 @@ const emits = defineEmits([
297
302
 
298
303
  ]);
299
304
 
300
- const checkboxes = ref([]);
305
+ const checkboxesInternal = ref([]);
301
306
  const page = ref(1);
302
307
  const sort = ref([]);
303
308
 
@@ -310,32 +315,41 @@ watch(() => sort.value, (newSort) => {
310
315
  emits('update:sort', newSort);
311
316
  });
312
317
 
313
- watch(() => checkboxes.value, (newCheckboxes) => {
318
+ watch(() => checkboxesInternal.value, (newCheckboxes) => {
319
+ console.log('checkboxesInternal ch changed, emiting', newCheckboxes)
320
+
314
321
  emits('update:checkboxes', newCheckboxes);
315
322
  });
316
323
 
324
+ watch(() => props.checkboxes, (newCheckboxes) => {
325
+ console.log('Props ch changed', newCheckboxes)
326
+ checkboxesInternal.value = newCheckboxes;
327
+ });
328
+
317
329
  function addToCheckedValues(id) {
318
- if (checkboxes.value.includes(id)) {
319
- checkboxes.value = checkboxes.value.filter((item) => item !== id);
330
+ if (checkboxesInternal.value.includes(id)) {
331
+ checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== id);
320
332
  } else {
321
- checkboxes.value.push(id);
333
+ checkboxesInternal.value.push(id);
322
334
  }
335
+ checkboxesInternal.value = [ ...checkboxesInternal.value ]
323
336
  }
324
337
 
325
338
  const columnsListed = computed(() => props.resource?.columns?.filter(c => c.showIn.includes('list')));
326
339
 
327
340
  async function selectAll(value) {
328
- rows.value.forEach((r) => {
329
- if (!checkboxes.value.includes(r.id)) {
330
- checkboxes.value.push(r.id)
341
+ props.rows.forEach((r) => {
342
+ if (!checkboxesInternal.value.includes(r.id)) {
343
+ checkboxesInternal.value.push(r.id)
331
344
  } else {
332
- checkboxes.value = checkboxes.value.filter((item) => item !== r.id)
345
+ checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== r.id)
333
346
  }
334
347
  });
348
+ checkboxesInternal.value = [ ...checkboxesInternal.value ]
335
349
  // checkboxes.value = rows.value.map((v) => v.id);
336
350
  }
337
351
 
338
- const totalPages = computed(() => Math.ceil(props.totalRows.value / props.pageSize.value));
352
+ const totalPages = computed(() => Math.ceil(props.totalRows / props.pageSize));
339
353
 
340
354
 
341
355
  const allFromThisPageChecked = computed(() => {
@@ -1,6 +1,11 @@
1
- import type { FrontendAPIInterface, ConfirmParams, AlertParams } from '../types/FrontendAPI';
1
+ import type { FrontendAPIInterface, ConfirmParams, AlertParams, FilterParams,Operator } from '../types/FrontendAPI';
2
2
  import { useToastStore } from '../stores/toast';
3
3
  import { useModalStore } from '../stores/modal';
4
+ import { useCoreStore } from '@/stores/core';
5
+ import { useFiltersStore } from '@/stores/filters';
6
+ import router from '@/router'
7
+
8
+
4
9
 
5
10
 
6
11
 
@@ -9,13 +14,19 @@ declare global {
9
14
  adminforth: {
10
15
  confirm: (params: ConfirmParams) => Promise<void>;
11
16
  alert: (params: AlertParams) => void;
17
+ setListFilter: (filter: any) => void;
18
+ updateListFilter: (filter: any) => void;
19
+ clearListFilters: () => void;
12
20
  };
13
21
  }
14
22
  }
15
23
 
16
24
  export class FrontendAPI implements FrontendAPIInterface {
25
+ private validOperators: Operator[] = ['lte', 'gte', 'in','ilike'];
17
26
  private toastStore:any
18
27
  private modalStore:any
28
+ private filtersStore:any
29
+ private coreStore:any
19
30
  init() {
20
31
  if (window.adminforth) {
21
32
  throw new Error('adminforth already initialized');
@@ -25,7 +36,10 @@ export class FrontendAPI implements FrontendAPIInterface {
25
36
  console.log(this.toastStore, this.modalStore,'init of adminforth frontend api')
26
37
  window.adminforth = {
27
38
  confirm: this.confirm.bind(this),
28
- alert: this.alert.bind(this)
39
+ alert: this.alert.bind(this),
40
+ setListFilter: this.setListFilter.bind(this),
41
+ updateListFilter: this.updateListFilter.bind(this),
42
+ clearListFilters: this.clearListFilters.bind(this),
29
43
  }
30
44
  }
31
45
 
@@ -44,8 +58,54 @@ export class FrontendAPI implements FrontendAPIInterface {
44
58
  variant: params.variant
45
59
  })
46
60
  }
47
- }
48
61
 
62
+ listFilterValidation(filter: FilterParams): boolean {
63
+ if(router.currentRoute.value.meta.type !== 'list'){
64
+ throw new Error(`Cannot use ${this.setListFilter.name} filter on a list page`)
65
+ } else {
66
+ if(!this.coreStore) this.coreStore = useCoreStore()
67
+ console.log(this.coreStore.resourceColumnsWithFilters,'core store')
68
+ const filterField = this.coreStore.resourceColumnsWithFilters.find((col) => col.name === filter.field)
69
+ if(!filterField){
70
+ throw new Error(`Field ${filter.field} is not available for filtering`)
71
+ }
72
+ if(filterField) {
73
+ if(!this.validOperators.includes(filter.operator)){
74
+ throw new Error(`Operator ${filter.operator} is not valid`)
75
+ }
76
+ }
77
+ }
78
+ return true
79
+ }
80
+
81
+ setListFilter(filter: FilterParams): void {
82
+ if(this.listFilterValidation(filter)){
83
+ this.filtersStore = useFiltersStore()
84
+ if(this.filtersStore.filters.some((f) => {return f.field === filter.field && f.operator === filter.operator})){
85
+ throw new Error(`Filter ${filter.field} with operator ${filter.operator} already exists`)
86
+ } else {
87
+ this.filtersStore.setFilter(filter)
88
+ }
89
+ }
90
+ }
91
+
92
+ clearListFilters(): void {
93
+ this.filtersStore = useFiltersStore()
94
+ this.filtersStore.clearFilters()
95
+ }
96
+
97
+ updateListFilter(filter: FilterParams): void {
98
+ if(this.listFilterValidation(filter)){
99
+ this.filtersStore = useFiltersStore()
100
+ const index = this.filtersStore.filters.findIndex((f) => f.field === filter.field)
101
+ if(index === -1) {
102
+ this.filtersStore.setFilter(filter)
103
+ } else {
104
+ this.filtersStore.setFilters([...this.filtersStore.filters.slice(0, index), filter, ...this.filtersStore.filters.slice(index + 1)])
105
+ }
106
+ }
107
+ }
49
108
 
50
109
 
51
110
 
111
+ }
@@ -9,6 +9,12 @@ export const useCoreStore = defineStore('core', () => {
9
9
  const record = ref({});
10
10
  const resourceColumns = ref(null);
11
11
  const resource = ref(null);
12
+ const resourceColumnsWithFilters = computed(() => {
13
+ if (!resourceColumns.value) {
14
+ return [];
15
+ }
16
+ return resourceColumns.value.filter((col) => col.showIn.includes('filter'));
17
+ })
12
18
 
13
19
  const resourceOptions = ref(null);
14
20
  const resourceColumnsError = ref('');
@@ -132,5 +138,6 @@ export const useCoreStore = defineStore('core', () => {
132
138
  logout,
133
139
  resource,
134
140
  adminUser,
141
+ resourceColumnsWithFilters
135
142
  }
136
143
  })
@@ -0,0 +1,22 @@
1
+ import { ref } from 'vue'
2
+ import { defineStore } from 'pinia'
3
+ import { callAdminForthApi } from '@/utils';
4
+
5
+ export const useFiltersStore = defineStore('filters', () => {
6
+ const filters = ref([]);
7
+ const setFilter = (filter: any) => {
8
+ filters.value = [...filters.value, filter];
9
+ }
10
+ const setFilters = (f: any) => {
11
+ filters.value = [...f];
12
+ }
13
+ const getFilters = () => {
14
+ return filters.value;
15
+ }
16
+ const clearFilters = () => {
17
+ filters.value = [];
18
+ }
19
+ return {setFilter, getFilters,clearFilters, filters,setFilters}
20
+ })
21
+
22
+
@@ -1,6 +1,5 @@
1
1
  import { ref } from 'vue'
2
2
  import { defineStore } from 'pinia'
3
- import { callAdminForthApi } from '@/utils';
4
3
 
5
4
  type ModalContentType = {
6
5
  title?: string;
@@ -13,13 +13,13 @@
13
13
  <BreadcrumbsWithButtons>
14
14
  <!-- save and cancle -->
15
15
  <button @click="$router.back()"
16
- class="flex items-center py-1 px-3 me-2 mb-2 text-sm font-medium rounded-default 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"
16
+ class="flex items-center py-1 px-3 me-2 text-sm font-medium rounded-default 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"
17
17
  >
18
18
  Cancel
19
19
  </button>
20
20
 
21
21
  <button @click="saveRecord"
22
- class="flex items-center py-1 px-3 mb-2 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50"
22
+ class="flex items-center py-1 px-3 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50"
23
23
  :disabled="saving || (validating && !isValid)"
24
24
  >
25
25
  <svg v-if="saving" x
@@ -45,7 +45,7 @@
45
45
 
46
46
  <ResourceForm
47
47
  v-else
48
- :record="{}"
48
+ :record="initalValues"
49
49
  :resourceColumns="coreStore.resourceColumns"
50
50
  @update:record="onUpdateRecord"
51
51
  @update:isValid="isValid = $event"
@@ -77,6 +77,7 @@ import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions } from
77
77
  import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
78
78
  import { onMounted, ref } from 'vue';
79
79
  import { useRoute, useRouter } from 'vue-router';
80
+ import { computed } from 'vue';
80
81
 
81
82
 
82
83
  const isValid = ref(false);
@@ -93,6 +94,10 @@ let createComponentsPerColumn = {};
93
94
 
94
95
  const coreStore = useCoreStore();
95
96
 
97
+ const initalValues = computed(() => {
98
+ return route.query || {};
99
+ });
100
+
96
101
  async function onUpdateRecord(newRecord) {
97
102
  console.log('newRecord', newRecord);
98
103
  record.value = newRecord;
@@ -12,13 +12,13 @@
12
12
  <BreadcrumbsWithButtons>
13
13
  <!-- save and cancle -->
14
14
  <button @click="$router.back()"
15
- class="flex items-center py-1 px-3 me-2 mb-2 text-sm font-medium text-gray-900 rounded-default 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"
15
+ class="flex items-center py-1 px-3 me-2 text-sm font-medium text-gray-900 rounded-default 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"
16
16
  >
17
17
  Cancel
18
18
  </button>
19
19
 
20
20
  <button @click="saveRecord"
21
- class="flex items-center py-1 px-3 mb-2 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
21
+ class="flex items-center py-1 px-3 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
22
22
  :disabled="saving || (validating && !isValid)"
23
23
  >
24
24
  <IconFloppyDiskSolid class="w-4 h-4" />
@@ -3,7 +3,6 @@
3
3
  <Teleport to="body">
4
4
  <Filters
5
5
  :columns="coreStore.resourceColumns"
6
- v-model:filters="filters"
7
6
  :columnsMinMax="columnsMinMax" :show="filtersShow"
8
7
  @hide="filtersShow = false"
9
8
  />
@@ -23,7 +22,7 @@
23
22
  v-if="checkboxes.length"
24
23
  data-tooltip-target="tooltip-remove-all"
25
24
  data-tooltip-placement="bottom"
26
- 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"
25
+ class="flex gap-1 items-center py-1 px-3 me-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"
27
26
  >
28
27
  <IconBanOutline class="w-5 h-5 "/>
29
28
 
@@ -39,7 +38,7 @@
39
38
  v-for="(action,i) in coreStore.resource?.options?.bulkActions"
40
39
  :key="action.id"
41
40
  @click="startBulkAction(action.id)"
42
- 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"
41
+ class="flex gap-1 items-center py-1 px-3 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"
43
42
  :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',
44
43
  'bg-blue-100 text-blue-800 border-blue-400 dark:bg-blue-700 dark:text-blue-400 dark:border-blue-400':action.state==='active',
45
44
  }"
@@ -52,24 +51,24 @@
52
51
  {{ `${action.label} (${checkboxes.length})` }}
53
52
  </button>
54
53
 
55
- <RouterLink v-if="coreStore.resource?.options?.create"
54
+ <RouterLink v-if="coreStore.resource?.options?.allowedActions?.create"
56
55
  :to="{ name: 'resource-create', params: { resourceId: $route.params.resourceId } }"
57
- 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 rounded-default"
56
+ class="flex items-center py-1 px-3 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 rounded-default"
58
57
  >
59
58
  <IconPlusOutline class="w-4 h-4 me-2"/>
60
59
  Create
61
60
  </RouterLink>
62
61
 
63
62
  <button
64
- 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 rounded-default"
63
+ class="flex gap-1 items-center py-1 px-3 me-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 rounded-default"
65
64
  @click="()=>{filtersShow = !filtersShow}"
66
65
  >
67
66
  <IconFilterOutline class="w-4 h-4 me-2"/>
68
67
  Filter
69
68
  <span
70
69
  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"
71
- v-if="filters.length">
72
- {{ filters.length }}
70
+ v-if="filtersStore.filters.length">
71
+ {{ filtersStore.filters.length }}
73
72
  </span>
74
73
  </button>
75
74
  </BreadcrumbsWithButtons>
@@ -90,6 +89,7 @@
90
89
  @update:checkboxes="checkboxes = $event"
91
90
  :pageSize="pageSize"
92
91
  :totalRows="totalRows"
92
+ :checkboxes="checkboxes"
93
93
  />
94
94
 
95
95
  <component
@@ -108,6 +108,7 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
108
108
  import ResourceListTable from '@/components/ResourceListTable.vue';
109
109
  import { useCoreStore } from '@/stores/core';
110
110
  import { useModalStore } from '@/stores/modal';
111
+ import { useFiltersStore } from '@/stores/filters';
111
112
  import { callAdminForthApi, getIcon } from '@/utils';
112
113
  import { initFlowbite } from 'flowbite';
113
114
  import { computed, onMounted, ref, watch } from 'vue';
@@ -116,17 +117,15 @@ import { useRoute } from 'vue-router';
116
117
  import ValueRenderer from '@/components/ValueRenderer.vue';
117
118
  import { getCustomComponent } from '@/utils';
118
119
 
119
- import {
120
- IconInboxOutline,
121
- IconPlusOutline
122
- } from '@iconify-prerendered/vue-flowbite';
123
120
 
124
121
  import {
125
- IconBanOutline,
126
- IconEyeSolid,
127
- IconFilterOutline,
128
- IconPenSolid,
129
- IconTrashBinSolid
122
+ IconBanOutline,
123
+ IconEyeSolid,
124
+ IconFilterOutline,
125
+ IconPenSolid,
126
+ IconTrashBinSolid,
127
+ IconInboxOutline,
128
+ IconPlusOutline
130
129
  } from '@iconify-prerendered/vue-flowbite';
131
130
 
132
131
  import Filters from '@/components/Filters.vue';
@@ -135,11 +134,12 @@ const filtersShow = ref(false);
135
134
 
136
135
  const coreStore = useCoreStore();
137
136
  const modalStore = useModalStore();
137
+ const filtersStore = useFiltersStore();
138
138
 
139
139
  const route = useRoute();
140
140
 
141
141
  const page = ref(1);
142
- const filters = ref([]);
142
+ const filters = filtersStore.filters;
143
143
  const columnsMinMax = ref({});
144
144
  const sort = ref([]);
145
145
 
@@ -157,7 +157,7 @@ watch([page], async () => {
157
157
  await getList();
158
158
  });
159
159
 
160
- watch([filters], async () => {
160
+ watch(()=>filtersStore.filters, async () => {
161
161
  page.value = 1;
162
162
  await getList();
163
163
  }, {deep: true});
@@ -177,7 +177,7 @@ async function getList() {
177
177
  resourceId: route.params.resourceId,
178
178
  limit: pageSize.value,
179
179
  offset: (page.value - 1) * pageSize.value,
180
- filters: filters.value,
180
+ filters: filtersStore.filters,
181
181
  sort: sort.value,
182
182
  }
183
183
  });
@@ -265,7 +265,7 @@ onMounted(async () => {
265
265
 
266
266
  // on route param change
267
267
  watch(() => route.params.resourceId, async () => {
268
- filters.value = [];
268
+ filtersStore.setFilters([]);
269
269
  checkboxes.value = [];
270
270
  sort.value = [];
271
271
  await init();
@@ -10,14 +10,14 @@
10
10
  />
11
11
  <BreadcrumbsWithButtons>
12
12
  <RouterLink v-if="coreStore?.resourceOptions?.allowedActions?.edit" :to="{ name: 'resource-edit', params: { resourceId: $route.params.resourceId, primaryKey: $route.params.primaryKey } }"
13
- class="flex 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"
13
+ class="flex items-center py-1 px-3 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"
14
14
  >
15
15
  <IconPenSolid class="w-4 h-4" />
16
16
  Edit
17
17
  </RouterLink>
18
18
 
19
19
  <button v-if="coreStore?.resourceOptions?.allowedActions?.delete" @click="showDeleteModal"
20
- class="flex items-center py-1 px-3 mb-2 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
20
+ class="flex items-center py-1 px-3 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
21
21
  >
22
22
  <IconTrashBinSolid class="w-4 h-4" />
23
23
  Delete
@@ -70,7 +70,7 @@
70
70
  :record="coreStore.record"
71
71
  />
72
72
  <template v-else>
73
- <td class="px-6 py-4 whitespace-nowrap">
73
+ <td class="px-6 py-4 whitespace-nowrap "> <!--align-top-->
74
74
  {{ column.label }}
75
75
  </td>
76
76
  <td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap">
@@ -19,6 +19,17 @@ export interface GenericHttpServer {
19
19
  */
20
20
  setupSpaServer(): void;
21
21
 
22
+ /**
23
+ * Method which should register endpoint in HTTP server.
24
+ *
25
+ * @param options : Object with method, path and handler properties.
26
+ */
27
+ endpoint(options: {
28
+ method: string,
29
+ path: string,
30
+ handler: Function,
31
+ }): void;
32
+
22
33
  }
23
34
 
24
35
  export interface ExpressHttpServer extends GenericHttpServer {
@@ -53,6 +64,7 @@ export interface AdminForthClass {
53
64
  codeInjector: CodeInjectorType;
54
65
  express: GenericHttpServer;
55
66
 
67
+
56
68
  auth: {
57
69
 
58
70
  verify(jwt : string): any;
@@ -91,8 +103,26 @@ export interface AdminForthPluginType {
91
103
  adminforth: AdminForthClass;
92
104
  pluginDir: string;
93
105
  customFolderName: string;
106
+ pluginInstanceId: string;
107
+
108
+ /**
109
+ * AdminForth plugins concept is based on modification of full AdminForth configuration
110
+ * to add some custom functionality. For example plugin might simply add custom field to resource by reusing
111
+ * {@link AdminForthResourceColumn.components} object, then add some hook which will modify record before getting or saving it to database.
112
+ *
113
+ * So this method is core of AdminForth plugins. It allows to modify full resource configuration.
114
+ * @param adminforth Instance of AdminForthClass
115
+ * @param resourceConfig Resource configuration object which will be modified by plugin
116
+ */
94
117
  modifyResourceConfig(adminforth: AdminForthClass, resourceConfig: AdminForthResource): void;
95
118
  componentPath(componentFile: string): string;
119
+
120
+ /**
121
+ * Here you can register custom endpoints for your plugin.
122
+ *
123
+ * @param server
124
+ */
125
+ setupEndpoints(server: GenericHttpServer): void;
96
126
  }
97
127
 
98
128
  export enum AdminForthMenuTypes {
@@ -450,9 +480,10 @@ export type AdminForthResource = {
450
480
  bulkActions?: Array<{
451
481
  label: string,
452
482
  state: string,
453
- icon: string,
483
+ icon?: string,
454
484
  action: Function,
455
485
  id?: string,
486
+ confirm?: string,
456
487
  }>,
457
488
  allowedActions?: AllowedActions,
458
489
 
@@ -33,6 +33,35 @@ export interface FrontendAPIInterface {
33
33
  * @param params - The parameters of the alert
34
34
  */
35
35
  alert(params:AlertParams): void;
36
+ /**
37
+ * Add a filter to the list of filters
38
+ *
39
+ * Example:
40
+ *
41
+ * ```ts
42
+ * window.adminforth.updateListFilter({field: 'name', operator: 'ilike', value: 'john'})
43
+ * ```
44
+ *
45
+ * @param filter - The filter to add
46
+ */
47
+ setListFilter(filter: any): void;
48
+ /**
49
+ * Update a filter in the list of filters
50
+ *
51
+ * Example:
52
+ *
53
+ * ```ts
54
+ * window.adminforth.updateListFilter({field: 'name', operator: 'ilike', value: 'john'})
55
+ * ```
56
+ *
57
+ * @param filter - The filter to update
58
+ */
59
+ updateListFilter(filter: any): void;
60
+ /**
61
+ * Clear all filters from the list
62
+ */
63
+ clearListFilters(): void;
64
+
36
65
  }
37
66
 
38
67
  export type ConfirmParams = {
@@ -62,12 +91,30 @@ export type AlertParams = {
62
91
  variant?: AlertVariant;
63
92
  }
64
93
 
94
+ export type FilterParams = {
95
+ /**
96
+ * Field of resource to filter
97
+ */
98
+ field: string;
99
+ /**
100
+ * Operator of filter
101
+ */
102
+ operator: Operator;
103
+ /**
104
+ * Value of filter
105
+ */
106
+ value: string | number | boolean ;
107
+ }
108
+
65
109
  export enum AlertVariant {
66
110
  Danger = 'danger',
67
111
  Success = 'success',
68
112
  Warning = 'warning',
69
113
  Info = 'info'
70
114
  }
115
+
116
+ export type Operator = 'in' | 'ilike' | 'gte' | 'lte' ;
117
+
71
118
 
72
119
 
73
120