adminforth 1.0.31 → 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 (37) hide show
  1. package/dataConnectors/postgres.ts +9 -6
  2. package/dataConnectors/sqlite.ts +1 -0
  3. package/dist/dataConnectors/postgres.js +9 -6
  4. package/dist/dataConnectors/sqlite.js +1 -0
  5. package/dist/index.js +87 -65
  6. package/dist/modules/utils.js +9 -0
  7. package/dist/spa/spa/src/App.vue +0 -7
  8. package/dist/spa/spa/src/components/CustomDatePicker.vue +3 -3
  9. package/dist/spa/spa/src/components/CustomDateRangePicker.vue +3 -3
  10. package/dist/spa/spa/src/components/CustomRangePicker.vue +32 -19
  11. package/dist/spa/spa/src/components/Dropdown.vue +6 -2
  12. package/dist/spa/spa/src/components/ResourceForm.vue +125 -102
  13. package/dist/spa/spa/src/components/ValueRenderer.vue +11 -1
  14. package/dist/spa/spa/src/stores/core.ts +21 -19
  15. package/dist/spa/spa/src/views/CreateView.vue +16 -6
  16. package/dist/spa/spa/src/views/EditView.vue +15 -5
  17. package/dist/spa/spa/src/views/ListView.vue +31 -21
  18. package/dist/spa/spa/src/views/ResourceParent.vue +1 -1
  19. package/dist/spa/spa/src/views/ShowView.vue +21 -6
  20. package/dist/types/AdminForthConfig.js +1 -0
  21. package/index.ts +73 -141
  22. package/modules/utils.ts +14 -0
  23. package/package.json +1 -1
  24. package/spa/src/App.vue +0 -7
  25. package/spa/src/components/CustomDatePicker.vue +3 -3
  26. package/spa/src/components/CustomDateRangePicker.vue +3 -3
  27. package/spa/src/components/CustomRangePicker.vue +32 -19
  28. package/spa/src/components/Dropdown.vue +6 -2
  29. package/spa/src/components/ResourceForm.vue +125 -102
  30. package/spa/src/components/ValueRenderer.vue +11 -1
  31. package/spa/src/stores/core.ts +21 -19
  32. package/spa/src/views/CreateView.vue +16 -6
  33. package/spa/src/views/EditView.vue +15 -5
  34. package/spa/src/views/ListView.vue +31 -21
  35. package/spa/src/views/ResourceParent.vue +1 -1
  36. package/spa/src/views/ShowView.vue +21 -6
  37. package/types/AdminForthConfig.ts +159 -0
package/index.ts CHANGED
@@ -8,109 +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
-
11
+ import { ADMINFORTH_VERSION } from './modules/utils.js';
13
12
  import { AdminForthFilterOperators, AdminForthTypes, AdminForthTypesValues } from './types.js';
13
+ import { AdminForthConfig } from './types/AdminForthConfig.js';
14
14
 
15
15
  const AVAILABLE_SHOW_IN = ['list', 'edit', 'create', 'filter', 'show'];
16
16
  const DEFAULT_ALLOWED_ACTIONS = {create: true, edit: true, show: true, delete: true};
17
17
 
18
- type AdminForthConfigMenuItem = {
19
- label: string,
20
- icon?: string,
21
- path?: string,
22
- component?: string,
23
- resourceId?: string,
24
- homepage?: boolean,
25
- children?: Array<AdminForthConfigMenuItem>,
26
- }
27
-
28
-
29
- type AdminForthResourceColumn = {
30
- name: string,
31
- label?: string,
32
- type?: AdminForthTypesValues,
33
- primaryKey?: boolean,
34
- required?: boolean | { create: boolean, edit: boolean },
35
- editingNote?: string | { create: string, edit: string },
36
- showIn?: Array<string>,
37
- fillOnCreate?: Function,
38
- isUnique?: boolean,
39
- virtual?: boolean,
40
- allowMinMaxQuery?: boolean,
41
- }
42
-
43
- type AdminForthResource = {
44
- resourceId: string,
45
- label?: string,
46
- table: string,
47
- dataSource: string,
48
- columns: Array<AdminForthResourceColumn>,
49
- itemLabel?: Function,
50
- hooks?: {
51
- show?: Function,
52
- create?: {
53
- beforeSave?: Function,
54
- afterSave?: Function,
55
- },
56
- edit?: {
57
- beforeSave?: Function,
58
- afterSave?: Function,
59
- },
60
- delete?: {
61
- beforeSave?: Function,
62
- afterSave?: Function,
63
- },
64
- },
65
- options?: {
66
- bulkActions?: Array<{
67
- label: string,
68
- state: string,
69
- icon: string,
70
- action: Function,
71
- }>,
72
- allowedActions?: AllowedActions,
73
-
74
- },
75
- }
76
-
77
- type AdminForthDataSource = {
78
- id: string,
79
- url: string,
80
- }
81
-
82
- type AdminForthConfig = {
83
- rootUser?: {
84
- username: string,
85
- password: string,
86
- },
87
- auth?: {
88
- resourceId: string,
89
- usernameField: string,
90
- passwordHashField: string,
91
- loginBackgroundImage?: string,
92
- userFullName?: string,
93
- },
94
- resources: Array<any>,
95
- menu: Array<AdminForthConfigMenuItem>,
96
- databaseConnectors?: any,
97
- dataSources: Array<any>,
98
- customization?: {
99
- customComponentsDir?: string,
100
- vueUsesFile?: string,
101
- },
102
- baseUrl?: string,
103
- brandName?: string,
104
- datesFormat?: string,
105
- deleteConfirmation?: boolean,
106
- }
107
-
108
- type AllowedActions = {
109
- create: boolean,
110
- edit: boolean,
111
- show: boolean,
112
- delete: boolean,
113
- }
114
18
 
115
19
  class AdminForth {
116
20
  static Types = AdminForthTypes;
@@ -124,8 +28,6 @@ class AdminForth {
124
28
 
125
29
  #defaultConfig = {
126
30
  deleteConfirmation: true,
127
-
128
-
129
31
  }
130
32
 
131
33
  config: AdminForthConfig;
@@ -150,6 +52,7 @@ class AdminForth {
150
52
  this.codeInjector = new CodeInjector(this);
151
53
  this.connectors = {};
152
54
  this.statuses = {}
55
+ console.log(`🚀 AdminForth v${ADMINFORTH_VERSION} starting up`)
153
56
  }
154
57
 
155
58
  validateConfig() {
@@ -396,7 +299,7 @@ class AdminForth {
396
299
  if (!this.config.databaseConnectors[dbType]) {
397
300
  throw new Error(`Database type ${dbType} is not supported, consider using databaseConnectors in AdminForth config`);
398
301
  }
399
- 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});
400
303
  });
401
304
 
402
305
  await Promise.all(this.config.resources.map(async (res) => {
@@ -434,10 +337,6 @@ class AdminForth {
434
337
  // console.log('⚙️⚙️⚙️ Database discovery done', JSON.stringify(this.config.resources, null, 2));
435
338
  }
436
339
 
437
- async init() {
438
- console.log('AdminForth init');
439
- }
440
-
441
340
  async bundleNow({ hotReload=false, verbose=false }) {
442
341
  this.codeInjector.bundleNow({ hotReload, verbose });
443
342
  }
@@ -546,12 +445,12 @@ class AdminForth {
546
445
  return { error: 'Unauthorized' };
547
446
  }
548
447
  username = user.data[0][this.config.auth.usernameField];
549
- userFullName = user.data[0][this.config.auth.userFullName];
448
+ userFullName = user.data[0][this.config.auth.userFullNameField];
550
449
  }
551
450
 
552
451
  const userData = {
553
452
  [this.config.auth.usernameField]: username,
554
- [this.config.auth.userFullName]: userFullName
453
+ [this.config.auth.userFullNameField]: userFullName
555
454
  };
556
455
  return {
557
456
  user: userData,
@@ -568,6 +467,7 @@ class AdminForth {
568
467
  usernameField: this.config.auth.usernameField,
569
468
  },
570
469
  adminUser,
470
+ version: ADMINFORTH_VERSION,
571
471
  };
572
472
  },
573
473
  });
@@ -593,8 +493,11 @@ class AdminForth {
593
493
  server.endpoint({
594
494
  method: 'POST',
595
495
  path: '/get_resource_data',
596
- handler: async ({ body }) => {
597
- 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
+ }
598
501
  if (!this.statuses.dbDiscover) {
599
502
  return { error: 'Database discovery not started' };
600
503
  }
@@ -605,6 +508,19 @@ class AdminForth {
605
508
  if (!resource) {
606
509
  return { error: `Resource ${resourceId} not found` };
607
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
+
608
524
  const data = await this.connectors[resource.dataSource].getData({
609
525
  resource,
610
526
  limit,
@@ -612,6 +528,50 @@ class AdminForth {
612
528
  filters,
613
529
  sort,
614
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
+
615
575
  return {...data, options: resource?.options };
616
576
  },
617
577
  });
@@ -666,8 +626,8 @@ class AdminForth {
666
626
  const response = {
667
627
  items
668
628
  };
669
- if (columnConfig.foreignResource.hooks?.afterDatasourceRequest) {
670
- 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 });
671
631
  if (!resp || (!resp.ok && !resp.error)) {
672
632
  throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
673
633
  }
@@ -676,6 +636,7 @@ class AdminForth {
676
636
  return { error: resp.error };
677
637
  }
678
638
  }
639
+
679
640
  return response;
680
641
  },
681
642
  });
@@ -709,36 +670,7 @@ class AdminForth {
709
670
  return item;
710
671
  },
711
672
  });
712
- server.endpoint({
713
- method: 'POST',
714
- path: '/get_record',
715
- handler: async ({ body, adminUser }) => {
716
- const { resourceId, primaryKey } = body;
717
- const resource = this.config.resources.find((res) => res.resourceId == resourceId);
718
- const primaryKeyColumn = resource.columns.find((col) => col.primaryKey);
719
- const connector = this.connectors[resource.dataSource];
720
- const record = await connector.getRecordByPrimaryKey(resource, primaryKey);
721
- if (!record) {
722
- return { error: `Record with ${primaryKeyColumn.name} ${primaryKey} not found` };
723
- }
724
-
725
- // execute hook if needed
726
- if (resource.hooks?.show) {
727
- const resp = await resource.hooks?.show({ resource, record, adminUser });
728
- if (!resp || (!resp.ok && !resp.error)) {
729
- throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
730
- }
731
-
732
- if (resp.error) {
733
- return { error: resp.error };
734
- }
735
- }
736
673
 
737
- const labler = resource.itemLabel || ((record) => `${resource.label} ${record[primaryKeyColumn.name]}`);
738
- record._label = labler(record);
739
- return record;
740
- }
741
- });
742
674
  server.endpoint({
743
675
  noAuth: true, // TODO
744
676
  method: 'POST',
@@ -771,7 +703,7 @@ class AdminForth {
771
703
  });
772
704
  }
773
705
  }
774
- if (column.required?.create && body['record'][column.name] === undefined) {
706
+ if ((column.required as {create?: boolean, edit?: boolean}) ?.create && body['record'][column.name] === undefined) {
775
707
  return { error: `Column '${column.name}' is required` };
776
708
  }
777
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.31",
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' }}
@@ -141,10 +138,6 @@ const routerIsReady = ref(false);
141
138
 
142
139
  const loggedIn = computed(() => route.name !== 'login' && routerIsReady.value);
143
140
 
144
- watch(loggedIn, (value) => {
145
- console.log('🔻🔻🔻 loggedIn', value);
146
- });
147
-
148
141
  const theme = ref('light');
149
142
 
150
143
  function toggleTheme() {
@@ -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>
@@ -76,10 +76,14 @@ const showDropdown = ref(false);
76
76
  const selectedItems = ref([]);
77
77
 
78
78
  function updateFromProps() {
79
- console.log('⚡ updateFromProps', props.modelValue)
80
79
  if (props.modelValue !== undefined) {
81
80
  if (props.single) {
82
- 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
+ }
83
87
  } else {
84
88
  selectedItems.value = props.options.filter(item => props.modelValue.includes(item.value));
85
89
  }