adminforth 2.49.3 → 2.50.0-next.10

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 (78) hide show
  1. package/commands/createApp/templates/.agents/skills/adminforth/SKILL.md.hbs +42 -0
  2. package/commands/createApp/templates/.agents/skills/adminforth-custom-vue/SKILL.md.hbs +340 -0
  3. package/commands/createApp/templates/.agents/skills/adminforth-hooks/SKILL.md.hbs +70 -0
  4. package/commands/createApp/templates/.agents/skills/adminforth-permissions/SKILL.md.hbs +59 -0
  5. package/commands/createApp/templates/AGENTS.md.hbs +71 -0
  6. package/commands/createApp/templates/CLAUDE.md.hbs +5 -0
  7. package/commands/createApp/templates/Dockerfile.hbs +5 -16
  8. package/commands/createApp/templates/package.json.hbs +6 -18
  9. package/commands/createApp/templates/readme.md.hbs +5 -24
  10. package/commands/createApp/utils.js +52 -5
  11. package/dist/dataConnectors/clickhouse.d.ts +16 -2
  12. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  13. package/dist/dataConnectors/clickhouse.js +65 -0
  14. package/dist/dataConnectors/clickhouse.js.map +1 -1
  15. package/dist/dataConnectors/mongo.d.ts +12 -1
  16. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  17. package/dist/dataConnectors/mongo.js +87 -0
  18. package/dist/dataConnectors/mongo.js.map +1 -1
  19. package/dist/dataConnectors/mysql.d.ts +12 -1
  20. package/dist/dataConnectors/mysql.d.ts.map +1 -1
  21. package/dist/dataConnectors/mysql.js +126 -1
  22. package/dist/dataConnectors/mysql.js.map +1 -1
  23. package/dist/dataConnectors/postgres.d.ts.map +1 -1
  24. package/dist/dataConnectors/postgres.js +2 -1
  25. package/dist/dataConnectors/postgres.js.map +1 -1
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +2 -3
  29. package/dist/index.js.map +1 -1
  30. package/dist/modules/configValidator.d.ts.map +1 -1
  31. package/dist/modules/configValidator.js +3 -2
  32. package/dist/modules/configValidator.js.map +1 -1
  33. package/dist/modules/restApi.d.ts.map +1 -1
  34. package/dist/modules/restApi.js +9 -1
  35. package/dist/modules/restApi.js.map +1 -1
  36. package/dist/servers/express.d.ts +2 -0
  37. package/dist/servers/express.d.ts.map +1 -1
  38. package/dist/servers/express.js +34 -7
  39. package/dist/servers/express.js.map +1 -1
  40. package/dist/servers/openapi.d.ts.map +1 -1
  41. package/dist/servers/openapi.js +2 -0
  42. package/dist/servers/openapi.js.map +1 -1
  43. package/dist/spa/package-lock.json +103 -10
  44. package/dist/spa/package.json +2 -1
  45. package/dist/spa/pnpm-lock.yaml +298 -282
  46. package/dist/spa/src/adminforth.ts +5 -3
  47. package/dist/spa/src/afcl/Link.vue +24 -4
  48. package/dist/spa/src/afcl/Modal.vue +4 -0
  49. package/dist/spa/src/components/AcceptModal.vue +51 -16
  50. package/dist/spa/src/components/CustomRangePicker.vue +47 -73
  51. package/dist/spa/src/components/ResourceForm.vue +7 -0
  52. package/dist/spa/src/components/ResourceListTable.vue +6 -3
  53. package/dist/spa/src/components/Sidebar.vue +1 -1
  54. package/dist/spa/src/stores/modal.ts +5 -1
  55. package/dist/spa/src/types/Back.ts +10 -1
  56. package/dist/spa/src/types/Common.ts +1 -1
  57. package/dist/spa/src/types/FrontendAPI.ts +10 -0
  58. package/dist/spa/src/types/adapters/AudioAdapter.ts +73 -0
  59. package/dist/spa/src/types/adapters/index.ts +10 -0
  60. package/dist/spa/src/utils/listUtils.ts +4 -1
  61. package/dist/spa/src/utils/utils.ts +33 -23
  62. package/dist/spa/src/views/ListView.vue +1 -1
  63. package/dist/spa/src/views/ShowView.vue +2 -1
  64. package/dist/types/Back.d.ts +14 -1
  65. package/dist/types/Back.d.ts.map +1 -1
  66. package/dist/types/Back.js.map +1 -1
  67. package/dist/types/Common.d.ts +1 -1
  68. package/dist/types/Common.d.ts.map +1 -1
  69. package/dist/types/FrontendAPI.d.ts +8 -0
  70. package/dist/types/FrontendAPI.d.ts.map +1 -1
  71. package/dist/types/FrontendAPI.js.map +1 -1
  72. package/dist/types/adapters/AudioAdapter.d.ts +52 -0
  73. package/dist/types/adapters/AudioAdapter.d.ts.map +1 -0
  74. package/dist/types/adapters/AudioAdapter.js +2 -0
  75. package/dist/types/adapters/AudioAdapter.js.map +1 -0
  76. package/dist/types/adapters/index.d.ts +1 -0
  77. package/dist/types/adapters/index.d.ts.map +1 -1
  78. package/package.json +2 -1
@@ -118,11 +118,13 @@ class FrontendAPI implements FrontendAPIInterface {
118
118
 
119
119
  confirm(params: Parameters<FrontendAPIInterface['confirm']>[0]): ReturnType<FrontendAPIInterface['confirm']> {
120
120
  return new Promise((resolve, reject) => {
121
- this.modalStore.setModalContent({
122
- content: params.message,
121
+ this.modalStore.setModalContent({
122
+ title: params.title,
123
+ guardMessage: params.guardMessage,
124
+ content: params.message,
123
125
  contentHTML: params.messageHtml,
124
126
  acceptText: params.yes || 'Yes',
125
- cancelText: params.no || 'Cancel'
127
+ cancelText: params.no || 'Cancel'
126
128
  })
127
129
  this.modalStore.onAcceptFunction = resolve
128
130
  this.modalStore.onCancelFunction = reject
@@ -1,17 +1,37 @@
1
1
  <template>
2
+ <a
3
+ v-if="isExternal"
4
+ v-bind="$attrs"
5
+ :href="to"
6
+ :target="target"
7
+ rel="noopener noreferrer"
8
+ :class="linkClasses"
9
+ >
10
+ <slot></slot>
11
+ </a>
12
+
2
13
  <router-link
14
+ v-else
3
15
  v-bind="$attrs"
4
16
  :to="to"
5
- class="afcl-link text-lightPrimary underline dark:text-darkPrimary hover:no-underline hover:brightness-110
6
- cursor-pointer"
17
+ :target="target"
18
+ :class="linkClasses"
7
19
  >
8
20
  <slot></slot>
9
21
  </router-link>
10
22
  </template>
11
23
 
12
24
  <script setup lang="ts">
25
+ import { computed } from 'vue';
13
26
 
14
- defineProps<{
27
+ const props = defineProps<{
15
28
  to: string,
16
- }>()
29
+ target?: 'blank' | 'self' | 'parent' | 'top'
30
+ }>();
31
+
32
+ const isExternal = computed(() => {
33
+ return typeof props.to === 'string' && props.to.startsWith('http');
34
+ });
35
+
36
+ const linkClasses = "afcl-link text-lightPrimary underline dark:text-darkPrimary hover:no-underline hover:brightness-110 cursor-pointer";
17
37
  </script>
@@ -104,6 +104,10 @@ async function close() {
104
104
  isModalOpen.value = false;
105
105
  }
106
106
 
107
+ defineOptions({
108
+ inheritAttrs: false,
109
+ })
110
+
107
111
  defineExpose({
108
112
  open: open,
109
113
  close: close,
@@ -4,26 +4,59 @@
4
4
  ref="modalRef"
5
5
  :beforeCloseFunction="()=>{modalStore.onAcceptFunction(false);modalStore.isOpened=false}"
6
6
  backgroundCustomClasses="z-[998]"
7
- modalCustomClasses="z-[999]"
7
+ modalCustomClasses="z-[999] flex items-center justify-center"
8
8
  >
9
- <div class="relative p-4 w-full max-w-md max-h-full" >
10
- <button type="button" @click="modalStore.togleModal()" class="absolute top-3 end-2.5 text-lightAcceptModalCloseIcon bg-transparent hover:bg-lightAcceptModalCloseIconHoverBackground hover:text-lightAcceptModalCloseIconHover rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:text-darkAcceptModalCloseIcon dark:hover:bg-darkAcceptModalCloseIconHoverBackground dark:hover:text-darkAcceptModalCloseIconHover" >
11
- <svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
9
+ <div class="relative p-6 sm:p-8 w-[440px] bg-white rounded-lg shadow-xl dark:bg-gray-800" >
10
+
11
+ <button type="button" @click="modalStore.togleModal()" class="absolute top-4 right-4 text-gray-400 hover:text-gray-900 transition-colors dark:hover:text-white" >
12
+ <svg class="w-3.5 h-3.5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
12
13
  <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"/>
13
14
  </svg>
14
- <span class="sr-only">{{ $t('Close modal') }}</span>
15
15
  </button>
16
- <div class="p-4 md:p-5 text-center">
17
- <svg class="mx-auto mb-4 text-lightAcceptModalWarningIcon w-12 h-12 dark:text-darkAcceptModalWarningIcon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
18
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
19
- </svg>
20
- <h3 class="afcl-confirmation-title mb-5 text-lg font-normal text-lightAcceptModalText dark:text-darkAcceptModalText">{{ modalStore?.modalContent?.content }}</h3>
21
- <h3 class=" afcl-confirmation-title mb-5 text-lg font-normal text-lightAcceptModalText dark:text-darkAcceptModalText" v-html="modalStore?.modalContent?.contentHTML"></h3>
22
16
 
23
- <button @click="()=>{ modalStore.onAcceptFunction(true);modalStore.togleModal()}" type="button" class="afcl-confirmation-accept-button text-lightAcceptModalConfirmButtonText bg-lightAcceptModalConfirmButtonBackground hover:bg-lightAcceptModalConfirmButtonBackgroundHover focus:ring-4 focus:outline-none focus:ring-lightAcceptModalConfirmButtonFocus font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center dark:text-darkAcceptModalConfirmButtonText dark:bg-darkAcceptModalConfirmButtonBackground dark:hover:bg-darkAcceptModalConfirmButtonBackgroundHover dark:focus:ring-darkAcceptModalConfirmButtonFocus">
24
- {{ modalStore?.modalContent?.acceptText }}
25
- </button>
26
- <button @click="()=>{modalStore.onAcceptFunction(false);modalStore.togleModal()}" type="button" class="afcl-confirmation-cancel-button py-2.5 px-5 ms-3 text-sm font-medium text-lightAcceptModalCancelButtonText focus:outline-none bg-lightAcceptModalCancelButtonBackground rounded-lg border border-lightAcceptModalCancelButtonBorder hover:bg-lightAcceptModalCancelButtonBackgroundHover hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-lightAcceptModalCancelButtonFocus dark:focus:ring-darkAcceptModalCancelButtonFocus dark:bg-darkAcceptModalCancelButtonBackground dark:text-darkAcceptModalCancelButtonText dark:border-darkAcceptModalCancelButtonBorder dark:hover:text-darkAcceptModalCancelButtonTextHover dark:hover:bg-darkAcceptModalCancelButtonBackgroundHover">{{ modalStore?.modalContent?.cancelText }}</button>
17
+ <div class="text-center flex flex-col">
18
+ <div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-red-50 mb-4 relative dark:bg-red-900/20">
19
+ <IconClipboardDocumentSolid class="w-10 h-10 text-red-500" />
20
+ <div class="absolute bottom-1 right-1 bg-red-500 rounded-full w-[18px] h-[18px] flex items-center justify-center border-2 border-white dark:border-gray-800">
21
+ <span class="text-white text-[10px] font-medium">!</span>
22
+ </div>
23
+ </div>
24
+
25
+ <div class="flex flex-col gap-3">
26
+ <h3 v-if="modalStore?.modalContent?.title" class="text-2xl font-bold text-gray-900 dark:text-white">
27
+ {{ modalStore.modalContent.title }}
28
+ </h3>
29
+
30
+ <div class="text-[15px] text-gray-600 dark:text-gray-300">
31
+ <div v-if="modalStore?.modalContent?.contentHTML" class="font-medium" v-html="modalStore.modalContent.contentHTML"></div>
32
+ <p v-else-if="modalStore?.modalContent?.content" class="font-medium">{{ modalStore.modalContent.content }}</p>
33
+ </div>
34
+ </div>
35
+
36
+ <hr class="border-t-2 border-gray-300 dark:border-gray-700 my-6">
37
+
38
+ <div class="flex justify-center gap-4 w-full">
39
+ <button @click="()=>{modalStore.onAcceptFunction(false);modalStore.togleModal()}" type="button" class="flex-1 py-2.5 px-4 text-sm font-medium text-gray-700 bg-white rounded-lg border border-gray-200 hover:bg-gray-50 focus:ring-4 focus:ring-gray-100 transition-all dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700">
40
+ {{ modalStore?.modalContent?.cancelText }}
41
+ </button>
42
+
43
+ <button
44
+ @click="()=>{ modalStore.onAcceptFunction(true);modalStore.togleModal()}"
45
+ type="button"
46
+ class="flex-1 flex items-center justify-center py-2.5 px-4 text-sm font-medium transition-all focus:outline-none
47
+ text-white bg-red-600 hover:bg-red-700
48
+ dark:bg-red-500 dark:hover:bg-red-600
49
+ border-none rounded-lg shadow-sm focus:z-10 focus:ring-4
50
+ focus:ring-red-100 dark:focus:ring-red-900 gap-1">
51
+ {{ modalStore?.modalContent?.acceptText }}
52
+ </button>
53
+ </div>
54
+
55
+ <div v-if="modalStore?.modalContent?.guardMessage" class="flex items-center justify-center mt-6 text-xs text-gray-400 gap-1.5 font-medium">
56
+ <IconShieldCheck class="w-4 h-4" />
57
+ <span> {{ modalStore?.modalContent?.guardMessage }}</span>
58
+ </div>
59
+
27
60
  </div>
28
61
  </div>
29
62
  </Modal>
@@ -31,9 +64,11 @@
31
64
  </template>
32
65
 
33
66
  <script setup lang="ts">
34
- import { watch, onMounted, nextTick, ref } from 'vue';
67
+ import { watch, ref } from 'vue';
35
68
  import { useModalStore } from '@/stores/modal';
36
69
  import { Modal } from '@/afcl';
70
+ import { IconClipboardDocumentSolid, IconShieldCheck } from '@iconify-prerendered/vue-heroicons';
71
+
37
72
 
38
73
  const modalRef = ref();
39
74
  const modalStore = useModalStore();
@@ -18,7 +18,7 @@
18
18
  v-model="end"
19
19
  >
20
20
 
21
- <div v-if="min && max" class="w-full px-2.5">
21
+ <div v-if="min !== undefined && max !== undefined" class="w-full px-2.5">
22
22
  <RangePicker
23
23
  :dot-size="20"
24
24
  height="7.99px"
@@ -31,96 +31,70 @@
31
31
  </div>
32
32
  </template>
33
33
  <script setup lang="ts">
34
- import {computed, onMounted, ref, watch} from "vue";
34
+ import { computed, ref, watch } from "vue";
35
35
  import debounce from 'debounce'
36
36
  import RangePicker from './RangePicker.vue';
37
37
 
38
38
  const props = defineProps<{
39
- valueStart: number | null,
40
- valueEnd: number | null,
41
- min: number,
42
- max: number,
39
+ valueStart: number | null | string,
40
+ valueEnd: number | null | string,
41
+ min: number | string | undefined,
42
+ max: number | string | undefined,
43
43
  }>()
44
44
 
45
45
  const emit = defineEmits(['update:valueStart', 'update:valueEnd']);
46
46
 
47
- const minFormatted = computed(() => Math.floor(<number>props.min));
48
- const maxFormatted = computed(() => Math.ceil(<number>props.max));
47
+ const minFormatted = computed(() => {
48
+ const v = Number(props.min);
49
+ return isNaN(v) ? 0 : Math.floor(v);
50
+ });
49
51
 
52
+ const maxFormatted = computed(() => {
53
+ const v = Number(props.max);
54
+ return isNaN(v) ? 100 : Math.ceil(v);
55
+ });
50
56
 
51
- const start = ref<number | null>(props.valueStart);
52
- const end = ref<number | null>(props.valueEnd);
57
+ const normalize = (val: any) => {
58
+ if (val === "" || val === null || val === undefined) return null;
59
+ const numericValue = Number(val);
60
+ return isNaN(numericValue) ? null : numericValue;
61
+ };
53
62
 
54
- const sliderValue = ref<[number, number]>([minFormatted.value, maxFormatted.value]);
63
+ const start = ref<number | null>(normalize(props.valueStart));
64
+ const end = ref<number | null>(normalize(props.valueEnd));
55
65
 
56
- watch([start, end], () => {
57
- if ( !start.value && end.value ) {
58
- setSliderValues(minFormatted.value, end.value);
59
- } else if ( start.value && !end.value ) {
60
- setSliderValues(start.value, maxFormatted.value);
61
- } else if ( !start.value && !end.value ) {
62
- setSliderValues(minFormatted.value, maxFormatted.value);
63
- } else {
64
- setSliderValues(start.value, end.value);
65
- }
66
- })
66
+ const sliderValue = ref<[number, number]>([
67
+ start.value ?? minFormatted.value,
68
+ end.value ?? maxFormatted.value
69
+ ]);
67
70
 
68
- const updateFromSlider =
69
- debounce((value: [number, number]) => {
70
- start.value = value[0] === minFormatted.value ? null : value[0];
71
- end.value = value[1] === maxFormatted.value ? null : value[1];
72
- }, 500);
71
+ function setSliderValues(s: number | null, e: number | null) {
72
+ sliderValue.value = [s ?? minFormatted.value, e ?? maxFormatted.value];
73
+ }
73
74
 
74
- onMounted(() => {
75
- updateStartFromProps();
76
- updateEndFromProps();
75
+ watch([start, end], () => {
76
+ setSliderValues(start.value, end.value);
77
+ }, { immediate: true });
77
78
 
78
- watch(() => props.valueStart, (value) => {
79
- updateStartFromProps();
80
- });
79
+ const updateFromSlider = debounce((value: [number, number]) => {
80
+ start.value = value[0] === minFormatted.value ? null : value[0];
81
+ end.value = value[1] === maxFormatted.value ? null : value[1];
82
+ }, 500);
81
83
 
82
- watch(() => props.valueEnd, (value) => {
83
- updateEndFromProps();
84
- });
85
- })
84
+ watch(() => props.valueStart, (newVal) => {
85
+ const v = normalize(newVal);
86
+ if (v !== start.value) start.value = v;
87
+ });
86
88
 
87
- function updateStartFromProps() {
88
- if (props.valueStart == start.value) {
89
- return;
90
- }
91
- start.value = props.valueStart;
92
- setSliderValues(start.value, end.value)
93
- }
89
+ watch(() => props.valueEnd, (newVal) => {
90
+ const v = normalize(newVal);
91
+ if (v !== end.value) end.value = v;
92
+ });
94
93
 
95
- function updateEndFromProps() {
96
- if (props.valueEnd == end.value) {
97
- return;
98
- }
99
- end.value = props.valueEnd;
100
- setSliderValues(start.value, end.value)
101
- }
94
+ watch(start, (newVal) => emit('update:valueStart', newVal));
95
+ watch(end, (newVal) => emit('update:valueEnd', newVal));
102
96
 
103
- watch(start, () => {
104
- emit('update:valueStart', start.value)
97
+ watch([minFormatted, maxFormatted], () => {
98
+ setSliderValues(start.value, end.value);
105
99
  })
106
-
107
- watch(end, () => {
108
- emit('update:valueEnd', end.value);
109
- })
110
-
111
- watch([minFormatted,maxFormatted], () => {
112
- if ( !start.value && end.value ) {
113
- setSliderValues(minFormatted.value, end.value);
114
- } else if ( start.value && !end.value ) {
115
- setSliderValues(start.value, maxFormatted.value);
116
- } else if ( !start.value && !end.value ) {
117
- setSliderValues(minFormatted.value, maxFormatted.value);
118
- } else {
119
- setSliderValues(start.value, end.value);
120
- }
121
- })
122
-
123
- function setSliderValues(start: any, end: any) {
124
- sliderValue.value = [start || minFormatted.value, end || maxFormatted.value];
125
- }
126
100
  </script>
@@ -490,6 +490,13 @@ async function validateUsingUserValidationFunction(editableColumnsInner: AdminFo
490
490
  }
491
491
  }
492
492
 
493
+ watch(customComponentsInValidity, () => {
494
+ editableColumns.value?.forEach(column => {
495
+ checkIfColumnHasError(column);
496
+ });
497
+ isValid.value = checkIfAnyColumnHasErrors();
498
+ });
499
+
493
500
  defineExpose({
494
501
  columnError,
495
502
  editableColumns,
@@ -372,7 +372,7 @@ import CallActionWrapper from '@/components/CallActionWrapper.vue'
372
372
  const coreStore = useCoreStore();
373
373
  const { t } = useI18n();
374
374
  const { alert, confirm } = useAdminforth();
375
- const props = defineProps<{
375
+ const props = withDefaults(defineProps<{
376
376
  page: number,
377
377
  resource: AdminForthResourceFrontend | null,
378
378
  rows: any[] | null,
@@ -389,7 +389,9 @@ const props = defineProps<{
389
389
  customActionIconsThreeDotsMenuItems?: AdminForthComponentDeclaration[]
390
390
  tableRowReplaceInjection?: AdminForthComponentDeclaration,
391
391
  isVirtualScrollEnabled: boolean
392
- }>();
392
+ }>(), {
393
+ sort: () => []
394
+ });
393
395
 
394
396
  //select between all rows or rows, that should be rendered in virtual scroll
395
397
  const rowsToRender = computed(() => {
@@ -582,7 +584,8 @@ async function onClick(e: any, row: any) {
582
584
 
583
585
  async function deleteRecord(row: any) {
584
586
  const data = await confirm({
585
- message: t('Are you sure you want to delete this item?'),
587
+ title: t('Are you sure you want to delete this item?'),
588
+ message: t(`This process is irreversible.`),
586
589
  yes: t('Delete'),
587
590
  no: t('Cancel'),
588
591
  });
@@ -32,7 +32,7 @@
32
32
  <img v-if="coreStore.config?.iconOnlySidebar?.logo" :src="loadFile(coreStore.config?.iconOnlySidebar?.logo || '')" :alt="`${ coreStore.config?.brandName } Logo`" class="af-sidebar-icon-only-logo h-8" :class="{ 'hidden': !(coreStore.config?.showBrandLogoInSidebar !== false && coreStore.config?.iconOnlySidebar?.logo && iconOnlySidebarEnabled && isSidebarIconOnly && !isSidebarHovering) }" />
33
33
  <span
34
34
  v-if="coreStore.config?.showBrandNameInSidebar && (!iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))"
35
- class="af-title self-center text-lightNavbarText-size font-semibold sm:text-lightNavbarText-size whitespace-nowrap dark:text-darkSidebarText text-lightSidebarText"
35
+ class="af-title self-center text-lightNavbarText-size font-semibold sm:text-lightNavbarText-size whitespace-nowrap dark:text-darkSidebarText text-lightSidebarText truncate"
36
36
  >
37
37
  {{ coreStore.config?.brandName }}
38
38
  </span>
@@ -3,6 +3,7 @@ import { defineStore } from 'pinia'
3
3
 
4
4
  type ModalContentType = {
5
5
  title?: string;
6
+ guardMessage?: string;
6
7
  content?: string;
7
8
  contentHTML?: string;
8
9
  acceptText?: string;
@@ -13,6 +14,7 @@ import { defineStore } from 'pinia'
13
14
  export const useModalStore = defineStore('modal', () => {
14
15
  const modalContent = ref({
15
16
  title: 'title',
17
+ guardMessage: 'guardMessage',
16
18
  content: '',
17
19
  contentHTML: '',
18
20
  acceptText: 'acceptText',
@@ -32,7 +34,8 @@ export const useModalStore = defineStore('modal', () => {
32
34
  }
33
35
  function setModalContent(content: ModalContentType) {
34
36
  modalContent.value = {
35
- title: content.title || 'title',
37
+ title: content.title || '',
38
+ guardMessage: content.guardMessage || '',
36
39
  content: content.content || '',
37
40
  contentHTML: content.contentHTML || '',
38
41
  acceptText: content.acceptText || 'acceptText',
@@ -43,6 +46,7 @@ export const useModalStore = defineStore('modal', () => {
43
46
  isOpened.value = false;
44
47
  modalContent.value = {
45
48
  title: 'title',
49
+ guardMessage: 'guardMessage',
46
50
  content: 'content',
47
51
  contentHTML: '',
48
52
  acceptText: 'acceptText',
@@ -62,6 +62,8 @@ export interface IAdminForthEndpointOptions {
62
62
  request_schema?: AnySchemaObject,
63
63
  response_schema?: AnySchemaObject,
64
64
  responce_schema?: AnySchemaObject,
65
+ meta?: Record<string, unknown>,
66
+ target?: 'json' | 'upload',
65
67
  handler: (input: IAdminForthEndpointHandlerInput) => void | Promise<any>,
66
68
  }
67
69
 
@@ -82,14 +84,21 @@ export interface IAdminForthExpressRouteSchema {
82
84
  * JSON schema or Zod schema describing the JSON response body for a custom Express route.
83
85
  */
84
86
  response?: AdminForthExpressSchemaInput;
87
+
88
+ /**
89
+ * Internal metadata for AdminForth integrations. This is not rendered in the OpenAPI document.
90
+ */
91
+ meta?: Record<string, unknown>;
85
92
  }
86
93
 
87
94
  export interface IRegisteredApiSchema {
88
95
  method: string;
89
96
  path: string;
90
97
  description?: string;
98
+ meta?: Record<string, unknown>;
91
99
  request_schema?: AnySchemaObject;
92
100
  response_schema?: AnySchemaObject;
101
+ handler?: (input: IAdminForthEndpointHandlerInput) => void | Promise<any>;
93
102
  }
94
103
 
95
104
  export interface IAdminForthApiValidationError {
@@ -1416,7 +1425,7 @@ export interface AdminForthActionInput {
1416
1425
  adminUser: AdminUser;
1417
1426
  standardAllowedActions: AllowedActions;
1418
1427
  }) => boolean | Promise<boolean>);
1419
- url?: string;
1428
+ url?: string | ((params: { adminUser: AdminUser; resource: AdminForthResource; recordId: string, record: any }) => string);
1420
1429
  bulkHandler?: (params: {
1421
1430
  adminforth: IAdminForth;
1422
1431
  resource: AdminForthResource;
@@ -66,7 +66,7 @@ export enum ActionCheckSource {
66
66
  EditRequest = 'editRequest',
67
67
  CreateRequest = 'createRequest',
68
68
  DeleteRequest = 'deleteRequest',
69
- BulkActionRequest = 'bulkActionRequest',
69
+ BulkActionRequest = 'bulkActionRequest', // @deprecated beacuse whole bulk action is deprecated in favor of custom actions, never use this value in new code
70
70
  CustomActionRequest = 'customActionRequest',
71
71
  }
72
72
 
@@ -160,6 +160,16 @@ export interface FrontendAPIInterface {
160
160
  }
161
161
 
162
162
  export type ConfirmParams = {
163
+ /**
164
+ * The title to display in the dialog
165
+ */
166
+ title?: string;
167
+
168
+ /**
169
+ * The message to display in the dialog as a warning that action is irreversible
170
+ */
171
+ guardMessage?: string;
172
+
163
173
  /**
164
174
  * The message to display in the dialog
165
175
  */
@@ -0,0 +1,73 @@
1
+ export type SpeechToTextInput = {
2
+ buffer: Buffer;
3
+ filename: string;
4
+ mimeType: string;
5
+ language?: string;
6
+ prompt?: string;
7
+ };
8
+
9
+ export type SpeechToTextResult = {
10
+ text: string;
11
+ language?: string;
12
+ raw?: unknown;
13
+ };
14
+
15
+ export interface SpeechToTextAdapter {
16
+ name: string;
17
+
18
+ validate(): void;
19
+
20
+ transcribe(input: SpeechToTextInput): Promise<SpeechToTextResult>;
21
+ }
22
+
23
+ export type TtsAudioFormat =
24
+ | "mp3"
25
+ | "opus"
26
+ | "aac"
27
+ | "flac"
28
+ | "wav"
29
+ | "pcm";
30
+
31
+ export type TextToSpeechInput<Voice extends string = string> = {
32
+ text: string;
33
+ voice?: Voice;
34
+ format?: TtsAudioFormat;
35
+ speed?: number;
36
+ instructions?: string;
37
+ stream?: false;
38
+ };
39
+
40
+ export type TextToSpeechResult = {
41
+ audio: Buffer;
42
+ mimeType: string;
43
+ format: TtsAudioFormat;
44
+ raw?: unknown;
45
+ };
46
+
47
+ export type TtsStreamFormat = "audio" | "sse";
48
+
49
+ export type TextToSpeechStreamInput<Voice extends string = string> =
50
+ Omit<TextToSpeechInput<Voice>, "stream"> & {
51
+ stream: true;
52
+ streamFormat?: TtsStreamFormat;
53
+ };
54
+
55
+ export type TextToSpeechStreamResult = {
56
+ audioStream: ReadableStream<Uint8Array>;
57
+ mimeType: string;
58
+ format: TtsAudioFormat;
59
+ streamFormat: TtsStreamFormat;
60
+ raw?: unknown;
61
+ };
62
+
63
+ export interface TextToSpeechAdapter<Voice extends string = string> {
64
+ name: string;
65
+
66
+ validate(): void;
67
+
68
+ synthesize(input: TextToSpeechStreamInput<Voice>): Promise<TextToSpeechStreamResult>;
69
+ synthesize(input: TextToSpeechInput<Voice>): Promise<TextToSpeechResult>;
70
+ }
71
+
72
+ export type AudioAdapter<Voice extends string = string> =
73
+ SpeechToTextAdapter & TextToSpeechAdapter<Voice>;
@@ -15,3 +15,13 @@ export type { ImageVisionAdapter } from './ImageVisionAdapter.js';
15
15
  export type { OAuth2Adapter } from './OAuth2Adapter.js';
16
16
  export type { StorageAdapter } from './StorageAdapter.js';
17
17
  export type { CaptchaAdapter } from './CaptchaAdapter.js';
18
+ export type {
19
+ AudioAdapter,
20
+ SpeechToTextAdapter,
21
+ SpeechToTextInput,
22
+ SpeechToTextResult,
23
+ TextToSpeechAdapter,
24
+ TextToSpeechInput,
25
+ TextToSpeechResult,
26
+ TtsAudioFormat,
27
+ } from './AudioAdapter.js';
@@ -3,6 +3,7 @@ import { callAdminForthApi } from '@/utils';
3
3
  import { type AdminForthResourceFrontend } from '../types/Common';
4
4
  import { useAdminforth } from '@/adminforth';
5
5
  import { showErrorTost } from '@/composables/useFrontendApi'
6
+ import { useI18n } from 'vue-i18n';
6
7
 
7
8
  let getResourceDataLastAbortController: AbortController | null = null;
8
9
  export async function getList(resource: AdminForthResourceFrontend, isPageLoaded: boolean, page: number | null , pageSize: number, sort: any, checkboxes:{ value: any[] }, filters: any = [] ) {
@@ -57,10 +58,12 @@ export async function startBulkAction(actionId: string, resource: AdminForthReso
57
58
  bulkActionLoadingStates: {value: Record<string, boolean>}, getListInner: () => Promise<any>) {
58
59
  const action = resource?.options?.bulkActions?.find(a => a.id === actionId);
59
60
  const { confirm, alert } = useAdminforth();
61
+ const { t } = useI18n();
60
62
 
61
63
  if (action?.confirm) {
62
64
  const confirmed = await confirm({
63
- message: action.confirm,
65
+ title: action.confirm,
66
+ message: t(`Deleting ${checkboxes.value.length} ${checkboxes.value.length === 1 ? 'item' : 'items'}. This process is irreversible.`),
64
67
  });
65
68
  if (!confirmed) {
66
69
  return;