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.
@@ -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 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]);
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
- 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?.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 { 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();
@@ -91,12 +91,10 @@ onMounted(async () => {
91
91
 
92
92
  editComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
93
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
- }, {});
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,
@@ -432,12 +433,10 @@ async function getList() {
432
433
  });
433
434
  listComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
434
435
  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
- }, {});
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('@@')) {
@@ -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
- 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]);
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 }) => {
@@ -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.49",
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?.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 { 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();
@@ -91,12 +91,10 @@ onMounted(async () => {
91
91
 
92
92
  editComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
93
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
- }, {});
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,
@@ -432,12 +433,10 @@ async function getList() {
432
433
  });
433
434
  listComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
434
435
  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
- }, {});
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 = {