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.
Files changed (52) hide show
  1. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  2. package/dist/dataConnectors/mongo.js +6 -3
  3. package/dist/dataConnectors/mongo.js.map +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +2 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/modules/codeInjector.d.ts.map +1 -1
  8. package/dist/modules/codeInjector.js +15 -12
  9. package/dist/modules/codeInjector.js.map +1 -1
  10. package/dist/modules/configValidator.d.ts.map +1 -1
  11. package/dist/modules/configValidator.js +9 -2
  12. package/dist/modules/configValidator.js.map +1 -1
  13. package/dist/modules/restApi.d.ts.map +1 -1
  14. package/dist/modules/restApi.js +31 -22
  15. package/dist/modules/restApi.js.map +1 -1
  16. package/dist/modules/socketBroker.d.ts +2 -1
  17. package/dist/modules/socketBroker.d.ts.map +1 -1
  18. package/dist/modules/socketBroker.js +28 -13
  19. package/dist/modules/socketBroker.js.map +1 -1
  20. package/dist/modules/utils.d.ts.map +1 -1
  21. package/dist/modules/utils.js +0 -2
  22. package/dist/modules/utils.js.map +1 -1
  23. package/dist/servers/express.d.ts.map +1 -1
  24. package/dist/servers/express.js +5 -1
  25. package/dist/servers/express.js.map +1 -1
  26. package/dist/spa/src/App.vue +2 -3
  27. package/dist/spa/src/afcl/Select.vue +8 -0
  28. package/dist/spa/src/components/AcceptModal.vue +1 -1
  29. package/dist/spa/src/components/CustomDatePicker.vue +8 -5
  30. package/dist/spa/src/components/Filters.vue +6 -1
  31. package/dist/spa/src/components/GroupsTable.vue +30 -8
  32. package/dist/spa/src/components/ResourceForm.vue +13 -9
  33. package/dist/spa/src/components/ResourceListTable.vue +28 -9
  34. package/dist/spa/src/i18n.ts +40 -2
  35. package/dist/spa/src/main.ts +2 -1
  36. package/dist/spa/src/router/index.ts +8 -2
  37. package/dist/spa/src/stores/core.ts +31 -5
  38. package/dist/spa/src/stores/user.ts +8 -2
  39. package/dist/spa/src/types/Adapters.ts +7 -2
  40. package/dist/spa/src/types/Back.ts +50 -14
  41. package/dist/spa/src/types/Common.ts +4 -2
  42. package/dist/spa/src/utils.ts +13 -9
  43. package/dist/spa/src/views/ListView.vue +1 -2
  44. package/dist/spa/src/websocket.ts +7 -1
  45. package/dist/types/Adapters.d.ts +3 -1
  46. package/dist/types/Adapters.d.ts.map +1 -1
  47. package/dist/types/Back.d.ts +50 -14
  48. package/dist/types/Back.d.ts.map +1 -1
  49. package/dist/types/Back.js.map +1 -1
  50. package/dist/types/Common.d.ts +1 -1
  51. package/dist/types/Common.d.ts.map +1 -1
  52. 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-[calc(100%-1rem)] max-h-full">
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" type="text"
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="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: undefined }]"
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, 'opacity-50 pointer-events-none': column.editReadonly && source === $t('edit')}"
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 ?'Select...': 'There are no options available'"
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
- onfocus="this.removeAttribute('readonly');"
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
- const customComponentsInValidity: Ref<Record<string, boolean>> = ref({});
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 coreStore = useCoreStore();
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' ? $t('create') : $t('edit'));
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 `This field must be shorter than ${column.maxLength} characters`;
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 `This field must be longer than ${column.minLength} characters`;
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 `This field must be greater than ${column.minValue}`;
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 `This field must be less than ${column.maxValue}`;
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
- <span class="hidden sm:inline"
238
- v-html="$t('Showing {from} to {to} of {total} Entries', {from: ((page || 1) - 1) * pageSize + 1, to: Math.min((page || 1) * pageSize, totalRows), total: totalRows})
239
- .replace(/\d+/g, '<strong>$&</strong>')">
240
- </span>
241
- <span class="sm:hidden"
242
- v-html="$t('{from} - {to} of {total}', {from: ((page || 1) - 1) * pageSize + 1, to: Math.min((page || 1) * pageSize, totalRows), total: totalRows})
243
- .replace(/\d+/g, '<strong>$&</strong>')
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);
@@ -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
  }
@@ -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
- initI18n(app);
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({name: 'home'})
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[], _itemId: string): AdminForthConfigMenuItem | undefined {
69
+ function findItemWithId(items: AdminForthConfigMenuItem[], itemId: string): AdminForthConfigMenuItem | undefined {
68
70
  for (const item of items) {
69
- if (item._itemId === _itemId) {
71
+ if (item.itemId === itemId) {
70
72
  return item;
71
73
  }
72
74
  if (item.children) {
73
- const found = findItemWithId(item.children, _itemId);
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(([_itemId, badge]: [string, string]) => {
89
- const item: AdminForthConfigMenuItem | undefined = findItemWithId(menu.value, _itemId);
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(); // TODO not sure we need this approach with localStorage
25
+ authorize();
25
26
  reconnect();
26
- await router.push('/');
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;