adminforth 1.0.47 → 1.0.48
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.
- package/dataConnectors/sqlite.ts +0 -1
- package/dist/dataConnectors/sqlite.js +0 -1
- package/dist/index.js +34 -7
- package/dist/modules/codeInjector.js +48 -11
- package/dist/spa/spa/src/utils.ts +3 -3
- package/dist/spa/spa/src/views/CreateView.vue +6 -8
- package/dist/spa/spa/src/views/EditView.vue +16 -10
- package/dist/spa/spa/src/views/ListView.vue +7 -8
- package/dist/spa/spa/src/views/ShowView.vue +5 -9
- package/index.ts +44 -9
- package/modules/codeInjector.ts +47 -7
- package/package.json +1 -1
- package/spa/src/utils.ts +3 -3
- package/spa/src/views/CreateView.vue +6 -8
- package/spa/src/views/EditView.vue +16 -10
- package/spa/src/views/ListView.vue +7 -8
- package/spa/src/views/ShowView.vue +5 -9
- package/types/AdminForthConfig.ts +3 -2
package/dataConnectors/sqlite.ts
CHANGED
|
@@ -138,7 +138,6 @@ class SQLiteConnector {
|
|
|
138
138
|
|
|
139
139
|
|
|
140
140
|
getData({ resource, limit, offset, sort, filters }) {
|
|
141
|
-
console.log('SQLITE getData', { resource, limit, offset, sort, filters });
|
|
142
141
|
const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
|
|
143
142
|
const tableName = resource.table;
|
|
144
143
|
|
|
@@ -148,7 +148,6 @@ class SQLiteConnector {
|
|
|
148
148
|
return value;
|
|
149
149
|
}
|
|
150
150
|
getData({ resource, limit, offset, sort, filters }) {
|
|
151
|
-
console.log('SQLITE getData', { resource, limit, offset, sort, filters });
|
|
152
151
|
const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
|
|
153
152
|
const tableName = resource.table;
|
|
154
153
|
const where = filters.length ? `WHERE ${filters.map((f, i) => {
|
package/dist/index.js
CHANGED
|
@@ -193,6 +193,15 @@ class AdminForth {
|
|
|
193
193
|
if (item.component && !item.path) {
|
|
194
194
|
errors.push(`Menu item with component must have path : ${JSON.stringify(item)}`);
|
|
195
195
|
}
|
|
196
|
+
if (item.type === 'resource' && !item.resourceId) {
|
|
197
|
+
errors.push(`Menu item with type 'resource' must have resourceId : ${JSON.stringify(item)}`);
|
|
198
|
+
}
|
|
199
|
+
if (item.resourceId && !this.config.resources.find((res) => res.resourceId === item.resourceId)) {
|
|
200
|
+
errors.push(`Menu item with type 'resourceId' has resourceId which is not in resources: ${JSON.stringify(item)}`);
|
|
201
|
+
}
|
|
202
|
+
if (item.type === 'component' && !item.component) {
|
|
203
|
+
errors.push(`Menu item with type 'component' must have component : ${JSON.stringify(item)}`);
|
|
204
|
+
}
|
|
196
205
|
// make sure component starts with @@
|
|
197
206
|
if (item.component) {
|
|
198
207
|
if (!item.component.startsWith('@@')) {
|
|
@@ -535,6 +544,13 @@ class AdminForth {
|
|
|
535
544
|
data.data.forEach((item) => {
|
|
536
545
|
item[col.name] = targetDataMap[item[col.name]];
|
|
537
546
|
});
|
|
547
|
+
data.data.forEach((item) => {
|
|
548
|
+
Object.keys(item).forEach((key) => {
|
|
549
|
+
if (!targetResource.columns.find((col) => col.name === key) || targetResource.columns.find((col) => col.name === key && col.backendOnly)) {
|
|
550
|
+
delete item[key];
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
});
|
|
538
554
|
})));
|
|
539
555
|
data.data.forEach((item) => {
|
|
540
556
|
item._label = resource.itemLabel(item);
|
|
@@ -548,6 +564,14 @@ class AdminForth {
|
|
|
548
564
|
return { error: resp.error };
|
|
549
565
|
}
|
|
550
566
|
}
|
|
567
|
+
// remove all columns which are not defined in resources, or defined but backendOnly
|
|
568
|
+
data.data.forEach((item) => {
|
|
569
|
+
Object.keys(item).forEach((key) => {
|
|
570
|
+
if (!resource.columns.find((col) => col.name === key) || resource.columns.find((col) => col.name === key && col.backendOnly)) {
|
|
571
|
+
delete item[key];
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
});
|
|
551
575
|
return Object.assign(Object.assign({}, data), { options: resource === null || resource === void 0 ? void 0 : resource.options });
|
|
552
576
|
}),
|
|
553
577
|
});
|
|
@@ -647,7 +671,6 @@ class AdminForth {
|
|
|
647
671
|
}),
|
|
648
672
|
});
|
|
649
673
|
server.endpoint({
|
|
650
|
-
noAuth: true, // TODO
|
|
651
674
|
method: 'POST',
|
|
652
675
|
path: '/create_record',
|
|
653
676
|
handler: (_y) => __awaiter(this, [_y], void 0, function* ({ body, adminUser }) {
|
|
@@ -716,7 +739,6 @@ class AdminForth {
|
|
|
716
739
|
})
|
|
717
740
|
});
|
|
718
741
|
server.endpoint({
|
|
719
|
-
noAuth: true, // TODO
|
|
720
742
|
method: 'POST',
|
|
721
743
|
path: '/update_record',
|
|
722
744
|
handler: (_8) => __awaiter(this, [_8], void 0, function* ({ body, adminUser }) {
|
|
@@ -744,11 +766,18 @@ class AdminForth {
|
|
|
744
766
|
}
|
|
745
767
|
}
|
|
746
768
|
const newValues = {};
|
|
747
|
-
for (const
|
|
748
|
-
if (record[
|
|
749
|
-
|
|
769
|
+
for (const recordField in record) {
|
|
770
|
+
if (record[recordField] !== oldRecord[recordField]) {
|
|
771
|
+
const column = resource.columns.find((col) => col.name === recordField);
|
|
772
|
+
if (column) {
|
|
773
|
+
newValues[recordField] = connector.setFieldValue(column, record[recordField]);
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
newValues[recordField] = record[recordField];
|
|
777
|
+
}
|
|
750
778
|
}
|
|
751
779
|
}
|
|
780
|
+
console.log('✅ newValues', newValues);
|
|
752
781
|
if (Object.keys(newValues).length > 0) {
|
|
753
782
|
yield connector.updateRecord({ resource, recordId, record, newValues });
|
|
754
783
|
}
|
|
@@ -768,7 +797,6 @@ class AdminForth {
|
|
|
768
797
|
})
|
|
769
798
|
});
|
|
770
799
|
server.endpoint({
|
|
771
|
-
noAuth: true, // TODO
|
|
772
800
|
method: 'POST',
|
|
773
801
|
path: '/delete_record',
|
|
774
802
|
handler: (_17) => __awaiter(this, [_17], void 0, function* ({ body, adminUser }) {
|
|
@@ -806,7 +834,6 @@ class AdminForth {
|
|
|
806
834
|
})
|
|
807
835
|
});
|
|
808
836
|
server.endpoint({
|
|
809
|
-
noAuth: true, // TODO
|
|
810
837
|
method: 'POST',
|
|
811
838
|
path: '/start_bulk_action',
|
|
812
839
|
handler: (_26) => __awaiter(this, [_26], void 0, function* ({ body }) {
|
|
@@ -7,14 +7,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
import fs from 'fs';
|
|
11
|
-
import fsExtra from 'fs-extra';
|
|
12
|
-
import filewatcher from 'filewatcher';
|
|
13
10
|
import { exec, spawn } from 'child_process';
|
|
14
|
-
import { promisify } from 'util';
|
|
15
|
-
import path from 'path';
|
|
16
11
|
import crypto from 'crypto';
|
|
12
|
+
import filewatcher from 'filewatcher';
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import fsExtra from 'fs-extra';
|
|
17
15
|
import os from 'os';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import { promisify } from 'util';
|
|
18
18
|
import { ADMIN_FORTH_ABSOLUTE_PATH } from './utils.js';
|
|
19
19
|
let TMP_DIR;
|
|
20
20
|
try {
|
|
@@ -71,7 +71,7 @@ class CodeInjector {
|
|
|
71
71
|
}
|
|
72
72
|
prepareSources(_a) {
|
|
73
73
|
return __awaiter(this, arguments, void 0, function* ({ filesUpdated, verbose = false }) {
|
|
74
|
-
var _b, _c, _d, _e, _f, _g;
|
|
74
|
+
var _b, _c, _d, _e, _f, _g, _h;
|
|
75
75
|
// check SPA_TMP_PATH exists and create if not
|
|
76
76
|
try {
|
|
77
77
|
yield fs.promises.access(CodeInjector.SPA_TMP_PATH, fs.constants.F_OK);
|
|
@@ -154,6 +154,25 @@ class CodeInjector {
|
|
|
154
154
|
}).join('');
|
|
155
155
|
return `import { ${PascalIconName} } from '@iconify-prerendered/vue-${collection}';`;
|
|
156
156
|
}).join('\n');
|
|
157
|
+
// for each custom component generate import statement
|
|
158
|
+
const customComponentsDir = (_c = this.adminforth.config.customization) === null || _c === void 0 ? void 0 : _c.customComponentsDir;
|
|
159
|
+
let customComponentsImports = '';
|
|
160
|
+
if (customComponentsDir) {
|
|
161
|
+
// if file - return, if dir - go recursively
|
|
162
|
+
const customComponents = yield fs.promises.readdir(customComponentsDir);
|
|
163
|
+
for (const filePath of customComponents) {
|
|
164
|
+
fs.stat(`${customComponentsDir}/${filePath}`, (err, data) => {
|
|
165
|
+
if (err) {
|
|
166
|
+
console.log(err);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (data.isFile()) {
|
|
170
|
+
const componentName = filePath.split('.')[0].replace('/', '');
|
|
171
|
+
customComponentsImports += `import ${componentName} from '@/custom/${filePath}';\n`;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
157
176
|
// Generate Vue.component statements for each icon
|
|
158
177
|
const iconComponents = uniqueIcons.map((icon) => {
|
|
159
178
|
const [collection, iconName] = icon.split(':');
|
|
@@ -162,26 +181,44 @@ class CodeInjector {
|
|
|
162
181
|
}).join('');
|
|
163
182
|
return `app.component('${PascalIconName}', ${PascalIconName});`;
|
|
164
183
|
}).join('\n');
|
|
184
|
+
// Generate Vue.component statements for each custom component
|
|
185
|
+
let customComponentsComponents = '';
|
|
186
|
+
if (customComponentsDir) {
|
|
187
|
+
const customComponents = yield fs.promises.readdir(customComponentsDir);
|
|
188
|
+
for (const filePath of customComponents) {
|
|
189
|
+
fs.stat(`${customComponentsDir}/${filePath}`, (err, data) => {
|
|
190
|
+
if (err) {
|
|
191
|
+
console.log('Custom components importing error: ', err);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (data.isFile()) {
|
|
195
|
+
const componentName = filePath.split('.')[0].replace('/', '');
|
|
196
|
+
customComponentsComponents += `app.component('${componentName}', ${componentName});\n`;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
165
201
|
let imports = iconImports + '\n';
|
|
166
|
-
|
|
202
|
+
imports += customComponentsImports + '\n';
|
|
203
|
+
if ((_d = this.adminforth.config.customization) === null || _d === void 0 ? void 0 : _d.vueUsesFile) {
|
|
167
204
|
imports += `import addCustomUses from '${this.adminforth.config.customization.vueUsesFile}';\n`;
|
|
168
205
|
}
|
|
169
206
|
// inject that code into spa_tmp/src/App.vue
|
|
170
207
|
const appVuePath = path.join(CodeInjector.SPA_TMP_PATH, 'src', 'main.ts');
|
|
171
208
|
let appVueContent = yield fs.promises.readFile(appVuePath, 'utf-8');
|
|
172
209
|
appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH IMPORTS */', imports);
|
|
173
|
-
appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH COMPONENT REGISTRATIONS */', iconComponents + '\n');
|
|
174
|
-
if ((
|
|
210
|
+
appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH COMPONENT REGISTRATIONS */', iconComponents + '\n' + customComponentsComponents + '\n');
|
|
211
|
+
if ((_e = this.adminforth.config.customization) === null || _e === void 0 ? void 0 : _e.vueUsesFile) {
|
|
175
212
|
appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH CUSTOM USES */', 'addCustomUses(app);');
|
|
176
213
|
}
|
|
177
214
|
yield fs.promises.writeFile(appVuePath, appVueContent);
|
|
178
215
|
/* generate custom rotes */
|
|
179
216
|
const homepageMenuItem = this.adminforth.config.menu.find((mi) => mi.homepage);
|
|
180
217
|
let childrenHomePageMenuItem = this.adminforth.config.menu.find((mi) => { var _a; return mi.children && ((_a = mi.children) === null || _a === void 0 ? void 0 : _a.find((mi) => mi.homepage)); });
|
|
181
|
-
let childrenHomepage = (
|
|
218
|
+
let childrenHomepage = (_f = childrenHomePageMenuItem === null || childrenHomePageMenuItem === void 0 ? void 0 : childrenHomePageMenuItem.children) === null || _f === void 0 ? void 0 : _f.find((mi) => mi.homepage);
|
|
182
219
|
let homePagePath = (homepageMenuItem === null || homepageMenuItem === void 0 ? void 0 : homepageMenuItem.path) || `/resource/${childrenHomepage === null || childrenHomepage === void 0 ? void 0 : childrenHomepage.resourceId}`;
|
|
183
220
|
if (!homePagePath) {
|
|
184
|
-
homePagePath = ((
|
|
221
|
+
homePagePath = ((_g = this.adminforth.config.menu.filter((mi) => mi.path)[0]) === null || _g === void 0 ? void 0 : _g.path) || `/resource/${(_h = this.adminforth.config.menu.filter((mi) => mi.children)[0]) === null || _h === void 0 ? void 0 : _h.resourceId}`;
|
|
185
222
|
}
|
|
186
223
|
routes += `{
|
|
187
224
|
path: '/',
|
|
@@ -29,9 +29,9 @@ export async function callAdminForthApi({ path, method, body=undefined }) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
export function getCustomComponent(name) {
|
|
33
|
+
return resolveComponent(name);
|
|
34
|
+
}
|
|
35
35
|
|
|
36
36
|
export function getIcon(icon: string) {
|
|
37
37
|
// icon format is "feather:icon-name". We need to get IconName in pascal case
|
|
@@ -45,9 +45,9 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
|
45
45
|
import ResourceForm from '@/components/ResourceForm.vue';
|
|
46
46
|
import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
|
|
47
47
|
import { useCoreStore } from '@/stores/core';
|
|
48
|
-
import { callAdminForthApi } from '@/utils';
|
|
48
|
+
import { callAdminForthApi, getCustomComponent } from '@/utils';
|
|
49
49
|
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
|
|
50
|
-
import {
|
|
50
|
+
import { onMounted, ref } from 'vue';
|
|
51
51
|
import { useRoute, useRouter } from 'vue-router';
|
|
52
52
|
|
|
53
53
|
const isValid = ref(false);
|
|
@@ -75,12 +75,10 @@ onMounted(async () => {
|
|
|
75
75
|
resourceId: route.params.resourceId
|
|
76
76
|
});
|
|
77
77
|
createComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
acc
|
|
82
|
-
}
|
|
83
|
-
return acc;
|
|
78
|
+
if (column.component?.show) {
|
|
79
|
+
acc[column.name] = getCustomComponent(column.component.create.replace('@@', '').replace('./custom/', '')).split('.')[0];
|
|
80
|
+
}
|
|
81
|
+
return acc;
|
|
84
82
|
}, {});
|
|
85
83
|
loading.value = false;
|
|
86
84
|
});
|
|
@@ -41,9 +41,9 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
|
41
41
|
import ResourceForm from '@/components/ResourceForm.vue';
|
|
42
42
|
import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
|
|
43
43
|
import { useCoreStore } from '@/stores/core';
|
|
44
|
-
import { callAdminForthApi } from '@/utils';
|
|
44
|
+
import { callAdminForthApi, getCustomComponent } from '@/utils';
|
|
45
45
|
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
|
|
46
|
-
import {
|
|
46
|
+
import { computed, onMounted, ref } from 'vue';
|
|
47
47
|
import { useRoute, useRouter } from 'vue-router';
|
|
48
48
|
|
|
49
49
|
const coreStore = useCoreStore();
|
|
@@ -90,13 +90,11 @@ onMounted(async () => {
|
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
editComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
|
|
93
|
-
if (column.component?.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
return acc;
|
|
99
|
-
}, {});
|
|
93
|
+
if (column.component?.show) {
|
|
94
|
+
acc[column.name] = getCustomComponent(column.component.edit.replace('@@', '').replace('./custom/', '')).split('.')[0];
|
|
95
|
+
}
|
|
96
|
+
return acc;
|
|
97
|
+
}, {});
|
|
100
98
|
loading.value = false;
|
|
101
99
|
});
|
|
102
100
|
|
|
@@ -109,13 +107,21 @@ async function saveRecord() {
|
|
|
109
107
|
}
|
|
110
108
|
|
|
111
109
|
saving.value = true;
|
|
110
|
+
|
|
111
|
+
const updates = {};
|
|
112
|
+
for (const key in record.value) {
|
|
113
|
+
if (record.value[key] !== coreStore.record[key]) {
|
|
114
|
+
updates[key] = record.value[key];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
112
118
|
await callAdminForthApi({
|
|
113
119
|
method: 'POST',
|
|
114
120
|
path: `/update_record`,
|
|
115
121
|
body: {
|
|
116
122
|
resourceId: route.params.resourceId,
|
|
117
123
|
recordId: route.params.primaryKey,
|
|
118
|
-
record:
|
|
124
|
+
record: updates,
|
|
119
125
|
},
|
|
120
126
|
});
|
|
121
127
|
saving.value = false;
|
|
@@ -320,10 +320,11 @@ import { useCoreStore } from '@/stores/core';
|
|
|
320
320
|
import { useModalStore } from '@/stores/modal';
|
|
321
321
|
import { callAdminForthApi, getIcon } from '@/utils';
|
|
322
322
|
import { initFlowbite } from 'flowbite';
|
|
323
|
-
import { computed,
|
|
323
|
+
import { computed, onMounted, ref, watch } from 'vue';
|
|
324
324
|
import { useRoute } from 'vue-router';
|
|
325
325
|
|
|
326
326
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
327
|
+
import { getCustomComponent } from '@/utils';
|
|
327
328
|
|
|
328
329
|
import {
|
|
329
330
|
IconInboxOutline,
|
|
@@ -431,13 +432,11 @@ async function getList() {
|
|
|
431
432
|
}
|
|
432
433
|
});
|
|
433
434
|
listComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
|
|
434
|
-
if (column.component?.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
return acc;
|
|
440
|
-
}, {});
|
|
435
|
+
if (column.component?.show) {
|
|
436
|
+
acc[column.name] = getCustomComponent(column.component.list.replace('@@', '').replace('./custom/', '')).split('.')[0];
|
|
437
|
+
}
|
|
438
|
+
return acc;
|
|
439
|
+
}, {});
|
|
441
440
|
|
|
442
441
|
fetchStatus.value.pending = false;
|
|
443
442
|
rows.value = data.data?.map(row => {
|
|
@@ -48,7 +48,6 @@
|
|
|
48
48
|
{{ column.label }}
|
|
49
49
|
</td>
|
|
50
50
|
<td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap">
|
|
51
|
-
<!-- if column.name in showComponentsPerColumn, render it. If not, render ValueRenderer -->
|
|
52
51
|
<component
|
|
53
52
|
:is="showComponentsPerColumn[column.name] || ValueRenderer"
|
|
54
53
|
:column="column"
|
|
@@ -71,8 +70,9 @@
|
|
|
71
70
|
import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
72
71
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
73
72
|
import { useCoreStore } from '@/stores/core';
|
|
73
|
+
import { getCustomComponent } from '@/utils';
|
|
74
74
|
import { IconPenSolid, IconTrashBinSolid } from '@iconify-prerendered/vue-flowbite';
|
|
75
|
-
import {
|
|
75
|
+
import { onMounted, ref } from 'vue';
|
|
76
76
|
import { useRoute } from 'vue-router';
|
|
77
77
|
|
|
78
78
|
|
|
@@ -94,14 +94,10 @@ onMounted(async () => {
|
|
|
94
94
|
});
|
|
95
95
|
showComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
|
|
96
96
|
if (column.component?.show) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
acc[column.name] = component;
|
|
101
|
-
}
|
|
102
|
-
return acc;
|
|
97
|
+
acc[column.name] = getCustomComponent(column.component.show.replace('@@', '').replace('./custom/', '')).split('.')[0];
|
|
98
|
+
}
|
|
99
|
+
return acc;
|
|
103
100
|
}, {});
|
|
104
|
-
console.log('showComponentsPerColumn', showComponentsPerColumn);
|
|
105
101
|
loading.value = false;
|
|
106
102
|
});
|
|
107
103
|
|
package/index.ts
CHANGED
|
@@ -230,6 +230,19 @@ class AdminForth {
|
|
|
230
230
|
if (item.component && !item.path) {
|
|
231
231
|
errors.push(`Menu item with component must have path : ${JSON.stringify(item)}`);
|
|
232
232
|
}
|
|
233
|
+
|
|
234
|
+
if (item.type === 'resource' && !item.resourceId) {
|
|
235
|
+
errors.push(`Menu item with type 'resource' must have resourceId : ${JSON.stringify(item)}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (item.resourceId && !this.config.resources.find((res) => res.resourceId === item.resourceId)) {
|
|
239
|
+
errors.push(`Menu item with type 'resourceId' has resourceId which is not in resources: ${JSON.stringify(item)}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (item.type === 'component' && !item.component) {
|
|
243
|
+
errors.push(`Menu item with type 'component' must have component : ${JSON.stringify(item)}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
233
246
|
// make sure component starts with @@
|
|
234
247
|
if (item.component) {
|
|
235
248
|
if (!item.component.startsWith('@@')) {
|
|
@@ -604,11 +617,20 @@ class AdminForth {
|
|
|
604
617
|
data.data.forEach((item) => {
|
|
605
618
|
item[col.name] = targetDataMap[item[col.name]];
|
|
606
619
|
});
|
|
620
|
+
|
|
621
|
+
data.data.forEach((item) => {
|
|
622
|
+
Object.keys(item).forEach((key) => {
|
|
623
|
+
if (!targetResource.columns.find((col) => col.name === key) || targetResource.columns.find((col) => col.name === key && col.backendOnly)) {
|
|
624
|
+
delete item[key];
|
|
625
|
+
}
|
|
626
|
+
})
|
|
627
|
+
});
|
|
628
|
+
|
|
607
629
|
})
|
|
608
630
|
);
|
|
609
631
|
|
|
610
632
|
data.data.forEach((item) => {
|
|
611
|
-
item._label = resource.itemLabel(item)
|
|
633
|
+
item._label = resource.itemLabel(item);
|
|
612
634
|
})
|
|
613
635
|
|
|
614
636
|
|
|
@@ -623,6 +645,15 @@ class AdminForth {
|
|
|
623
645
|
}
|
|
624
646
|
}
|
|
625
647
|
|
|
648
|
+
// remove all columns which are not defined in resources, or defined but backendOnly
|
|
649
|
+
data.data.forEach((item) => {
|
|
650
|
+
Object.keys(item).forEach((key) => {
|
|
651
|
+
if (!resource.columns.find((col) => col.name === key) || resource.columns.find((col) => col.name === key && col.backendOnly)) {
|
|
652
|
+
delete item[key];
|
|
653
|
+
}
|
|
654
|
+
})
|
|
655
|
+
});
|
|
656
|
+
|
|
626
657
|
return {...data, options: resource?.options };
|
|
627
658
|
},
|
|
628
659
|
});
|
|
@@ -726,7 +757,6 @@ class AdminForth {
|
|
|
726
757
|
});
|
|
727
758
|
|
|
728
759
|
server.endpoint({
|
|
729
|
-
noAuth: true, // TODO
|
|
730
760
|
method: 'POST',
|
|
731
761
|
path: '/create_record',
|
|
732
762
|
handler: async ({ body, adminUser }) => {
|
|
@@ -801,7 +831,6 @@ class AdminForth {
|
|
|
801
831
|
}
|
|
802
832
|
});
|
|
803
833
|
server.endpoint({
|
|
804
|
-
noAuth: true, // TODO
|
|
805
834
|
method: 'POST',
|
|
806
835
|
path: '/update_record',
|
|
807
836
|
handler: async ({ body, adminUser }) => {
|
|
@@ -832,11 +861,19 @@ class AdminForth {
|
|
|
832
861
|
}
|
|
833
862
|
|
|
834
863
|
const newValues = {};
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
864
|
+
|
|
865
|
+
for (const recordField in record) {
|
|
866
|
+
if (record[recordField] !== oldRecord[recordField]) {
|
|
867
|
+
const column = resource.columns.find((col) => col.name === recordField);
|
|
868
|
+
if (column) {
|
|
869
|
+
newValues[recordField] = connector.setFieldValue(column, record[recordField]);
|
|
870
|
+
} else {
|
|
871
|
+
newValues[recordField] = record[recordField];
|
|
838
872
|
}
|
|
839
|
-
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
console.log('✅ newValues', newValues)
|
|
840
877
|
if (Object.keys(newValues).length > 0) {
|
|
841
878
|
await connector.updateRecord({ resource, recordId, record, newValues});
|
|
842
879
|
}
|
|
@@ -859,7 +896,6 @@ class AdminForth {
|
|
|
859
896
|
}
|
|
860
897
|
});
|
|
861
898
|
server.endpoint({
|
|
862
|
-
noAuth: true, // TODO
|
|
863
899
|
method: 'POST',
|
|
864
900
|
path: '/delete_record',
|
|
865
901
|
handler: async ({ body, adminUser }) => {
|
|
@@ -901,7 +937,6 @@ class AdminForth {
|
|
|
901
937
|
}
|
|
902
938
|
});
|
|
903
939
|
server.endpoint({
|
|
904
|
-
noAuth: true, // TODO
|
|
905
940
|
method: 'POST',
|
|
906
941
|
path: '/start_bulk_action',
|
|
907
942
|
handler: async ({ body }) => {
|
package/modules/codeInjector.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import fsExtra from 'fs-extra';
|
|
3
|
-
import filewatcher from 'filewatcher';
|
|
4
1
|
import { exec, spawn } from 'child_process';
|
|
5
|
-
import { promisify } from 'util';
|
|
6
|
-
import path from 'path';
|
|
7
|
-
import { fileURLToPath } from 'url';
|
|
8
2
|
import crypto from 'crypto';
|
|
3
|
+
import filewatcher from 'filewatcher';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import fsExtra from 'fs-extra';
|
|
9
6
|
import os from 'os';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { promisify } from 'util';
|
|
10
9
|
import AdminForth from '../index.js';
|
|
11
10
|
import { ADMIN_FORTH_ABSOLUTE_PATH } from './utils.js';
|
|
12
11
|
|
|
@@ -173,6 +172,26 @@ class CodeInjector {
|
|
|
173
172
|
return `import { ${PascalIconName} } from '@iconify-prerendered/vue-${collection}';`;
|
|
174
173
|
}).join('\n');
|
|
175
174
|
|
|
175
|
+
// for each custom component generate import statement
|
|
176
|
+
const customComponentsDir = this.adminforth.config.customization?.customComponentsDir;
|
|
177
|
+
let customComponentsImports = '';
|
|
178
|
+
if (customComponentsDir) {
|
|
179
|
+
// if file - return, if dir - go recursively
|
|
180
|
+
const customComponents = await fs.promises.readdir(customComponentsDir);
|
|
181
|
+
for (const filePath of customComponents) {
|
|
182
|
+
fs.stat(`${customComponentsDir}/${filePath}`, (err, data) => {
|
|
183
|
+
if (err) {
|
|
184
|
+
console.log(err);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (data.isFile()) {
|
|
188
|
+
const componentName = filePath.split('.')[0].replace('/', '');
|
|
189
|
+
customComponentsImports += `import ${componentName} from '@/custom/${filePath}';\n`;
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
176
195
|
// Generate Vue.component statements for each icon
|
|
177
196
|
const iconComponents = uniqueIcons.map((icon) => {
|
|
178
197
|
const [ collection, iconName ] = icon.split(':');
|
|
@@ -182,7 +201,28 @@ class CodeInjector {
|
|
|
182
201
|
return `app.component('${PascalIconName}', ${PascalIconName});`;
|
|
183
202
|
}).join('\n');
|
|
184
203
|
|
|
204
|
+
// Generate Vue.component statements for each custom component
|
|
205
|
+
let customComponentsComponents = '';
|
|
206
|
+
if (customComponentsDir) {
|
|
207
|
+
const customComponents = await fs.promises.readdir(customComponentsDir);
|
|
208
|
+
for (const filePath of customComponents) {
|
|
209
|
+
fs.stat(`${customComponentsDir}/${filePath}`, (err, data) => {
|
|
210
|
+
if (err) {
|
|
211
|
+
console.log('Custom components importing error: ', err);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (data.isFile()) {
|
|
215
|
+
const componentName = filePath.split('.')[0].replace('/', '');
|
|
216
|
+
customComponentsComponents += `app.component('${componentName}', ${componentName});\n`;
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
185
223
|
let imports = iconImports + '\n';
|
|
224
|
+
imports += customComponentsImports + '\n';
|
|
225
|
+
|
|
186
226
|
|
|
187
227
|
if (this.adminforth.config.customization?.vueUsesFile) {
|
|
188
228
|
imports += `import addCustomUses from '${this.adminforth.config.customization.vueUsesFile}';\n`;
|
|
@@ -192,7 +232,7 @@ class CodeInjector {
|
|
|
192
232
|
const appVuePath = path.join(CodeInjector.SPA_TMP_PATH, 'src', 'main.ts');
|
|
193
233
|
let appVueContent = await fs.promises.readFile(appVuePath, 'utf-8');
|
|
194
234
|
appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH IMPORTS */', imports);
|
|
195
|
-
appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH COMPONENT REGISTRATIONS */', iconComponents + '\n' );
|
|
235
|
+
appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH COMPONENT REGISTRATIONS */', iconComponents + '\n' + customComponentsComponents + '\n');
|
|
196
236
|
if (this.adminforth.config.customization?.vueUsesFile) {
|
|
197
237
|
appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH CUSTOM USES */', 'addCustomUses(app);');
|
|
198
238
|
}
|
package/package.json
CHANGED
package/spa/src/utils.ts
CHANGED
|
@@ -29,9 +29,9 @@ export async function callAdminForthApi({ path, method, body=undefined }) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
export function getCustomComponent(name) {
|
|
33
|
+
return resolveComponent(name);
|
|
34
|
+
}
|
|
35
35
|
|
|
36
36
|
export function getIcon(icon: string) {
|
|
37
37
|
// icon format is "feather:icon-name". We need to get IconName in pascal case
|
|
@@ -45,9 +45,9 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
|
45
45
|
import ResourceForm from '@/components/ResourceForm.vue';
|
|
46
46
|
import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
|
|
47
47
|
import { useCoreStore } from '@/stores/core';
|
|
48
|
-
import { callAdminForthApi } from '@/utils';
|
|
48
|
+
import { callAdminForthApi, getCustomComponent } from '@/utils';
|
|
49
49
|
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
|
|
50
|
-
import {
|
|
50
|
+
import { onMounted, ref } from 'vue';
|
|
51
51
|
import { useRoute, useRouter } from 'vue-router';
|
|
52
52
|
|
|
53
53
|
const isValid = ref(false);
|
|
@@ -75,12 +75,10 @@ onMounted(async () => {
|
|
|
75
75
|
resourceId: route.params.resourceId
|
|
76
76
|
});
|
|
77
77
|
createComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
acc
|
|
82
|
-
}
|
|
83
|
-
return acc;
|
|
78
|
+
if (column.component?.show) {
|
|
79
|
+
acc[column.name] = getCustomComponent(column.component.create.replace('@@', '').replace('./custom/', '')).split('.')[0];
|
|
80
|
+
}
|
|
81
|
+
return acc;
|
|
84
82
|
}, {});
|
|
85
83
|
loading.value = false;
|
|
86
84
|
});
|
|
@@ -41,9 +41,9 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
|
41
41
|
import ResourceForm from '@/components/ResourceForm.vue';
|
|
42
42
|
import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
|
|
43
43
|
import { useCoreStore } from '@/stores/core';
|
|
44
|
-
import { callAdminForthApi } from '@/utils';
|
|
44
|
+
import { callAdminForthApi, getCustomComponent } from '@/utils';
|
|
45
45
|
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
|
|
46
|
-
import {
|
|
46
|
+
import { computed, onMounted, ref } from 'vue';
|
|
47
47
|
import { useRoute, useRouter } from 'vue-router';
|
|
48
48
|
|
|
49
49
|
const coreStore = useCoreStore();
|
|
@@ -90,13 +90,11 @@ onMounted(async () => {
|
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
editComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
|
|
93
|
-
if (column.component?.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
return acc;
|
|
99
|
-
}, {});
|
|
93
|
+
if (column.component?.show) {
|
|
94
|
+
acc[column.name] = getCustomComponent(column.component.edit.replace('@@', '').replace('./custom/', '')).split('.')[0];
|
|
95
|
+
}
|
|
96
|
+
return acc;
|
|
97
|
+
}, {});
|
|
100
98
|
loading.value = false;
|
|
101
99
|
});
|
|
102
100
|
|
|
@@ -109,13 +107,21 @@ async function saveRecord() {
|
|
|
109
107
|
}
|
|
110
108
|
|
|
111
109
|
saving.value = true;
|
|
110
|
+
|
|
111
|
+
const updates = {};
|
|
112
|
+
for (const key in record.value) {
|
|
113
|
+
if (record.value[key] !== coreStore.record[key]) {
|
|
114
|
+
updates[key] = record.value[key];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
112
118
|
await callAdminForthApi({
|
|
113
119
|
method: 'POST',
|
|
114
120
|
path: `/update_record`,
|
|
115
121
|
body: {
|
|
116
122
|
resourceId: route.params.resourceId,
|
|
117
123
|
recordId: route.params.primaryKey,
|
|
118
|
-
record:
|
|
124
|
+
record: updates,
|
|
119
125
|
},
|
|
120
126
|
});
|
|
121
127
|
saving.value = false;
|
|
@@ -320,10 +320,11 @@ import { useCoreStore } from '@/stores/core';
|
|
|
320
320
|
import { useModalStore } from '@/stores/modal';
|
|
321
321
|
import { callAdminForthApi, getIcon } from '@/utils';
|
|
322
322
|
import { initFlowbite } from 'flowbite';
|
|
323
|
-
import { computed,
|
|
323
|
+
import { computed, onMounted, ref, watch } from 'vue';
|
|
324
324
|
import { useRoute } from 'vue-router';
|
|
325
325
|
|
|
326
326
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
327
|
+
import { getCustomComponent } from '@/utils';
|
|
327
328
|
|
|
328
329
|
import {
|
|
329
330
|
IconInboxOutline,
|
|
@@ -431,13 +432,11 @@ async function getList() {
|
|
|
431
432
|
}
|
|
432
433
|
});
|
|
433
434
|
listComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
|
|
434
|
-
if (column.component?.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
return acc;
|
|
440
|
-
}, {});
|
|
435
|
+
if (column.component?.show) {
|
|
436
|
+
acc[column.name] = getCustomComponent(column.component.list.replace('@@', '').replace('./custom/', '')).split('.')[0];
|
|
437
|
+
}
|
|
438
|
+
return acc;
|
|
439
|
+
}, {});
|
|
441
440
|
|
|
442
441
|
fetchStatus.value.pending = false;
|
|
443
442
|
rows.value = data.data?.map(row => {
|
|
@@ -48,7 +48,6 @@
|
|
|
48
48
|
{{ column.label }}
|
|
49
49
|
</td>
|
|
50
50
|
<td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap">
|
|
51
|
-
<!-- if column.name in showComponentsPerColumn, render it. If not, render ValueRenderer -->
|
|
52
51
|
<component
|
|
53
52
|
:is="showComponentsPerColumn[column.name] || ValueRenderer"
|
|
54
53
|
:column="column"
|
|
@@ -71,8 +70,9 @@
|
|
|
71
70
|
import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
72
71
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
73
72
|
import { useCoreStore } from '@/stores/core';
|
|
73
|
+
import { getCustomComponent } from '@/utils';
|
|
74
74
|
import { IconPenSolid, IconTrashBinSolid } from '@iconify-prerendered/vue-flowbite';
|
|
75
|
-
import {
|
|
75
|
+
import { onMounted, ref } from 'vue';
|
|
76
76
|
import { useRoute } from 'vue-router';
|
|
77
77
|
|
|
78
78
|
|
|
@@ -94,14 +94,10 @@ onMounted(async () => {
|
|
|
94
94
|
});
|
|
95
95
|
showComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
|
|
96
96
|
if (column.component?.show) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
acc[column.name] = component;
|
|
101
|
-
}
|
|
102
|
-
return acc;
|
|
97
|
+
acc[column.name] = getCustomComponent(column.component.show.replace('@@', '').replace('./custom/', '')).split('.')[0];
|
|
98
|
+
}
|
|
99
|
+
return acc;
|
|
103
100
|
}, {});
|
|
104
|
-
console.log('showComponentsPerColumn', showComponentsPerColumn);
|
|
105
101
|
loading.value = false;
|
|
106
102
|
});
|
|
107
103
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
3
|
export type AdminForthConfigMenuItem = {
|
|
4
|
-
type
|
|
5
|
-
label
|
|
4
|
+
type?: 'heading' | 'group' | 'resource' | 'page' | 'gap' | 'divider',
|
|
5
|
+
label?: string,
|
|
6
6
|
icon?: string,
|
|
7
7
|
path?: string,
|
|
8
8
|
component?: string,
|
|
@@ -34,6 +34,7 @@ export type AdminForthResourceColumn = {
|
|
|
34
34
|
enum?: Array<AdminForthResourceColumnEnumElement>,
|
|
35
35
|
foreignResource?:AdminForthResourceColumnForeignResource,
|
|
36
36
|
sortable?: boolean,
|
|
37
|
+
backendOnly?: boolean, // if true field will not be passed to UI under no circumstances, but will be presented in hooks
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export type AdminForthResource = {
|