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.
@@ -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 col of resource.columns.filter((col) => !col.virtual)) {
748
- if (record[col.name] !== oldRecord[col.name]) {
749
- newValues[col.name] = connector.setFieldValue(col, record[col.name]);
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
- if ((_c = this.adminforth.config.customization) === null || _c === void 0 ? void 0 : _c.vueUsesFile) {
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 ((_d = this.adminforth.config.customization) === null || _d === void 0 ? void 0 : _d.vueUsesFile) {
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 = (_e = childrenHomePageMenuItem === null || childrenHomePageMenuItem === void 0 ? void 0 : childrenHomePageMenuItem.children) === null || _e === void 0 ? void 0 : _e.find((mi) => mi.homepage);
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 = ((_f = this.adminforth.config.menu.filter((mi) => mi.path)[0]) === null || _f === void 0 ? void 0 : _f.path) || `/resource/${(_g = this.adminforth.config.menu.filter((mi) => mi.children)[0]) === null || _g === void 0 ? void 0 : _g.resourceId}`;
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 { defineAsyncComponent, onMounted, ref } from 'vue';
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
- if (column.component?.create) {
79
- const path = column.component.create.replace('@@', '../custom');
80
- let component = defineAsyncComponent(() => import(`${path}`))
81
- acc[column.name] = component;
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 { defineAsyncComponent, onMounted, ref, computed } from 'vue';
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?.edit) {
94
- const path = column.component.edit.replace('@@', '../custom');
95
- let component = defineAsyncComponent(() => import(`${path}`))
96
- acc[column.name] = component;
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: record.value,
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, defineAsyncComponent, onMounted, ref, watch } from 'vue';
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?.list) {
435
- const path = column.component.list.replace('@@', '../custom');
436
- let component = defineAsyncComponent(() => import(`${path}`))
437
- acc[column.name] = component;
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 { defineAsyncComponent, onMounted, ref } from 'vue';
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
- const path = column.component.show.replace('@@', '../custom');
98
- console.log('path', path);
99
- let component = defineAsyncComponent(() => import(`${path}`))
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
- for (const col of resource.columns.filter((col) => !col.virtual)) {
836
- if (record[col.name] !== oldRecord[col.name]) {
837
- newValues[col.name] = connector.setFieldValue(col, record[col.name]);
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 }) => {
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.0.47",
3
+ "version": "1.0.48",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
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 { defineAsyncComponent, onMounted, ref } from 'vue';
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
- if (column.component?.create) {
79
- const path = column.component.create.replace('@@', '../custom');
80
- let component = defineAsyncComponent(() => import(`${path}`))
81
- acc[column.name] = component;
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 { defineAsyncComponent, onMounted, ref, computed } from 'vue';
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?.edit) {
94
- const path = column.component.edit.replace('@@', '../custom');
95
- let component = defineAsyncComponent(() => import(`${path}`))
96
- acc[column.name] = component;
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: record.value,
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, defineAsyncComponent, onMounted, ref, watch } from 'vue';
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?.list) {
435
- const path = column.component.list.replace('@@', '../custom');
436
- let component = defineAsyncComponent(() => import(`${path}`))
437
- acc[column.name] = component;
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 { defineAsyncComponent, onMounted, ref } from 'vue';
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
- const path = column.component.show.replace('@@', '../custom');
98
- console.log('path', path);
99
- let component = defineAsyncComponent(() => import(`${path}`))
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: 'heading' | 'group' | 'resource' | 'page' | undefined,
5
- label: string,
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 = {