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
@@ -0,0 +1,52 @@
1
+ export const styles = () => `
2
+
3
+ "colors": {
4
+ "html-bg": "#FFFFFF", // Changed to white
5
+ "header-bg": "#EDEDED", // Light grey
6
+ "header-bg-hover": "#D3D3D3", // Light grey hover
7
+ "header-text": "#333333", // Dark grey text
8
+ "header-text-hover": "#333333", // Dark grey text hover
9
+ "header-icons": "#333333", // Dark grey icons
10
+ "header-icons-hover": "#333333", // Dark grey icons hover
11
+ "header-logo-color": "#333333", // Dark grey logo
12
+ "header-border-color": "#CCCCCC", // Light grey border
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
18
+ "nav-menu-active": "#CCCCCC", // Light grey active
19
+ "nav-menu-active-text": "#333333", // Dark grey active text
20
+ "nav-menu-text": "#333333", // Dark grey text
21
+ "nav-menu-text-hover": "#333333", // Dark grey text hover
22
+ "nav-menu-text-active": "#333333", // Dark grey text active
23
+ "nav-menu-devider": "#E0E0E0", // Light grey divider
24
+ "nav-menu-icons": "#333333", // Dark grey icons
25
+ "nav-menu-heading": "#333333", // Dark grey heading
26
+
27
+ "list-view-bg": "#FFFFFF", // White background
28
+ "list-view-table-bg": "#FFFFFF", // White table background
29
+ "list-view-table-heading": "#F5F5F5", // Very light grey heading
30
+ "list-view-table-heading-text": "#333333", // Dark grey heading text
31
+ "list-view-table-text": "#333333", // Dark grey text
32
+ "list-view-table-row-hover": "#F0F0F0", // Very light grey row hover
33
+ "list-view-bread-crumbs-text": "#666666", // Medium grey bread crumbs
34
+ "list-view-border-color": "#DDDDDD", // Light grey border
35
+
36
+ "form-view-bg": "#FFFFFF", // White form background
37
+ "form-view-border-color": "#F5F5F5", // Very light grey border
38
+ "form-view-heading": "#EDEDED" // Dark grey heading
39
+ },
40
+ "boxShadow": {
41
+ "custom-light": "0 4px 8px rgba(0, 0, 0, 0.1)", // Lighter shadow
42
+ "header-shadow": "0 4px 8px rgba(0, 0, 0, 0.1)", // Lighter shadow
43
+ "list-table-shadow": "0 4px 8px rgba(0, 0, 0, 0.1)", // Lighter shadow
44
+ "resourse-form-shadow": "0 4px 8px rgba(0, 0, 0, 0.1)" // Lighter shadow
45
+ },
46
+ "fontSize": {
47
+ "header-text-size": "1rem"
48
+ },
49
+ "borderRadius": {
50
+ "default": "2px"
51
+ }
52
+ `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,5 +1,113 @@
1
1
  <template>
2
2
  <td colspan="2">
3
- text
3
+ <h4 v-if="listResource"
4
+ class="px-6 py-4"
5
+
6
+ >{{ listResource.label }} inline records</h4>
7
+
8
+ <ResourceListTable
9
+ :resource="listResource"
10
+ :rows="rows"
11
+ @update:page="page = $event"
12
+ @update:sort="sort = $event"
13
+ @update:checkboxes="checkboxes = $event"
14
+ :pageSize="pageSize"
15
+ :totalRows="totalRows"
16
+ />
4
17
  </td>
5
- </template>
18
+ </template>
19
+
20
+ <script setup>
21
+ import { callAdminForthApi } from '@/utils';
22
+ import { ref, onMounted, watch, computed } from 'vue';
23
+ import ResourceListTable from '@/components/ResourceListTable.vue';
24
+
25
+ const props = defineProps(['column', 'record', 'meta', 'resource', 'adminUser']);
26
+
27
+ const listResource = ref(null);
28
+ const loading = ref(true);
29
+
30
+ const page = ref(1);
31
+ const sort = ref([]);
32
+ const checkboxes = ref([]);
33
+ const pageSize = computed(() => props.meta.pageSize || 10);
34
+
35
+ const rows = ref(null);
36
+ const totalRows = ref(0);
37
+
38
+
39
+
40
+ const filters = computed(() => {
41
+ if (!listResource.value) {
42
+ return [];
43
+ }
44
+ console.log('listResource', listResource.value)
45
+ // get name of the column that is foreign key
46
+ const refColumn = listResource.value.columns.find(c => c.foreignResource?.resourceId === props.resource.resourceId);
47
+
48
+ const primaryKeyColumn = listResource.value.columns.find(c => c.primaryKey);
49
+
50
+ if (!refColumn) {
51
+ window.adminforth.alert({
52
+ message: `Column with foreignResource.resourceId which is equal to ${props.resource.resourceId} not found in resource which is specified as foreighResourceId (${resource.resourceId})`,
53
+ variant: 'error',
54
+ });
55
+ return [];
56
+ }
57
+ return [
58
+ {
59
+ field: refColumn.name,
60
+ operator: 'eq',
61
+ value: props.record[primaryKeyColumn.name],
62
+ },
63
+ ];
64
+ });
65
+
66
+ watch([page], async () => {
67
+ await getList();
68
+ });
69
+
70
+ watch([sort], async () => {
71
+ await getList();
72
+ }, {deep: true});
73
+
74
+
75
+ async function getList() {
76
+ rows.value = null;
77
+ console.log('getList', listResource.value)
78
+ const data = await callAdminForthApi({
79
+ path: '/get_resource_data',
80
+ method: 'POST',
81
+ body: {
82
+ source: 'list',
83
+ resourceId: listResource.value.resourceId,
84
+ limit: pageSize.value,
85
+ offset: (page.value - 1) * pageSize.value,
86
+ filters: filters.value,
87
+ sort: sort.value,
88
+ }
89
+ });
90
+
91
+ rows.value = data.data?.map(row => {
92
+ row._primaryKeyValue = row[listResource.value.columns.find(c => c.primaryKey).name];
93
+ return row;
94
+ });
95
+ totalRows.value = data.total;
96
+ }
97
+
98
+
99
+ onMounted( async () => {
100
+ loading.value = true;
101
+ const foreighResourceId = props.meta.foreignResourceId;
102
+ listResource.value = (await callAdminForthApi({
103
+ path: '/get_resource',
104
+ method: 'POST',
105
+ body: {
106
+ resourceId: foreighResourceId,
107
+ }
108
+ })).resource;
109
+ loading.value = false;
110
+ await getList();
111
+ });
112
+
113
+ </script>
@@ -25,14 +25,14 @@ export default class ForeignInlineListPlugin extends AdminForthPlugin {
25
25
  if (!this.foreignResource) {
26
26
  throw new Error(`ForeignInlineListPlugin: Resource with ID "${this.options.foreignResourceId}" not found`);
27
27
  }
28
-
28
+ console.log('🔌 ForeignInlineListPlugin', this.options);
29
29
  resourceConfig.columns.push({
30
30
  name: `foreignInlineList_${this.foreignResource.resourceId}`,
31
31
  label: 'Foreign Inline List',
32
32
  virtual: true,
33
33
  showIn: [AdminForthResourcePages.show],
34
34
  components: {
35
- showRow: this.componentPath('InlineList.vue'),
35
+ showRow: { file: this.componentPath('InlineList.vue'), meta: this.options }
36
36
  },
37
37
  });
38
38
  }
package/spa/src/App.vue CHANGED
@@ -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
  })
package/spa/src/utils.ts CHANGED
@@ -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"