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
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ class AdminForth {
35
35
  });
36
36
  this.config = Object.assign(Object.assign({}, __classPrivateFieldGet(this, _AdminForth_defaultConfig, "f")), config);
37
37
  this.codeInjector = new CodeInjector(this);
38
+ this.activatedPlugins = [];
38
39
  this.validateConfig();
39
40
  this.activatePlugins();
40
41
  this.validateConfig(); // revalidate after plugins
@@ -48,6 +49,7 @@ class AdminForth {
48
49
  for (let resource of this.config.resources) {
49
50
  for (let pluginInstance of resource.plugins || []) {
50
51
  pluginInstance.modifyResourceConfig(this, resource);
52
+ this.activatedPlugins.push(pluginInstance);
51
53
  }
52
54
  }
53
55
  ;
@@ -950,6 +952,10 @@ class AdminForth {
950
952
  };
951
953
  })
952
954
  });
955
+ // setup endpoints for all plugins
956
+ this.activatedPlugins.forEach((plugin) => {
957
+ plugin.setupEndpoints(server);
958
+ });
953
959
  }
954
960
  }
955
961
  _a = AdminForth, _AdminForth_defaultConfig = new WeakMap();
@@ -2,19 +2,20 @@ export const styles = () => `
2
2
 
3
3
  "colors": {
4
4
  "html-bg": "#FFFFFF", // Changed to white
5
- "header-bg": "#EDEDED", // Light grey
5
+ "header-bg": "#FFFFFF", // Light grey
6
6
  "header-bg-hover": "#D3D3D3", // Light grey hover
7
7
  "header-text": "#333333", // Dark grey text
8
8
  "header-text-hover": "#333333", // Dark grey text hover
9
9
  "header-icons": "#333333", // Dark grey icons
10
10
  "header-icons-hover": "#333333", // Dark grey icons hover
11
11
  "header-logo-color": "#333333", // Dark grey logo
12
- "header-border-color": "#CCCCCC", // Light grey border
12
+ "header-border-color": "rgb(229 231 235)", // Light grey border
13
13
 
14
- "nav-menu-bg": "#F5F5F5", // Very light grey
15
- "nav-menu-bg-hover": "#E0E0E0", // Light grey hover
16
- "nav-menu-bg-active": "#CCCCCC", // Light grey active
17
- "nav-menu-hover": "#E0E0E0", // Light grey hover
14
+ "nav-menu-bg": "#f9fafb", // Very light grey
15
+ "nav-menu-border": "rgb(229 231 235)",
16
+ "nav-menu-bg-hover": "rgb(243 244 246)", // Light grey hover
17
+ "nav-menu-bg-active": "rgb(233, 234, 236)", // Light grey active
18
+ "nav-menu-hover": "#rgb(209 213 219)", // Light grey hover
18
19
  "nav-menu-active": "#CCCCCC", // Light grey active
19
20
  "nav-menu-active-text": "#333333", // Dark grey active text
20
21
  "nav-menu-text": "#333333", // Dark grey text
@@ -1,3 +1,12 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
1
10
  import { AdminForthResourcePages } from "../../types/AdminForthConfig.js";
2
11
  import AdminForthPlugin from "../base.js";
3
12
  export default class ForeignInlineListPlugin extends AdminForthPlugin {
@@ -5,8 +14,27 @@ export default class ForeignInlineListPlugin extends AdminForthPlugin {
5
14
  super(options, import.meta.url);
6
15
  this.options = options;
7
16
  }
17
+ setupEndpoints(server) {
18
+ server.endpoint({
19
+ method: 'POST',
20
+ path: `/plugin/${this.pluginInstanceId}/get_resource`,
21
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body }) {
22
+ const resource = this.adminforth.config.resources.find((res) => this.options.foreignResourceId === res.resourceId);
23
+ if (!resource) {
24
+ return { error: `Resource ${this.options.foreignResourceId} not found` };
25
+ }
26
+ // exclude "plugins" key
27
+ const resourceCopy = Object.assign(Object.assign({}, resource), { plugins: undefined });
28
+ if (this.options.modifyTableResourceConfig) {
29
+ this.options.modifyTableResourceConfig(resourceCopy);
30
+ }
31
+ return { resource: resourceCopy };
32
+ }),
33
+ });
34
+ }
8
35
  modifyResourceConfig(adminforth, resourceConfig) {
9
36
  super.modifyResourceConfig(adminforth, resourceConfig);
37
+ this.adminforth = adminforth;
10
38
  // get resource with foreignResourceId
11
39
  this.foreignResource = adminforth.config.resources.find((resource) => resource.resourceId === this.options.foreignResourceId);
12
40
  if (!this.foreignResource) {
@@ -19,7 +47,10 @@ export default class ForeignInlineListPlugin extends AdminForthPlugin {
19
47
  virtual: true,
20
48
  showIn: [AdminForthResourcePages.show],
21
49
  components: {
22
- showRow: { file: this.componentPath('InlineList.vue'), meta: this.options }
50
+ showRow: {
51
+ file: this.componentPath('InlineList.vue'),
52
+ meta: Object.assign(Object.assign({}, this.options), { pluginInstanceId: this.pluginInstanceId })
53
+ }
23
54
  },
24
55
  });
25
56
  }
@@ -2,11 +2,15 @@ import { getComponentNameFromPath } from '../modules/utils.js';
2
2
  import { currentFileDir } from '../modules/utils.js';
3
3
  import path from 'path';
4
4
  import fs from 'fs';
5
+ import { v4 as uuidv4 } from 'uuid';
5
6
  export default class AdminForthPlugin {
6
7
  constructor(pluginOptions, metaUrl) {
7
8
  this.customFolderName = 'custom';
8
9
  // set up plugin here
9
10
  this.pluginDir = currentFileDir(metaUrl);
11
+ this.pluginInstanceId = uuidv4();
12
+ }
13
+ setupEndpoints(server) {
10
14
  }
11
15
  modifyResourceConfig(adminforth, resourceConfig) {
12
16
  this.adminforth = adminforth;
@@ -59,7 +59,7 @@
59
59
 
60
60
  v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady"
61
61
 
62
- id="logo-sidebar" class="fixed border-none top-0 left-0 z-50 w-64 h-screen transition-transform bg-nav-menu-bg border-r border- sm:translate-x-0 dark:bg-gray-800 dark:border-gray-700"
62
+ id="logo-sidebar" class="fixed border-none top-0 left-0 z-20 w-64 h-screen transition-transform bg-nav-menu-bg border-r border-nav-menu-border sm:translate-x-0 dark:bg-gray-800 dark:border-gray-700"
63
63
  :class="{ '-translate-x-full': !sideBarOpen, 'transform-none': sideBarOpen }"
64
64
  aria-label="Sidebar"
65
65
  >
@@ -136,7 +136,7 @@
136
136
  <div v-if="sideBarOpen"
137
137
  @click="sideBarOpen = false"
138
138
 
139
- drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"></div>
139
+ drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-5"></div>
140
140
 
141
141
 
142
142
 
@@ -2,7 +2,7 @@
2
2
  <div>
3
3
  <div class="flex items-center justify-between mb-3 flex-wrap gap-y-2">
4
4
  <Breadcrumbs />
5
- <div class="flex items-center space-x-1">
5
+ <div class="flex items-center space-x-1 flex-wrap gap-y-1">
6
6
  <slot></slot>
7
7
  </div>
8
8
  </div>
@@ -26,13 +26,13 @@
26
26
  v-if="c.foreignResource"
27
27
  :options="columnOptions[c.name] || []"
28
28
  @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
29
- :modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
29
+ :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
30
30
  />
31
31
  <Dropdown
32
32
  v-else-if="c.type === 'boolean'"
33
33
  :options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
34
34
  @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
35
- :modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
35
+ :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
36
36
  />
37
37
 
38
38
  <Dropdown
@@ -40,7 +40,7 @@
40
40
  :options="c.enum"
41
41
  :allowCustom="c.allowCustom"
42
42
  @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
43
- :modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
43
+ :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
44
44
  />
45
45
 
46
46
  <input
@@ -54,9 +54,9 @@
54
54
  <CustomDateRangePicker
55
55
  v-else-if="['datetime'].includes(c.type)"
56
56
  :column="c"
57
- :valueStart="filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
57
+ :valueStart="filtersStore.filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
58
58
  @update:valueStart="setFilterItem({ column: c, operator: 'gte', value: $event || undefined })"
59
- :valueEnd="filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
59
+ :valueEnd="filtersStore.filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
60
60
  @update:valueEnd="setFilterItem({ column: c, operator: 'lte', value: $event || undefined })"
61
61
  />
62
62
 
@@ -101,9 +101,9 @@
101
101
 
102
102
  <div class="flex justify-end gap-2">
103
103
  <button
104
- :disabled="!filters.length"
104
+ :disabled="!filtersStore.filters.length"
105
105
  type="button"
106
- 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 disabled:opacity-50 disabled:cursor-not-allowed"
106
+ 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 disabled:opacity-50 disabled:cursor-not-allowed"
107
107
  @click="clear">Clear all</button>
108
108
 
109
109
  </div>
@@ -122,6 +122,10 @@ import { callAdminForthApi } from '@/utils';
122
122
  import { useRouter } from 'vue-router';
123
123
  import { computedAsync } from '@vueuse/core'
124
124
  import CustomRangePicker from "@/components/CustomRangePicker.vue";
125
+ import { useFiltersStore } from '@/stores/filters';
126
+
127
+ const filtersStore = useFiltersStore();
128
+
125
129
 
126
130
  // props: columns
127
131
  // add support for v-model:filers
@@ -180,28 +184,28 @@ watch(() => props.show, (show) => {
180
184
 
181
185
  function setFilterItem({ column, operator, value }) {
182
186
 
183
- const index = props.filters.findIndex(f => f.field === column.name && f.operator === operator);
187
+ const index = filtersStore.filters.findIndex(f => f.field === column.name && f.operator === operator);
184
188
  if (value === undefined) {
185
189
  if (index !== -1) {
186
- props.filters.splice(index, 1);
190
+ filtersStore.filters.splice(index, 1);
187
191
  }
188
- emits('update:filters', [...props.filters]);
192
+ emits('update:filters', [...filtersStore.filters]);
189
193
  return;
190
194
  } else {
191
195
  if (index === -1) {
192
- props.filters.push({ field: column.name, value, operator });
196
+ filtersStore.setFilter({ field: column.name, value, operator });
193
197
  } else {
194
- props.filters[index].value = value;
198
+ filtersStore.setFilters([...filtersStore.filters.slice(0, index), { field: column.name, value, operator }, ...filtersStore.filters.slice(index + 1)])
195
199
  }
196
200
  }
197
- emits('update:filters', [...props.filters]);
201
+ emits('update:filters', [...filtersStore.filters]);
198
202
  }
199
203
 
200
204
  function getFilterItem({ column, operator }) {
201
- return props.filters.find(f => f.field === column.name && f.operator === operator)?.value || '';
205
+ return filtersStore.filters.find(f => f.field === column.name && f.operator === operator)?.value || '';
202
206
  }
203
207
 
204
208
  async function clear() {
205
- emits('update:filters', []);
209
+ filtersStore.clearFilters();
206
210
  }
207
211
  </script>
@@ -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" />