adminforth 1.0.47 → 1.0.49
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 +28 -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 +15 -9
- package/dist/spa/spa/src/views/ListView.vue +6 -7
- package/dist/spa/spa/src/views/ShowView.vue +5 -9
- package/index.ts +36 -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 +15 -9
- package/spa/src/views/ListView.vue +6 -7
- 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('@@')) {
|
|
@@ -548,6 +557,15 @@ class AdminForth {
|
|
|
548
557
|
return { error: resp.error };
|
|
549
558
|
}
|
|
550
559
|
}
|
|
560
|
+
// remove all columns which are not defined in resources, or defined but backendOnly
|
|
561
|
+
data.data.forEach((item) => {
|
|
562
|
+
Object.keys(item).forEach((key) => {
|
|
563
|
+
if (!resource.columns.find((col) => col.name === key) || resource.columns.find((col) => col.name === key && col.backendOnly)) {
|
|
564
|
+
delete item[key];
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
console.log('data', data);
|
|
551
569
|
return Object.assign(Object.assign({}, data), { options: resource === null || resource === void 0 ? void 0 : resource.options });
|
|
552
570
|
}),
|
|
553
571
|
});
|
|
@@ -647,7 +665,6 @@ class AdminForth {
|
|
|
647
665
|
}),
|
|
648
666
|
});
|
|
649
667
|
server.endpoint({
|
|
650
|
-
noAuth: true, // TODO
|
|
651
668
|
method: 'POST',
|
|
652
669
|
path: '/create_record',
|
|
653
670
|
handler: (_y) => __awaiter(this, [_y], void 0, function* ({ body, adminUser }) {
|
|
@@ -716,7 +733,6 @@ class AdminForth {
|
|
|
716
733
|
})
|
|
717
734
|
});
|
|
718
735
|
server.endpoint({
|
|
719
|
-
noAuth: true, // TODO
|
|
720
736
|
method: 'POST',
|
|
721
737
|
path: '/update_record',
|
|
722
738
|
handler: (_8) => __awaiter(this, [_8], void 0, function* ({ body, adminUser }) {
|
|
@@ -744,11 +760,18 @@ class AdminForth {
|
|
|
744
760
|
}
|
|
745
761
|
}
|
|
746
762
|
const newValues = {};
|
|
747
|
-
for (const
|
|
748
|
-
if (record[
|
|
749
|
-
|
|
763
|
+
for (const recordField in record) {
|
|
764
|
+
if (record[recordField] !== oldRecord[recordField]) {
|
|
765
|
+
const column = resource.columns.find((col) => col.name === recordField);
|
|
766
|
+
if (column) {
|
|
767
|
+
newValues[recordField] = connector.setFieldValue(column, record[recordField]);
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
newValues[recordField] = record[recordField];
|
|
771
|
+
}
|
|
750
772
|
}
|
|
751
773
|
}
|
|
774
|
+
console.log('✅ newValues', newValues);
|
|
752
775
|
if (Object.keys(newValues).length > 0) {
|
|
753
776
|
yield connector.updateRecord({ resource, recordId, record, newValues });
|
|
754
777
|
}
|
|
@@ -768,7 +791,6 @@ class AdminForth {
|
|
|
768
791
|
})
|
|
769
792
|
});
|
|
770
793
|
server.endpoint({
|
|
771
|
-
noAuth: true, // TODO
|
|
772
794
|
method: 'POST',
|
|
773
795
|
path: '/delete_record',
|
|
774
796
|
handler: (_17) => __awaiter(this, [_17], void 0, function* ({ body, adminUser }) {
|
|
@@ -806,7 +828,6 @@ class AdminForth {
|
|
|
806
828
|
})
|
|
807
829
|
});
|
|
808
830
|
server.endpoint({
|
|
809
|
-
noAuth: true, // TODO
|
|
810
831
|
method: 'POST',
|
|
811
832
|
path: '/start_bulk_action',
|
|
812
833
|
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?.create) {
|
|
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();
|
|
@@ -91,12 +91,10 @@ onMounted(async () => {
|
|
|
91
91
|
|
|
92
92
|
editComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
|
|
93
93
|
if (column.component?.edit) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
return acc;
|
|
99
|
-
}, {});
|
|
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,
|
|
@@ -432,12 +433,10 @@ async function getList() {
|
|
|
432
433
|
});
|
|
433
434
|
listComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
|
|
434
435
|
if (column.component?.list) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
return acc;
|
|
440
|
-
}, {});
|
|
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('@@')) {
|
|
@@ -608,7 +621,7 @@ class AdminForth {
|
|
|
608
621
|
);
|
|
609
622
|
|
|
610
623
|
data.data.forEach((item) => {
|
|
611
|
-
item._label = resource.itemLabel(item)
|
|
624
|
+
item._label = resource.itemLabel(item);
|
|
612
625
|
})
|
|
613
626
|
|
|
614
627
|
|
|
@@ -623,6 +636,16 @@ class AdminForth {
|
|
|
623
636
|
}
|
|
624
637
|
}
|
|
625
638
|
|
|
639
|
+
// remove all columns which are not defined in resources, or defined but backendOnly
|
|
640
|
+
data.data.forEach((item) => {
|
|
641
|
+
Object.keys(item).forEach((key) => {
|
|
642
|
+
if (!resource.columns.find((col) => col.name === key) || resource.columns.find((col) => col.name === key && col.backendOnly)) {
|
|
643
|
+
delete item[key];
|
|
644
|
+
}
|
|
645
|
+
})
|
|
646
|
+
});
|
|
647
|
+
console.log('data', data);
|
|
648
|
+
|
|
626
649
|
return {...data, options: resource?.options };
|
|
627
650
|
},
|
|
628
651
|
});
|
|
@@ -726,7 +749,6 @@ class AdminForth {
|
|
|
726
749
|
});
|
|
727
750
|
|
|
728
751
|
server.endpoint({
|
|
729
|
-
noAuth: true, // TODO
|
|
730
752
|
method: 'POST',
|
|
731
753
|
path: '/create_record',
|
|
732
754
|
handler: async ({ body, adminUser }) => {
|
|
@@ -801,7 +823,6 @@ class AdminForth {
|
|
|
801
823
|
}
|
|
802
824
|
});
|
|
803
825
|
server.endpoint({
|
|
804
|
-
noAuth: true, // TODO
|
|
805
826
|
method: 'POST',
|
|
806
827
|
path: '/update_record',
|
|
807
828
|
handler: async ({ body, adminUser }) => {
|
|
@@ -832,11 +853,19 @@ class AdminForth {
|
|
|
832
853
|
}
|
|
833
854
|
|
|
834
855
|
const newValues = {};
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
856
|
+
|
|
857
|
+
for (const recordField in record) {
|
|
858
|
+
if (record[recordField] !== oldRecord[recordField]) {
|
|
859
|
+
const column = resource.columns.find((col) => col.name === recordField);
|
|
860
|
+
if (column) {
|
|
861
|
+
newValues[recordField] = connector.setFieldValue(column, record[recordField]);
|
|
862
|
+
} else {
|
|
863
|
+
newValues[recordField] = record[recordField];
|
|
838
864
|
}
|
|
839
|
-
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
console.log('✅ newValues', newValues)
|
|
840
869
|
if (Object.keys(newValues).length > 0) {
|
|
841
870
|
await connector.updateRecord({ resource, recordId, record, newValues});
|
|
842
871
|
}
|
|
@@ -859,7 +888,6 @@ class AdminForth {
|
|
|
859
888
|
}
|
|
860
889
|
});
|
|
861
890
|
server.endpoint({
|
|
862
|
-
noAuth: true, // TODO
|
|
863
891
|
method: 'POST',
|
|
864
892
|
path: '/delete_record',
|
|
865
893
|
handler: async ({ body, adminUser }) => {
|
|
@@ -901,7 +929,6 @@ class AdminForth {
|
|
|
901
929
|
}
|
|
902
930
|
});
|
|
903
931
|
server.endpoint({
|
|
904
|
-
noAuth: true, // TODO
|
|
905
932
|
method: 'POST',
|
|
906
933
|
path: '/start_bulk_action',
|
|
907
934
|
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?.create) {
|
|
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();
|
|
@@ -91,12 +91,10 @@ onMounted(async () => {
|
|
|
91
91
|
|
|
92
92
|
editComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
|
|
93
93
|
if (column.component?.edit) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
return acc;
|
|
99
|
-
}, {});
|
|
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,
|
|
@@ -432,12 +433,10 @@ async function getList() {
|
|
|
432
433
|
});
|
|
433
434
|
listComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
|
|
434
435
|
if (column.component?.list) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
return acc;
|
|
440
|
-
}, {});
|
|
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 = {
|