adminforth 1.0.32 → 1.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dataConnectors/sqlite.ts +1 -0
  2. package/dist/dataConnectors/sqlite.js +1 -0
  3. package/dist/index.js +87 -74
  4. package/dist/modules/utils.js +9 -0
  5. package/dist/spa/spa/src/App.vue +0 -3
  6. package/dist/spa/spa/src/components/CustomDatePicker.vue +3 -3
  7. package/dist/spa/spa/src/components/CustomDateRangePicker.vue +3 -3
  8. package/dist/spa/spa/src/components/CustomRangePicker.vue +32 -19
  9. package/dist/spa/spa/src/components/Dropdown.vue +6 -1
  10. package/dist/spa/spa/src/components/ResourceForm.vue +125 -102
  11. package/dist/spa/spa/src/components/ValueRenderer.vue +11 -1
  12. package/dist/spa/spa/src/stores/core.ts +21 -19
  13. package/dist/spa/spa/src/views/CreateView.vue +16 -6
  14. package/dist/spa/spa/src/views/EditView.vue +15 -5
  15. package/dist/spa/spa/src/views/ListView.vue +31 -17
  16. package/dist/spa/spa/src/views/ResourceParent.vue +1 -1
  17. package/dist/spa/spa/src/views/ShowView.vue +21 -6
  18. package/dist/types/AdminForthConfig.js +1 -0
  19. package/index.ts +73 -149
  20. package/modules/utils.ts +14 -0
  21. package/package.json +1 -1
  22. package/spa/src/App.vue +0 -3
  23. package/spa/src/components/CustomDatePicker.vue +3 -3
  24. package/spa/src/components/CustomDateRangePicker.vue +3 -3
  25. package/spa/src/components/CustomRangePicker.vue +32 -19
  26. package/spa/src/components/Dropdown.vue +6 -1
  27. package/spa/src/components/ResourceForm.vue +125 -102
  28. package/spa/src/components/ValueRenderer.vue +11 -1
  29. package/spa/src/stores/core.ts +21 -19
  30. package/spa/src/views/CreateView.vue +16 -6
  31. package/spa/src/views/EditView.vue +15 -5
  32. package/spa/src/views/ListView.vue +31 -17
  33. package/spa/src/views/ResourceParent.vue +1 -1
  34. package/spa/src/views/ShowView.vue +21 -6
  35. package/types/AdminForthConfig.ts +159 -0
package/index.ts CHANGED
@@ -8,116 +8,13 @@ import { guessLabelFromName } from './modules/utils.js';
8
8
  import ExpressServer from './servers/express.js';
9
9
  import {v1 as uuid} from 'uuid';
10
10
  import fs from 'fs';
11
-
12
- let package_json;
13
- if (fs.existsSync('../package.json')) {
14
- // in prod we are in dist folder
15
- package_json = JSON.parse(fs.readFileSync('../package.json', 'utf8'));
16
- } else {
17
- package_json = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
18
- }
19
-
11
+ import { ADMINFORTH_VERSION } from './modules/utils.js';
20
12
  import { AdminForthFilterOperators, AdminForthTypes, AdminForthTypesValues } from './types.js';
13
+ import { AdminForthConfig } from './types/AdminForthConfig.js';
21
14
 
22
15
  const AVAILABLE_SHOW_IN = ['list', 'edit', 'create', 'filter', 'show'];
23
16
  const DEFAULT_ALLOWED_ACTIONS = {create: true, edit: true, show: true, delete: true};
24
17
 
25
- type AdminForthConfigMenuItem = {
26
- label: string,
27
- icon?: string,
28
- path?: string,
29
- component?: string,
30
- resourceId?: string,
31
- homepage?: boolean,
32
- children?: Array<AdminForthConfigMenuItem>,
33
- }
34
-
35
-
36
- type AdminForthResourceColumn = {
37
- name: string,
38
- label?: string,
39
- type?: AdminForthTypesValues,
40
- primaryKey?: boolean,
41
- required?: boolean | { create: boolean, edit: boolean },
42
- editingNote?: string | { create: string, edit: string },
43
- showIn?: Array<string>,
44
- fillOnCreate?: Function,
45
- isUnique?: boolean,
46
- virtual?: boolean,
47
- allowMinMaxQuery?: boolean,
48
- }
49
-
50
- type AdminForthResource = {
51
- resourceId: string,
52
- label?: string,
53
- table: string,
54
- dataSource: string,
55
- columns: Array<AdminForthResourceColumn>,
56
- itemLabel?: Function,
57
- hooks?: {
58
- show?: Function,
59
- create?: {
60
- beforeSave?: Function,
61
- afterSave?: Function,
62
- },
63
- edit?: {
64
- beforeSave?: Function,
65
- afterSave?: Function,
66
- },
67
- delete?: {
68
- beforeSave?: Function,
69
- afterSave?: Function,
70
- },
71
- },
72
- options?: {
73
- bulkActions?: Array<{
74
- label: string,
75
- state: string,
76
- icon: string,
77
- action: Function,
78
- }>,
79
- allowedActions?: AllowedActions,
80
-
81
- },
82
- }
83
-
84
- type AdminForthDataSource = {
85
- id: string,
86
- url: string,
87
- }
88
-
89
- type AdminForthConfig = {
90
- rootUser?: {
91
- username: string,
92
- password: string,
93
- },
94
- auth?: {
95
- resourceId: string,
96
- usernameField: string,
97
- passwordHashField: string,
98
- loginBackgroundImage?: string,
99
- userFullName?: string,
100
- },
101
- resources: Array<any>,
102
- menu: Array<AdminForthConfigMenuItem>,
103
- databaseConnectors?: any,
104
- dataSources: Array<any>,
105
- customization?: {
106
- customComponentsDir?: string,
107
- vueUsesFile?: string,
108
- },
109
- baseUrl?: string,
110
- brandName?: string,
111
- datesFormat?: string,
112
- deleteConfirmation?: boolean,
113
- }
114
-
115
- type AllowedActions = {
116
- create: boolean,
117
- edit: boolean,
118
- show: boolean,
119
- delete: boolean,
120
- }
121
18
 
122
19
  class AdminForth {
123
20
  static Types = AdminForthTypes;
@@ -131,8 +28,6 @@ class AdminForth {
131
28
 
132
29
  #defaultConfig = {
133
30
  deleteConfirmation: true,
134
-
135
-
136
31
  }
137
32
 
138
33
  config: AdminForthConfig;
@@ -157,6 +52,7 @@ class AdminForth {
157
52
  this.codeInjector = new CodeInjector(this);
158
53
  this.connectors = {};
159
54
  this.statuses = {}
55
+ console.log(`🚀 AdminForth v${ADMINFORTH_VERSION} starting up`)
160
56
  }
161
57
 
162
58
  validateConfig() {
@@ -403,7 +299,7 @@ class AdminForth {
403
299
  if (!this.config.databaseConnectors[dbType]) {
404
300
  throw new Error(`Database type ${dbType} is not supported, consider using databaseConnectors in AdminForth config`);
405
301
  }
406
- this.connectors[ds.id] = new this.config.databaseConnectors[dbType]({url: ds.url , fieldtypesByTable: ds.fieldtypesByTable});
302
+ this.connectors[ds.id] = new this.config.databaseConnectors[dbType]({url: ds.url});
407
303
  });
408
304
 
409
305
  await Promise.all(this.config.resources.map(async (res) => {
@@ -441,10 +337,6 @@ class AdminForth {
441
337
  // console.log('⚙️⚙️⚙️ Database discovery done', JSON.stringify(this.config.resources, null, 2));
442
338
  }
443
339
 
444
- async init() {
445
- console.log('AdminForth init');
446
- }
447
-
448
340
  async bundleNow({ hotReload=false, verbose=false }) {
449
341
  this.codeInjector.bundleNow({ hotReload, verbose });
450
342
  }
@@ -553,12 +445,12 @@ class AdminForth {
553
445
  return { error: 'Unauthorized' };
554
446
  }
555
447
  username = user.data[0][this.config.auth.usernameField];
556
- userFullName = user.data[0][this.config.auth.userFullName];
448
+ userFullName = user.data[0][this.config.auth.userFullNameField];
557
449
  }
558
450
 
559
451
  const userData = {
560
452
  [this.config.auth.usernameField]: username,
561
- [this.config.auth.userFullName]: userFullName
453
+ [this.config.auth.userFullNameField]: userFullName
562
454
  };
563
455
  return {
564
456
  user: userData,
@@ -575,7 +467,7 @@ class AdminForth {
575
467
  usernameField: this.config.auth.usernameField,
576
468
  },
577
469
  adminUser,
578
- version: package_json.version,
470
+ version: ADMINFORTH_VERSION,
579
471
  };
580
472
  },
581
473
  });
@@ -601,8 +493,11 @@ class AdminForth {
601
493
  server.endpoint({
602
494
  method: 'POST',
603
495
  path: '/get_resource_data',
604
- handler: async ({ body }) => {
605
- const { resourceId, limit, offset, filters, sort } = body;
496
+ handler: async ({ body, adminUser }) => {
497
+ const { resourceId, source } = body;
498
+ if (['show', 'list'].includes(source) === false) {
499
+ return { error: 'Invalid source, should be list or show' };
500
+ }
606
501
  if (!this.statuses.dbDiscover) {
607
502
  return { error: 'Database discovery not started' };
608
503
  }
@@ -613,6 +508,19 @@ class AdminForth {
613
508
  if (!resource) {
614
509
  return { error: `Resource ${resourceId} not found` };
615
510
  }
511
+ if (resource.hooks?.[source]?.beforeDatasourceRequest) {
512
+ const resp = await resource.hooks?.[source]?.beforeDatasourceRequest({ resource, query: body, adminUser });
513
+ if (!resp || (!resp.ok && !resp.error)) {
514
+ throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
515
+ }
516
+
517
+ if (resp.error) {
518
+ return { error: resp.error };
519
+ }
520
+ }
521
+ const { limit, offset, filters, sort } = body;
522
+
523
+
616
524
  const data = await this.connectors[resource.dataSource].getData({
617
525
  resource,
618
526
  limit,
@@ -620,6 +528,50 @@ class AdminForth {
620
528
  filters,
621
529
  sort,
622
530
  });
531
+ // for foreign keys, add references
532
+ await Promise.all(
533
+ resource.columns.filter((col) => col.foreignResource).map(async (col) => {
534
+ const targetResource = this.config.resources.find((res) => res.resourceId == col.foreignResource.resourceId);
535
+ const targetConnector = this.connectors[targetResource.dataSource];
536
+ const targetResourcePkField = targetResource.columns.find((col) => col.primaryKey).name;
537
+ const targetData = await targetConnector.getData({
538
+ resource: targetResource,
539
+ limit: limit,
540
+ offset: 0,
541
+ filters: [
542
+ {
543
+ field: targetResourcePkField,
544
+ operator: AdminForthFilterOperators.IN,
545
+ value: data.data.map((item) => item[col.name]),
546
+ }
547
+ ],
548
+ sort: [],
549
+ });
550
+ const targetDataMap = targetData.data.reduce((acc, item) => {
551
+ acc[item[targetResourcePkField]] = {
552
+ label: targetResource.itemLabel ? targetResource.itemLabel(item) : item[targetResourcePkField],
553
+ pk: item[targetResourcePkField],
554
+ }
555
+ return acc;
556
+ }, {});
557
+ data.data.forEach((item) => {
558
+ item[col.name] = targetDataMap[item[col.name]];
559
+ });
560
+ })
561
+ );
562
+
563
+
564
+ if (resource.hooks?.[source]?.afterDatasourceRequest) {
565
+ const resp = await resource.hooks?.[source]?.afterDatasourceRequest({ resource, response: data.data, adminUser });
566
+ if (!resp || (!resp.ok && !resp.error)) {
567
+ throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
568
+ }
569
+
570
+ if (resp.error) {
571
+ return { error: resp.error };
572
+ }
573
+ }
574
+
623
575
  return {...data, options: resource?.options };
624
576
  },
625
577
  });
@@ -674,8 +626,8 @@ class AdminForth {
674
626
  const response = {
675
627
  items
676
628
  };
677
- if (columnConfig.foreignResource.hooks?.afterDatasourceRequest) {
678
- const resp = await column.foreignResource.hooks?.afterDatasourceRequest({ response, adminUser });
629
+ if (columnConfig.foreignResource.hooks?.afterDatasourceResponse) {
630
+ const resp = await column.foreignResource.hooks?.afterDatasourceResponse({ response, adminUser });
679
631
  if (!resp || (!resp.ok && !resp.error)) {
680
632
  throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
681
633
  }
@@ -684,6 +636,7 @@ class AdminForth {
684
636
  return { error: resp.error };
685
637
  }
686
638
  }
639
+
687
640
  return response;
688
641
  },
689
642
  });
@@ -717,36 +670,7 @@ class AdminForth {
717
670
  return item;
718
671
  },
719
672
  });
720
- server.endpoint({
721
- method: 'POST',
722
- path: '/get_record',
723
- handler: async ({ body, adminUser }) => {
724
- const { resourceId, primaryKey } = body;
725
- const resource = this.config.resources.find((res) => res.resourceId == resourceId);
726
- const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
727
- const connector = this.connectors[resource.dataSource];
728
- const record = await connector.getRecordByPrimaryKey(resource, primaryKey);
729
- if (!record) {
730
- return { error: `Record with ${primaryKeyColumn.name} ${primaryKey} not found` };
731
- }
732
-
733
- // execute hook if needed
734
- if (resource.hooks?.show) {
735
- const resp = await resource.hooks?.show({ resource, record, adminUser });
736
- if (!resp || (!resp.ok && !resp.error)) {
737
- throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
738
- }
739
-
740
- if (resp.error) {
741
- return { error: resp.error };
742
- }
743
- }
744
673
 
745
- const labler = resource.itemLabel || ((record) => `${resource.label} ${record[primaryKeyColumn.name]}`);
746
- record._label = labler(record);
747
- return record;
748
- }
749
- });
750
674
  server.endpoint({
751
675
  noAuth: true, // TODO
752
676
  method: 'POST',
@@ -779,7 +703,7 @@ class AdminForth {
779
703
  });
780
704
  }
781
705
  }
782
- if (column.required?.create && body['record'][column.name] === undefined) {
706
+ if ((column.required as {create?: boolean, edit?: boolean}) ?.create && body['record'][column.name] === undefined) {
783
707
  return { error: `Column '${column.name}' is required` };
784
708
  }
785
709
 
package/modules/utils.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ import fs from 'fs';
1
4
 
2
5
 
3
6
  export function guessLabelFromName(name) {
@@ -10,3 +13,14 @@ export function guessLabelFromName(name) {
10
13
  return name.split(/(?=[A-Z])/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
11
14
  }
12
15
  }
16
+
17
+
18
+ let package_json;
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = path.join(path.dirname(__filename), '..');
21
+
22
+ export const ADMIN_FORTH_ABSOLUTE_PATH = __dirname;
23
+
24
+ package_json = JSON.parse(fs.readFileSync(path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'package.json'), 'utf8'));
25
+
26
+ export const ADMINFORTH_VERSION = package_json.version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.0.32",
3
+ "version": "1.0.33",
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/App.vue CHANGED
@@ -42,9 +42,6 @@
42
42
  </p>
43
43
  </div>
44
44
  <ul class="py-1" role="none">
45
- <li>
46
- <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white" role="menuitem">Dashboard</a>
47
- </li>
48
45
  <li >
49
46
  <span @click="toggleTheme" class=" cursor-pointer flex items-center gap-1 block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white" role="menuitem">
50
47
  {{ theme === 'dark' ? 'Light' : 'Dark' }}
@@ -5,7 +5,7 @@
5
5
  <label for="start-time" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ label }}</label>
6
6
 
7
7
  <div class="relative">
8
- <div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5">
8
+ <div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5 pointer-events-none">
9
9
  <IconCalendar class="w-4 h-4 text-gray-500 dark:text-gray-400"/>
10
10
  </div>
11
11
 
@@ -97,7 +97,7 @@ const start = computed(() => {
97
97
  })
98
98
 
99
99
  function updateFromProps() {
100
- if (props.valueStart === undefined) {
100
+ if (!props.valueStart) {
101
101
  datepickerStartEl.value.value = '';
102
102
  startTime.value = '';
103
103
  }
@@ -157,5 +157,5 @@ onMounted(() => {
157
157
 
158
158
  onBeforeUnmount(() => {
159
159
  removeChangeDateListener();
160
- })
160
+ });
161
161
  </script>
@@ -26,7 +26,7 @@
26
26
  <div class="mx-auto grid grid-cols-2 gap-4 mb-2" :class="{hidden: !showTimeInputs}">
27
27
  <div>
28
28
  <div class="relative">
29
- <div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5">
29
+ <div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5 pointer-events-none">
30
30
  <IconTime class="w-4 h-4 text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-700"/>
31
31
  </div>
32
32
 
@@ -132,11 +132,11 @@ const end = computed(() => {
132
132
  })
133
133
 
134
134
  function updateFromProps() {
135
- if (props.valueStart === undefined) {
135
+ if (!props.valueStart) {
136
136
  datepickerStartEl.value.value = '';
137
137
  startTime.value = '';
138
138
  }
139
- if (props.valueEnd === undefined) {
139
+ if (!props.valueEnd) {
140
140
  datepickerEndEl.value.value = '';
141
141
  endTime.value = '';
142
142
  }
@@ -71,33 +71,52 @@ const sliderValue = ref([start.value, end.value]);
71
71
 
72
72
  const updateFromSlider =
73
73
  debounce((value: [number, number]) => {
74
+ console.log('start end', value)
74
75
  start.value = value[0];
75
76
  end.value = value[1];
76
77
  }, 500);
77
78
 
78
- function updateFromProps() {
79
- if (props.valueStart || props.valueEnd) {
80
- setFromProps(props.valueStart, props.valueEnd)
81
- } else {
82
- clear();
83
- }
84
- }
85
-
86
79
  onMounted(() => {
87
- updateFromProps();
80
+ updateStartFromProps();
81
+ updateEndFromProps();
88
82
 
89
- watch(() => [props.valueStart, props.valueEnd], (value) => {
90
- updateFromProps();
83
+ watch(() => props.valueStart, (value) => {
84
+ updateStartFromProps();
85
+ });
86
+
87
+ watch(() => props.valueEnd, (value) => {
88
+ updateEndFromProps();
91
89
  });
92
90
  })
93
91
 
92
+ function updateStartFromProps() {
93
+ if (props.valueStart || props.valueStart === 0) {
94
+ start.value = props.valueStart ? props.valueStart : minFormatted.value;
95
+ sliderValue.value = [start.value, end.value]
96
+ } else {
97
+ console.log(props.valueStart)
98
+ start.value = minFormatted.value;
99
+ sliderValue.value = [minFormatted.value, end.value];
100
+ }
101
+ }
102
+
103
+ function updateEndFromProps() {
104
+ if (props.valueEnd || props.valueStart === 0) {
105
+ end.value = props.valueEnd ? props.valueEnd : minFormatted.value;
106
+ sliderValue.value = [start.value, end.value]
107
+ } else {
108
+ end.value = maxFormatted.value;
109
+ sliderValue.value = [start.value, maxFormatted.value];
110
+ }
111
+ }
112
+
94
113
  watch(start, () => {
95
- //console.log('⚡ emit', start.value)
114
+ console.log('⚡ emit', start.value)
96
115
  emit('update:valueStart', start.value)
97
116
  })
98
117
 
99
118
  watch(end, () => {
100
- //console.log('⚡ emit', end.value)
119
+ console.log('⚡ emit', end.value)
101
120
  emit('update:valueEnd', end.value);
102
121
  })
103
122
 
@@ -106,12 +125,6 @@ const clear = () => {
106
125
  end.value = maxFormatted.value;
107
126
  sliderValue.value = [start.value, end.value]
108
127
  }
109
-
110
- function setFromProps(startValue: number, endValue: number) {
111
- start.value = startValue ? startValue : minFormatted.value
112
- end.value = endValue ? endValue : maxFormatted.value
113
- sliderValue.value = [start.value, end.value]
114
- }
115
128
  </script>
116
129
 
117
130
  <style lang="scss" scoped>
@@ -78,7 +78,12 @@ const selectedItems = ref([]);
78
78
  function updateFromProps() {
79
79
  if (props.modelValue !== undefined) {
80
80
  if (props.single) {
81
- selectedItems.value = [props.options.find(item => item.value === props.modelValue)];
81
+ const el = props.options.find(item => item.value === props.modelValue);
82
+ if (el) {
83
+ selectedItems.value = [el];
84
+ } else {
85
+ selectedItems.value = [];
86
+ }
82
87
  } else {
83
88
  selectedItems.value = props.options.filter(item => props.modelValue.includes(item.value));
84
89
  }