adminforth 1.0.30 → 1.0.32

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.
@@ -0,0 +1,143 @@
1
+ <template>
2
+ <div class="flex flex-wrap gap-2">
3
+ <input
4
+ type="number" aria-describedby="helper-text-explanation"
5
+ class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-20 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
6
+ placeholder="From"
7
+ v-model="start"
8
+ >
9
+
10
+ <input
11
+ type="number" aria-describedby="helper-text-explanation"
12
+ class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-20 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
13
+ placeholder="To"
14
+ v-model="end"
15
+ >
16
+
17
+ <button
18
+ v-if="isChanged"
19
+ type="button"
20
+ class="flex items-center p-0.5 ml-auto px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
21
+ @click="clear">Clear
22
+ </button>
23
+
24
+ <div class="w-full">
25
+ <vue-slider
26
+ class="custom-slider"
27
+ :dot-size="20"
28
+ height="7.99px"
29
+ :min="minFormatted"
30
+ :max="maxFormatted"
31
+ v-model="sliderValue"
32
+ @update:model-value="updateFromSlider($event)"
33
+ />
34
+ </div>
35
+ </div>
36
+ </template>
37
+ <script setup lang="ts">
38
+ import VueSlider from 'vue-slider-component';
39
+ import 'vue-slider-component/theme/antd.css'
40
+ import {computed, onMounted, ref, watch} from "vue";
41
+ import debounce from 'debounce'
42
+
43
+ const props = defineProps({
44
+ valueStart: {
45
+ default: 0,
46
+ },
47
+ valueEnd: {
48
+ default: 10,
49
+ },
50
+ min: {
51
+ default: 0,
52
+ },
53
+ max: {
54
+ default: 10,
55
+ },
56
+ });
57
+
58
+ const emit = defineEmits(['update:valueStart', 'update:valueEnd']);
59
+
60
+ const minFormatted = computed(() => Math.floor(props.min));
61
+ const maxFormatted = computed(() => Math.ceil(props.max));
62
+
63
+ const isChanged = computed(() => {
64
+ return start.value !== minFormatted.value || end.value !== maxFormatted.value;
65
+ });
66
+
67
+ const start = ref(minFormatted.value);
68
+ const end = ref(maxFormatted.value);
69
+
70
+ const sliderValue = ref([start.value, end.value]);
71
+
72
+ const updateFromSlider =
73
+ debounce((value: [number, number]) => {
74
+ start.value = value[0];
75
+ end.value = value[1];
76
+ }, 500);
77
+
78
+ function updateFromProps() {
79
+ if (props.valueStart || props.valueEnd) {
80
+ setFromProps(props.valueStart, props.valueEnd)
81
+ } else {
82
+ clear();
83
+ }
84
+ }
85
+
86
+ onMounted(() => {
87
+ updateFromProps();
88
+
89
+ watch(() => [props.valueStart, props.valueEnd], (value) => {
90
+ updateFromProps();
91
+ });
92
+ })
93
+
94
+ watch(start, () => {
95
+ //console.log('⚡ emit', start.value)
96
+ emit('update:valueStart', start.value)
97
+ })
98
+
99
+ watch(end, () => {
100
+ //console.log('⚡ emit', end.value)
101
+ emit('update:valueEnd', end.value);
102
+ })
103
+
104
+ const clear = () => {
105
+ start.value = minFormatted.value;
106
+ end.value = maxFormatted.value;
107
+ sliderValue.value = [start.value, end.value]
108
+ }
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
+ </script>
116
+
117
+ <style lang="scss" scoped>
118
+ .custom-slider {
119
+ &:deep(.vue-slider-rail) {
120
+ background-color: rgb(229 231 235);
121
+ }
122
+
123
+ &:deep(.vue-slider-dot-handle) {
124
+ background-color: #1c64f2;
125
+ border: none;
126
+ box-shadow: none;
127
+ }
128
+
129
+ &:deep(.vue-slider-dot-handle:hover) {
130
+ background-color: #1c64f2;
131
+ border: none;
132
+ box-shadow: none;
133
+ }
134
+
135
+ &:deep(.vue-slider-process) {
136
+ background-color: rgb(225 239 254);
137
+ }
138
+
139
+ &:deep(.vue-slider-process:hover) {
140
+ background-color: rgb(225 239 254);
141
+ }
142
+ }
143
+ </style>
@@ -34,7 +34,7 @@
34
34
  <IconCaretUpSolid v-else class="h-5 w-5 text-gray-400" />
35
35
  </div>
36
36
  </div>
37
- <div v-if="showDropdown" class="absolute z-10 mt-1 w-full bg-white shadow-lg rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
37
+ <div v-if="showDropdown" class="absolute z-10 mt-1 w-full bg-white shadow-lg dark:shadow-black rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
38
38
  <div
39
39
  v-for="item in filteredItems"
40
40
  :key="item.value"
@@ -76,7 +76,6 @@ 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
81
  selectedItems.value = [props.options.find(item => item.value === props.modelValue)];
@@ -2,7 +2,7 @@
2
2
  <!-- drawer component -->
3
3
  <div id="drawer-navigation"
4
4
 
5
- class="fixed top-14 right-0 z-40 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl"
5
+ class="fixed top-14 right-0 z-40 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl dark:shadow-black"
6
6
 
7
7
  :class="show ? 'top-0 transform-none' : ''"
8
8
  tabindex="-1" aria-labelledby="drawer-navigation-label"
@@ -22,8 +22,14 @@
22
22
  <li v-for="c in columnsWithFilter" :key="c">
23
23
  <p class="dark:text-gray-400">{{ c.label }}</p>
24
24
 
25
- <Dropdown
26
- v-if="c.type === 'boolean'"
25
+ <Dropdown
26
+ v-if="c.foreignResource"
27
+ :options="columnOptions[c.name] || []"
28
+ @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
29
+ :modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
30
+ />
31
+ <Dropdown
32
+ v-else-if="c.type === 'boolean'"
27
33
  :options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
28
34
  @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
29
35
  :modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
@@ -62,22 +68,15 @@
62
68
  :value="getFilterItem({ column: c, operator: 'ilike' })"
63
69
  >
64
70
 
65
- <div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
66
- <input
67
- type="number" aria-describedby="helper-text-explanation"
68
- class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-20 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
69
- placeholder="From"
70
- @input="setFilterItem({ column: c, operator: 'gte', value: $event.target.value || undefined })"
71
- :value="getFilterItem({ column: c, operator: 'gte' })"
72
- >
73
- <input
74
- type="number" aria-describedby="helper-text-explanation"
75
- class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-20 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
76
- placeholder="To"
77
- @input="setFilterItem({ column: c, operator: 'lte', value: $event.target.value || undefined})"
78
- :value="getFilterItem({ column: c, operator: 'lte' })"
79
- >
80
- </div>
71
+ <CustomRangePicker
72
+ v-else-if="['integer', 'decimal', 'float'].includes(c.type)"
73
+ :min="c.min"
74
+ :max="c.max"
75
+ :valueStart="getFilterItem({ column: c, operator: 'gte' })"
76
+ @update:valueStart="setFilterItem({ column: c, operator: 'gte', value: $event || undefined })"
77
+ :valueEnd="getFilterItem({ column: c, operator: 'lte' })"
78
+ @update:valueEnd="setFilterItem({ column: c, operator: 'lte', value: $event || undefined })"
79
+ />
81
80
 
82
81
  </li>
83
82
  </ul>
@@ -99,19 +98,53 @@
99
98
  </template>
100
99
 
101
100
  <script setup>
102
- import { watch, computed } from 'vue'
101
+ import { watch, computed, ref, onMounted } from 'vue'
103
102
  import Dropdown from '@/components/Dropdown.vue';
104
103
  import CustomDateRangePicker from '@/components/CustomDateRangePicker.vue';
104
+ import { callAdminForthApi } from '@/utils';
105
+ import { useRouter } from 'vue-router';
106
+ import { computedAsync } from '@vueuse/core'
107
+ import CustomRangePicker from "@/components/CustomRangePicker.vue";
105
108
 
106
109
  // props: columns
107
110
  // add support for v-model:filers
108
111
  const props = defineProps(['columns', 'filters', 'show', 'columnsMinMax']);
109
112
  const emits = defineEmits(['update:filters', 'hide']);
110
113
 
111
- const columnsWithFilter = computed(
114
+ const router = useRouter();
115
+
116
+
117
+ const columnsWithFilter = computed(
112
118
  () => props.columns?.filter(column => column.showIn.includes('filter')) || []
113
119
  );
114
120
 
121
+ const columnOptions = computedAsync(async () => {
122
+ const ret = {};
123
+ if (!props.columns) {
124
+ return ret;
125
+ }
126
+ await Promise.all(
127
+ Object.values(props.columns).map(async (column) => {
128
+ if (column.foreignResource) {
129
+ const list = await callAdminForthApi({
130
+ method: 'POST',
131
+ path: `/get_resource_foreign_data`,
132
+ body: {
133
+ resourceId: router.currentRoute.value.params.resourceId,
134
+ column: column.name,
135
+ limit: 1000,
136
+ offset: 0,
137
+ },
138
+ });
139
+ ret[column.name] = list.items;
140
+ }
141
+ })
142
+ );
143
+
144
+ return ret;
145
+ }, {});
146
+
147
+
115
148
  // sync 'body' class 'overflow-hidden' with show prop show
116
149
  watch(() => props.show, (show) => {
117
150
  if (show) {
@@ -2,7 +2,7 @@
2
2
  <div>
3
3
 
4
4
  <div
5
- class="relative shadow-md sm:rounded-lg dark:shadow-2xl"
5
+ class="relative shadow-md sm:rounded-lg dark:shadow-2xl dark:shadow-black"
6
6
  >
7
7
  <form autocomplete="off" @submit.prevent>
8
8
  <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
@@ -36,7 +36,14 @@
36
36
  <td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap relative">
37
37
  <Dropdown
38
38
  single
39
- v-if="column.enum"
39
+ v-if="column.foreignResource"
40
+ :options="columnOptions[column.name] || []"
41
+ :modelValue="currentValues[column.name]"
42
+ @update:modelValue="setCurrentValue(column.name, $event)"
43
+ />
44
+ <Dropdown
45
+ single
46
+ v-else-if="column.enum"
40
47
  :options="column.enum"
41
48
  :modelValue="currentValues[column.name]"
42
49
  @update:modelValue="setCurrentValue(column.name, $event)"
@@ -128,6 +135,11 @@ import { IconExclamationCircleSolid } from '@iconify-prerendered/vue-flowbite';
128
135
  import { initFlowbite } from 'flowbite'
129
136
  import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
130
137
  import CustomDatePicker from "@/components/CustomDatePicker.vue";
138
+ import { callAdminForthApi } from '@/utils';
139
+ import { useRouter } from 'vue-router';
140
+ import { computedAsync } from '@vueuse/core';
141
+
142
+ const router = useRouter();
131
143
 
132
144
  const props = defineProps({
133
145
  loading: Boolean,
@@ -145,6 +157,7 @@ const emit = defineEmits(['update:record', 'update:isValid']);
145
157
 
146
158
  const currentValues = ref({});
147
159
 
160
+
148
161
  const columnError = (column) => {
149
162
  const val = computed(() => {
150
163
  if ( column.required[mode.value] && (currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '') ) {
@@ -172,8 +185,6 @@ const columnError = (column) => {
172
185
  };
173
186
 
174
187
 
175
-
176
-
177
188
  const setCurrentValue = (key, value) => {
178
189
  currentValues.value[key] = value;
179
190
 
@@ -185,8 +196,31 @@ onMounted(() => {
185
196
  currentValues.value[key] = props.record[key];
186
197
  });
187
198
  initFlowbite();
199
+
200
+
188
201
  });
189
202
 
203
+ const columnOptions = computedAsync(async () => {
204
+ return (await Promise.all(
205
+ Object.values(props.resourceColumns).map(async (column) => {
206
+ if (column.foreignResource) {
207
+ const list = await callAdminForthApi({
208
+ method: 'POST',
209
+ path: `/get_resource_foreign_data`,
210
+ body: {
211
+ resourceId: router.currentRoute.value.params.resourceId,
212
+ column: column.name,
213
+ limit: 1000,
214
+ offset: 0,
215
+ },
216
+ });
217
+ return { [column.name]: list.items };
218
+ }
219
+ })
220
+ )).reduce((acc, val) => Object.assign(acc, val), {})
221
+
222
+ }, {});
223
+
190
224
  const coreStore = useCoreStore();
191
225
 
192
226
  const editableColumns = computed(() => {
@@ -12,7 +12,9 @@ const router = createRouter({
12
12
  {
13
13
  path: '/',
14
14
  name: 'home',
15
- component: HomeView
15
+ //redirect to login
16
+ /* IMPORTANT:ADMINFORTH REDIRECT */
17
+
16
18
  },
17
19
  {
18
20
  path: '/login',
@@ -42,20 +42,7 @@ export const useCoreStore = defineStore('core', () => {
42
42
  user.value = resp.user;
43
43
 
44
44
  // find homepage:true in menu recuresively
45
- if (import.meta.env.DEV){
46
- return
47
- } else {
48
- const homepage = await findHomepage(menu.value);
49
- if (homepage) {
50
- if (homepage.resourceId) {
51
- // redirect to homepage
52
- router.push({ name: 'resource-list', params: { resourceId: homepage.resourceId } });
53
- } else {
54
- // redirect to path
55
- router.push(homepage.path);
56
- }
57
- }
58
- }
45
+
59
46
  }
60
47
 
61
48
  async function fetchRecord({ resourceId, primaryKey }) {
@@ -65,7 +65,7 @@
65
65
  </BreadcrumbsWithButtons>
66
66
 
67
67
  <!-- table -->
68
- <div class="relative overflow-x-auto shadow-md sm:rounded-lg dark:shadow-2xl">
68
+ <div class="relative overflow-x-auto shadow-md dark:shadow-black sm:rounded-lg dark:s">
69
69
 
70
70
 
71
71
  <!-- skelet loader -->
@@ -357,7 +357,6 @@ const totalRows = ref(0);
357
357
  const allCheckedActions = ref([]);
358
358
  const allowedActions = ref({});
359
359
 
360
-
361
360
  const DEFAULT_PAGE_SIZE = 10;
362
361
 
363
362
  const columnsListed = computed(() => coreStore.resourceColumns?.filter(c => c.showIn.includes('list')));
@@ -11,7 +11,7 @@
11
11
  <div id="authentication-modal" tabindex="-1" class=" overflow-y-auto overflow-x-hidden z-50 min-w-[400px] justify-center items-center md:inset-0 h-[calc(100%-1rem)] max-h-full">
12
12
  <div class="relative p-4 w-full max-w-md max-h-full">
13
13
  <!-- Modal content -->
14
- <div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
14
+ <div class="relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black" >
15
15
  <!-- Modal header -->
16
16
  <div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
17
17
  <h3 class="text-xl font-semibold text-gray-900 dark:text-white">
package/index.ts CHANGED
@@ -9,6 +9,13 @@ import ExpressServer from './servers/express.js';
9
9
  import {v1 as uuid} from 'uuid';
10
10
  import fs from 'fs';
11
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
+ }
12
19
 
13
20
  import { AdminForthFilterOperators, AdminForthTypes, AdminForthTypesValues } from './types.js';
14
21
 
@@ -568,6 +575,7 @@ class AdminForth {
568
575
  usernameField: this.config.auth.usernameField,
569
576
  },
570
577
  adminUser,
578
+ version: package_json.version,
571
579
  };
572
580
  },
573
581
  });
@@ -615,6 +623,71 @@ class AdminForth {
615
623
  return {...data, options: resource?.options };
616
624
  },
617
625
  });
626
+ server.endpoint({
627
+ method: 'POST',
628
+ path: '/get_resource_foreign_data',
629
+ handler: async ({ body, adminUser }) => {
630
+ const { resourceId, column } = body;
631
+ if (!this.statuses.dbDiscover) {
632
+ return { error: 'Database discovery not started' };
633
+ }
634
+ if (this.statuses.dbDiscover !== 'done') {
635
+ return { error : 'Database discovery is still in progress, please try later' };
636
+ }
637
+ const resource = this.config.resources.find((res) => res.resourceId == resourceId);
638
+ if (!resource) {
639
+ return { error: `Resource ${resourceId} not found` };
640
+ }
641
+ const columnConfig = resource.columns.find((col) => col.name == column);
642
+ if (!columnConfig.foreignResource) {
643
+ return { error: `Column ${column} in resource ${resourceId} is not a foreign key` };
644
+ }
645
+ const targetResourceId = columnConfig.foreignResource.resourceId;
646
+ const targetResource = this.config.resources.find((res) => res.resourceId == targetResourceId);
647
+ if (columnConfig.foreignResource.hooks?.beforeDatasourceRequest) {
648
+ const resp = await column.foreignResource.hooks?.beforeDatasourceRequest({ query: body, adminUser });
649
+ if (!resp || (!resp.ok && !resp.error)) {
650
+ throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
651
+ }
652
+
653
+ if (resp.error) {
654
+ return { error: resp.error };
655
+ }
656
+ }
657
+ const { limit, offset, filters, sort } = body;
658
+ const dbDataItems = await this.connectors[targetResource.dataSource].getData({
659
+ resource: targetResource,
660
+ limit,
661
+ offset,
662
+ filters: filters || [],
663
+ sort: sort || [],
664
+ });
665
+ const items = dbDataItems.data.map((item) => {
666
+ const pk = item[targetResource.columns.find((col) => col.primaryKey).name];
667
+ const labler = targetResource.itemLabel || ((record) => `${targetResource.label} ${pk}`);
668
+ return {
669
+ value: pk,
670
+ label: labler(item),
671
+ _item: item, // user might need it in hook to form new label
672
+ }
673
+ });
674
+ const response = {
675
+ items
676
+ };
677
+ if (columnConfig.foreignResource.hooks?.afterDatasourceRequest) {
678
+ const resp = await column.foreignResource.hooks?.afterDatasourceRequest({ response, adminUser });
679
+ if (!resp || (!resp.ok && !resp.error)) {
680
+ throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
681
+ }
682
+
683
+ if (resp.error) {
684
+ return { error: resp.error };
685
+ }
686
+ }
687
+ return response;
688
+ },
689
+ });
690
+
618
691
  server.endpoint({
619
692
  method: 'POST',
620
693
  path: '/get_min_max_for_columns',
@@ -75,9 +75,8 @@ class CodeInjector {
75
75
 
76
76
  async rmTmpDir() {
77
77
  // remove spa_tmp folder if it is exists
78
- const spaTmpPath = CodeInjector.SPA_TMP_PATH;
79
78
  try {
80
- await fs.promises.rm(spaTmpPath, { recursive: true });
79
+ await fs.promises.rm(CodeInjector.SPA_TMP_PATH, { recursive: true });
81
80
  } catch (e) {
82
81
  // ignore
83
82
  }
@@ -210,6 +209,24 @@ class CodeInjector {
210
209
 
211
210
 
212
211
  /* generate custom rotes */
212
+ const homepageMenuItem = this.adminforth.config.menu.find((mi)=>mi.homepage);
213
+ let childrenHomePageMenuItem = this.adminforth.config.menu.find((mi)=>mi.children && mi.children?.find((mi)=>mi.homepage));
214
+ let childrenHomepage = childrenHomePageMenuItem?.children?.find((mi)=>mi.homepage)
215
+ let homePagePath = homepageMenuItem?.path || `resource/${childrenHomepage?.resourceId}`;
216
+ if (!homePagePath) {
217
+ homePagePath=this.adminforth.config.menu.filter((mi)=>mi.path)[0]?.path || `resource/${this.adminforth.config.menu.filter((mi)=>mi.children)[0]?.resourceId}` ;
218
+ }
219
+
220
+
221
+
222
+
223
+
224
+ routes += `{
225
+ path: '/',
226
+ name: 'home',
227
+ //redirect to login
228
+ redirect: '/${homePagePath}'
229
+ },\n`;
213
230
  const routerVuePath = path.join(spaTmpPath, 'src', 'router', 'index.ts');
214
231
  let routerVueContent = await fs.promises.readFile(routerVuePath, 'utf-8');
215
232
  routerVueContent = routerVueContent.replace('/* IMPORTANT:ADMINFORTH ROUTES */', routes);
@@ -217,21 +234,21 @@ class CodeInjector {
217
234
 
218
235
 
219
236
  /* hash checking */
220
- const packageLockPath = path.join(spaTmpPath, 'package-lock.json');
221
- const packageLock = JSON.parse(await fs.promises.readFile(packageLockPath, 'utf-8'));
222
- const lockHash = hashify(packageLock);
237
+ const spaPackageLockPath = path.join(spaTmpPath, 'package-lock.json');
238
+ const spaPackageLock = JSON.parse(await fs.promises.readFile(spaPackageLockPath, 'utf-8'));
239
+ const spaLockHash = hashify(spaPackageLock);
240
+
223
241
  /* customPackageLock */
224
- const customPackagePath = path.join('./package.json');
225
- const customPackage = JSON.parse(await fs.promises.readFile(customPackagePath, 'utf-8'));
226
- const customPackageHash = hashify(customPackage);
242
+ const usersPackagePath = path.join('./package.json');
243
+ const usersPackage = JSON.parse(await fs.promises.readFile(usersPackagePath, 'utf-8'));
227
244
 
228
- const customLockPath = path.join('./package-lock.json');
229
- const customLock = JSON.parse(await fs.promises.readFile(customLockPath, 'utf-8'));
230
- const customLockHash = hashify(customLock);
245
+ const usersLockPath = path.join('./package-lock.json');
246
+ const usersLock = JSON.parse(await fs.promises.readFile(usersLockPath, 'utf-8'));
247
+ const usersLockHash = hashify(usersLock);
231
248
 
232
249
  const packagesNamesHash = hashify(packageNames);
233
250
 
234
- const fullHash = hashify([lockHash, packagesNamesHash, customLockHash]);
251
+ const fullHash = `${spaLockHash}::${packagesNamesHash}::${usersLockHash}`;
235
252
  const hashPath = path.join(spaTmpPath, 'node_modules', '.adminforth_hash');
236
253
 
237
254
  try {
@@ -239,20 +256,30 @@ class CodeInjector {
239
256
  if (existingHash === fullHash) {
240
257
  console.log('Hashes match, skipping npm ci/install');
241
258
  return;
259
+ } else {
260
+ if (verbose) {
261
+ console.log(`Hashes do not match: existing ${existingHash} new ${fullHash}, proceeding with npm ci/install`);
262
+ }
242
263
  }
243
264
  } catch (e) {
244
265
  // ignore
266
+ if (verbose) {
267
+ console.log('Hash file does not exist, proceeding with npm ci/install');
268
+ }
245
269
  }
246
270
 
247
271
  await this.runNpmShell({command: 'ci', verbose, cwd: spaTmpPath});
248
272
 
249
273
  // get packages with version from customPackage
250
- const customPackgeNames = [...Object.keys(customPackage.dependencies), ...Object.keys(customPackage.devDependencies || [])].reduce(
251
- (acc, packageName) => {
252
- const version = customLock.packages[`node_modules/${packageName}`].version;
253
- acc.push(`${packageName}@${version}`);
254
- return acc;
255
- }, []);
274
+ const IGNORE_PACKAGES = ['tsx', 'typescript', 'express', 'nodemon'];
275
+ const customPackgeNames = [...Object.keys(usersPackage.dependencies), ...Object.keys(usersPackage.devDependencies || [])]
276
+ .filter((packageName) => !IGNORE_PACKAGES.includes(packageName))
277
+ .reduce(
278
+ (acc, packageName) => {
279
+ const version = usersLock.packages[`node_modules/${packageName}`].version;
280
+ acc.push(`${packageName}@${version}`);
281
+ return acc;
282
+ }, []);
256
283
 
257
284
  if (packageNames.length) {
258
285
  const npmInstallCommand = `install ${[...packageNames, ...customPackgeNames].join(' ')}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",