adminforth 1.0.30 → 1.0.31
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.
- package/dist/index.js +88 -25
- package/dist/modules/codeInjector.js +37 -15
- package/dist/spa/spa/package-lock.json +108 -1
- package/dist/spa/spa/package.json +4 -1
- package/dist/spa/spa/src/App.vue +6 -2
- package/dist/spa/spa/src/components/AcceptModal.vue +1 -1
- package/dist/spa/spa/src/components/CustomRangePicker.vue +143 -0
- package/dist/spa/spa/src/components/Dropdown.vue +1 -1
- package/dist/spa/spa/src/components/Filters.vue +54 -21
- package/dist/spa/spa/src/components/ResourceForm.vue +38 -4
- package/dist/spa/spa/src/router/index.ts +3 -1
- package/dist/spa/spa/src/stores/core.ts +1 -14
- package/dist/spa/spa/src/views/ListView.vue +4 -1
- package/dist/spa/spa/src/views/LoginView.vue +1 -1
- package/index.ts +65 -0
- package/modules/codeInjector.ts +45 -18
- package/package.json +1 -1
- package/spa/package-lock.json +108 -1
- package/spa/package.json +4 -1
- package/spa/src/App.vue +6 -2
- package/spa/src/components/AcceptModal.vue +1 -1
- package/spa/src/components/CustomRangePicker.vue +143 -0
- package/spa/src/components/Dropdown.vue +1 -1
- package/spa/src/components/Filters.vue +54 -21
- package/spa/src/components/ResourceForm.vue +38 -4
- package/spa/src/router/index.ts +3 -1
- package/spa/src/stores/core.ts +1 -14
- package/spa/src/views/ListView.vue +4 -1
- package/spa/src/views/LoginView.vue +1 -1
|
@@ -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"
|
|
@@ -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.
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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.
|
|
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(() => {
|
|
@@ -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
|
-
|
|
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:
|
|
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,6 +357,9 @@ const totalRows = ref(0);
|
|
|
357
357
|
const allCheckedActions = ref([]);
|
|
358
358
|
const allowedActions = ref({});
|
|
359
359
|
|
|
360
|
+
onMounted(() => {
|
|
361
|
+
console.log('LISTVIEW mounted⌛⌛⌛⌛⌛⌛🔻🔻🔻');
|
|
362
|
+
});
|
|
360
363
|
|
|
361
364
|
const DEFAULT_PAGE_SIZE = 10;
|
|
362
365
|
|
|
@@ -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
|
@@ -615,6 +615,71 @@ class AdminForth {
|
|
|
615
615
|
return {...data, options: resource?.options };
|
|
616
616
|
},
|
|
617
617
|
});
|
|
618
|
+
server.endpoint({
|
|
619
|
+
method: 'POST',
|
|
620
|
+
path: '/get_resource_foreign_data',
|
|
621
|
+
handler: async ({ body, adminUser }) => {
|
|
622
|
+
const { resourceId, column } = body;
|
|
623
|
+
if (!this.statuses.dbDiscover) {
|
|
624
|
+
return { error: 'Database discovery not started' };
|
|
625
|
+
}
|
|
626
|
+
if (this.statuses.dbDiscover !== 'done') {
|
|
627
|
+
return { error : 'Database discovery is still in progress, please try later' };
|
|
628
|
+
}
|
|
629
|
+
const resource = this.config.resources.find((res) => res.resourceId == resourceId);
|
|
630
|
+
if (!resource) {
|
|
631
|
+
return { error: `Resource ${resourceId} not found` };
|
|
632
|
+
}
|
|
633
|
+
const columnConfig = resource.columns.find((col) => col.name == column);
|
|
634
|
+
if (!columnConfig.foreignResource) {
|
|
635
|
+
return { error: `Column ${column} in resource ${resourceId} is not a foreign key` };
|
|
636
|
+
}
|
|
637
|
+
const targetResourceId = columnConfig.foreignResource.resourceId;
|
|
638
|
+
const targetResource = this.config.resources.find((res) => res.resourceId == targetResourceId);
|
|
639
|
+
if (columnConfig.foreignResource.hooks?.beforeDatasourceRequest) {
|
|
640
|
+
const resp = await column.foreignResource.hooks?.beforeDatasourceRequest({ query: body, adminUser });
|
|
641
|
+
if (!resp || (!resp.ok && !resp.error)) {
|
|
642
|
+
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (resp.error) {
|
|
646
|
+
return { error: resp.error };
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const { limit, offset, filters, sort } = body;
|
|
650
|
+
const dbDataItems = await this.connectors[targetResource.dataSource].getData({
|
|
651
|
+
resource: targetResource,
|
|
652
|
+
limit,
|
|
653
|
+
offset,
|
|
654
|
+
filters: filters || [],
|
|
655
|
+
sort: sort || [],
|
|
656
|
+
});
|
|
657
|
+
const items = dbDataItems.data.map((item) => {
|
|
658
|
+
const pk = item[targetResource.columns.find((col) => col.primaryKey).name];
|
|
659
|
+
const labler = targetResource.itemLabel || ((record) => `${targetResource.label} ${pk}`);
|
|
660
|
+
return {
|
|
661
|
+
value: pk,
|
|
662
|
+
label: labler(item),
|
|
663
|
+
_item: item, // user might need it in hook to form new label
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
const response = {
|
|
667
|
+
items
|
|
668
|
+
};
|
|
669
|
+
if (columnConfig.foreignResource.hooks?.afterDatasourceRequest) {
|
|
670
|
+
const resp = await column.foreignResource.hooks?.afterDatasourceRequest({ response, adminUser });
|
|
671
|
+
if (!resp || (!resp.ok && !resp.error)) {
|
|
672
|
+
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (resp.error) {
|
|
676
|
+
return { error: resp.error };
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return response;
|
|
680
|
+
},
|
|
681
|
+
});
|
|
682
|
+
|
|
618
683
|
server.endpoint({
|
|
619
684
|
method: 'POST',
|
|
620
685
|
path: '/get_min_max_for_columns',
|
package/modules/codeInjector.ts
CHANGED
|
@@ -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(
|
|
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
|
|
221
|
-
const
|
|
222
|
-
const
|
|
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
|
|
225
|
-
const
|
|
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
|
|
229
|
-
const
|
|
230
|
-
const
|
|
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 =
|
|
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
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
package/spa/package-lock.json
CHANGED
|
@@ -9,12 +9,15 @@
|
|
|
9
9
|
"version": "0.0.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@iconify-prerendered/vue-flowbite": "^0.23.1714023977",
|
|
12
|
+
"@vueuse/core": "^10.10.0",
|
|
12
13
|
"dayjs": "^1.11.11",
|
|
14
|
+
"debounce": "^2.1.0",
|
|
13
15
|
"flowbite": "^2.3.0",
|
|
14
16
|
"flowbite-datepicker": "^1.2.6",
|
|
15
17
|
"pinia": "^2.1.7",
|
|
16
18
|
"vue": "^3.4.21",
|
|
17
|
-
"vue-router": "^4.3.0"
|
|
19
|
+
"vue-router": "^4.3.0",
|
|
20
|
+
"vue-slider-component": "^4.1.0-beta.7"
|
|
18
21
|
},
|
|
19
22
|
"devDependencies": {
|
|
20
23
|
"@rushstack/eslint-patch": "^1.8.0",
|
|
@@ -950,6 +953,11 @@
|
|
|
950
953
|
"undici-types": "~5.26.4"
|
|
951
954
|
}
|
|
952
955
|
},
|
|
956
|
+
"node_modules/@types/web-bluetooth": {
|
|
957
|
+
"version": "0.0.20",
|
|
958
|
+
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
|
|
959
|
+
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
|
|
960
|
+
},
|
|
953
961
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
|
954
962
|
"version": "7.9.0",
|
|
955
963
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz",
|
|
@@ -1330,6 +1338,89 @@
|
|
|
1330
1338
|
"integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==",
|
|
1331
1339
|
"dev": true
|
|
1332
1340
|
},
|
|
1341
|
+
"node_modules/@vueuse/core": {
|
|
1342
|
+
"version": "10.10.0",
|
|
1343
|
+
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.10.0.tgz",
|
|
1344
|
+
"integrity": "sha512-vexJ/YXYs2S42B783rI95lMt3GzEwkxzC8Hb0Ndpd8rD+p+Lk/Za4bd797Ym7yq4jXqdSyj3JLChunF/vyYjUw==",
|
|
1345
|
+
"dependencies": {
|
|
1346
|
+
"@types/web-bluetooth": "^0.0.20",
|
|
1347
|
+
"@vueuse/metadata": "10.10.0",
|
|
1348
|
+
"@vueuse/shared": "10.10.0",
|
|
1349
|
+
"vue-demi": ">=0.14.7"
|
|
1350
|
+
},
|
|
1351
|
+
"funding": {
|
|
1352
|
+
"url": "https://github.com/sponsors/antfu"
|
|
1353
|
+
}
|
|
1354
|
+
},
|
|
1355
|
+
"node_modules/@vueuse/core/node_modules/vue-demi": {
|
|
1356
|
+
"version": "0.14.8",
|
|
1357
|
+
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.8.tgz",
|
|
1358
|
+
"integrity": "sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==",
|
|
1359
|
+
"hasInstallScript": true,
|
|
1360
|
+
"bin": {
|
|
1361
|
+
"vue-demi-fix": "bin/vue-demi-fix.js",
|
|
1362
|
+
"vue-demi-switch": "bin/vue-demi-switch.js"
|
|
1363
|
+
},
|
|
1364
|
+
"engines": {
|
|
1365
|
+
"node": ">=12"
|
|
1366
|
+
},
|
|
1367
|
+
"funding": {
|
|
1368
|
+
"url": "https://github.com/sponsors/antfu"
|
|
1369
|
+
},
|
|
1370
|
+
"peerDependencies": {
|
|
1371
|
+
"@vue/composition-api": "^1.0.0-rc.1",
|
|
1372
|
+
"vue": "^3.0.0-0 || ^2.6.0"
|
|
1373
|
+
},
|
|
1374
|
+
"peerDependenciesMeta": {
|
|
1375
|
+
"@vue/composition-api": {
|
|
1376
|
+
"optional": true
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
},
|
|
1380
|
+
"node_modules/@vueuse/metadata": {
|
|
1381
|
+
"version": "10.10.0",
|
|
1382
|
+
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.10.0.tgz",
|
|
1383
|
+
"integrity": "sha512-UNAo2sTCAW5ge6OErPEHb5z7NEAg3XcO9Cj7OK45aZXfLLH1QkexDcZD77HBi5zvEiLOm1An+p/4b5K3Worpug==",
|
|
1384
|
+
"funding": {
|
|
1385
|
+
"url": "https://github.com/sponsors/antfu"
|
|
1386
|
+
}
|
|
1387
|
+
},
|
|
1388
|
+
"node_modules/@vueuse/shared": {
|
|
1389
|
+
"version": "10.10.0",
|
|
1390
|
+
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.10.0.tgz",
|
|
1391
|
+
"integrity": "sha512-2aW33Ac0Uk0U+9yo3Ypg9s5KcR42cuehRWl7vnUHadQyFvCktseyxxEPBi1Eiq4D2yBGACOnqLZpx1eMc7g5Og==",
|
|
1392
|
+
"dependencies": {
|
|
1393
|
+
"vue-demi": ">=0.14.7"
|
|
1394
|
+
},
|
|
1395
|
+
"funding": {
|
|
1396
|
+
"url": "https://github.com/sponsors/antfu"
|
|
1397
|
+
}
|
|
1398
|
+
},
|
|
1399
|
+
"node_modules/@vueuse/shared/node_modules/vue-demi": {
|
|
1400
|
+
"version": "0.14.8",
|
|
1401
|
+
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.8.tgz",
|
|
1402
|
+
"integrity": "sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==",
|
|
1403
|
+
"hasInstallScript": true,
|
|
1404
|
+
"bin": {
|
|
1405
|
+
"vue-demi-fix": "bin/vue-demi-fix.js",
|
|
1406
|
+
"vue-demi-switch": "bin/vue-demi-switch.js"
|
|
1407
|
+
},
|
|
1408
|
+
"engines": {
|
|
1409
|
+
"node": ">=12"
|
|
1410
|
+
},
|
|
1411
|
+
"funding": {
|
|
1412
|
+
"url": "https://github.com/sponsors/antfu"
|
|
1413
|
+
},
|
|
1414
|
+
"peerDependencies": {
|
|
1415
|
+
"@vue/composition-api": "^1.0.0-rc.1",
|
|
1416
|
+
"vue": "^3.0.0-0 || ^2.6.0"
|
|
1417
|
+
},
|
|
1418
|
+
"peerDependenciesMeta": {
|
|
1419
|
+
"@vue/composition-api": {
|
|
1420
|
+
"optional": true
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
},
|
|
1333
1424
|
"node_modules/acorn": {
|
|
1334
1425
|
"version": "8.11.3",
|
|
1335
1426
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
|
@@ -1716,6 +1807,17 @@
|
|
|
1716
1807
|
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
|
|
1717
1808
|
"dev": true
|
|
1718
1809
|
},
|
|
1810
|
+
"node_modules/debounce": {
|
|
1811
|
+
"version": "2.1.0",
|
|
1812
|
+
"resolved": "https://registry.npmjs.org/debounce/-/debounce-2.1.0.tgz",
|
|
1813
|
+
"integrity": "sha512-OkL3+0pPWCqoBc/nhO9u6TIQNTK44fnBnzuVtJAbp13Naxw9R6u21x+8tVTka87AhDZ3htqZ2pSSsZl9fqL2Wg==",
|
|
1814
|
+
"engines": {
|
|
1815
|
+
"node": ">=18"
|
|
1816
|
+
},
|
|
1817
|
+
"funding": {
|
|
1818
|
+
"url": "https://github.com/sponsors/sindresorhus"
|
|
1819
|
+
}
|
|
1820
|
+
},
|
|
1719
1821
|
"node_modules/debug": {
|
|
1720
1822
|
"version": "4.3.4",
|
|
1721
1823
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
|
@@ -3970,6 +4072,11 @@
|
|
|
3970
4072
|
"vue": "^3.2.0"
|
|
3971
4073
|
}
|
|
3972
4074
|
},
|
|
4075
|
+
"node_modules/vue-slider-component": {
|
|
4076
|
+
"version": "4.1.0-beta.7",
|
|
4077
|
+
"resolved": "https://registry.npmjs.org/vue-slider-component/-/vue-slider-component-4.1.0-beta.7.tgz",
|
|
4078
|
+
"integrity": "sha512-Qb7K920ZG7PoQswoF6Ias+i3W2rd3k4fpk04JUl82kEUcN86Yg6et7bVSKWt/7VpQe8a5IT3BqCKSCOZ7AJgCA=="
|
|
4079
|
+
},
|
|
3973
4080
|
"node_modules/vue-template-compiler": {
|
|
3974
4081
|
"version": "2.7.16",
|
|
3975
4082
|
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz",
|
package/spa/package.json
CHANGED
|
@@ -13,12 +13,15 @@
|
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@iconify-prerendered/vue-flowbite": "^0.23.1714023977",
|
|
16
|
+
"@vueuse/core": "^10.10.0",
|
|
16
17
|
"dayjs": "^1.11.11",
|
|
18
|
+
"debounce": "^2.1.0",
|
|
17
19
|
"flowbite": "^2.3.0",
|
|
18
20
|
"flowbite-datepicker": "^1.2.6",
|
|
19
21
|
"pinia": "^2.1.7",
|
|
20
22
|
"vue": "^3.4.21",
|
|
21
|
-
"vue-router": "^4.3.0"
|
|
23
|
+
"vue-router": "^4.3.0",
|
|
24
|
+
"vue-slider-component": "^4.1.0-beta.7"
|
|
22
25
|
},
|
|
23
26
|
"devDependencies": {
|
|
24
27
|
"@rushstack/eslint-patch": "^1.8.0",
|
package/spa/src/App.vue
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
</button>
|
|
34
34
|
</div>
|
|
35
|
-
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded shadow dark:bg-gray-700 dark:divide-gray-600 dark:shadow-
|
|
35
|
+
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded shadow dark:shadow-black dark:bg-gray-700 dark:divide-gray-600 dark:shadow-black" id="dropdown-user">
|
|
36
36
|
<div class="px-4 py-3" role="none">
|
|
37
37
|
<p class="text-sm text-gray-900 dark:text-white" role="none" v-if="coreStore.userFullname">
|
|
38
38
|
{{ coreStore.userFullname }}
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
</div>
|
|
113
113
|
</div>
|
|
114
114
|
|
|
115
|
-
<div v-else>
|
|
115
|
+
<div v-else-if="routerIsReady">
|
|
116
116
|
<RouterView/>
|
|
117
117
|
</div>
|
|
118
118
|
<AcceptModal />
|
|
@@ -141,6 +141,10 @@ const routerIsReady = ref(false);
|
|
|
141
141
|
|
|
142
142
|
const loggedIn = computed(() => route.name !== 'login' && routerIsReady.value);
|
|
143
143
|
|
|
144
|
+
watch(loggedIn, (value) => {
|
|
145
|
+
console.log('🔻🔻🔻 loggedIn', value);
|
|
146
|
+
});
|
|
147
|
+
|
|
144
148
|
const theme = ref('light');
|
|
145
149
|
|
|
146
150
|
function toggleTheme() {
|
|
@@ -16,7 +16,7 @@ const modalStore = useModalStore();
|
|
|
16
16
|
<Teleport to="body">
|
|
17
17
|
<div v-if="modalStore.isOpened" class="bg-gray-900/50 dark:bg-gray-900/80 overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
|
18
18
|
<div class="relative p-4 w-full max-w-md max-h-full top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 " >
|
|
19
|
-
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
|
19
|
+
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black">
|
|
20
20
|
<button type="button"@click="modalStore.togleModal" class="absolute top-3 end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" >
|
|
21
21
|
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
|
22
22
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|