adminforth 1.5.8-next.9 โ 1.5.8
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/dataConnectors/mongo.d.ts.map +1 -1
- package/dist/dataConnectors/mongo.js +6 -3
- package/dist/dataConnectors/mongo.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/codeInjector.d.ts.map +1 -1
- package/dist/modules/codeInjector.js +15 -12
- package/dist/modules/codeInjector.js.map +1 -1
- package/dist/modules/configValidator.d.ts.map +1 -1
- package/dist/modules/configValidator.js +9 -2
- package/dist/modules/configValidator.js.map +1 -1
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +31 -22
- package/dist/modules/restApi.js.map +1 -1
- package/dist/modules/socketBroker.d.ts +2 -1
- package/dist/modules/socketBroker.d.ts.map +1 -1
- package/dist/modules/socketBroker.js +28 -13
- package/dist/modules/socketBroker.js.map +1 -1
- package/dist/modules/utils.d.ts.map +1 -1
- package/dist/modules/utils.js +0 -2
- package/dist/modules/utils.js.map +1 -1
- package/dist/servers/express.d.ts.map +1 -1
- package/dist/servers/express.js +5 -1
- package/dist/servers/express.js.map +1 -1
- package/dist/spa/src/App.vue +2 -3
- package/dist/spa/src/afcl/Select.vue +8 -0
- package/dist/spa/src/components/AcceptModal.vue +1 -1
- package/dist/spa/src/components/CustomDatePicker.vue +8 -5
- package/dist/spa/src/components/Filters.vue +6 -1
- package/dist/spa/src/components/GroupsTable.vue +30 -8
- package/dist/spa/src/components/ResourceForm.vue +13 -9
- package/dist/spa/src/components/ResourceListTable.vue +28 -9
- package/dist/spa/src/i18n.ts +40 -2
- package/dist/spa/src/main.ts +2 -1
- package/dist/spa/src/router/index.ts +8 -2
- package/dist/spa/src/stores/core.ts +31 -5
- package/dist/spa/src/stores/user.ts +8 -2
- package/dist/spa/src/types/Adapters.ts +7 -2
- package/dist/spa/src/types/Back.ts +50 -14
- package/dist/spa/src/types/Common.ts +4 -2
- package/dist/spa/src/utils.ts +13 -9
- package/dist/spa/src/views/ListView.vue +1 -2
- package/dist/spa/src/websocket.ts +7 -1
- package/dist/types/Adapters.d.ts +3 -1
- package/dist/types/Adapters.d.ts.map +1 -1
- package/dist/types/Back.d.ts +50 -14
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +1 -1
- package/dist/types/Common.d.ts.map +1 -1
- package/package.json +1 -2
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<input
|
|
5
5
|
ref="inputEl"
|
|
6
6
|
type="text"
|
|
7
|
+
:readonly="isReadonly"
|
|
7
8
|
v-model="search"
|
|
8
9
|
@click="inputClick"
|
|
9
10
|
@input="inputInput"
|
|
@@ -101,6 +102,10 @@ const props = defineProps({
|
|
|
101
102
|
type: String,
|
|
102
103
|
default: '',
|
|
103
104
|
},
|
|
105
|
+
isReadonly: {
|
|
106
|
+
type: Boolean,
|
|
107
|
+
default: false,
|
|
108
|
+
},
|
|
104
109
|
});
|
|
105
110
|
|
|
106
111
|
const emit = defineEmits(['update:modelValue']);
|
|
@@ -141,6 +146,9 @@ function updateFromProps() {
|
|
|
141
146
|
}
|
|
142
147
|
|
|
143
148
|
function inputClick() {
|
|
149
|
+
if (props.isReadonly) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
144
152
|
if (!showDropdown.value) {
|
|
145
153
|
showDropdown.value = true;
|
|
146
154
|
} else {
|
|
@@ -6,7 +6,7 @@ const modalStore = useModalStore();
|
|
|
6
6
|
|
|
7
7
|
<template>
|
|
8
8
|
<Teleport to="body">
|
|
9
|
-
<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-
|
|
9
|
+
<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-full max-h-full">
|
|
10
10
|
<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 " >
|
|
11
11
|
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black">
|
|
12
12
|
<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" >
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
<IconCalendar class="w-4 h-4 text-gray-500 dark:text-gray-400"/>
|
|
10
10
|
</div>
|
|
11
11
|
|
|
12
|
-
<input ref="datepickerStartEl"
|
|
12
|
+
<input ref="datepickerStartEl" type="text"
|
|
13
13
|
class="bg-gray-50 border leading-none border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full 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"
|
|
14
|
-
:placeholder="$t('Select date')"
|
|
15
|
-
|
|
14
|
+
:placeholder="$t('Select date')" :disabled="isReadonly" />
|
|
15
|
+
|
|
16
16
|
</div>
|
|
17
17
|
</div>
|
|
18
18
|
</div>
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
<input v-model="startTime" type="time" id="start-time" onfocus="this.showPicker()" onclick="this.showPicker()" step="1"
|
|
29
29
|
class="bg-gray-50 border leading-none border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full 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"
|
|
30
|
-
value="00:00" required/>
|
|
30
|
+
value="00:00" :disabled="isReadonly" required/>
|
|
31
31
|
</div>
|
|
32
32
|
</div>
|
|
33
33
|
</div>
|
|
@@ -71,7 +71,10 @@ const props = defineProps({
|
|
|
71
71
|
},
|
|
72
72
|
autoHide: {
|
|
73
73
|
type: Boolean,
|
|
74
|
-
}
|
|
74
|
+
},
|
|
75
|
+
isReadonly: {
|
|
76
|
+
type: Boolean,
|
|
77
|
+
},
|
|
75
78
|
});
|
|
76
79
|
|
|
77
80
|
const emit = defineEmits(['update:valueStart']);
|
|
@@ -34,7 +34,12 @@
|
|
|
34
34
|
multiple
|
|
35
35
|
class="w-full"
|
|
36
36
|
v-else-if="c.type === 'boolean'"
|
|
37
|
-
:options="[
|
|
37
|
+
:options="[
|
|
38
|
+
{ label: $t('Yes'), value: true },
|
|
39
|
+
{ label: $t('No'), value: false },
|
|
40
|
+
// if field is not required, undefined might be there, and user might want to filter by it
|
|
41
|
+
...(c.required ? [] : [ { label: $t('Unset'), value: undefined } ])
|
|
42
|
+
]"
|
|
38
43
|
@update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event || undefined })"
|
|
39
44
|
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
40
45
|
/>
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
:key="column.name"
|
|
21
21
|
v-if="currentValues !== null"
|
|
22
22
|
class="bg-ligftForm dark:bg-gray-800 dark:border-gray-700 block md:table-row"
|
|
23
|
-
:class="{ 'border-b': i !== group.columns.length - 1
|
|
23
|
+
:class="{ 'border-b': i !== group.columns.length - 1}"
|
|
24
24
|
>
|
|
25
25
|
<td class="px-6 py-4 flex items-center block md:table-cell pb-0 md:pb-4"
|
|
26
26
|
:class="{'rounded-bl-lg border-b-none': i === group.columns.length - 1}"> <!--align-top-->
|
|
@@ -57,8 +57,9 @@
|
|
|
57
57
|
class="w-full"
|
|
58
58
|
v-if="column.foreignResource"
|
|
59
59
|
:options="columnOptions[column.name] || []"
|
|
60
|
-
:placeholder = "columnOptions[column.name]?.length
|
|
60
|
+
:placeholder = "columnOptions[column.name]?.length ?$t('Select...'): $t('There are no options available')"
|
|
61
61
|
:modelValue="currentValues[column.name]"
|
|
62
|
+
:isReadonly="column.editReadonly && source === 'edit'"
|
|
62
63
|
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
63
64
|
></Select>
|
|
64
65
|
<Select
|
|
@@ -66,6 +67,7 @@
|
|
|
66
67
|
v-else-if="column.enum"
|
|
67
68
|
:options="column.enum"
|
|
68
69
|
:modelValue="currentValues[column.name]"
|
|
70
|
+
:isReadonly="column.editReadonly && source === 'edit'"
|
|
69
71
|
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
70
72
|
></Select>
|
|
71
73
|
<Select
|
|
@@ -73,6 +75,7 @@
|
|
|
73
75
|
v-else-if="column.type === 'boolean'"
|
|
74
76
|
:options="getBooleanOptions(column)"
|
|
75
77
|
:modelValue="currentValues[column.name]"
|
|
78
|
+
:isReadonly="column.editReadonly && source === 'edit'"
|
|
76
79
|
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
77
80
|
></Select>
|
|
78
81
|
<input
|
|
@@ -81,6 +84,7 @@
|
|
|
81
84
|
step="1"
|
|
82
85
|
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-40 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"
|
|
83
86
|
placeholder="0"
|
|
87
|
+
:isReadonly="column.editReadonly && source === 'edit'"
|
|
84
88
|
:value="currentValues[column.name]"
|
|
85
89
|
@input="setCurrentValue(column.name, $event.target.value)"
|
|
86
90
|
>
|
|
@@ -90,6 +94,7 @@
|
|
|
90
94
|
:valueStart="currentValues[column.name]"
|
|
91
95
|
auto-hide
|
|
92
96
|
@update:valueStart="setCurrentValue(column.name, $event)"
|
|
97
|
+
:isReadonly="column.editReadonly && source === 'edit'"
|
|
93
98
|
/>
|
|
94
99
|
<input
|
|
95
100
|
v-else-if="['decimal', 'float'].includes(column.type)"
|
|
@@ -99,6 +104,7 @@
|
|
|
99
104
|
placeholder="0.0"
|
|
100
105
|
:value="currentValues[column.name]"
|
|
101
106
|
@input="setCurrentValue(column.name, $event.target.value)"
|
|
107
|
+
:readonly="column.editReadonly && source === 'edit'"
|
|
102
108
|
/>
|
|
103
109
|
<textarea
|
|
104
110
|
v-else-if="['text', 'richtext'].includes(column.type)"
|
|
@@ -106,6 +112,7 @@
|
|
|
106
112
|
:placeholder="$t('Text')"
|
|
107
113
|
:value="currentValues[column.name]"
|
|
108
114
|
@input="setCurrentValue(column.name, $event.target.value)"
|
|
115
|
+
:readonly="column.editReadonly && source === 'edit'"
|
|
109
116
|
>
|
|
110
117
|
</textarea>
|
|
111
118
|
<textarea
|
|
@@ -126,7 +133,8 @@
|
|
|
126
133
|
autocomplete="false"
|
|
127
134
|
data-lpignore="true"
|
|
128
135
|
readonly
|
|
129
|
-
|
|
136
|
+
ref="readonlyInputs"
|
|
137
|
+
@focus="onFocusHandler($event, column, source)"
|
|
130
138
|
>
|
|
131
139
|
|
|
132
140
|
<button
|
|
@@ -155,6 +163,9 @@
|
|
|
155
163
|
import { getCustomComponent } from '@/utils';
|
|
156
164
|
import { Tooltip } from '@/afcl';
|
|
157
165
|
import { ref, computed, watch, type Ref } from 'vue';
|
|
166
|
+
import { useI18n } from 'vue-i18n';
|
|
167
|
+
|
|
168
|
+
const { t } = useI18n();
|
|
158
169
|
|
|
159
170
|
const props = defineProps<{
|
|
160
171
|
source: 'create' | 'edit',
|
|
@@ -168,22 +179,33 @@
|
|
|
168
179
|
columnOptions: any,
|
|
169
180
|
}>();
|
|
170
181
|
|
|
182
|
+
const customComponentsInValidity: Ref<Record<string, boolean>> = ref({});
|
|
183
|
+
const customComponentsEmptiness: Ref<Record<string, boolean>> = ref({});
|
|
184
|
+
|
|
171
185
|
const getBooleanOptions = (column: any) => {
|
|
172
186
|
const options: Array<{ label: string; value: boolean | null }> = [
|
|
173
|
-
{ label: 'Yes', value: true },
|
|
174
|
-
{ label: 'No', value: false },
|
|
187
|
+
{ label: t('Yes'), value: true },
|
|
188
|
+
{ label: t('No'), value: false },
|
|
175
189
|
];
|
|
176
190
|
if (!column.required[props.mode]) {
|
|
177
|
-
options.push({ label: 'Unset', value: null });
|
|
191
|
+
options.push({ label: t('Unset'), value: null });
|
|
178
192
|
}
|
|
179
193
|
return options;
|
|
180
194
|
};
|
|
195
|
+
function onFocusHandler(event:FocusEvent, column:any, source:string, ) {
|
|
196
|
+
const focusedInput = event.target as HTMLInputElement;
|
|
197
|
+
if(!focusedInput) return;
|
|
198
|
+
if (column.editReadonly && source === 'edit') return;
|
|
199
|
+
else {
|
|
200
|
+
focusedInput.removeAttribute('readonly');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
181
204
|
|
|
182
205
|
const emit = defineEmits(['update:customComponentsInValidity', 'update:customComponentsEmptiness']);
|
|
183
206
|
|
|
184
207
|
|
|
185
|
-
|
|
186
|
-
const customComponentsEmptiness: Ref<Record<string, boolean>> = ref({});
|
|
208
|
+
|
|
187
209
|
|
|
188
210
|
watch(customComponentsInValidity, (newVal) => {
|
|
189
211
|
emit('update:customComponentsInValidity', newVal);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="rounded-default">
|
|
3
|
+
<pre>
|
|
4
|
+
</pre>
|
|
3
5
|
<form autocomplete="off" @submit.prevent>
|
|
4
6
|
<div v-if="!groups || groups.length === 0">
|
|
5
7
|
<GroupsTable
|
|
@@ -37,7 +39,7 @@
|
|
|
37
39
|
<div v-if="otherColumns.length > 0">
|
|
38
40
|
<GroupsTable
|
|
39
41
|
:source="source"
|
|
40
|
-
:group="{groupName: 'Other', columns: otherColumns}"
|
|
42
|
+
:group="{groupName: $t('Other'), columns: otherColumns}"
|
|
41
43
|
:currentValues="currentValues"
|
|
42
44
|
:editableColumns="editableColumns"
|
|
43
45
|
:mode="mode"
|
|
@@ -64,9 +66,11 @@ import { computed, onMounted, ref, watch } from 'vue';
|
|
|
64
66
|
import { useRouter, useRoute } from 'vue-router';
|
|
65
67
|
import { useCoreStore } from "@/stores/core";
|
|
66
68
|
import GroupsTable from '@/components/GroupsTable.vue';
|
|
69
|
+
import { useI18n } from 'vue-i18n';
|
|
67
70
|
|
|
68
|
-
const
|
|
71
|
+
const { t } = useI18n();
|
|
69
72
|
|
|
73
|
+
const coreStore = useCoreStore();
|
|
70
74
|
const router = useRouter();
|
|
71
75
|
const route = useRoute();
|
|
72
76
|
const props = defineProps({
|
|
@@ -78,7 +82,7 @@ const props = defineProps({
|
|
|
78
82
|
|
|
79
83
|
const unmasked = ref({});
|
|
80
84
|
|
|
81
|
-
const mode = computed(() => route.name === 'resource-create' ?
|
|
85
|
+
const mode = computed(() => route.name === 'resource-create' ? 'create' : 'edit');
|
|
82
86
|
|
|
83
87
|
const emit = defineEmits(['update:record', 'update:isValid']);
|
|
84
88
|
|
|
@@ -103,18 +107,18 @@ const columnError = (column) => {
|
|
|
103
107
|
(customComponentsEmptiness.value[column.name] !== false)
|
|
104
108
|
|
|
105
109
|
) {
|
|
106
|
-
return 'This field is required';
|
|
110
|
+
return t('This field is required');
|
|
107
111
|
}
|
|
108
112
|
if (column.type === 'json' && currentValues.value[column.name]) {
|
|
109
113
|
try {
|
|
110
114
|
JSON.parse(currentValues.value[column.name]);
|
|
111
115
|
} catch (e) {
|
|
112
|
-
return 'Invalid JSON';
|
|
116
|
+
return t('Invalid JSON');
|
|
113
117
|
}
|
|
114
118
|
}
|
|
115
119
|
if ( column.type === 'string' || column.type === 'text' ) {
|
|
116
120
|
if ( column.maxLength && currentValues.value[column.name]?.length > column.maxLength ) {
|
|
117
|
-
return
|
|
121
|
+
return t('This field must be shorter than {maxLength} characters', { maxLength: column.maxLength });
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
if ( column.minLength && currentValues.value[column.name]?.length < column.minLength ) {
|
|
@@ -123,7 +127,7 @@ const columnError = (column) => {
|
|
|
123
127
|
if (!needToCheckEmpty) {
|
|
124
128
|
return null;
|
|
125
129
|
}
|
|
126
|
-
return
|
|
130
|
+
return t('This field must be longer than {minLength} characters', { minLength: column.minLength });
|
|
127
131
|
}
|
|
128
132
|
}
|
|
129
133
|
if ( ['integer', 'decimal', 'float'].includes(column.type) ) {
|
|
@@ -131,10 +135,10 @@ const columnError = (column) => {
|
|
|
131
135
|
&& currentValues.value[column.name] !== null
|
|
132
136
|
&& currentValues.value[column.name] < column.minValue
|
|
133
137
|
) {
|
|
134
|
-
return
|
|
138
|
+
return t('This field must be greater than {minValue}', { minValue: column.minValue });
|
|
135
139
|
}
|
|
136
140
|
if ( column.maxValue !== undefined && currentValues.value[column.name] > column.maxValue ) {
|
|
137
|
-
return
|
|
141
|
+
return t('This field must be less than {maxValue}', { maxValue: column.maxValue });
|
|
138
142
|
}
|
|
139
143
|
}
|
|
140
144
|
if (currentValues.value[column.name] && column.validation) {
|
|
@@ -234,16 +234,33 @@
|
|
|
234
234
|
<span class="text-sm text-gray-700 dark:text-gray-400">
|
|
235
235
|
<span v-if="((page || 1) - 1) * pageSize + 1 > totalRows">{{ $t('Wrong Page') }} </span>
|
|
236
236
|
<template v-else>
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
237
|
+
|
|
238
|
+
<span class="hidden sm:inline">
|
|
239
|
+
<i18n-t keypath="Showing {from} to {to} of {total} Entries" tag="p" >
|
|
240
|
+
<template v-slot:from>
|
|
241
|
+
<strong>{{ from }}</strong>
|
|
242
|
+
</template>
|
|
243
|
+
<template v-slot:to>
|
|
244
|
+
<strong>{{ to }}</strong>
|
|
245
|
+
</template>
|
|
246
|
+
<template v-slot:total>
|
|
247
|
+
<strong>{{ totalRows }}</strong>
|
|
248
|
+
</template>
|
|
249
|
+
</i18n-t>
|
|
245
250
|
</span>
|
|
246
|
-
|
|
251
|
+
<span class="sm:hidden">
|
|
252
|
+
<i18n-t keypath="{from} - {to} of {total}" tag="p" >
|
|
253
|
+
<template v-slot:from>
|
|
254
|
+
<strong>{{ from }}</strong>
|
|
255
|
+
</template>
|
|
256
|
+
<template v-slot:to>
|
|
257
|
+
<strong>{{ to }}</strong>
|
|
258
|
+
</template>
|
|
259
|
+
<template v-slot:total>
|
|
260
|
+
<strong>{{ totalRows }}</strong>
|
|
261
|
+
</template>
|
|
262
|
+
</i18n-t>
|
|
263
|
+
</span>
|
|
247
264
|
</template>
|
|
248
265
|
</span>
|
|
249
266
|
</div>
|
|
@@ -302,6 +319,8 @@ const page = ref(1);
|
|
|
302
319
|
const sort = ref([]);
|
|
303
320
|
|
|
304
321
|
|
|
322
|
+
const from = computed(() => ((page.value || 1) - 1) * props.pageSize + 1);
|
|
323
|
+
const to = computed(() => Math.min((page.value || 1) * props.pageSize, props.totalRows));
|
|
305
324
|
|
|
306
325
|
watch(() => page.value, (newPage) => {
|
|
307
326
|
emits('update:page', newPage);
|
package/dist/spa/src/i18n.ts
CHANGED
|
@@ -2,15 +2,53 @@ import { createI18n } from 'vue-i18n';
|
|
|
2
2
|
import { createApp } from 'vue';
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
// taken from here https://vue-i18n.intlify.dev/guide/essentials/pluralization.html#custom-pluralization
|
|
6
|
+
function slavicPluralRule(choice, choicesLength, orgRule) {
|
|
7
|
+
if (choice === 0) {
|
|
8
|
+
return 0
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const teen = choice > 10 && choice < 20
|
|
12
|
+
const endsWithOne = choice % 10 === 1
|
|
13
|
+
|
|
14
|
+
if (!teen && endsWithOne) {
|
|
15
|
+
return 1
|
|
16
|
+
}
|
|
17
|
+
if (!teen && choice % 10 >= 2 && choice % 10 <= 4) {
|
|
18
|
+
return 2
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return choicesLength < 4 ? 2 : 3
|
|
22
|
+
}
|
|
23
|
+
|
|
5
24
|
export function initI18n(app: ReturnType<typeof createApp>) {
|
|
6
25
|
const i18n = createI18n({
|
|
26
|
+
legacy: false,
|
|
27
|
+
|
|
28
|
+
missingWarn: false,
|
|
29
|
+
fallbackWarn: false,
|
|
30
|
+
|
|
31
|
+
pluralRules: {
|
|
32
|
+
'uk': slavicPluralRule,
|
|
33
|
+
'bg': slavicPluralRule,
|
|
34
|
+
'cs': slavicPluralRule,
|
|
35
|
+
'hr': slavicPluralRule,
|
|
36
|
+
'mk': slavicPluralRule,
|
|
37
|
+
'pl': slavicPluralRule,
|
|
38
|
+
'sk': slavicPluralRule,
|
|
39
|
+
'sl': slavicPluralRule,
|
|
40
|
+
'sr': slavicPluralRule,
|
|
41
|
+
'be': slavicPluralRule,
|
|
42
|
+
'ru': slavicPluralRule,
|
|
43
|
+
},
|
|
44
|
+
|
|
7
45
|
missing: (locale, key) => {
|
|
8
46
|
// very very dirty hack to make work $t("a {key} b", { key: "c" }) as "a c b" when translation is missing
|
|
9
47
|
// e.g. relevant for "Showing {from} to {to} of {total} entries" on list page
|
|
10
48
|
return key + ' ';
|
|
11
49
|
},
|
|
12
|
-
})
|
|
50
|
+
});
|
|
13
51
|
|
|
14
52
|
app.use(i18n);
|
|
15
|
-
|
|
53
|
+
return i18n
|
|
16
54
|
}
|
package/dist/spa/src/main.ts
CHANGED
|
@@ -13,7 +13,8 @@ export const app: ReturnType<typeof createApp> = createApp(App)
|
|
|
13
13
|
app.use(createPinia())
|
|
14
14
|
app.use(router)
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// get access to i18n instance outside components
|
|
17
|
+
window.i18n = initI18n(app);
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
/* IMPORTANT:ADMINFORTH CUSTOM USES */
|
|
@@ -16,8 +16,14 @@ const router = createRouter({
|
|
|
16
16
|
customLayout: true
|
|
17
17
|
},
|
|
18
18
|
beforeEnter: async (to, from, next) => {
|
|
19
|
-
if(localStorage.getItem('isAuthorized') === 'true'){
|
|
20
|
-
next
|
|
19
|
+
if(localStorage.getItem('isAuthorized') === 'true') {
|
|
20
|
+
// check if url has next=... and redirect to it
|
|
21
|
+
console.log('to.query', to.query)
|
|
22
|
+
if (to.query.next) {
|
|
23
|
+
next(to.query.next.toString())
|
|
24
|
+
} else {
|
|
25
|
+
next({name: 'home'});
|
|
26
|
+
}
|
|
21
27
|
} else {
|
|
22
28
|
next()
|
|
23
29
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ref, computed } from 'vue'
|
|
2
2
|
import { defineStore } from 'pinia'
|
|
3
3
|
import { callAdminForthApi } from '@/utils';
|
|
4
|
+
import websocket from '@/websocket';
|
|
5
|
+
|
|
4
6
|
import type { AdminForthResourceCommon, AdminForthResourceColumnCommon, GetBaseConfigResponse, ResourceVeryShort, AdminUser, UserData, AdminForthConfigMenuItem, AdminForthConfigForFrontend } from '@/types/Common';
|
|
5
7
|
import type { Ref } from 'vue'
|
|
6
8
|
|
|
@@ -64,19 +66,40 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
64
66
|
console.log('๐ AdminForth v', resp.version);
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
function findItemWithId(items: AdminForthConfigMenuItem[],
|
|
69
|
+
function findItemWithId(items: AdminForthConfigMenuItem[], itemId: string): AdminForthConfigMenuItem | undefined {
|
|
68
70
|
for (const item of items) {
|
|
69
|
-
if (item.
|
|
71
|
+
if (item.itemId === itemId) {
|
|
70
72
|
return item;
|
|
71
73
|
}
|
|
72
74
|
if (item.children) {
|
|
73
|
-
const found = findItemWithId(item.children,
|
|
75
|
+
const found = findItemWithId(item.children, itemId);
|
|
74
76
|
if (found) {
|
|
75
77
|
return found;
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
}
|
|
82
|
+
async function subscribeToMenuBadges() {
|
|
83
|
+
const processItem = (mi: AdminForthConfigMenuItem) => {
|
|
84
|
+
if (mi.badge) {
|
|
85
|
+
console.log('mi ๐งช subsc', mi)
|
|
86
|
+
websocket.subscribe(`/opentopic/update-menu-badge/${mi.itemId}`, ({ badge }) => {
|
|
87
|
+
mi.badge = badge;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
menu.value.forEach((mi) => {
|
|
93
|
+
processItem(mi);
|
|
94
|
+
if (mi.children) {
|
|
95
|
+
mi.children.forEach((child) => {
|
|
96
|
+
console.log('mi ๐งช', JSON.stringify(child), mi.badge)
|
|
97
|
+
|
|
98
|
+
processItem(child);
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
}
|
|
80
103
|
async function fetchMenuBadges() {
|
|
81
104
|
const resp: Record<string, string> = await callAdminForthApi({
|
|
82
105
|
path: '/get_menu_badges',
|
|
@@ -85,12 +108,15 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
85
108
|
if (!resp) {
|
|
86
109
|
return;
|
|
87
110
|
}
|
|
88
|
-
Object.entries(resp).forEach(([
|
|
89
|
-
const item: AdminForthConfigMenuItem | undefined = findItemWithId(menu.value,
|
|
111
|
+
Object.entries(resp).forEach(([itemId, badge]: [string, string]) => {
|
|
112
|
+
const item: AdminForthConfigMenuItem | undefined = findItemWithId(menu.value, itemId);
|
|
90
113
|
if (item) {
|
|
91
114
|
item.badge = badge;
|
|
92
115
|
}
|
|
93
116
|
});
|
|
117
|
+
|
|
118
|
+
subscribeToMenuBadges();
|
|
119
|
+
|
|
94
120
|
}
|
|
95
121
|
|
|
96
122
|
|
|
@@ -10,6 +10,7 @@ export const useUserStore = defineStore('user', () => {
|
|
|
10
10
|
const isAuthorized = ref(false);
|
|
11
11
|
|
|
12
12
|
function authorize() {
|
|
13
|
+
// syncing isAuthorized allows us to use navigation guards without waiting for user api response
|
|
13
14
|
isAuthorized.value = true;
|
|
14
15
|
localStorage.setItem('isAuthorized', 'true');
|
|
15
16
|
}
|
|
@@ -21,9 +22,14 @@ export const useUserStore = defineStore('user', () => {
|
|
|
21
22
|
|
|
22
23
|
async function finishLogin() {
|
|
23
24
|
const coreStore = useCoreStore();
|
|
24
|
-
authorize();
|
|
25
|
+
authorize();
|
|
25
26
|
reconnect();
|
|
26
|
-
|
|
27
|
+
// if next param in route, redirect to it
|
|
28
|
+
if (router.currentRoute.value.query.next) {
|
|
29
|
+
await router.push(router.currentRoute.value.query.next.toString());
|
|
30
|
+
} else {
|
|
31
|
+
await router.push('/');
|
|
32
|
+
}
|
|
27
33
|
await router.isReady();
|
|
28
34
|
await coreStore.fetchMenuAndResource();
|
|
29
35
|
}
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
export interface EmailAdapter {
|
|
2
|
+
validate(): Promise<void>;
|
|
3
|
+
|
|
2
4
|
sendEmail(
|
|
3
5
|
from: string,
|
|
4
6
|
to: string,
|
|
5
7
|
text: string,
|
|
6
8
|
html: string,
|
|
7
9
|
subject: string
|
|
8
|
-
)
|
|
10
|
+
): Promise<void>;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export interface CompletionAdapter {
|
|
14
|
+
|
|
15
|
+
validate(): Promise<void>;
|
|
16
|
+
|
|
12
17
|
complete(
|
|
13
18
|
content: string,
|
|
14
19
|
stop: string[],
|
|
15
|
-
maxTokens: number
|
|
20
|
+
maxTokens: number,
|
|
16
21
|
): Promise<{
|
|
17
22
|
content?: string;
|
|
18
23
|
finishReason?: string;
|