adminforth 1.1.1 → 1.1.3

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 (60) hide show
  1. package/dist/index.js +23 -7
  2. package/dist/modules/codeInjector.js +9 -9
  3. package/dist/plugins/ForeignInlineListPlugin/index.js +2 -1
  4. package/dist/spa/spa/src/App.vue +1 -1
  5. package/dist/spa/spa/src/components/ResourceListTable.vue +363 -0
  6. package/dist/spa/spa/src/stores/core.ts +7 -9
  7. package/dist/spa/spa/src/utils.ts +2 -2
  8. package/dist/spa/spa/src/views/CreateView.vue +3 -0
  9. package/dist/spa/spa/src/views/EditView.vue +3 -0
  10. package/dist/spa/spa/src/views/ListView.vue +24 -317
  11. package/dist/spa/spa/src/views/ShowView.vue +8 -11
  12. package/index.ts +29 -11
  13. package/modules/codeInjector.ts +12 -12
  14. package/modules/styles.ts +52 -0
  15. package/package.json +1 -1
  16. package/plugins/ForeignInlineListPlugin/custom/InlineList.vue +110 -2
  17. package/plugins/ForeignInlineListPlugin/index.ts +2 -2
  18. package/spa/src/App.vue +1 -1
  19. package/spa/src/components/ResourceListTable.vue +363 -0
  20. package/spa/src/stores/core.ts +7 -9
  21. package/spa/src/utils.ts +2 -2
  22. package/spa/src/views/CreateView.vue +3 -0
  23. package/spa/src/views/EditView.vue +3 -0
  24. package/spa/src/views/ListView.vue +24 -317
  25. package/spa/src/views/ShowView.vue +8 -11
  26. package/types/AdminForthConfig.ts +79 -23
  27. package/documentation/README.md +0 -41
  28. package/documentation/babel.config.js +0 -3
  29. package/documentation/blog/2019-05-28-first-blog-post.md +0 -12
  30. package/documentation/blog/2019-05-29-long-blog-post.md +0 -44
  31. package/documentation/blog/2021-08-01-mdx-blog-post.mdx +0 -20
  32. package/documentation/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
  33. package/documentation/blog/2021-08-26-welcome/index.md +0 -25
  34. package/documentation/blog/authors.yml +0 -17
  35. package/documentation/blog/tags.yml +0 -16
  36. package/documentation/docs/Getting Started.md +0 -374
  37. package/documentation/docs/Glossary.md +0 -37
  38. package/documentation/docs/image.png +0 -0
  39. package/documentation/docusaurus.config.ts +0 -171
  40. package/documentation/package-lock.json +0 -14655
  41. package/documentation/package.json +0 -51
  42. package/documentation/sidebars.ts +0 -51
  43. package/documentation/src/components/HomepageFeatures/index.tsx +0 -68
  44. package/documentation/src/components/HomepageFeatures/styles.module.css +0 -11
  45. package/documentation/src/css/custom.css +0 -30
  46. package/documentation/src/pages/index.module.css +0 -24
  47. package/documentation/src/pages/index.tsx +0 -43
  48. package/documentation/src/pages/markdown-page.md +0 -7
  49. package/documentation/static/.nojekyll +0 -0
  50. package/documentation/static/CNAME +0 -1
  51. package/documentation/static/img/css.png +0 -0
  52. package/documentation/static/img/db.png +0 -0
  53. package/documentation/static/img/db2.png +0 -0
  54. package/documentation/static/img/favicon.png +0 -0
  55. package/documentation/static/img/gen.sh +0 -24
  56. package/documentation/static/img/logo.svg +0 -19
  57. package/documentation/static/img/tail.png +0 -0
  58. package/documentation/static/img/vue.png +0 -0
  59. package/documentation/typedoc-plugin.mjs +0 -11
  60. /package/{styles.js → dist/modules/styles.js} +0 -0
package/dist/index.js CHANGED
@@ -61,6 +61,17 @@ class AdminForth {
61
61
  }
62
62
  return [];
63
63
  }
64
+ validateComponent(component, errors) {
65
+ if (!component) {
66
+ return component;
67
+ }
68
+ let obj = component;
69
+ if (typeof obj === 'string') {
70
+ obj = { file: component, meta: {} };
71
+ }
72
+ errors.push(...this.checkCustomFileExists(this.config.auth.loginBackgroundImage));
73
+ return obj;
74
+ }
64
75
  validateConfig() {
65
76
  var _b;
66
77
  const errors = [];
@@ -209,11 +220,12 @@ class AdminForth {
209
220
  Object.entries(res.options.pageInjections).map(([key, value]) => {
210
221
  Object.entries(value).map(([injection, target]) => {
211
222
  if (possibleInjections.includes(injection)) {
212
- if (typeof target === 'string') {
223
+ if (!Array.isArray(res.options.pageInjections[key][injection])) {
224
+ // not array
213
225
  res.options.pageInjections[key][injection] = [target];
214
226
  }
215
- res.options.pageInjections[key][injection].forEach((target) => {
216
- errors.push(...this.checkCustomFileExists(target));
227
+ res.options.pageInjections[key][injection].forEach((target, i) => {
228
+ res.options.pageInjections[key][injection][i] = this.validateComponent(target, errors);
217
229
  });
218
230
  }
219
231
  else {
@@ -296,13 +308,17 @@ class AdminForth {
296
308
  for (const resource of this.config.resources) {
297
309
  for (const column of resource.columns) {
298
310
  if (column.components) {
299
- for (const [key, value] of Object.entries(column.components)) {
300
- if (this.codeInjector.allComponentNames[value]) {
311
+ Object.entries(column.components).forEach(([key, comp]) => {
312
+ column.components[key] = this.validateComponent(comp, errors);
313
+ });
314
+ console.log('🔧🔧🔧 Validating components for resource', column.components);
315
+ for (const [key, { file }] of Object.entries(column.components)) {
316
+ if (this.codeInjector.allComponentNames[file]) {
301
317
  // not obvious, but if we are in this if, it means that this is plugin component
302
318
  // and there is no sense to check if it exists in users folder
303
319
  continue;
304
320
  }
305
- const path = value.replace('@@', this.config.customization.customComponentsDir);
321
+ const path = file.replace('@@', this.config.customization.customComponentsDir);
306
322
  if (!fs.existsSync(path)) {
307
323
  throw new Error(`Component file ${path} does not exist`);
308
324
  }
@@ -518,7 +534,7 @@ class AdminForth {
518
534
  });
519
535
  server.endpoint({
520
536
  method: 'POST',
521
- path: '/get_resource_columns',
537
+ path: '/get_resource',
522
538
  handler: (_k) => __awaiter(this, [_k], void 0, function* ({ body }) {
523
539
  const { resourceId } = body;
524
540
  if (!this.statuses.dbDiscover) {
@@ -17,7 +17,7 @@ import path from 'path';
17
17
  import { promisify } from 'util';
18
18
  import { ADMIN_FORTH_ABSOLUTE_PATH } from './utils.js';
19
19
  import { getComponentNameFromPath } from './utils.js';
20
- import { styles } from '../styles.js';
20
+ import { styles } from './styles.js';
21
21
  let TMP_DIR;
22
22
  try {
23
23
  TMP_DIR = os.tmpdir();
@@ -164,8 +164,8 @@ class CodeInjector {
164
164
  yield Promise.all(filesUpdated.map((file) => __awaiter(this, void 0, void 0, function* () {
165
165
  const src = path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'spa', file);
166
166
  const dest = path.join(CodeInjector.SPA_TMP_PATH, file);
167
+ // overwrite:true can't be used to not destroy cache
167
168
  yield fsExtra.copy(src, dest, {
168
- overwrite: true,
169
169
  dereference: true, // needed to dereference types
170
170
  });
171
171
  if (process.env.HEAVY_DEBUG) {
@@ -184,6 +184,7 @@ class CodeInjector {
184
184
  catch (e) {
185
185
  // ignore
186
186
  }
187
+ // overwrite can't be used to not destroy cache
187
188
  yield fsExtra.copy(path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'spa'), CodeInjector.SPA_TMP_PATH, {
188
189
  filter: (src) => {
189
190
  if (process.env.HEAVY_DEBUG) {
@@ -191,7 +192,6 @@ class CodeInjector {
191
192
  }
192
193
  return !src.includes('/adminforth/spa/node_modules') && !src.includes('/adminforth/spa/dist');
193
194
  },
194
- overwrite: true,
195
195
  dereference: true, // needed to dereference types
196
196
  });
197
197
  // copy whole custom directory
@@ -238,18 +238,18 @@ class CodeInjector {
238
238
  var _a;
239
239
  resource.columns.forEach((field) => {
240
240
  if (field.components) {
241
- Object.values(field.components).forEach((filePath) => {
242
- if (!customResourceComponents.includes(filePath)) {
243
- customResourceComponents.push(filePath);
241
+ Object.values(field.components).forEach(({ file }) => {
242
+ if (!customResourceComponents.includes(file)) {
243
+ customResourceComponents.push(file);
244
244
  }
245
245
  });
246
246
  }
247
247
  });
248
248
  (Object.values(((_a = resource.options) === null || _a === void 0 ? void 0 : _a.pageInjections) || {})).forEach((injection) => {
249
249
  Object.values(injection).forEach((filePathes) => {
250
- filePathes.forEach((filePath) => {
251
- if (!customResourceComponents.includes(filePath)) {
252
- customResourceComponents.push(filePath);
250
+ filePathes.forEach(({ file }) => {
251
+ if (!customResourceComponents.includes(file)) {
252
+ customResourceComponents.push(file);
253
253
  }
254
254
  });
255
255
  });
@@ -12,13 +12,14 @@ export default class ForeignInlineListPlugin extends AdminForthPlugin {
12
12
  if (!this.foreignResource) {
13
13
  throw new Error(`ForeignInlineListPlugin: Resource with ID "${this.options.foreignResourceId}" not found`);
14
14
  }
15
+ console.log('🔌 ForeignInlineListPlugin', this.options);
15
16
  resourceConfig.columns.push({
16
17
  name: `foreignInlineList_${this.foreignResource.resourceId}`,
17
18
  label: 'Foreign Inline List',
18
19
  virtual: true,
19
20
  showIn: [AdminForthResourcePages.show],
20
21
  components: {
21
- showRow: this.componentPath('InlineList.vue'),
22
+ showRow: { file: this.componentPath('InlineList.vue'), meta: this.options }
22
23
  },
23
24
  });
24
25
  }
@@ -176,7 +176,7 @@ import { useRoute, useRouter } from 'vue-router';
176
176
  import { getIcon } from '@/utils';
177
177
  import { useHead } from 'unhead'
178
178
  import { createHead } from 'unhead'
179
- import { loadFile } from './utils';
179
+ import { loadFile } from '@/utils';
180
180
  import Toast from './components/Toast.vue';
181
181
  import {useToastStore} from '@/stores/toast';
182
182
  import { FrontendAPI } from '@/composables/useStores'
@@ -0,0 +1,363 @@
1
+ <template>
2
+ <!-- table -->
3
+ <div class="relative overflow-x-auto shadow-list-table-shadow dark:shadow-black overflow-y-hidden rounded-default">
4
+
5
+ <!-- skelet loader -->
6
+ <div role="status" v-if="!resource || !resource.columns"
7
+ class="max-w p-4 space-y-4 divide-y divide-gray-200 rounded shadow animate-pulse dark:divide-gray-700 md:p-6 dark:border-gray-700">
8
+ <div class="flex items-center justify-between h-16">
9
+ <div>
10
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
11
+ <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
12
+ </div>
13
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
14
+ </div>
15
+
16
+ <div class="flex items-center justify-between h-16 pt-4" v-for="i in new Array(10)">
17
+ <div>
18
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
19
+ <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
20
+ </div>
21
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
22
+ </div>
23
+ <span class="sr-only">Loading...</span>
24
+ </div>
25
+
26
+ <table v-else class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 rounded-default">
27
+ <thead class="text-xs text-view-table-heading-text bg-list-view-table-heading dark:bg-gray-700 dark:text-gray-400">
28
+ <tr>
29
+ <th scope="col" class="p-4">
30
+ <div v-if="rows && rows.length" class="flex items-center">
31
+ <input id="checkbox-all-search" type="checkbox" :checked="allFromThisPageChecked" @change="selectAll()"
32
+ 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">
33
+ <label for="checkbox-all-search" class="sr-only">checkbox</label>
34
+ </div>
35
+ </th>
36
+
37
+ <th v-for="c in columnsListed" scope="col" class="px-6 py-3">
38
+ <div @click="() => c.sortable && onSortButtonClick(c.name)" class="flex items-center " :class="{'cursor-pointer':c.sortable}">
39
+ {{ c.label }}
40
+
41
+ <div v-if="c.sortable"
42
+ :style="{ 'color':ascArr.includes(c.name)?'green':descArr.includes(c.name)?'red':'currentColor'}">
43
+ <svg v-if="ascArr.includes(c.name) || descArr.includes(c.name)" class="w-3 h-3 ms-1.5"
44
+ :class="{'rotate-180':descArr.includes(c.name)}" viewBox="0 0 24 24">
45
+ <path
46
+ d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847"/>
47
+ </svg>
48
+ <svg v-else class="w-3 h-3 ms-1.5 opacity-30" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
49
+ fill='currentColor'
50
+ viewBox="0 0 24 24">
51
+ <path
52
+ d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z"/>
53
+ </svg>
54
+ </div>
55
+ </div>
56
+ </th>
57
+
58
+ <th scope="col" class="px-6 py-3">
59
+ Actions
60
+ </th>
61
+ </tr>
62
+ </thead>
63
+ <tbody>
64
+ <tr v-if="!rows" class="bg-list-view-table-bg border-b dark:bg-gray-800 dark:border-gray-700">
65
+ <td :colspan="resource?.columns.length + 2">
66
+
67
+ <div role="status"
68
+ class="max-w p-4 space-y-4 divide-y divide-gray-200 rounded animate-pulse dark:divide-gray-700 md:p-6 dark:border-gray-700">
69
+ <div class="flex items-center justify-between">
70
+ <div>
71
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
72
+ <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
73
+ </div>
74
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
75
+ </div>
76
+ <div class="flex items-center justify-between pt-4">
77
+ <div>
78
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
79
+ <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
80
+ </div>
81
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
82
+ </div>
83
+ <div class="flex items-center justify-between pt-4">
84
+ <div>
85
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
86
+ <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
87
+ </div>
88
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
89
+ </div>
90
+ <div class="flex items-center justify-between pt-4">
91
+ <div>
92
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
93
+ <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
94
+ </div>
95
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
96
+ </div>
97
+ <div class="flex items-center justify-between pt-4">
98
+ <div>
99
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
100
+ <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
101
+ </div>
102
+ <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
103
+ </div>
104
+ <span class="sr-only">Loading...</span>
105
+ </div>
106
+ </td>
107
+ </tr>
108
+ <tr v-else-if="rows.length === 0" class="bg-list-view-table-bg border-b dark:bg-gray-800 dark:border-gray-700">
109
+ <td :colspan="resource?.columns.length + 2">
110
+
111
+ <div id="toast-simple"
112
+ class=" mx-auto my-5 flex items-center w-full max-w-xs p-4 space-x-4 rtl:space-x-reverse text-gray-500 bg-white divide-x rtl:divide-x-reverse divide-gray-200 dark:text-gray-400 dark:divide-gray-700 space-x dark:bg-gray-800"
113
+ role="alert">
114
+ <IconInboxOutline class="w-6 h-6 text-gray-500 dark:text-gray-400"/>
115
+ <div class="ps-4 text-sm font-normal">No items here yet</div>
116
+ </div>
117
+
118
+ </td>
119
+ </tr>
120
+
121
+ <tr v-else v-for="(row, rowI) in rows" :key="row.id"
122
+ class="bg-list-view-table-bg border-b dark:bg-gray-800 border-list-view-border-color dark:border-gray-700 hover:bg-list-view-table-row-hover dark:hover:bg-gray-600">
123
+ <td class="w-4 p-4">
124
+ <div class="flex items center">
125
+ <input
126
+ id="checkbox-table-search-1"
127
+ type="checkbox"
128
+ :checked="checkboxes.includes(row.id)"
129
+ @change="(e)=>{addToCheckedValues(row.id)}"
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
+ <label for="checkbox-table-search-1" class="sr-only">checkbox</label>
132
+ </div>
133
+ </td>
134
+ <td v-for="c in columnsListed" class="px-6 py-4">
135
+ <!-- if c.name in listComponentsPerColumn, render it. If not, render ValueRenderer -->
136
+ <component
137
+ :is="c?.components?.list ? getCustomComponent(c.components.list) : ValueRenderer"
138
+ :meta="c?.components?.list.meta"
139
+ :column="c"
140
+ :record="row"
141
+ :adminUser="coreStore.adminUser"
142
+ :resource="resource"
143
+ />
144
+ </td>
145
+ <td class=" items-center px-6 py-4">
146
+ <div class="flex">
147
+ <RouterLink
148
+ v-if="resource.options?.allowedActions.show"
149
+ :to="{ name: 'resource-show', params: { resourceId: $route.params.resourceId, primaryKey: row._primaryKeyValue } }"
150
+ class="font-medium text-blue-600 dark:text-blue-500 hover:underline"
151
+ :data-tooltip-target="`tooltip-show-${rowI}`"
152
+ >
153
+ <IconEyeSolid class="w-5 h-5 me-2"/>
154
+ </RouterLink>
155
+
156
+ <div :id="`tooltip-show-${rowI}`"
157
+ role="tooltip"
158
+ 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">
159
+ Show item
160
+ <div class="tooltip-arrow" data-popper-arrow></div>
161
+ </div>
162
+
163
+ <RouterLink v-if="resource.options?.allowedActions.edit"
164
+ :to="{ name: 'resource-edit', params: {
165
+ resourceId: resource.id,
166
+ primaryKey: row._primaryKeyValue
167
+ }
168
+ }"
169
+ class="font-medium text-blue-600 dark:text-blue-500 hover:underline ms-3"
170
+ :data-tooltip-target="`tooltip-edit-${rowI}`"
171
+ >
172
+ <IconPenSolid class="w-5 h-5 me-2"/>
173
+ </RouterLink>
174
+
175
+ <div :id="`tooltip-edit-${rowI}`"
176
+ role="tooltip"
177
+ 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">
178
+ Edit
179
+ <div class="tooltip-arrow" data-popper-arrow></div>
180
+ </div>
181
+
182
+ <button v-if="resource.options?.allowedActions.delete"
183
+ class="font-medium text-red-600 dark:text-red-500 hover:underline ms-3"
184
+ :data-tooltip-target="`tooltip-delete-${rowI}`"
185
+ @click="showDeleteModal(row)"
186
+ >
187
+ <IconTrashBinSolid class="w-5 h-5 me-2"/>
188
+ </button>
189
+
190
+ <div :id="`tooltip-delete-${rowI}`"
191
+ role="tooltip"
192
+ 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">
193
+ Delete
194
+ <div class="tooltip-arrow" data-popper-arrow></div>
195
+ </div>
196
+ </div>
197
+ </td>
198
+ </tr>
199
+ </tbody>
200
+ </table>
201
+ </div>
202
+ <!-- pagination -->
203
+ <div class="flex flex-col items-center mt-4 xs:flex-row xs:justify-between xs:items-center"
204
+ v-if="rows && totalRows >= pageSize && totalRows > 0"
205
+ >
206
+ <!-- Help text -->
207
+ <span class="text-sm text-gray-700 dark:text-gray-400">
208
+ Showing <span class="font-semibold text-gray-900 dark:text-white">
209
+ {{ (page - 1) * pageSize + 1 }}
210
+ </span> to <span class="font-semibold text-gray-900 dark:text-white">
211
+ {{ Math.min(page * pageSize, totalRows) }}
212
+ </span> of <span class="font-semibold text-gray-900 dark:text-white">{{
213
+ totalRows
214
+ }}</span> Entries
215
+ </span>
216
+ <div class="inline-flex mt-2 xs:mt-0">
217
+ <!-- Buttons -->
218
+ <button
219
+ class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-r-0 rounded-s 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"
220
+ @click="page--" :disabled="page <= 1">
221
+ <svg class="w-3.5 h-3.5 me-2 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
222
+ viewBox="0 0 14 10">
223
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
224
+ d="M13 5H1m0 0 4 4M1 5l4-4"/>
225
+ </svg>
226
+ Prev
227
+ </button>
228
+ <button
229
+ class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-r-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"
230
+ @click="page = 1" :disabled="page <= 1">
231
+ <!-- <IconChevronDoubleLeftOutline class="w-4 h-4" /> -->
232
+ 1
233
+ </button>
234
+ <input type="text"
235
+ class="w-10 py-1.5 px-3 text-sm text-center text-gray-700 border border-gray-300 dark:border-gray-700 dark:text-gray-400 dark:bg-gray-800 z-10"
236
+ v-model="page"/>
237
+
238
+ <button
239
+ 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
+ @click="page = totalPages" :disabled="page >= totalPages">
241
+ {{ totalPages }}
242
+ <!-- <IconChevronDoubleRightOutline class="w-4 h-4" /> -->
243
+ </button>
244
+ <button
245
+ class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-l-0 rounded-e 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"
246
+
247
+ @click="page++" :disabled="page >= totalPages">
248
+ Next
249
+ <svg class="w-3.5 h-3.5 ms-2 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
250
+ viewBox="0 0 14 10">
251
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
252
+ d="M1 5h12m0 0L9 1m4 4L9 9"/>
253
+ </svg>
254
+ </button>
255
+ </div>
256
+ </div>
257
+ </template>
258
+
259
+ <script setup>
260
+
261
+
262
+ import { initFlowbite } from 'flowbite';
263
+ import { computed, onMounted, ref, watch } from 'vue';
264
+ import { useRoute } from 'vue-router';
265
+
266
+ import ValueRenderer from '@/components/ValueRenderer.vue';
267
+ import { getCustomComponent } from '@/utils';
268
+ import { useCoreStore } from '@/stores/core';
269
+
270
+ import {
271
+ IconInboxOutline,
272
+ IconPlusOutline
273
+ } from '@iconify-prerendered/vue-flowbite';
274
+
275
+ import {
276
+ IconBanOutline,
277
+ IconEyeSolid,
278
+ IconFilterOutline,
279
+ IconPenSolid,
280
+ IconTrashBinSolid
281
+ } from '@iconify-prerendered/vue-flowbite';
282
+
283
+ const coreStore = useCoreStore();
284
+
285
+ const props = defineProps([
286
+ 'resource',
287
+ 'rows',
288
+ 'totalRows',
289
+ 'pageSize',
290
+ ])
291
+
292
+ // emits, update page
293
+ const emits = defineEmits([
294
+ 'update:page',
295
+ 'update:sort',
296
+ 'update:checkboxes',
297
+
298
+ ]);
299
+
300
+ const checkboxes = ref([]);
301
+ const page = ref(1);
302
+ const sort = ref([]);
303
+
304
+
305
+ watch(() => page.value, (newPage) => {
306
+ emits('update:page', newPage);
307
+ });
308
+
309
+ watch(() => sort.value, (newSort) => {
310
+ emits('update:sort', newSort);
311
+ });
312
+
313
+ watch(() => checkboxes.value, (newCheckboxes) => {
314
+ emits('update:checkboxes', newCheckboxes);
315
+ });
316
+
317
+ function addToCheckedValues(id) {
318
+ if (checkboxes.value.includes(id)) {
319
+ checkboxes.value = checkboxes.value.filter((item) => item !== id);
320
+ } else {
321
+ checkboxes.value.push(id);
322
+ }
323
+ }
324
+
325
+ const columnsListed = computed(() => props.resource?.columns?.filter(c => c.showIn.includes('list')));
326
+
327
+ async function selectAll(value) {
328
+ rows.value.forEach((r) => {
329
+ if (!checkboxes.value.includes(r.id)) {
330
+ checkboxes.value.push(r.id)
331
+ } else {
332
+ checkboxes.value = checkboxes.value.filter((item) => item !== r.id)
333
+ }
334
+ });
335
+ // checkboxes.value = rows.value.map((v) => v.id);
336
+ }
337
+
338
+ const totalPages = computed(() => Math.ceil(props.totalRows.value / props.pageSize.value));
339
+
340
+
341
+ const allFromThisPageChecked = computed(() => {
342
+ if (!props.rows.value) return false;
343
+ return props.rows.value.every((r) => checkboxes.value.includes(r.id));
344
+ });
345
+ const ascArr = computed(() => sort.value.filter((s) => s.direction === 'asc').map((s) => s.field));
346
+ const descArr = computed(() => sort.value.filter((s) => s.direction === 'desc').map((s) => s.field));
347
+
348
+
349
+ function onSortButtonClick(field) {
350
+ const sortIndex = sort.value.findIndex((s) => s.field === field);
351
+ if (sortIndex === -1) {
352
+ sort.value = [{field, direction: 'asc'}, ...sort.value];
353
+ } else {
354
+ const sortField = sort.value[sortIndex];
355
+ if (sortField.direction === 'asc') {
356
+ sort.value[sortIndex].direction = 'desc';
357
+ } else {
358
+ sort.value.splice(sortIndex, 1);
359
+ }
360
+ }
361
+ }
362
+
363
+ </script>
@@ -13,7 +13,7 @@ export const useCoreStore = defineStore('core', () => {
13
13
  const resourceOptions = ref(null);
14
14
  const resourceColumnsError = ref('');
15
15
  const resourceColumnsId = ref(null);
16
- const user = ref(null);
16
+ const adminUser = ref(null);
17
17
 
18
18
  async function fetchMenuAndResource() {
19
19
  const resp = await callAdminForthApi({
@@ -29,10 +29,8 @@ export const useCoreStore = defineStore('core', () => {
29
29
  return acc;
30
30
  }, {});
31
31
  config.value = resp.config;
32
- user.value = resp.user;
32
+ adminUser.value = resp.user;
33
33
  console.log('🌍 AdminForth v', resp.version);
34
-
35
-
36
34
  }
37
35
 
38
36
  async function fetchRecord({ resourceId, primaryKey }) {
@@ -75,13 +73,12 @@ export const useCoreStore = defineStore('core', () => {
75
73
  resourceColumns.value = null;
76
74
  resourceColumnsError.value = '';
77
75
  const res = await callAdminForthApi({
78
- path: '/get_resource_columns',
76
+ path: '/get_resource',
79
77
  method: 'POST',
80
78
  body: {
81
79
  resourceId,
82
80
  }
83
- }
84
- );
81
+ });
85
82
  if (res.error) {
86
83
  resourceColumnsError.value = res.error;
87
84
  } else {
@@ -109,12 +106,12 @@ export const useCoreStore = defineStore('core', () => {
109
106
 
110
107
  const username = computed(() => {
111
108
  const usernameField = config.value.usernameField;
112
- return user.value && user.value[usernameField];
109
+ return adminUser.value && adminUser.value[usernameField];
113
110
  });
114
111
 
115
112
  const userFullname = computed(() => {
116
113
  const userFullnameField = config.value.userFullnameField;
117
- return user.value && user.value[userFullnameField];
114
+ return adminUser.value && adminUser.value[userFullnameField];
118
115
  })
119
116
 
120
117
 
@@ -134,5 +131,6 @@ export const useCoreStore = defineStore('core', () => {
134
131
  resourceOptions,
135
132
  logout,
136
133
  resource,
134
+ adminUser,
137
135
  }
138
136
  })
@@ -32,8 +32,8 @@ export async function callAdminForthApi({ path, method, body=undefined }) {
32
32
  }
33
33
  }
34
34
 
35
- export function getCustomComponent(filePath: string) {
36
- const name = filePath.replace(/@/g, '').replace(/\./g, '').replace(/\//g, '');
35
+ export function getCustomComponent({ file, meta }: { file: string, meta: any }) {
36
+ const name = file.replace(/@/g, '').replace(/\./g, '').replace(/\//g, '');
37
37
  console.log('resolving name', name);
38
38
  return resolveComponent(name);
39
39
  }
@@ -4,6 +4,7 @@
4
4
  <component
5
5
  v-for="c in coreStore?.resourceOptions?.pageInjections?.create?.beforeBreadcrumbs || []"
6
6
  :is="getCustomComponent(c)"
7
+ :meta="c.meta"
7
8
  :record="coreStore.record"
8
9
  :resource="coreStore.resource"
9
10
  :adminUser="coreStore.adminUser"
@@ -33,6 +34,7 @@
33
34
  <component
34
35
  v-for="c in coreStore?.resourceOptions?.pageInjections?.create?.afterBreadcrumbs || []"
35
36
  :is="getCustomComponent(c)"
37
+ :meta="c.meta"
36
38
  :record="coreStore.record"
37
39
  :resource="coreStore.resource"
38
40
  :adminUser="coreStore.adminUser"
@@ -55,6 +57,7 @@
55
57
  <component
56
58
  v-for="c in coreStore?.resourceOptions?.pageInjections?.create?.bottom || []"
57
59
  :is="getCustomComponent(c)"
60
+ :meta="c.meta"
58
61
  :record="coreStore.record"
59
62
  :resource="coreStore.resource"
60
63
  :adminUser="coreStore.adminUser"
@@ -3,6 +3,7 @@
3
3
  <component
4
4
  v-for="c in coreStore?.resourceOptions?.pageInjections?.edit?.beforeBreadcrumbs || []"
5
5
  :is="getCustomComponent(c)"
6
+ :meta="c.meta"
6
7
  :record="editableRecord"
7
8
  :resource="coreStore.resource"
8
9
  :adminUser="coreStore.adminUser"
@@ -29,6 +30,7 @@
29
30
  <component
30
31
  v-for="c in coreStore?.resourceOptions?.pageInjections?.edit?.afterBreadcrumbs || []"
31
32
  :is="getCustomComponent(c)"
33
+ :meta="c.meta"
32
34
  :record="coreStore.record"
33
35
  :resource="coreStore.resource"
34
36
  :adminUser="coreStore.adminUser"
@@ -51,6 +53,7 @@
51
53
  <component
52
54
  v-for="c in coreStore?.resourceOptions?.pageInjections?.edit?.bottom || []"
53
55
  :is="getCustomComponent(c)"
56
+ :meta="c.meta"
54
57
  :record="coreStore.record"
55
58
  :resource="coreStore.resource"
56
59
  :adminUser="coreStore.adminUser"