adminforth 2.4.0-next.3 → 2.4.0-next.300

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 (206) hide show
  1. package/commands/callTsProxy.js +14 -4
  2. package/commands/cli.js +12 -4
  3. package/commands/createApp/templates/api.ts.hbs +10 -0
  4. package/commands/createApp/templates/custom/tsconfig.json.hbs +2 -3
  5. package/commands/createApp/templates/index.ts.hbs +13 -5
  6. package/commands/createApp/templates/package.json.hbs +1 -1
  7. package/commands/createApp/utils.js +45 -7
  8. package/commands/createCustomComponent/configLoader.js +17 -4
  9. package/commands/createCustomComponent/configUpdater.js +25 -21
  10. package/commands/createCustomComponent/fileGenerator.js +1 -1
  11. package/commands/createCustomComponent/main.js +15 -8
  12. package/commands/createCustomComponent/templates/customCrud/beforeActionButtons.vue.hbs +38 -0
  13. package/commands/createCustomComponent/templates/customCrud/saveButton.vue.hbs +28 -0
  14. package/commands/createCustomComponent/templates/login/beforeLogin.vue.hbs +18 -0
  15. package/commands/createPlugin/templates/custom/tsconfig.json.hbs +2 -5
  16. package/commands/createPlugin/templates/package.json.hbs +1 -1
  17. package/commands/generateModels.js +30 -22
  18. package/dist/auth.d.ts +9 -1
  19. package/dist/auth.d.ts.map +1 -1
  20. package/dist/auth.js +21 -2
  21. package/dist/auth.js.map +1 -1
  22. package/dist/dataConnectors/baseConnector.d.ts +1 -1
  23. package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  24. package/dist/dataConnectors/baseConnector.js +69 -17
  25. package/dist/dataConnectors/baseConnector.js.map +1 -1
  26. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  27. package/dist/dataConnectors/clickhouse.js +15 -0
  28. package/dist/dataConnectors/clickhouse.js.map +1 -1
  29. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  30. package/dist/dataConnectors/mongo.js +50 -15
  31. package/dist/dataConnectors/mongo.js.map +1 -1
  32. package/dist/dataConnectors/mysql.d.ts.map +1 -1
  33. package/dist/dataConnectors/mysql.js +11 -0
  34. package/dist/dataConnectors/mysql.js.map +1 -1
  35. package/dist/dataConnectors/postgres.d.ts.map +1 -1
  36. package/dist/dataConnectors/postgres.js +43 -14
  37. package/dist/dataConnectors/postgres.js.map +1 -1
  38. package/dist/dataConnectors/sqlite.d.ts.map +1 -1
  39. package/dist/dataConnectors/sqlite.js +11 -0
  40. package/dist/dataConnectors/sqlite.js.map +1 -1
  41. package/dist/index.d.ts +12 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +42 -21
  44. package/dist/index.js.map +1 -1
  45. package/dist/modules/codeInjector.d.ts +2 -0
  46. package/dist/modules/codeInjector.d.ts.map +1 -1
  47. package/dist/modules/codeInjector.js +67 -12
  48. package/dist/modules/codeInjector.js.map +1 -1
  49. package/dist/modules/configValidator.d.ts +6 -0
  50. package/dist/modules/configValidator.d.ts.map +1 -1
  51. package/dist/modules/configValidator.js +203 -25
  52. package/dist/modules/configValidator.js.map +1 -1
  53. package/dist/modules/restApi.d.ts +1 -1
  54. package/dist/modules/restApi.d.ts.map +1 -1
  55. package/dist/modules/restApi.js +172 -31
  56. package/dist/modules/restApi.js.map +1 -1
  57. package/dist/modules/styles.d.ts +499 -13
  58. package/dist/modules/styles.d.ts.map +1 -1
  59. package/dist/modules/styles.js +555 -31
  60. package/dist/modules/styles.js.map +1 -1
  61. package/dist/modules/utils.d.ts +7 -15
  62. package/dist/modules/utils.d.ts.map +1 -1
  63. package/dist/modules/utils.js +45 -68
  64. package/dist/modules/utils.js.map +1 -1
  65. package/dist/servers/express.d.ts +5 -0
  66. package/dist/servers/express.d.ts.map +1 -1
  67. package/dist/servers/express.js +40 -1
  68. package/dist/servers/express.js.map +1 -1
  69. package/dist/spa/index.html +1 -1
  70. package/dist/spa/package-lock.json +1208 -708
  71. package/dist/spa/package.json +34 -34
  72. package/dist/spa/src/App.vue +58 -173
  73. package/dist/spa/src/adminforth.ts +42 -18
  74. package/dist/spa/src/afcl/AreaChart.vue +0 -1
  75. package/dist/spa/src/afcl/BarChart.vue +2 -2
  76. package/dist/spa/src/afcl/Button.vue +6 -6
  77. package/dist/spa/src/afcl/ButtonGroup.vue +91 -0
  78. package/dist/spa/src/afcl/Card.vue +25 -0
  79. package/dist/spa/src/afcl/Checkbox.vue +21 -13
  80. package/dist/spa/src/afcl/CountryFlag.vue +4 -1
  81. package/dist/spa/src/{components/CustomDatePicker.vue → afcl/DatePicker.vue} +95 -9
  82. package/dist/spa/src/afcl/Dialog.vue +47 -27
  83. package/dist/spa/src/afcl/Dropzone.vue +103 -44
  84. package/dist/spa/src/afcl/Input.vue +18 -8
  85. package/dist/spa/src/afcl/JsonViewer.vue +25 -0
  86. package/dist/spa/src/afcl/Link.vue +1 -1
  87. package/dist/spa/src/afcl/LinkButton.vue +3 -3
  88. package/dist/spa/src/afcl/PieChart.vue +5 -5
  89. package/dist/spa/src/afcl/ProgressBar.vue +7 -7
  90. package/dist/spa/src/afcl/Select.vue +82 -34
  91. package/dist/spa/src/afcl/Skeleton.vue +6 -6
  92. package/dist/spa/src/afcl/Table.vue +309 -73
  93. package/dist/spa/src/afcl/Textarea.vue +31 -0
  94. package/dist/spa/src/afcl/Toggle.vue +32 -0
  95. package/dist/spa/src/afcl/Tooltip.vue +28 -18
  96. package/dist/spa/src/afcl/VerticalTabs.vue +16 -7
  97. package/dist/spa/src/afcl/index.ts +6 -3
  98. package/dist/spa/src/components/AcceptModal.vue +48 -14
  99. package/dist/spa/src/components/Breadcrumbs.vue +5 -5
  100. package/dist/spa/src/components/CallActionWrapper.vue +15 -0
  101. package/dist/spa/src/components/ColumnValueInput.vue +38 -18
  102. package/dist/spa/src/components/ColumnValueInputWrapper.vue +4 -3
  103. package/dist/spa/src/components/CustomDateRangePicker.vue +9 -8
  104. package/dist/spa/src/components/CustomRangePicker.vue +37 -21
  105. package/dist/spa/src/components/ErrorMessage.vue +21 -0
  106. package/dist/spa/src/components/Filters.vue +194 -131
  107. package/dist/spa/src/components/GroupsTable.vue +9 -8
  108. package/dist/spa/src/components/MenuLink.vue +90 -23
  109. package/dist/spa/src/components/ResourceForm.vue +94 -51
  110. package/dist/spa/src/components/ResourceListTable.vue +120 -90
  111. package/dist/spa/src/components/ResourceListTableVirtual.vue +118 -84
  112. package/dist/spa/src/components/ShowTable.vue +21 -15
  113. package/dist/spa/src/components/Sidebar.vue +470 -0
  114. package/dist/spa/src/components/SingleSkeletLoader.vue +6 -6
  115. package/dist/spa/src/components/SkeleteLoader.vue +3 -3
  116. package/dist/spa/src/components/ThreeDotsMenu.vue +84 -15
  117. package/dist/spa/src/components/Toast.vue +40 -29
  118. package/dist/spa/src/components/UserMenuSettingsButton.vue +69 -0
  119. package/dist/spa/src/components/ValueRenderer.vue +44 -17
  120. package/dist/spa/src/controls/BoolToggle.vue +34 -0
  121. package/dist/spa/src/i18n.ts +5 -3
  122. package/dist/spa/src/main.ts +1 -1
  123. package/dist/spa/src/renderers/CompactField.vue +1 -1
  124. package/dist/spa/src/renderers/CompactUUID.vue +1 -1
  125. package/dist/spa/src/router/index.ts +8 -0
  126. package/dist/spa/src/shims-vue.d.ts +5 -0
  127. package/dist/spa/src/spa_types/core.ts +13 -1
  128. package/dist/spa/src/stores/core.ts +13 -1
  129. package/dist/spa/src/stores/filters.ts +33 -2
  130. package/dist/spa/src/stores/modal.ts +6 -1
  131. package/dist/spa/src/stores/toast.ts +22 -3
  132. package/dist/spa/src/types/Back.ts +164 -23
  133. package/dist/spa/src/types/Common.ts +92 -32
  134. package/dist/spa/src/types/FrontendAPI.ts +31 -5
  135. package/dist/spa/src/types/adapters/CaptchaAdapter.ts +34 -0
  136. package/dist/spa/src/types/adapters/CompletionAdapter.ts +25 -0
  137. package/dist/spa/src/types/adapters/EmailAdapter.ts +27 -0
  138. package/dist/spa/src/types/adapters/ImageGenerationAdapter.ts +50 -0
  139. package/dist/spa/src/types/adapters/ImageVisionAdapter.ts +30 -0
  140. package/dist/spa/src/types/adapters/KeyValueAdapter.ts +16 -0
  141. package/dist/spa/src/types/adapters/OAuth2Adapter.ts +34 -0
  142. package/dist/spa/src/types/adapters/StorageAdapter.ts +73 -0
  143. package/dist/spa/src/types/adapters/index.ts +8 -0
  144. package/dist/spa/src/utils.ts +291 -11
  145. package/dist/spa/src/views/CreateView.vue +63 -21
  146. package/dist/spa/src/views/EditView.vue +55 -22
  147. package/dist/spa/src/views/ListView.vue +144 -87
  148. package/dist/spa/src/views/LoginView.vue +60 -55
  149. package/dist/spa/src/views/ResourceParent.vue +2 -2
  150. package/dist/spa/src/views/SettingsView.vue +121 -0
  151. package/dist/spa/src/views/ShowView.vue +83 -53
  152. package/dist/spa/src/websocket.ts +6 -1
  153. package/dist/spa/tsconfig.app.json +1 -1
  154. package/dist/spa/vite.config.ts +45 -2
  155. package/dist/types/Back.d.ts +147 -14
  156. package/dist/types/Back.d.ts.map +1 -1
  157. package/dist/types/Back.js +15 -0
  158. package/dist/types/Back.js.map +1 -1
  159. package/dist/types/Common.d.ts +107 -29
  160. package/dist/types/Common.d.ts.map +1 -1
  161. package/dist/types/Common.js.map +1 -1
  162. package/dist/types/FrontendAPI.d.ts +31 -3
  163. package/dist/types/FrontendAPI.d.ts.map +1 -1
  164. package/dist/types/FrontendAPI.js.map +1 -1
  165. package/dist/types/adapters/CaptchaAdapter.d.ts +30 -0
  166. package/dist/types/adapters/CaptchaAdapter.d.ts.map +1 -0
  167. package/dist/types/adapters/CaptchaAdapter.js +5 -0
  168. package/dist/types/adapters/CaptchaAdapter.js.map +1 -0
  169. package/dist/types/adapters/CompletionAdapter.d.ts +20 -0
  170. package/dist/types/adapters/CompletionAdapter.d.ts.map +1 -0
  171. package/dist/types/adapters/CompletionAdapter.js +2 -0
  172. package/dist/types/adapters/CompletionAdapter.js.map +1 -0
  173. package/dist/types/adapters/EmailAdapter.d.ts +20 -0
  174. package/dist/types/adapters/EmailAdapter.d.ts.map +1 -0
  175. package/dist/types/adapters/EmailAdapter.js +2 -0
  176. package/dist/types/adapters/EmailAdapter.js.map +1 -0
  177. package/dist/types/adapters/ImageGenerationAdapter.d.ts +37 -0
  178. package/dist/types/adapters/ImageGenerationAdapter.d.ts.map +1 -0
  179. package/dist/types/adapters/ImageGenerationAdapter.js +2 -0
  180. package/dist/types/adapters/ImageGenerationAdapter.js.map +1 -0
  181. package/dist/types/adapters/ImageVisionAdapter.d.ts +25 -0
  182. package/dist/types/adapters/ImageVisionAdapter.d.ts.map +1 -0
  183. package/dist/types/adapters/ImageVisionAdapter.js +2 -0
  184. package/dist/types/adapters/ImageVisionAdapter.js.map +1 -0
  185. package/dist/types/adapters/KeyValueAdapter.d.ts +10 -0
  186. package/dist/types/adapters/KeyValueAdapter.d.ts.map +1 -0
  187. package/dist/types/adapters/KeyValueAdapter.js +2 -0
  188. package/dist/types/adapters/KeyValueAdapter.js.map +1 -0
  189. package/dist/types/adapters/OAuth2Adapter.d.ts +32 -0
  190. package/dist/types/adapters/OAuth2Adapter.d.ts.map +1 -0
  191. package/dist/types/adapters/OAuth2Adapter.js +2 -0
  192. package/dist/types/adapters/OAuth2Adapter.js.map +1 -0
  193. package/dist/types/adapters/StorageAdapter.d.ts +63 -0
  194. package/dist/types/adapters/StorageAdapter.d.ts.map +1 -0
  195. package/dist/types/adapters/StorageAdapter.js +2 -0
  196. package/dist/types/adapters/StorageAdapter.js.map +1 -0
  197. package/dist/types/adapters/index.d.ts +9 -0
  198. package/dist/types/adapters/index.d.ts.map +1 -0
  199. package/dist/types/adapters/index.js +2 -0
  200. package/dist/types/adapters/index.js.map +1 -0
  201. package/package.json +4 -2
  202. package/dist/spa/src/types/Adapters.ts +0 -213
  203. package/dist/types/Adapters.d.ts +0 -168
  204. package/dist/types/Adapters.d.ts.map +0 -1
  205. package/dist/types/Adapters.js +0 -2
  206. package/dist/types/Adapters.js.map +0 -1
@@ -1,10 +1,10 @@
1
1
  <template>
2
- <div class="relative flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-800 relative w-screen h-screen"
2
+ <div class="relative flex items-center justify-center min-h-screen bg-lightHtml dark:bg-darkHtml w-screen h-screen"
3
3
  :style="coreStore.config?.loginBackgroundImage && backgroundPosition === 'over' ? {
4
4
  'background-image': 'url(' + loadFile(coreStore.config?.loginBackgroundImage) + ')',
5
5
  'background-size': 'cover',
6
6
  'background-position': 'center',
7
- 'background-blend-mode': 'darken'
7
+ 'background-blend-mode': coreStore.config?.removeBackgroundBlendMode ? 'normal' : 'darken'
8
8
  }: {}"
9
9
  >
10
10
 
@@ -23,44 +23,57 @@
23
23
 
24
24
  <!-- Main modal -->
25
25
  <div id="authentication-modal" tabindex="-1"
26
- class="overflow-y-auto flex flex-grow
26
+ class="af-login-modal overflow-y-auto flex flex-grow
27
27
  overflow-x-hidden z-50 min-w-[350px] justify-center items-center md:inset-0 h-[calc(100%-1rem)] max-h-full">
28
28
  <div class="relative p-4 w-full max-h-full max-w-[400px]">
29
29
  <!-- Modal content -->
30
- <div class="relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black" >
30
+ <div class="af-login-popup af-login-modal-content relative bg-lightLoginViewBackground rounded-lg shadow dark:bg-darkLoginViewBackground dark:shadow-black" :class=" { 'rounded-b-none overflow-hidden': error } ">
31
31
  <!-- Modal header -->
32
- <div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
33
- <h3 class="text-xl font-semibold text-gray-900 dark:text-white">
32
+ <div class="af-login-modal-header flex items-center justify-between flex-col p-4 md:p-5 border-b rounded-t dark:border-gray-600">
33
+
34
+ <template v-if="coreStore?.config?.loginPageInjections?.panelHeader.length > 0">
35
+ <component
36
+ v-for="(c, index) in coreStore?.config?.loginPageInjections?.panelHeader || []"
37
+ :key="index"
38
+ :is="getCustomComponent(c)"
39
+ :meta="c.meta"
40
+ />
41
+ </template>
42
+ <h3 v-else class="text-xl font-semibold text-lightLoginViewText dark:text-darkLoginViewTextColor">
34
43
  {{ $t('Sign in to') }} {{ coreStore.config?.brandName }}
35
44
  </h3>
36
45
  </div>
37
46
  <!-- Modal body -->
38
- <div class="p-4 md:p-5">
47
+ <div class="af-login-modal-body p-4 md:p-5">
39
48
  <form class="space-y-4" @submit.prevent>
40
49
  <div>
41
- <label for="username" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ $t('Your') }} {{ coreStore.config?.usernameFieldName?.toLowerCase() }}</label>
42
- <input
50
+ <label for="username" class="block mb-2 text-sm font-medium text-lightLoginViewText dark:text-darkLoginViewTextColor">{{ $t('Your') }} {{ coreStore.config?.usernameFieldName?.toLowerCase() }}</label>
51
+ <Input
52
+ v-model="username"
43
53
  autocomplete="username"
44
54
  type="username"
45
55
  name="username"
46
56
  id="username"
47
57
  ref="usernameInput"
48
- oninput="setCustomValidity('')"
49
58
  @keydown.enter="passwordInput.focus()"
50
- 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-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" placeholder="name@company.com" required />
59
+ class="w-full"
60
+ placeholder="name@company.com" required />
51
61
  </div>
52
- <div class="relative">
53
- <label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ $t('Your password') }}</label>
54
- <input
62
+ <div class="">
63
+ <label for="password" class="block mb-2 text-sm font-medium text-lightLoginViewText dark:text-darkLoginViewTextColor">{{ $t('Your password') }}</label>
64
+ <Input
65
+ v-model="password"
55
66
  ref="passwordInput"
56
67
  autocomplete="current-password"
57
- oninput="setCustomValidity('')"
58
68
  @keydown.enter="login"
59
- :type="!showPw ? 'password': 'text'" name="password" id="password" placeholder="••••••••" 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-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" required />
60
- <button type="button" @click="showPw = !showPw" class="absolute top-12 right-3 -translate-y-1/2 text-gray-400 dark:text-gray-300">
61
- <IconEyeSolid class="w-5 h-5" v-if="!showPw" />
62
- <IconEyeSlashSolid class="w-5 h-5" v-else />
63
- </button>
69
+ :type="!showPw ? 'password': 'text'" name="password" id="password" placeholder="••••••••" class="w-full" required>
70
+ <template #rightIcon>
71
+ <button type="button" @click="showPw = !showPw" class="text-lightLoginViewSubTextColor dark:text-darkLoginViewSubTextColor">
72
+ <IconEyeSolid class="w-5 h-5" v-if="!showPw" />
73
+ <IconEyeSlashSolid class="w-5 h-5" v-else />
74
+ </button>
75
+ </template>
76
+ </Input>
64
77
  </div>
65
78
 
66
79
  <div v-if="coreStore.config.rememberMeDays"
@@ -77,20 +90,11 @@
77
90
  v-for="c in coreStore?.config?.loginPageInjections?.underInputs || []"
78
91
  :is="getCustomComponent(c)"
79
92
  :meta="c.meta"
93
+ @update:disableLoginButton="setDisableLoginButton($event)"
80
94
  />
81
-
82
- <div v-if="error" class="flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
83
- <svg class="flex-shrink-0 inline w-4 h-4 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
84
- <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
85
- </svg>
86
- <span class="sr-only">{{ $t('Info') }}</span>
87
- <div>
88
- {{ error }}
89
- </div>
90
- </div>
91
-
95
+
92
96
  <div v-if="coreStore.config?.loginPromptHTML"
93
- class="flex items-center p-4 mb-4 text-sm text-gray-800 rounded-lg bg-gray-50 dark:bg-gray-800 dark:text-gray-400" role="alert"
97
+ class="flex items-center p-4 mb-4 text-sm text-lightLoginViewPromptText rounded-lg bg-lightLoginViewPromptBackground dark:bg-darkLoginViewPromptBackground dark:text-darkLoginViewPromptText" role="alert"
94
98
  >
95
99
  <svg class="flex-shrink-0 inline w-4 h-4 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
96
100
  <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
@@ -98,13 +102,19 @@
98
102
  <span class="sr-only">{{ $t('Info') }}</span>
99
103
  <div v-html="coreStore.config?.loginPromptHTML"></div>
100
104
  </div>
101
- <Button @click="login" :loader="inProgress" :disabled="inProgress" class="w-full">
105
+ <Button @click="login" :loader="inProgress" :disabled="inProgress || disableLoginButton" class="w-full">
102
106
  {{ $t('Login to your account') }}
103
107
  </Button>
108
+ <component
109
+ v-for="c in coreStore?.config?.loginPageInjections?.underLoginButton || []"
110
+ :is="getCustomComponent(c)"
111
+ :meta="c.meta"
112
+ @update:disableLoginButton="setDisableLoginButton($event)"
113
+ />
104
114
  </form>
105
-
106
115
  </div>
107
116
  </div>
117
+ <ErrorMessage v-if="error" :error="error" class="absolute left-4 right-4 rounded-t-none mb-0 shadow px-9" />
108
118
  </div>
109
119
  </div>
110
120
 
@@ -112,7 +122,7 @@
112
122
  </template>
113
123
 
114
124
 
115
- <script setup>
125
+ <script setup lang="ts">
116
126
 
117
127
  import { getCustomComponent } from '@/utils';
118
128
  import { onBeforeMount, onMounted, ref, computed } from 'vue';
@@ -121,30 +131,34 @@ import { useUserStore } from '@/stores/user';
121
131
  import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
122
132
  import { callAdminForthApi, loadFile } from '@/utils';
123
133
  import { useRoute, useRouter } from 'vue-router';
124
- import { Button, Checkbox } from '@/afcl';
134
+ import { Button, Checkbox, Input } from '@/afcl';
125
135
  import { useI18n } from 'vue-i18n';
136
+ import ErrorMessage from '@/components/ErrorMessage.vue';
126
137
 
127
138
  const { t } = useI18n();
128
139
 
129
140
  const passwordInput = ref(null);
130
141
  const usernameInput = ref(null);
131
142
  const rememberMeValue= ref(false);
143
+ const username = ref('');
144
+ const password = ref('');
132
145
 
133
146
  const route = useRoute();
134
147
  const router = useRouter();
135
148
  const inProgress = ref(false);
136
-
137
149
  const coreStore = useCoreStore();
138
150
  const user = useUserStore();
139
151
 
140
152
  const showPw = ref(false);
141
153
 
142
154
  const error = ref(null);
155
+ const disableLoginButton = ref(false);
143
156
 
144
157
  const backgroundPosition = computed(() => {
145
158
  return coreStore.config?.loginBackgroundPosition || '1/2';
146
159
  });
147
160
 
161
+
148
162
  onBeforeMount(() => {
149
163
  if (localStorage.getItem('isAuthorized') === 'true') {
150
164
  // if route has next param, redirect
@@ -158,29 +172,17 @@ onBeforeMount(() => {
158
172
  })
159
173
 
160
174
  onMounted(async () => {
175
+ coreStore.getLoginFormConfig();
161
176
  if (coreStore.config?.demoCredentials) {
162
- const [username, password] = coreStore.config.demoCredentials.split(':');
163
- usernameInput.value.value = username;
164
- passwordInput.value.value = password;
177
+ const [demoUsername, demoPassword] = coreStore.config.demoCredentials.split(':');
178
+ username.value = demoUsername;
179
+ password.value = demoPassword;
165
180
  }
166
181
  usernameInput.value.focus();
167
182
  });
168
183
 
169
184
 
170
185
  async function login() {
171
-
172
- const username = usernameInput.value.value;
173
- const password = passwordInput.value.value;
174
-
175
- if (!username) {
176
- usernameInput.value.setCustomValidity(t('Please fill out this field.'));
177
- return;
178
- }
179
- if (!password) {
180
- passwordInput.value.setCustomValidity(t('Please fill out this field.'));
181
- return;
182
- }
183
-
184
186
  if (inProgress.value) {
185
187
  return;
186
188
  }
@@ -189,8 +191,8 @@ async function login() {
189
191
  path: '/login',
190
192
  method: 'POST',
191
193
  body: {
192
- username,
193
- password,
194
+ username: username.value,
195
+ password: password.value,
194
196
  rememberMe: rememberMeValue.value,
195
197
  }
196
198
  });
@@ -206,5 +208,8 @@ async function login() {
206
208
 
207
209
  }
208
210
 
211
+ function setDisableLoginButton(value: boolean) {
212
+ disableLoginButton.value = value;
213
+ }
209
214
 
210
215
  </script>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :key="`${$route?.params.resourceId}---${$route?.params.primaryKey}`" class="p-4 flex"
2
+ <div :key="`${$route?.params.resourceId}---${$route?.params.primaryKey}`" class="af-resource-parent p-4 flex"
3
3
  :class="limitHeightToPage ? 'h-[calc(100dvh-3.5rem)]': undefined"
4
4
  >
5
5
  <RouterView/>
@@ -33,7 +33,7 @@ const limitHeightToPage = computed(() => {
33
33
  }
34
34
  const listPageInjects = coreStore.resource.options.pageInjections.list;
35
35
 
36
- for (const pi of [listPageInjects.beforeBreadcrumbs, listPageInjects.afterBreadcrumbs, listPageInjects.bottom]) {
36
+ for (const pi of [listPageInjects.beforeBreadcrumbs, listPageInjects.beforeActionButtons, listPageInjects.afterBreadcrumbs, listPageInjects.bottom]) {
37
37
  if (pi) {
38
38
  for (const piItem of pi) {
39
39
  if (!piItem.meta?.thinEnoughToShrinkTable) {
@@ -0,0 +1,121 @@
1
+ <template>
2
+ <div class="mt-20 h-full w-full" :class="{ 'hidden': initialTabSet === false }">
3
+ <div v-if="!coreStore?.config?.settingPages || coreStore?.config?.settingPages.length === 0">
4
+ <p>{{ $t('No setting pages configured or still loading...') }}</p>
5
+ </div>
6
+ <VerticalTabs v-else ref="VerticalTabsRef" v-model:active-tab="activeTab" @update:active-tab="setURL({slug: $event, pageLabel: ''})">
7
+ <template v-for="(c,i) in coreStore?.config?.settingPages" :key="`tab:${settingPageSlotName(c,i)}`" v-slot:['tab:'+c.slug]>
8
+ <div class="flex items-center justify-center whitespace-nowrap w-full px-4 gap-2" @click="setURL(c)">
9
+ <component v-if="c.icon" :is="getIcon(c.icon)" class="w-5 h-5 group-hover:text-lightSidebarIconsHover transition duration-75 dark:group-hover:text-darkSidebarIconsHover dark:text-darkSidebarIcons" ></component>
10
+ {{ c.pageLabel }}
11
+ </div>
12
+ </template>
13
+
14
+ <template v-for="(c,i) in coreStore?.config?.settingPages" :key="`${settingPageSlotName(c,i)}-content`" v-slot:[c.slug]>
15
+ <component
16
+ :is="getCustomComponent({file: c.component || ''})"
17
+ :resource="coreStore.resource"
18
+ :adminUser="coreStore.adminUser"
19
+ />
20
+ </template>
21
+ </VerticalTabs>
22
+ </div>
23
+ </template>
24
+
25
+ <script setup lang="ts">
26
+ import { ref, onMounted, watch } from 'vue';
27
+ import { useRouter } from 'vue-router';
28
+ import { useCoreStore } from '@/stores/core';
29
+ import { getCustomComponent, getIcon } from '@/utils';
30
+ import { Dropdown } from 'flowbite';
31
+ import adminforth from '@/adminforth';
32
+ import { VerticalTabs } from '@/afcl'
33
+ import { useRoute } from 'vue-router'
34
+
35
+ const route = useRoute()
36
+ const coreStore = useCoreStore();
37
+ const router = useRouter();
38
+
39
+ const routerIsReady = ref(false);
40
+ const loginRedirectCheckIsReady = ref(false);
41
+ const dropdownUserButton = ref<HTMLElement | null>(null);
42
+ const VerticalTabsRef = ref();
43
+ const activeTab = ref('');
44
+ const initialTabSet = ref(false);
45
+
46
+ watch(() => route?.params?.page, (val) => {
47
+ handleURLChange(val as string | null);
48
+ });
49
+
50
+ async function initRouter() {
51
+ await router.isReady();
52
+ routerIsReady.value = true;
53
+ }
54
+
55
+ watch(dropdownUserButton, (el) => {
56
+ if (el) {
57
+ const dd = new Dropdown(
58
+ document.querySelector('#dropdown-user') as HTMLElement,
59
+ document.querySelector('[data-dropdown-toggle="dropdown-user"]') as HTMLElement,
60
+ );
61
+ adminforth.closeUserMenuDropdown = () => dd.hide();
62
+ }
63
+ });
64
+
65
+ onMounted(async () => {
66
+ if (coreStore.adminUser) {
67
+ await loadMenu();
68
+ loginRedirectCheckIsReady.value = true;
69
+ const routeParamsPage = route?.params?.page;
70
+ if (!routeParamsPage) {
71
+ if (coreStore.config?.settingPages?.[0]) {
72
+ setURL(coreStore.config.settingPages[0]);
73
+ }
74
+ } else {
75
+ handleURLChange(routeParamsPage as string | null);
76
+ }
77
+ }
78
+ });
79
+
80
+ async function loadMenu() {
81
+ await initRouter();
82
+ await coreStore.fetchMenuAndResource();
83
+ }
84
+
85
+ function slugifyString(str: string): string {
86
+ return str
87
+ .toString()
88
+ .toLowerCase()
89
+ .replace(/\s+/g, '-')
90
+ .replace(/[^a-z0-9-_]/g, '-');
91
+ }
92
+
93
+ function setURL(item: {
94
+ pageLabel: string;
95
+ slug?: string | undefined;
96
+ }) {
97
+ router.replace({
98
+ name: 'settings',
99
+ params: { page: item?.slug }
100
+ });
101
+ }
102
+
103
+ function handleURLChange(val: string | null) {
104
+ let isParamInTabs;
105
+ for (const c of coreStore?.config?.settingPages || []) {
106
+ if (c.slug ? c.slug === val : slugifyString(c.pageLabel) === val) {
107
+ isParamInTabs = true;
108
+ break;
109
+ }
110
+ }
111
+ if (isParamInTabs) {
112
+ VerticalTabsRef.value.setActiveTab(val);
113
+ activeTab.value = val as string;
114
+ if (!initialTabSet.value) initialTabSet.value = true;
115
+ } else {
116
+ if (coreStore.config?.settingPages?.[0]) {
117
+ setURL(coreStore.config.settingPages[0]);
118
+ }
119
+ }
120
+ }
121
+ </script>
@@ -4,52 +4,60 @@
4
4
  v-if="!loading"
5
5
  v-for="c in coreStore?.resourceOptions?.pageInjections?.show?.beforeBreadcrumbs || []"
6
6
  :is="getCustomComponent(c)"
7
- :meta="c.meta"
7
+ :meta="(c as AdminForthComponentDeclarationFull).meta"
8
8
  :record="coreStore.record"
9
9
  :resource="coreStore.resource"
10
10
  :adminUser="coreStore.adminUser"
11
11
  />
12
12
  <BreadcrumbsWithButtons>
13
13
  <template v-if="coreStore.resource?.options?.actions">
14
- <button
15
- v-for="action in coreStore.resource.options.actions.filter(a => a.showIn?.showButton)"
16
- :key="action.id"
17
- @click="startCustomAction(action.id)"
18
- :disabled="actionLoadingStates[action.id]"
19
- class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-default border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
20
- >
21
- <component
22
- v-if="action.icon"
23
- :is="getIcon(action.icon)"
24
- class="w-4 h-4 me-2 text-lightPrimary dark:text-darkPrimary"
25
- />
26
- {{ action.name }}
27
- </button>
14
+
15
+ <template v-for="action in coreStore.resource.options.actions.filter(a => a.showIn?.showButton)" :key="action.id">
16
+ <component
17
+ :is="action?.customComponent ? getCustomComponent(action.customComponent) : CallActionWrapper"
18
+ :meta="action.customComponent?.meta"
19
+ @callAction="(payload?) => startCustomAction(action.id, payload)"
20
+ :disabled="actionLoadingStates[action.id]"
21
+ >
22
+ <button
23
+ :key="action.id"
24
+ :disabled="actionLoadingStates[action.id!]"
25
+ class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-default border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
26
+ >
27
+ <component
28
+ v-if="action.icon"
29
+ :is="getIcon(action.icon)"
30
+ class="w-4 h-4 me-2 text-lightPrimary dark:text-darkPrimary"
31
+ />
32
+ {{ action.name }}
33
+ </button>
34
+ </component>
35
+ </template>
28
36
  </template>
29
37
  <RouterLink v-if="coreStore.resource?.options?.allowedActions?.create"
30
38
  :to="{ name: 'resource-create', params: { resourceId: $route.params.resourceId } }"
31
- class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
39
+ class="af-add-new-button flex items-center py-1 px-3 text-sm font-medium text-lightShowViewButtonText focus:outline-none bg-lightShowViewButtonBackground rounded border border-lightShowViewButtonBorder hover:bg-lightShowViewButtonBackgroundHover hover:text-lightShowViewButtonTextHover focus:z-10 focus:ring-4 focus:ring-lightShowViewButtonFocusRing dark:focus:ring-darkShowViewButtonFocusRing dark:bg-darkShowViewButtonBackground dark:text-darkShowViewButtonText dark:border-darkShowViewButtonBorder dark:hover:text-darkShowViewButtonTextHover dark:hover:bg-darkShowViewButtonBackgroundHover rounded-default gap-1"
32
40
  >
33
- <IconPlusOutline class="w-4 h-4 me-2"/>
41
+ <IconPlusOutline class="w-4 h-4"/>
34
42
  {{ $t('Add new') }}
35
43
  </RouterLink>
36
44
 
37
45
  <RouterLink v-if="coreStore?.resourceOptions?.allowedActions?.edit" :to="{ name: 'resource-edit', params: { resourceId: $route.params.resourceId, primaryKey: $route.params.primaryKey } }"
38
- class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-default border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
46
+ class="flex items-center af-edit-button py-1 px-3 text-sm font-medium text-lightShowViewButtonText focus:outline-none bg-lightShowViewButtonBackground rounded-default border border-lightShowViewButtonBorder hover:bg-lightShowViewButtonBackgroundHover hover:text-lightShowViewButtonTextHover focus:z-10 focus:ring-4 focus:ring-lightShowViewButtonFocusRing dark:focus:ring-darkShowViewButtonFocusRing dark:bg-darkShowViewButtonBackground dark:text-darkShowViewButtonText dark:border-darkShowViewButtonBorder dark:hover:text-darkShowViewButtonTextHover dark:hover:bg-darkShowViewButtonBackgroundHover gap-1"
39
47
  >
40
48
  <IconPenSolid class="w-4 h-4" />
41
49
  {{ $t('Edit') }}
42
50
  </RouterLink>
43
51
 
44
52
  <button v-if="coreStore?.resourceOptions?.allowedActions?.delete" @click="deleteRecord"
45
- class="flex items-center py-1 px-3 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
53
+ class="flex items-center af-delete-button py-1 px-3 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-lightShowViewButtonBackground border border-lightShowViewButtonBorder hover:bg-lightShowViewButtonBackgroundHover hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-lightShowViewButtonFocusRing dark:focus:ring-darkShowViewButtonFocusRing dark:bg-darkShowViewButtonBackground dark:text-red-500 dark:border-darkShowViewButtonBorder dark:hover:text-darkShowViewButtonTextHover dark:hover:bg-darkShowViewButtonBackgroundHover gap-1"
46
54
  >
47
55
  <IconTrashBinSolid class="w-4 h-4" />
48
56
  {{ $t('Delete') }}
49
57
  </button>
50
58
 
51
59
  <ThreeDotsMenu
52
- :threeDotsDropdownItems="coreStore.resourceOptions?.pageInjections?.show?.threeDotsDropdownItems"
60
+ :threeDotsDropdownItems="(coreStore.resourceOptions?.pageInjections?.show?.threeDotsDropdownItems as [])"
53
61
  :customActions="customActions"
54
62
  ></ThreeDotsMenu>
55
63
  </BreadcrumbsWithButtons>
@@ -57,7 +65,7 @@
57
65
  <component
58
66
  v-for="c in coreStore?.resourceOptions?.pageInjections?.show?.afterBreadcrumbs || []"
59
67
  :is="getCustomComponent(c)"
60
- :meta="c.meta"
68
+ :meta="(c as AdminForthComponentDeclarationFull).meta"
61
69
  :record="coreStore.record"
62
70
  :resource="coreStore.resource"
63
71
  :adminUser="coreStore.adminUser"
@@ -76,11 +84,11 @@
76
84
  v-else-if="coreStore.record"
77
85
  class="relative w-full flex flex-col gap-4"
78
86
  >
79
- <div v-if="!groups.length && allColumns.length">
87
+ <div v-if="!groups.length && allColumns?.length">
80
88
  <ShowTable
81
- :columns="allColumns"
82
89
  :resource="coreStore.resource"
83
90
  :record="coreStore.record"
91
+ :columns="allColumns as Array<{ name: string; label?: string; components?: any }>"
84
92
  />
85
93
  </div>
86
94
  <template v-else>
@@ -93,12 +101,12 @@
93
101
  :record="coreStore.record"
94
102
  />
95
103
  </template>
96
- <template v-if="otherColumns.length > 0">
104
+ <template v-if="otherColumns && otherColumns.length > 0">
97
105
  <ShowTable
98
- :columns="otherColumns"
99
106
  groupName="Other Fields"
100
107
  :resource="coreStore.resource"
101
108
  :record="coreStore.record"
109
+ :columns="otherColumns as Array<{ name: string; label?: string; components?: any }>"
102
110
  />
103
111
  </template>
104
112
  </template>
@@ -112,8 +120,7 @@
112
120
  v-if="!loading"
113
121
  v-for="c in coreStore?.resourceOptions?.pageInjections?.show?.bottom || []"
114
122
  :is="getCustomComponent(c)"
115
- :meta="c.meta"
116
- :column="column"
123
+ :meta="(c as AdminForthComponentDeclarationFull).meta"
117
124
  :record="coreStore.record"
118
125
  :resource="coreStore.resource"
119
126
  :adminUser="coreStore.adminUser"
@@ -140,6 +147,8 @@ import ShowTable from '@/components/ShowTable.vue';
140
147
  import adminforth from "@/adminforth";
141
148
  import { useI18n } from 'vue-i18n';
142
149
  import { getIcon } from '@/utils';
150
+ import { type AdminForthComponentDeclarationFull } from '@/types/Common.js';
151
+ import CallActionWrapper from '@/components/CallActionWrapper.vue'
143
152
 
144
153
  const route = useRoute();
145
154
  const router = useRouter();
@@ -147,62 +156,65 @@ const loading = ref(true);
147
156
  const { t } = useI18n();
148
157
  const coreStore = useCoreStore();
149
158
 
150
- const actionLoadingStates = ref({});
159
+ const actionLoadingStates = ref<Record<string, boolean>>({});
151
160
 
152
161
  const customActions = computed(() => {
153
- return coreStore.resource?.options?.actions?.filter(a => a.showIn?.showThreeDotsMenu) || [];
162
+ return coreStore.resource?.options?.actions?.filter((a: any) => a.showIn?.showThreeDotsMenu) || [];
154
163
  });
155
164
 
156
165
  onMounted(async () => {
157
166
  loading.value = true;
158
167
  await coreStore.fetchResourceFull({
159
- resourceId: route.params.resourceId
168
+ resourceId: route.params.resourceId as string,
160
169
  });
161
170
  initThreeDotsDropdown();
162
171
  await coreStore.fetchRecord({
163
- resourceId: route.params.resourceId,
164
- primaryKey: route.params.primaryKey,
172
+ resourceId: route.params.resourceId as string,
173
+ primaryKey: route.params.primaryKey as string,
165
174
  source: 'show',
166
175
  });
167
- checkAcessByAllowedActions(coreStore.resourceOptions.allowedActions,'show');
176
+ if(coreStore.resourceOptions){
177
+ checkAcessByAllowedActions(coreStore.resourceOptions.allowedActions,'show');
178
+ }
168
179
  loading.value = false;
169
180
  });
170
181
 
171
182
  const groups = computed(() => {
172
183
  let fieldGroupType;
173
- if (coreStore.resource.options?.showFieldGroups) {
174
- fieldGroupType = coreStore.resource.options.showFieldGroups;
175
- } else if (coreStore.resource.options?.showFieldGroups === null) {
176
- fieldGroupType = [];
177
- } else {
178
- fieldGroupType = coreStore.resource.options?.fieldGroups;
184
+ if (coreStore.resource) {
185
+ if (coreStore.resource.options?.showFieldGroups) {
186
+ fieldGroupType = coreStore.resource.options.showFieldGroups;
187
+ } else if (coreStore.resource.options?.showFieldGroups === null) {
188
+ fieldGroupType = [];
189
+ } else {
190
+ fieldGroupType = coreStore.resource.options?.fieldGroups;
191
+ }
179
192
  }
193
+ const activeGroups: typeof fieldGroupType | [] = fieldGroupType ?? [];
180
194
 
181
- const activeGroups = fieldGroupType ?? [];
182
-
183
- return activeGroups.map(group => ({
195
+ return activeGroups.map((group: FieldGroup) => ({
184
196
  ...group,
185
- columns: coreStore.resource.columns.filter(
186
- col => group.columns.includes(col.name) && col.showIn.show
197
+ columns: coreStore.resource?.columns.filter(
198
+ col => group.columns.includes(col.name) && col.showIn?.show
187
199
  ),
188
200
  }));
189
201
  });
190
202
 
191
203
  const allColumns = computed(() => {
192
- return coreStore.resource.columns.filter(col => col.showIn.show);
204
+ return coreStore.resource?.columns.filter(col => col.showIn?.show);
193
205
  });
194
206
 
195
207
  const otherColumns = computed(() => {
196
208
  const groupedColumnNames = new Set(
197
- groups.value.flatMap(group => group.columns.map(col => col.name))
209
+ groups.value.flatMap(group => group.columns.map((col: AdminForthResourceColumnCommon) => col.name))
198
210
  );
199
211
 
200
- return coreStore.resource.columns.filter(
201
- col => !groupedColumnNames.has(col.name) && col.showIn.show
212
+ return coreStore.resource?.columns.filter(
213
+ col => !groupedColumnNames.has(col.name) && col.showIn?.show
202
214
  );
203
215
  });
204
216
 
205
- async function deleteRecord(row) {
217
+ async function deleteRecord() {
206
218
  const data = await adminforth.confirm({
207
219
  message: t('Are you sure you want to delete this item?'),
208
220
  yes: t('Delete'),
@@ -231,7 +243,7 @@ async function deleteRecord(row) {
231
243
 
232
244
  }
233
245
 
234
- async function startCustomAction(actionId) {
246
+ async function startCustomAction(actionId: string, extra: any) {
235
247
  actionLoadingStates.value[actionId] = true;
236
248
 
237
249
  const data = await callAdminForthApi({
@@ -240,7 +252,8 @@ async function startCustomAction(actionId) {
240
252
  body: {
241
253
  resourceId: route.params.resourceId,
242
254
  actionId: actionId,
243
- recordId: route.params.primaryKey
255
+ recordId: route.params.primaryKey,
256
+ extra: extra,
244
257
  }
245
258
  });
246
259
 
@@ -263,8 +276,8 @@ async function startCustomAction(actionId) {
263
276
 
264
277
  if (data?.ok) {
265
278
  await coreStore.fetchRecord({
266
- resourceId: route.params.resourceId,
267
- primaryKey: route.params.primaryKey,
279
+ resourceId: route.params.resourceId as string,
280
+ primaryKey: route.params.primaryKey as string,
268
281
  source: 'show',
269
282
  });
270
283
 
@@ -281,4 +294,21 @@ async function startCustomAction(actionId) {
281
294
  }
282
295
  }
283
296
 
297
+ adminforth.show.refresh = () => {
298
+ (async () => {
299
+ try {
300
+ loading.value = true;
301
+ await coreStore.fetchRecord({
302
+ resourceId: String(route.params.resourceId),
303
+ primaryKey: String(route.params.primaryKey),
304
+ source: 'show',
305
+ });
306
+ } catch (e) {
307
+ showErrorTost((e as Error).message);
308
+ } finally {
309
+ loading.value = false;
310
+ }
311
+ })();
312
+ }
313
+
284
314
  </script>