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
@@ -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,8 +45,8 @@
45
45
 
46
46
  <ResourceForm
47
47
  v-else
48
- :record="{}"
49
- :resourceColumns="coreStore.resourceColumns"
48
+ :record="initalValues"
49
+ :resource="coreStore.resource"
50
50
  @update:record="onUpdateRecord"
51
51
  @update:isValid="isValid = $event"
52
52
  :validating="validating"
@@ -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,13 @@ let createComponentsPerColumn = {};
93
94
 
94
95
  const coreStore = useCoreStore();
95
96
 
97
+ const initalValues = computed(() => {
98
+ if (!route.query.values) {
99
+ return {};
100
+ }
101
+ return JSON.parse(decodeURIComponent(route.query.values));
102
+ });
103
+
96
104
  async function onUpdateRecord(newRecord) {
97
105
  console.log('newRecord', newRecord);
98
106
  record.value = newRecord;
@@ -103,7 +111,7 @@ onMounted(async () => {
103
111
  await coreStore.fetchResourceFull({
104
112
  resourceId: route.params.resourceId
105
113
  });
106
- createComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
114
+ createComponentsPerColumn = coreStore.resource.columns.reduce((acc, column) => {
107
115
  if (column.components?.create) {
108
116
  acc[column.name] = getCustomComponent(column.components.create);
109
117
  }
@@ -130,11 +138,17 @@ async function saveRecord() {
130
138
  },
131
139
  });
132
140
  saving.value = false;
133
- router.push({ name: 'resource-show', params: {
134
- resourceId: route.params.resourceId,
135
- primaryKey: response.newRecordId
136
-
137
- } });
141
+ if (route.query.returnTo) {
142
+ router.push(route.query.returnTo);
143
+ } else {
144
+ router.push({
145
+ name: 'resource-show',
146
+ params: {
147
+ resourceId: route.params.resourceId,
148
+ primaryKey: response.newRecordId
149
+ }
150
+ });
151
+ }
138
152
  }
139
153
 
140
154
 
@@ -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" />
@@ -95,10 +95,10 @@ async function onUpdateRecord(newRecord) {
95
95
 
96
96
  const editableRecord = computed(() => {
97
97
  const newRecord = { ...coreStore.record };
98
- if (!coreStore.resourceColumns) {
98
+ if (!coreStore.resource) {
99
99
  return {};
100
100
  }
101
- coreStore.resourceColumns.forEach(column => {
101
+ coreStore.resource.columns.forEach(column => {
102
102
  if (column.foreignResource) {
103
103
  newRecord[column.name] = newRecord[column.name]?.pk
104
104
  }
@@ -119,7 +119,7 @@ onMounted(async () => {
119
119
  });
120
120
  checkAcessByAllowedActions(coreStore.resourceOptions.allowedActions,'edit');
121
121
 
122
- editComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
122
+ editComponentsPerColumn = coreStore.resource.columns.reduce((acc, column) => {
123
123
  if (column.components?.edit) {
124
124
  acc[column.name] = getCustomComponent(column.components.edit);
125
125
  }
@@ -2,8 +2,7 @@
2
2
  <div class="relative">
3
3
  <Teleport to="body">
4
4
  <Filters
5
- :columns="coreStore.resourceColumns"
6
- v-model:filters="filters"
5
+ :columns="coreStore.resource.columns"
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,8 +157,10 @@ 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
+ checkboxes.value = [];
163
+
162
164
  await getList();
163
165
  }, {deep: true});
164
166
 
@@ -177,13 +179,13 @@ async function getList() {
177
179
  resourceId: route.params.resourceId,
178
180
  limit: pageSize.value,
179
181
  offset: (page.value - 1) * pageSize.value,
180
- filters: filters.value,
182
+ filters: filtersStore.filters,
181
183
  sort: sort.value,
182
184
  }
183
185
  });
184
186
 
185
187
  rows.value = data.data?.map(row => {
186
- row._primaryKeyValue = row[coreStore.resourceColumns.find(c => c.primaryKey).name];
188
+ row._primaryKeyValue = row[coreStore.resource.columns.find(c => c.primaryKey).name];
187
189
  return row;
188
190
  });
189
191
  totalRows.value = data.total;
@@ -265,7 +267,7 @@ onMounted(async () => {
265
267
 
266
268
  // on route param change
267
269
  watch(() => route.params.resourceId, async () => {
268
- filters.value = [];
270
+ filtersStore.setFilters([]);
269
271
  checkboxes.value = [];
270
272
  sort.value = [];
271
273
  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
@@ -58,7 +58,7 @@
58
58
  </tr>
59
59
  </thead>
60
60
  <tbody>
61
- <tr v-for="column in coreStore.resourceColumns?.filter(c => c.showIn.includes('show'))" :key="column.name"
61
+ <tr v-for="column in coreStore.resource?.columns.filter(c => c.showIn.includes('show'))" :key="column.name"
62
62
  class="bg-form-view-bg odd:dark:bg-gray-900 even:dark:bg-gray-800 border-b dark:border-gray-700"
63
63
  >
64
64
  <component
@@ -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">
@@ -145,7 +145,7 @@ onMounted(async () => {
145
145
  });
146
146
  checkAcessByAllowedActions(coreStore.resourceOptions.allowedActions,'show');
147
147
 
148
- showComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
148
+ showComponentsPerColumn = coreStore.resource.columns.reduce((acc, column) => {
149
149
  if (column.components?.show) {
150
150
  acc[column.name] = getCustomComponent(column.components.show);
151
151
  }
package/index.ts CHANGED
@@ -13,6 +13,7 @@ import { AdminForthConfig, AdminForthClass, AdminForthComponentDeclaration,
13
13
  AdminForthFilterOperators, AdminForthDataTypes, AdminForthResourcePages } from './types/AdminForthConfig.js';
14
14
  import { getFunctionList } from './modules/utils.js';
15
15
  import path from 'path';
16
+ import AdminForthPlugin from './plugins/base.js';
16
17
 
17
18
 
18
19
  //get array from enum AdminForthResourcePages
@@ -40,6 +41,7 @@ class AdminForth implements AdminForthClass {
40
41
  connectors: any;
41
42
  connectorClasses: any;
42
43
  runningHotReload: boolean;
44
+ activatedPlugins: Array<AdminForthPlugin>;
43
45
 
44
46
  statuses: {
45
47
  dbDiscover?: 'running' | 'done',
@@ -48,6 +50,7 @@ class AdminForth implements AdminForthClass {
48
50
  constructor(config: AdminForthConfig) {
49
51
  this.config = {...this.#defaultConfig,...config};
50
52
  this.codeInjector = new CodeInjector(this);
53
+ this.activatedPlugins = [];
51
54
 
52
55
  this.validateConfig();
53
56
  this.activatePlugins();
@@ -57,6 +60,7 @@ class AdminForth implements AdminForthClass {
57
60
  this.auth = new Auth();
58
61
  this.connectors = {};
59
62
  this.statuses = {};
63
+
60
64
  console.log(`🚀 AdminForth v${ADMINFORTH_VERSION} starting up`)
61
65
  }
62
66
 
@@ -64,6 +68,7 @@ class AdminForth implements AdminForthClass {
64
68
  for (let resource of this.config.resources) {
65
69
  for (let pluginInstance of resource.plugins || []) {
66
70
  pluginInstance.modifyResourceConfig(this, resource);
71
+ this.activatedPlugins.push(pluginInstance);
67
72
  }
68
73
  };
69
74
  }
@@ -1062,6 +1067,11 @@ class AdminForth implements AdminForthClass {
1062
1067
  }
1063
1068
  }
1064
1069
  })
1070
+
1071
+ // setup endpoints for all plugins
1072
+ this.activatedPlugins.forEach((plugin) => {
1073
+ plugin.setupEndpoints(server);
1074
+ });
1065
1075
  }
1066
1076
  }
1067
1077
 
package/modules/styles.ts CHANGED
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,9 +1,84 @@
1
1
  <template>
2
- <td colspan="2">
3
- <h4 v-if="listResource"
4
- class="px-6 py-4"
2
+ <Teleport to="body">
3
+ <!-- todo exclude foreign column-->
4
+ <Filters
5
+ v-if="listResource"
6
+ :columns="listResource.columns.filter((c) => c.name !== listResourceRefColumn.name)"
7
+ v-model:filters="filters"
8
+ :columnsMinMax="columnsMinMax"
9
+ :show="filtersShow"
10
+ @hide="filtersShow = false"
11
+ />
12
+ </Teleport>
5
13
 
6
- >{{ listResource.label }} inline records</h4>
14
+ <td colspan="2">
15
+ <div class="flex items-center gap-1">
16
+ <h4 v-if="listResource"
17
+ class="px-6 py-4"
18
+ >{{ listResource.label }} inline records</h4>
19
+
20
+ <button
21
+ @click="()=>{checkboxes = []}"
22
+ v-if="checkboxes.length"
23
+ data-tooltip-target="tooltip-remove-all"
24
+ data-tooltip-placement="bottom"
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"
26
+ >
27
+ <IconBanOutline class="w-5 h-5 "/>
28
+ <div id="tooltip-remove-all" role="tooltip"
29
+ 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">
30
+ Remove selection
31
+ <div class="tooltip-arrow" data-popper-arrow></div>
32
+ </div>
33
+ </button>
34
+
35
+ <button
36
+ v-if="checkboxes.length"
37
+ v-for="(action,i) in listResource?.options?.bulkActions"
38
+ :key="action.id"
39
+ @click="startBulkAction(action.id)"
40
+ 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"
41
+ :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',
42
+ 'bg-blue-100 text-blue-800 border-blue-400 dark:bg-blue-700 dark:text-blue-400 dark:border-blue-400':action.state==='active',
43
+ }"
44
+ >
45
+ <component
46
+ v-if="action.icon"
47
+ :is="getIcon(action.icon)"
48
+ class="w-5 h-5 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white"></component>
49
+
50
+ {{ `${action.label} (${checkboxes.length})` }}
51
+ </button>
52
+
53
+ <RouterLink v-if="listResource?.options?.allowedActions?.create"
54
+ :to="{
55
+ name: 'resource-create',
56
+ params: { resourceId: listResource.resourceId },
57
+ query: {
58
+ values: encodeURIComponent(JSON.stringify({[listResourceRefColumn.name]: props.record[selfPrimaryKeyColumn.name]})),
59
+ returnTo: $route.fullPath,
60
+ },
61
+ }"
62
+ 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"
63
+ >
64
+ <IconPlusOutline class="w-4 h-4 me-2"/>
65
+ Create
66
+ </RouterLink>
67
+
68
+ <button
69
+ 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 rounded-default"
70
+ @click="()=>{filtersShow = !filtersShow}"
71
+ >
72
+ <IconFilterOutline class="w-4 h-4 me-2"/>
73
+ Filter
74
+ <span
75
+ class="bg-red-100 text-red-800 text-xs font-medium px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400"
76
+ v-if="filters.length">
77
+ {{ filters.length }}
78
+ </span>
79
+ </button>
80
+
81
+ </div>
7
82
 
8
83
  <ResourceListTable
9
84
  :resource="listResource"
@@ -13,6 +88,7 @@
13
88
  @update:checkboxes="checkboxes = $event"
14
89
  :pageSize="pageSize"
15
90
  :totalRows="totalRows"
91
+ :checkboxes="checkboxes"
16
92
  />
17
93
  </td>
18
94
  </template>
@@ -21,6 +97,14 @@
21
97
  import { callAdminForthApi } from '@/utils';
22
98
  import { ref, onMounted, watch, computed } from 'vue';
23
99
  import ResourceListTable from '@/components/ResourceListTable.vue';
100
+ import Filters from '@/components/Filters.vue';
101
+ import {
102
+ IconBanOutline,
103
+ IconFilterOutline,
104
+ IconPlusOutline,
105
+ } from '@iconify-prerendered/vue-flowbite';
106
+
107
+ import { getIcon } from '@/utils';
24
108
 
25
109
  const props = defineProps(['column', 'record', 'meta', 'resource', 'adminUser']);
26
110
 
@@ -30,21 +114,34 @@ const loading = ref(true);
30
114
  const page = ref(1);
31
115
  const sort = ref([]);
32
116
  const checkboxes = ref([]);
33
- const pageSize = computed(() => props.meta.tablePageSize || 10);
117
+ const pageSize = computed(() => props.meta.listPageSize || 10);
34
118
 
35
119
  const rows = ref(null);
36
120
  const totalRows = ref(0);
37
121
 
122
+ const filters = ref([]);
123
+ const filtersShow = ref(false);
124
+ const columnsMinMax = ref(null);
125
+
126
+ const listResourceRefColumn = computed(() => {
127
+ if (!listResource.value) {
128
+ return null;
129
+ }
130
+ return listResource.value.columns.find(c => c.foreignResource?.resourceId === props.resource.resourceId);
131
+ });
38
132
 
133
+ const selfPrimaryKeyColumn = computed(() => {
134
+ return props.resource.columns.find(c => c.primaryKey);
135
+ });
39
136
 
40
- const filters = computed(() => {
137
+ const endFilters = computed(() => {
41
138
  if (!listResource.value) {
42
139
  return [];
43
140
  }
44
141
  // get name of the column that is foreign key
45
- const refColumn = listResource.value.columns.find(c => c.foreignResource?.resourceId === props.resource.resourceId);
142
+ const refColumn = listResourceRefColumn.value;
46
143
 
47
- const primaryKeyColumn = listResource.value.columns.find(c => c.primaryKey);
144
+ const primaryKeyColumn = selfPrimaryKeyColumn.value;
48
145
 
49
146
  if (!refColumn) {
50
147
  window.adminforth.alert({
@@ -54,6 +151,7 @@ const filters = computed(() => {
54
151
  return [];
55
152
  }
56
153
  return [
154
+ ...filters.value,
57
155
  {
58
156
  field: refColumn.name,
59
157
  operator: 'eq',
@@ -70,6 +168,28 @@ watch([sort], async () => {
70
168
  await getList();
71
169
  }, {deep: true});
72
170
 
171
+ watch([filters], async () => {
172
+ page.value = 1;
173
+ checkboxes.value = [];
174
+ await getList();
175
+ }, {deep: true});
176
+
177
+
178
+ async function startBulkAction(actionId) {
179
+ const data = await callAdminForthApi({
180
+ path: '/start_bulk_action',
181
+ method: 'POST',
182
+ body: {
183
+ resourceId: listResource.value.resourceId,
184
+ actionId: actionId,
185
+ recordIds: checkboxes.value
186
+ }
187
+ });
188
+ if (data?.status === 'success') {
189
+ checkboxes.value = [];
190
+ }
191
+ await getList();
192
+ }
73
193
 
74
194
  async function getList() {
75
195
  rows.value = null;
@@ -82,7 +202,7 @@ async function getList() {
82
202
  resourceId: listResource.value.resourceId,
83
203
  limit: pageSize.value,
84
204
  offset: (page.value - 1) * pageSize.value,
85
- filters: filters.value,
205
+ filters: endFilters.value,
86
206
  sort: sort.value,
87
207
  }
88
208
  });
@@ -94,17 +214,21 @@ async function getList() {
94
214
  totalRows.value = data.total;
95
215
  }
96
216
 
97
-
98
217
  onMounted( async () => {
99
218
  loading.value = true;
100
219
  const foreighResourceId = props.meta.foreignResourceId;
101
220
  listResource.value = (await callAdminForthApi({
102
- path: '/get_resource',
221
+ path: `/plugin/${props.meta.pluginInstanceId}/get_resource`,
103
222
  method: 'POST',
104
- body: {
105
- resourceId: foreighResourceId,
106
- },
223
+ body: {},
107
224
  })).resource;
225
+ columnsMinMax.value = await callAdminForthApi({
226
+ path: '/get_min_max_for_columns',
227
+ method: 'POST',
228
+ body: {
229
+ resourceId: foreighResourceId,
230
+ }
231
+ });
108
232
  loading.value = false;
109
233
  await getList();
110
234
  });