adminforth 1.1.4 → 1.1.6

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 +15 -15
  9. package/dist/spa/spa/src/components/ResourceListTable.vue +36 -16
  10. package/dist/spa/spa/src/composables/useStores.ts +64 -3
  11. package/dist/spa/spa/src/stores/core.ts +17 -11
  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 +24 -10
  15. package/dist/spa/spa/src/views/EditView.vue +5 -5
  16. package/dist/spa/spa/src/views/ListView.vue +25 -23
  17. package/dist/spa/spa/src/views/ShowView.vue +5 -5
  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 +138 -14
  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 +15 -15
  28. package/spa/src/components/ResourceListTable.vue +36 -16
  29. package/spa/src/composables/useStores.ts +64 -3
  30. package/spa/src/stores/core.ts +17 -11
  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 +24 -10
  34. package/spa/src/views/EditView.vue +5 -5
  35. package/spa/src/views/ListView.vue +25 -23
  36. package/spa/src/views/ShowView.vue +5 -5
  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
 
@@ -152,7 +152,7 @@ const router = useRouter();
152
152
 
153
153
  const props = defineProps({
154
154
  loading: Boolean,
155
- resourceColumns: Object,
155
+ resource: Object,
156
156
  record: Object,
157
157
  validating: Boolean,
158
158
  customComponentsPerColumn: Object,
@@ -228,7 +228,7 @@ onMounted(() => {
228
228
 
229
229
  const columnOptions = computedAsync(async () => {
230
230
  return (await Promise.all(
231
- Object.values(props.resourceColumns).map(async (column) => {
231
+ Object.values(props.resource.columns).map(async (column) => {
232
232
  if (column.foreignResource) {
233
233
  const list = await callAdminForthApi({
234
234
  method: 'POST',
@@ -251,7 +251,7 @@ const coreStore = useCoreStore();
251
251
 
252
252
  const editableColumns = computed(() => {
253
253
  const mode = props.record ? 'edit' : 'create';
254
- return props.resourceColumns?.filter(column => column.showIn.includes(mode));
254
+ return props.resource?.columns?.filter(column => column.showIn.includes(mode));
255
255
  });
256
256
 
257
257
  const isValid = computed(() => {
@@ -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>
@@ -146,7 +146,13 @@
146
146
  <div class="flex">
147
147
  <RouterLink
148
148
  v-if="resource.options?.allowedActions.show"
149
- :to="{ name: 'resource-show', params: { resourceId: $route.params.resourceId, primaryKey: row._primaryKeyValue } }"
149
+ :to="{
150
+ name: 'resource-show',
151
+ params: {
152
+ resourceId: resource.resourceId,
153
+ primaryKey: row._primaryKeyValue,
154
+ }
155
+ }"
150
156
  class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
151
157
  :data-tooltip-target="`tooltip-show-${rowI}`"
152
158
  >
@@ -161,10 +167,12 @@
161
167
  </div>
162
168
 
163
169
  <RouterLink v-if="resource.options?.allowedActions.edit"
164
- :to="{ name: 'resource-edit', params: {
165
- resourceId: resource.id,
166
- primaryKey: row._primaryKeyValue
167
- }
170
+ :to="{
171
+ name: 'resource-edit',
172
+ params: {
173
+ resourceId: resource.resourceId,
174
+ primaryKey: row._primaryKeyValue
175
+ }
168
176
  }"
169
177
  class="font-medium text-blue-600 dark:text-blue-500 hover:underline ms-3"
170
178
  :data-tooltip-target="`tooltip-edit-${rowI}`"
@@ -239,6 +247,7 @@
239
247
  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
248
  @click="page = totalPages" :disabled="page >= totalPages">
241
249
  {{ totalPages }}
250
+
242
251
  <!-- <IconChevronDoubleRightOutline class="w-4 h-4" /> -->
243
252
  </button>
244
253
  <button
@@ -253,6 +262,7 @@
253
262
  </svg>
254
263
  </button>
255
264
  </div>
265
+
256
266
  </div>
257
267
  </template>
258
268
 
@@ -287,6 +297,7 @@ const props = defineProps([
287
297
  'rows',
288
298
  'totalRows',
289
299
  'pageSize',
300
+ 'checkboxes'
290
301
  ])
291
302
 
292
303
  // emits, update page
@@ -297,7 +308,7 @@ const emits = defineEmits([
297
308
 
298
309
  ]);
299
310
 
300
- const checkboxes = ref([]);
311
+ const checkboxesInternal = ref([]);
301
312
  const page = ref(1);
302
313
  const sort = ref([]);
303
314
 
@@ -310,32 +321,41 @@ watch(() => sort.value, (newSort) => {
310
321
  emits('update:sort', newSort);
311
322
  });
312
323
 
313
- watch(() => checkboxes.value, (newCheckboxes) => {
324
+ watch(() => checkboxesInternal.value, (newCheckboxes) => {
325
+ console.log('checkboxesInternal ch changed, emiting', newCheckboxes)
326
+
314
327
  emits('update:checkboxes', newCheckboxes);
315
328
  });
316
329
 
330
+ watch(() => props.checkboxes, (newCheckboxes) => {
331
+ console.log('Props ch changed', newCheckboxes)
332
+ checkboxesInternal.value = newCheckboxes;
333
+ });
334
+
317
335
  function addToCheckedValues(id) {
318
- if (checkboxes.value.includes(id)) {
319
- checkboxes.value = checkboxes.value.filter((item) => item !== id);
336
+ if (checkboxesInternal.value.includes(id)) {
337
+ checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== id);
320
338
  } else {
321
- checkboxes.value.push(id);
339
+ checkboxesInternal.value.push(id);
322
340
  }
341
+ checkboxesInternal.value = [ ...checkboxesInternal.value ]
323
342
  }
324
343
 
325
344
  const columnsListed = computed(() => props.resource?.columns?.filter(c => c.showIn.includes('list')));
326
345
 
327
346
  async function selectAll(value) {
328
- rows.value.forEach((r) => {
329
- if (!checkboxes.value.includes(r.id)) {
330
- checkboxes.value.push(r.id)
347
+ props.rows.forEach((r) => {
348
+ if (!checkboxesInternal.value.includes(r.id)) {
349
+ checkboxesInternal.value.push(r.id)
331
350
  } else {
332
- checkboxes.value = checkboxes.value.filter((item) => item !== r.id)
351
+ checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== r.id)
333
352
  }
334
353
  });
354
+ checkboxesInternal.value = [ ...checkboxesInternal.value ]
335
355
  // checkboxes.value = rows.value.map((v) => v.id);
336
356
  }
337
357
 
338
- const totalPages = computed(() => Math.ceil(props.totalRows.value / props.pageSize.value));
358
+ const totalPages = computed(() => Math.ceil(props.totalRows / props.pageSize));
339
359
 
340
360
 
341
361
  const allFromThisPageChecked = computed(() => {
@@ -1,6 +1,12 @@
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
+ import type { AdminForthResourceColumn } from '@/types/AdminForthConfig';
8
+
9
+
4
10
 
5
11
 
6
12
 
@@ -9,13 +15,19 @@ declare global {
9
15
  adminforth: {
10
16
  confirm: (params: ConfirmParams) => Promise<void>;
11
17
  alert: (params: AlertParams) => void;
18
+ setListFilter: (filter: any) => void;
19
+ updateListFilter: (filter: any) => void;
20
+ clearListFilters: () => void;
12
21
  };
13
22
  }
14
23
  }
15
24
 
16
25
  export class FrontendAPI implements FrontendAPIInterface {
26
+ private validOperators: Operator[] = ['lte', 'gte', 'in','ilike'];
17
27
  private toastStore:any
18
28
  private modalStore:any
29
+ private filtersStore:any
30
+ private coreStore:any
19
31
  init() {
20
32
  if (window.adminforth) {
21
33
  throw new Error('adminforth already initialized');
@@ -25,7 +37,10 @@ export class FrontendAPI implements FrontendAPIInterface {
25
37
  console.log(this.toastStore, this.modalStore,'init of adminforth frontend api')
26
38
  window.adminforth = {
27
39
  confirm: this.confirm.bind(this),
28
- alert: this.alert.bind(this)
40
+ alert: this.alert.bind(this),
41
+ setListFilter: this.setListFilter.bind(this),
42
+ updateListFilter: this.updateListFilter.bind(this),
43
+ clearListFilters: this.clearListFilters.bind(this),
29
44
  }
30
45
  }
31
46
 
@@ -44,8 +59,54 @@ export class FrontendAPI implements FrontendAPIInterface {
44
59
  variant: params.variant
45
60
  })
46
61
  }
47
- }
48
62
 
63
+ listFilterValidation(filter: FilterParams): boolean {
64
+ if(router.currentRoute.value.meta.type !== 'list'){
65
+ throw new Error(`Cannot use ${this.setListFilter.name} filter on a list page`)
66
+ } else {
67
+ if(!this.coreStore) this.coreStore = useCoreStore()
68
+ console.log(this.coreStore.resourceColumnsWithFilters,'core store')
69
+ const filterField = this.coreStore.resourceColumnsWithFilters.find((col: AdminForthResourceColumn) => col.name === filter.field)
70
+ if(!filterField){
71
+ throw new Error(`Field ${filter.field} is not available for filtering`)
72
+ }
73
+ if(filterField) {
74
+ if(!this.validOperators.includes(filter.operator)){
75
+ throw new Error(`Operator ${filter.operator} is not valid`)
76
+ }
77
+ }
78
+ }
79
+ return true
80
+ }
81
+
82
+ setListFilter(filter: FilterParams): void {
83
+ if(this.listFilterValidation(filter)){
84
+ this.filtersStore = useFiltersStore()
85
+ if(this.filtersStore.filters.some((f) => {return f.field === filter.field && f.operator === filter.operator})){
86
+ throw new Error(`Filter ${filter.field} with operator ${filter.operator} already exists`)
87
+ } else {
88
+ this.filtersStore.setFilter(filter)
89
+ }
90
+ }
91
+ }
92
+
93
+ clearListFilters(): void {
94
+ this.filtersStore = useFiltersStore()
95
+ this.filtersStore.clearFilters()
96
+ }
97
+
98
+ updateListFilter(filter: FilterParams): void {
99
+ if(this.listFilterValidation(filter)){
100
+ this.filtersStore = useFiltersStore()
101
+ const index = this.filtersStore.filters.findIndex((f: FilterParams) => f.field === filter.field)
102
+ if(index === -1) {
103
+ this.filtersStore.setFilter(filter)
104
+ } else {
105
+ this.filtersStore.setFilters([...this.filtersStore.filters.slice(0, index), filter, ...this.filtersStore.filters.slice(index + 1)])
106
+ }
107
+ }
108
+ }
49
109
 
50
110
 
51
111
 
112
+ }
@@ -1,14 +1,22 @@
1
1
  import { ref, computed } from 'vue'
2
2
  import { defineStore } from 'pinia'
3
3
  import { callAdminForthApi } from '@/utils';
4
+ import type { AdminForthResource, AdminForthResourceColumn } from '@/types/AdminForthConfig';
5
+ import type { Ref } from 'vue'
4
6
 
5
7
  export const useCoreStore = defineStore('core', () => {
6
- const resourceById = ref([]);
8
+ const resourceById: Ref<Object> = ref({});
7
9
  const menu = ref([]);
8
10
  const config = ref({});
9
11
  const record = ref({});
10
- const resourceColumns = ref(null);
11
- const resource = ref(null);
12
+ const resource: Ref<AdminForthResource | null> = ref(null);
13
+
14
+ const resourceColumnsWithFilters = computed(() => {
15
+ if (!resource.value) {
16
+ return [];
17
+ }
18
+ return resource.value.columns.filter((col: AdminForthResourceColumn) => col.showIn?.includes('filter'));
19
+ })
12
20
 
13
21
  const resourceOptions = ref(null);
14
22
  const resourceColumnsError = ref('');
@@ -24,7 +32,7 @@ export const useCoreStore = defineStore('core', () => {
24
32
  return
25
33
  }
26
34
  menu.value = resp.menu;
27
- resourceById.value = resp.resources.reduce((acc, resource) => {
35
+ resourceById.value = resp.resources.reduce((acc: Object, resource: AdminForthResource) => {
28
36
  acc[resource.resourceId] = resource;
29
37
  return acc;
30
38
  }, {});
@@ -36,7 +44,7 @@ export const useCoreStore = defineStore('core', () => {
36
44
  async function fetchRecord({ resourceId, primaryKey }) {
37
45
  record.value = null;
38
46
 
39
- if (!resourceColumns.value) {
47
+ if (!resource.value) {
40
48
  throw new Error('Columns not fetched yet');
41
49
  }
42
50
 
@@ -48,7 +56,7 @@ export const useCoreStore = defineStore('core', () => {
48
56
  resourceId: resourceId,
49
57
  filters: [
50
58
  {
51
- field: resourceColumns.value.find((col) => col.primaryKey).name,
59
+ field: resource.value.columns.find((col: AdminForthResourceColumn) => col.primaryKey).name,
52
60
  operator: 'eq',
53
61
  value: primaryKey
54
62
  }
@@ -64,13 +72,12 @@ export const useCoreStore = defineStore('core', () => {
64
72
  resourceOptions.value = respData.options;
65
73
  }
66
74
 
67
- async function fetchResourceFull({ resourceId }) {
68
- if (resourceColumnsId.value === resourceId && resourceColumns.value) {
75
+ async function fetchResourceFull({ resourceId }: { resourceId: string }) {
76
+ if (resourceColumnsId.value === resourceId && resource.value) {
69
77
  // already fetched
70
78
  return;
71
79
  }
72
80
  resourceColumnsId.value = resourceId;
73
- resourceColumns.value = null;
74
81
  resourceColumnsError.value = '';
75
82
  const res = await callAdminForthApi({
76
83
  path: '/get_resource',
@@ -82,7 +89,6 @@ export const useCoreStore = defineStore('core', () => {
82
89
  if (res.error) {
83
90
  resourceColumnsError.value = res.error;
84
91
  } else {
85
- resourceColumns.value = res.resource.columns;
86
92
  resourceById.value[resourceId] = res.resource;
87
93
  resource.value = res.resource;
88
94
  resourceOptions.value = res.resource.options;
@@ -125,12 +131,12 @@ export const useCoreStore = defineStore('core', () => {
125
131
  fetchMenuAndResource,
126
132
  fetchRecord,
127
133
  record,
128
- resourceColumns,
129
134
  fetchResourceFull,
130
135
  resourceColumnsError,
131
136
  resourceOptions,
132
137
  logout,
133
138
  resource,
134
139
  adminUser,
140
+ resourceColumnsWithFilters
135
141
  }
136
142
  })
@@ -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;