adminforth 1.5.8-next.1 → 1.5.8-next.11

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 (54) hide show
  1. package/commands/utils.js +36 -3
  2. package/dist/dataConnectors/postgres.d.ts.map +1 -1
  3. package/dist/dataConnectors/postgres.js +1 -1
  4. package/dist/dataConnectors/postgres.js.map +1 -1
  5. package/dist/dataConnectors/sqlite.d.ts.map +1 -1
  6. package/dist/dataConnectors/sqlite.js +12 -0
  7. package/dist/dataConnectors/sqlite.js.map +1 -1
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +9 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/modules/configValidator.d.ts.map +1 -1
  13. package/dist/modules/configValidator.js +7 -0
  14. package/dist/modules/configValidator.js.map +1 -1
  15. package/dist/modules/restApi.d.ts.map +1 -1
  16. package/dist/modules/restApi.js +55 -24
  17. package/dist/modules/restApi.js.map +1 -1
  18. package/dist/modules/socketBroker.d.ts +2 -1
  19. package/dist/modules/socketBroker.d.ts.map +1 -1
  20. package/dist/modules/socketBroker.js +20 -12
  21. package/dist/modules/socketBroker.js.map +1 -1
  22. package/dist/servers/express.d.ts +1 -0
  23. package/dist/servers/express.d.ts.map +1 -1
  24. package/dist/servers/express.js +11 -1
  25. package/dist/servers/express.js.map +1 -1
  26. package/dist/spa/src/App.vue +15 -5
  27. package/dist/spa/src/afcl/Select.vue +1 -1
  28. package/dist/spa/src/components/Breadcrumbs.vue +1 -1
  29. package/dist/spa/src/components/CustomDatePicker.vue +3 -2
  30. package/dist/spa/src/components/CustomDateRangePicker.vue +3 -3
  31. package/dist/spa/src/components/CustomRangePicker.vue +2 -2
  32. package/dist/spa/src/components/Filters.vue +4 -4
  33. package/dist/spa/src/components/GroupsTable.vue +4 -4
  34. package/dist/spa/src/components/MenuLink.vue +16 -2
  35. package/dist/spa/src/components/ResourceForm.vue +4 -1
  36. package/dist/spa/src/components/ResourceListTable.vue +12 -7
  37. package/dist/spa/src/i18n.ts +55 -0
  38. package/dist/spa/src/main.ts +5 -6
  39. package/dist/spa/src/renderers/CountryFlag.vue +2 -2
  40. package/dist/spa/src/stores/core.ts +31 -5
  41. package/dist/spa/src/types/Adapters.ts +5 -1
  42. package/dist/spa/src/types/Back.ts +18 -5
  43. package/dist/spa/src/types/Common.ts +8 -1
  44. package/dist/spa/src/utils.ts +2 -0
  45. package/dist/spa/src/views/ListView.vue +22 -2
  46. package/dist/spa/src/views/LoginView.vue +1 -1
  47. package/dist/types/Adapters.d.ts +3 -2
  48. package/dist/types/Adapters.d.ts.map +1 -1
  49. package/dist/types/Back.d.ts +18 -5
  50. package/dist/types/Back.d.ts.map +1 -1
  51. package/dist/types/Back.js.map +1 -1
  52. package/dist/types/Common.d.ts +5 -1
  53. package/dist/types/Common.d.ts.map +1 -1
  54. package/package.json +2 -3
@@ -5,7 +5,7 @@
5
5
  :max="maxFormatted"
6
6
  type="number" aria-describedby="helper-text-explanation"
7
7
  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"
8
- placeholder="From"
8
+ :placeholder="$t('From')"
9
9
  v-model="start"
10
10
  >
11
11
 
@@ -14,7 +14,7 @@
14
14
  :max="maxFormatted"
15
15
  type="number" aria-describedby="helper-text-explanation"
16
16
  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"
17
- placeholder="To"
17
+ :placeholder="$t('To')"
18
18
  v-model="end"
19
19
  >
20
20
 
@@ -51,7 +51,7 @@
51
51
  <input
52
52
  v-else-if="[ 'string', 'text' ].includes(c.type)"
53
53
  type="text" class="w-full py-1 px-2 border border-gray-300 rounded-md 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"
54
- placeholder="Search"
54
+ :placeholder="$t('Search')"
55
55
  @input="setFilterItem({ column: c, operator: 'ilike', value: $event.target.value || undefined })"
56
56
  :value="getFilterItem({ column: c, operator: 'ilike' })"
57
57
  >
@@ -68,7 +68,7 @@
68
68
  <input
69
69
  v-else-if="[ 'date', 'time' ].includes(c.type)"
70
70
  type="text" class="w-full py-1 px-2 border border-gray-300 rounded-md"
71
- placeholder="Search datetime"
71
+ :placeholder="$t('Search datetime')"
72
72
  @input="setFilterItem({ column: c, operator: 'ilike', value: $event.target.value || undefined })"
73
73
  :value="getFilterItem({ column: c, operator: 'ilike' })"
74
74
  >
@@ -87,14 +87,14 @@
87
87
  <input
88
88
  type="number" aria-describedby="helper-text-explanation"
89
89
  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"
90
- placeholder="From"
90
+ :placeholder="$t('From')"
91
91
  @input="setFilterItem({ column: c, operator: 'gte', value: $event.target.value || undefined })"
92
92
  :value="getFilterItem({ column: c, operator: 'gte' })"
93
93
  >
94
94
  <input
95
95
  type="number" aria-describedby="helper-text-explanation"
96
96
  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"
97
- placeholder="To"
97
+ :placeholder="$t('To')"
98
98
  @input="setFilterItem({ column: c, operator: 'lte', value: $event.target.value || undefined})"
99
99
  :value="getFilterItem({ column: c, operator: 'lte' })"
100
100
  >
@@ -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 === 'edit'}"
23
+ :class="{ 'border-b': i !== group.columns.length - 1, 'opacity-50 pointer-events-none': column.editReadonly && source === $t('edit')}"
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-->
@@ -103,7 +103,7 @@
103
103
  <textarea
104
104
  v-else-if="['text', 'richtext'].includes(column.type)"
105
105
  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-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"
106
- placeholder="Text"
106
+ :placeholder="$t('Text')"
107
107
  :value="currentValues[column.name]"
108
108
  @input="setCurrentValue(column.name, $event.target.value)"
109
109
  >
@@ -111,7 +111,7 @@
111
111
  <textarea
112
112
  v-else-if="['json'].includes(column.type)"
113
113
  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-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"
114
- placeholder="Text"
114
+ :placeholder="$t('Text')"
115
115
  :value="currentValues[column.name]"
116
116
  @input="setCurrentValue(column.name, $event.target.value)"
117
117
  >
@@ -120,7 +120,7 @@
120
120
  v-else
121
121
  :type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
122
122
  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-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"
123
- placeholder="Text"
123
+ :placeholder="$t('Text')"
124
124
  :value="currentValues[column.name]"
125
125
  @input="setCurrentValue(column.name, $event.target.value)"
126
126
  autocomplete="false"
@@ -13,14 +13,28 @@
13
13
  <component v-if="item.icon" :is="getIcon(item.icon)" class="w-5 h-5 text-lightSidebarIcons dark:text-darkSidebarIcons transition duration-75 group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover" ></component>
14
14
  <span class="ms-3">{{ item.label }}</span>
15
15
  <span v-if="item.badge" class="inline-flex items-center justify-center w-3 h-3 p-3 ms-3 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
16
- fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent">{{ item.badge }}</span>
16
+ fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent"
17
+ >
18
+
19
+ <Tooltip v-if="item.badgeTooltip">
20
+ {{ item.badge }}
21
+
22
+ <template #tooltip>
23
+ {{ item.badgeTooltip }}
24
+ </template>
25
+ </Tooltip>
26
+ <template v-else>
27
+ {{ item.badge }}
28
+ </template>
29
+
30
+ </span>
17
31
 
18
32
  </RouterLink>
19
33
  </template>
20
34
 
21
35
  <script setup lang="ts">
22
36
  import { getIcon } from '@/utils';
23
-
37
+ import { Tooltip } from '@/afcl';
24
38
  const props = defineProps(['item', 'isChild']);
25
39
 
26
40
 
@@ -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
@@ -64,9 +66,10 @@ 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();
69
71
 
72
+ const coreStore = useCoreStore();
70
73
  const router = useRouter();
71
74
  const route = useRoute();
72
75
  const props = defineProps({
@@ -233,13 +233,18 @@
233
233
  <!-- Help text -->
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
- <span v-else>{{ $t('Showing') }} </span>
237
- <span class="font-semibold text-gray-900 dark:text-white">
238
- {{ ((page || 1) - 1) * pageSize + 1 }}
239
- </span> {{ $t('to') }} <span class="font-semibold text-gray-900 dark:text-white">
240
- {{ Math.min((page || 1) * pageSize, totalRows) }}
241
- </span> {{ $t('of') }} <span class="font-semibold text-gray-900 dark:text-white">{{
242
- totalRows }}</span> <span class="hidden sm:inline">{{ $t('Entries') }}</span>
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
+ ">
245
+ </span>
246
+
247
+ </template>
243
248
  </span>
244
249
  </div>
245
250
  </template>
@@ -0,0 +1,55 @@
1
+ import { createI18n } from 'vue-i18n';
2
+ import { createApp } from 'vue';
3
+
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
+
24
+
25
+ export function initI18n(app: ReturnType<typeof createApp>) {
26
+ const i18n = createI18n({
27
+ legacy: false,
28
+
29
+ missingWarn: false,
30
+ fallbackWarn: false,
31
+
32
+ pluralRules: {
33
+ 'uk': slavicPluralRule,
34
+ 'bg': slavicPluralRule,
35
+ 'cs': slavicPluralRule,
36
+ 'hr': slavicPluralRule,
37
+ 'mk': slavicPluralRule,
38
+ 'pl': slavicPluralRule,
39
+ 'sk': slavicPluralRule,
40
+ 'sl': slavicPluralRule,
41
+ 'sr': slavicPluralRule,
42
+ 'be': slavicPluralRule,
43
+ 'ru': slavicPluralRule,
44
+ },
45
+
46
+ missing: (locale, key) => {
47
+ // very very dirty hack to make work $t("a {key} b", { key: "c" }) as "a c b" when translation is missing
48
+ // e.g. relevant for "Showing {from} to {to} of {total} entries" on list page
49
+ return key + ' ';
50
+ },
51
+ })
52
+
53
+ app.use(i18n);
54
+
55
+ }
@@ -1,21 +1,20 @@
1
1
  import { createApp } from 'vue'
2
- import { createI18n } from 'vue-i18n'
3
2
  import { createPinia } from 'pinia'
4
3
  /* IMPORTANT:ADMINFORTH IMPORTS */
5
4
 
6
5
 
7
6
  import App from './App.vue'
8
7
  import router from './router'
8
+ import { initI18n } from './i18n'
9
9
 
10
- export const app = createApp(App)
10
+ export const app: ReturnType<typeof createApp> = createApp(App)
11
11
  /* IMPORTANT:ADMINFORTH COMPONENT REGISTRATIONS */
12
12
 
13
- const i18n = createI18n({
14
- // something vue-i18n options here ...
15
- })
16
13
  app.use(createPinia())
17
14
  app.use(router)
18
- app.use(i18n)
15
+
16
+ initI18n(app);
17
+
19
18
 
20
19
  /* IMPORTANT:ADMINFORTH CUSTOM USES */
21
20
 
@@ -51,8 +51,8 @@ const countryName = computed(() => {
51
51
  <style scoped lang="scss">
52
52
 
53
53
  .flag-icon {
54
- width: 1.8rem;
55
- height: 1.35rem;
54
+ width: 1.6rem;
55
+ height: 1.2rem;
56
56
  flex-shrink: 0;
57
57
 
58
58
  // border radius for background
@@ -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
 
@@ -13,5 +13,9 @@ export interface CompletionAdapter {
13
13
  content: string,
14
14
  stop: string[],
15
15
  maxTokens: number
16
- ): Promise<{ content: string; finishReason: string }>;
16
+ ): Promise<{
17
+ content?: string;
18
+ finishReason?: string;
19
+ error?: string;
20
+ }>;
17
21
  }
@@ -301,6 +301,8 @@ export interface IAdminForth {
301
301
  [key: string]: IAdminForthDataSourceConnectorBase;
302
302
  };
303
303
 
304
+ tr(msg: string, category: string, lang: string, params: any): Promise<string>;
305
+
304
306
  createResourceRecord(
305
307
  params: { resource: AdminForthResource, record: any, adminUser: AdminUser, extra?: HttpExtra }
306
308
  ): Promise<{ error?: string, createdRecord?: any }>;
@@ -453,7 +455,16 @@ export type BeforeSaveFunction = (params: {
453
455
  recordId: any,
454
456
  adminUser: AdminUser,
455
457
  record: any,
456
- oldRecord?: any,
458
+ oldRecord: any,
459
+ adminforth: IAdminForth,
460
+ extra?: HttpExtra,
461
+ }) => Promise<{ok: boolean, error?: string}>;
462
+
463
+ export type BeforeCreateSaveFunction = (params: {
464
+ resource: AdminForthResource,
465
+ recordId: any,
466
+ adminUser: AdminUser,
467
+ record: any,
457
468
  adminforth: IAdminForth,
458
469
  extra?: HttpExtra,
459
470
  }) => Promise<{ok: boolean, error?: string}>;
@@ -711,7 +722,7 @@ export interface AdminForthResourceInput extends Omit<AdminForthResourceInputCom
711
722
  * - fill-in adminUser as creator of record
712
723
  * - Attach additional data to record before saving to database (mostly fillOnCreate should be used instead)
713
724
  */
714
- beforeSave?: BeforeSaveFunction | Array<BeforeSaveFunction>,
725
+ beforeSave?: BeforeCreateSaveFunction | Array<BeforeCreateSaveFunction>,
715
726
 
716
727
  /**
717
728
  * Typical use-cases:
@@ -1137,7 +1148,7 @@ export interface AdminForthResource extends Omit<AdminForthResourceInput, 'optio
1137
1148
  * - fill-in adminUser as creator of record
1138
1149
  * - Attach additional data to record before saving to database (mostly fillOnCreate should be used instead)
1139
1150
  */
1140
- beforeSave?: Array<BeforeSaveFunction>,
1151
+ beforeSave?: Array<BeforeCreateSaveFunction>,
1141
1152
 
1142
1153
  /**
1143
1154
  * Typical use-cases:
@@ -1191,7 +1202,9 @@ export interface AdminForthBulkAction extends AdminForthBulkActionCommon {
1191
1202
  * Callback which will be called on backend when user clicks on action button.
1192
1203
  * It should return Promise which will be resolved when action is done.
1193
1204
  */
1194
- action: ({ resource, selectedIds, adminUser }: { resource: AdminForthResource, selectedIds: Array<any>, adminUser: AdminUser }) => Promise<{ ok: boolean, error?: string, successMessage?: string }>,
1205
+ action: ({ resource, selectedIds, adminUser, tr }: {
1206
+ resource: AdminForthResource, selectedIds: Array<any>, adminUser: AdminUser, tr: (key: string, category?: string, params?: any) => string
1207
+ }) => Promise<{ ok: boolean, error?: string, successMessage?: string }>,
1195
1208
 
1196
1209
  /**
1197
1210
  * Allowed callback called to check whether action is allowed for user.
@@ -1284,7 +1297,7 @@ export interface IWebSocketClient {
1284
1297
  }
1285
1298
 
1286
1299
  export interface IWebSocketBroker {
1287
- publish: (topic: string, data: any) => void;
1300
+ publish: (topic: string, data: any, filterUsers?: (adminUser: AdminUser) => Promise<boolean>) => void;
1288
1301
 
1289
1302
  registerWsClient: (client: IWebSocketClient) => void;
1290
1303
  }
@@ -873,10 +873,17 @@ export interface AdminForthConfigMenuItem {
873
873
  */
874
874
  badge?: string | ((user: AdminUser) => Promise<string>),
875
875
 
876
+ /**
877
+ * Tooltip shown on hover for badge
878
+ */
879
+ badgeTooltip?: string,
880
+
881
+
882
+
876
883
  /**
877
884
  * Item id will be automatically generated from hashed resourceId+Path+label
878
885
  */
879
- _itemId?: string,
886
+ itemId?: string, // todo move to runtime type
880
887
  }
881
888
 
882
889
 
@@ -8,6 +8,7 @@ import { useCoreStore } from './stores/core';
8
8
  import { useUserStore } from './stores/user';
9
9
  import { Dropdown } from 'flowbite';
10
10
 
11
+ const LS_LANG_KEY = `afLanguage`;
11
12
 
12
13
  export async function callApi({path, method, body=undefined}: {
13
14
  path: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
@@ -17,6 +18,7 @@ export async function callApi({path, method, body=undefined}: {
17
18
  method,
18
19
  headers: {
19
20
  'Content-Type': 'application/json',
21
+ 'accept-language': localStorage.getItem(LS_LANG_KEY) || 'en',
20
22
  },
21
23
  body: JSON.stringify(body),
22
24
  };
@@ -43,10 +43,26 @@
43
43
  }"
44
44
  >
45
45
  <component
46
- v-if="action.icon"
46
+ v-if="action.icon && !bulkActionLoadingStates[action.id]"
47
47
  :is="getIcon(action.icon)"
48
48
  class="w-5 h-5 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white"></component>
49
-
49
+ <div v-if="bulkActionLoadingStates[action.id]">
50
+ <svg
51
+ aria-hidden="true"
52
+ class="w-5 h-5 animate-spin"
53
+ :class="{
54
+ 'text-gray-200 dark:text-gray-500 fill-gray-500 dark:fill-gray-300': action.state !== 'danger',
55
+ 'text-red-200 dark:text-red-800 fill-red-600 dark:fill-red-500': action.state === 'danger'
56
+ }"
57
+ viewBox="0 0 100 101"
58
+ fill="none"
59
+ xmlns="http://www.w3.org/2000/svg"
60
+ >
61
+ <path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
62
+ <path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
63
+ </svg>
64
+ <span class="sr-only">Loading...</span>
65
+ </div>
50
66
  {{ `${action.label} (${checkboxes.length})` }}
51
67
  </button>
52
68
 
@@ -151,6 +167,7 @@ watch(() => sort, async (to, from) => {
151
167
  const rows: Ref<any[]|null> = ref(null);
152
168
  const totalRows = ref(0);
153
169
  const checkboxes = ref([]);
170
+ const bulkActionLoadingStates = ref<{[key: string]: boolean}>({});
154
171
 
155
172
  const DEFAULT_PAGE_SIZE = 10;
156
173
 
@@ -252,6 +269,7 @@ async function startBulkAction(actionId) {
252
269
  return;
253
270
  }
254
271
  }
272
+ bulkActionLoadingStates.value[actionId] = true;
255
273
 
256
274
  const data = await callAdminForthApi({
257
275
  path: '/start_bulk_action',
@@ -264,6 +282,7 @@ async function startBulkAction(actionId) {
264
282
  }
265
283
  });
266
284
  if (data?.ok) {
285
+ bulkActionLoadingStates.value[actionId] = false;
267
286
  checkboxes.value = [];
268
287
  await getList();
269
288
 
@@ -276,6 +295,7 @@ async function startBulkAction(actionId) {
276
295
 
277
296
  }
278
297
  if (data?.error) {
298
+ bulkActionLoadingStates.value[actionId] = false;
279
299
  showErrorTost(data.error);
280
300
  }
281
301
  }
@@ -63,7 +63,7 @@
63
63
 
64
64
  <div v-if="coreStore.config.rememberMeDays"
65
65
  class="flex items-start mb-5"
66
- :title="`Stay logged in for ${coreStore.config.rememberMeDays} days`"
66
+ :title="$t(`Stay logged in for {days} days`, {days: coreStore.config.rememberMeDays})"
67
67
  >
68
68
  <Checkbox v-model="rememberMeValue" class="mr-2">
69
69
  {{ $t('Remember me') }}
@@ -3,8 +3,9 @@ export interface EmailAdapter {
3
3
  }
4
4
  export interface CompletionAdapter {
5
5
  complete(content: string, stop: string[], maxTokens: number): Promise<{
6
- content: string;
7
- finishReason: string;
6
+ content?: string;
7
+ finishReason?: string;
8
+ error?: string;
8
9
  }>;
9
10
  }
10
11
  //# sourceMappingURL=Adapters.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Adapters.d.ts","sourceRoot":"","sources":["../../types/Adapters.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,SAAS,CACP,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,OACf;CACH;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CACN,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACvD"}
1
+ {"version":3,"file":"Adapters.d.ts","sourceRoot":"","sources":["../../types/Adapters.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,SAAS,CACP,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,OACf;CACH;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CACN,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;QACT,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC,CAAC;CACJ"}
@@ -282,6 +282,7 @@ export interface IAdminForth {
282
282
  connectors: {
283
283
  [key: string]: IAdminForthDataSourceConnectorBase;
284
284
  };
285
+ tr(msg: string, category: string, lang: string, params: any): Promise<string>;
285
286
  createResourceRecord(params: {
286
287
  resource: AdminForthResource;
287
288
  record: any;
@@ -443,7 +444,18 @@ export type BeforeSaveFunction = (params: {
443
444
  recordId: any;
444
445
  adminUser: AdminUser;
445
446
  record: any;
446
- oldRecord?: any;
447
+ oldRecord: any;
448
+ adminforth: IAdminForth;
449
+ extra?: HttpExtra;
450
+ }) => Promise<{
451
+ ok: boolean;
452
+ error?: string;
453
+ }>;
454
+ export type BeforeCreateSaveFunction = (params: {
455
+ resource: AdminForthResource;
456
+ recordId: any;
457
+ adminUser: AdminUser;
458
+ record: any;
447
459
  adminforth: IAdminForth;
448
460
  extra?: HttpExtra;
449
461
  }) => Promise<{
@@ -680,7 +692,7 @@ export interface AdminForthResourceInput extends Omit<AdminForthResourceInputCom
680
692
  * - fill-in adminUser as creator of record
681
693
  * - Attach additional data to record before saving to database (mostly fillOnCreate should be used instead)
682
694
  */
683
- beforeSave?: BeforeSaveFunction | Array<BeforeSaveFunction>;
695
+ beforeSave?: BeforeCreateSaveFunction | Array<BeforeCreateSaveFunction>;
684
696
  /**
685
697
  * Typical use-cases:
686
698
  * - Initiate some trigger after record saved to database (e.g sync to another datasource)
@@ -1016,7 +1028,7 @@ export interface AdminForthResource extends Omit<AdminForthResourceInput, 'optio
1016
1028
  * - fill-in adminUser as creator of record
1017
1029
  * - Attach additional data to record before saving to database (mostly fillOnCreate should be used instead)
1018
1030
  */
1019
- beforeSave?: Array<BeforeSaveFunction>;
1031
+ beforeSave?: Array<BeforeCreateSaveFunction>;
1020
1032
  /**
1021
1033
  * Typical use-cases:
1022
1034
  * - Initiate some trigger after record saved to database (e.g sync to another datasource)
@@ -1060,10 +1072,11 @@ export interface AdminForthBulkAction extends AdminForthBulkActionCommon {
1060
1072
  * Callback which will be called on backend when user clicks on action button.
1061
1073
  * It should return Promise which will be resolved when action is done.
1062
1074
  */
1063
- action: ({ resource, selectedIds, adminUser }: {
1075
+ action: ({ resource, selectedIds, adminUser, tr }: {
1064
1076
  resource: AdminForthResource;
1065
1077
  selectedIds: Array<any>;
1066
1078
  adminUser: AdminUser;
1079
+ tr: (key: string, category?: string, params?: any) => string;
1067
1080
  }) => Promise<{
1068
1081
  ok: boolean;
1069
1082
  error?: string;
@@ -1151,7 +1164,7 @@ export interface IWebSocketClient {
1151
1164
  onClose: (handler: () => void) => void;
1152
1165
  }
1153
1166
  export interface IWebSocketBroker {
1154
- publish: (topic: string, data: any) => void;
1167
+ publish: (topic: string, data: any, filterUsers?: (adminUser: AdminUser) => Promise<boolean>) => void;
1155
1168
  registerWsClient: (client: IWebSocketClient) => void;
1156
1169
  }
1157
1170
  export {};