adminforth 2.4.0-next.31 → 2.4.0-next.310

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 (176) hide show
  1. package/commands/callTsProxy.js +14 -4
  2. package/commands/createApp/templates/api.ts.hbs +10 -0
  3. package/commands/createApp/templates/custom/tsconfig.json.hbs +2 -3
  4. package/commands/createApp/templates/index.ts.hbs +12 -1
  5. package/commands/createApp/templates/package.json.hbs +1 -1
  6. package/commands/createApp/templates/prisma.config.ts.hbs +8 -0
  7. package/commands/createApp/templates/schema.prisma.hbs +0 -1
  8. package/commands/createApp/utils.js +10 -0
  9. package/commands/createCustomComponent/configLoader.js +17 -4
  10. package/commands/createCustomComponent/main.js +13 -7
  11. package/commands/createCustomComponent/templates/customCrud/beforeActionButtons.vue.hbs +38 -0
  12. package/commands/createCustomComponent/templates/customCrud/saveButton.vue.hbs +28 -0
  13. package/commands/createPlugin/templates/custom/tsconfig.json.hbs +2 -5
  14. package/commands/createPlugin/templates/package.json.hbs +1 -1
  15. package/commands/generateModels.js +30 -22
  16. package/dist/auth.d.ts +9 -1
  17. package/dist/auth.d.ts.map +1 -1
  18. package/dist/auth.js +21 -2
  19. package/dist/auth.js.map +1 -1
  20. package/dist/dataConnectors/baseConnector.d.ts +1 -1
  21. package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  22. package/dist/dataConnectors/baseConnector.js +69 -17
  23. package/dist/dataConnectors/baseConnector.js.map +1 -1
  24. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  25. package/dist/dataConnectors/clickhouse.js +15 -0
  26. package/dist/dataConnectors/clickhouse.js.map +1 -1
  27. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  28. package/dist/dataConnectors/mongo.js +50 -15
  29. package/dist/dataConnectors/mongo.js.map +1 -1
  30. package/dist/dataConnectors/mysql.d.ts.map +1 -1
  31. package/dist/dataConnectors/mysql.js +11 -0
  32. package/dist/dataConnectors/mysql.js.map +1 -1
  33. package/dist/dataConnectors/postgres.d.ts.map +1 -1
  34. package/dist/dataConnectors/postgres.js +43 -14
  35. package/dist/dataConnectors/postgres.js.map +1 -1
  36. package/dist/dataConnectors/sqlite.d.ts.map +1 -1
  37. package/dist/dataConnectors/sqlite.js +11 -0
  38. package/dist/dataConnectors/sqlite.js.map +1 -1
  39. package/dist/index.d.ts +12 -2
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +45 -22
  42. package/dist/index.js.map +1 -1
  43. package/dist/modules/codeInjector.d.ts +2 -0
  44. package/dist/modules/codeInjector.d.ts.map +1 -1
  45. package/dist/modules/codeInjector.js +62 -6
  46. package/dist/modules/codeInjector.js.map +1 -1
  47. package/dist/modules/configValidator.d.ts +6 -0
  48. package/dist/modules/configValidator.d.ts.map +1 -1
  49. package/dist/modules/configValidator.js +202 -25
  50. package/dist/modules/configValidator.js.map +1 -1
  51. package/dist/modules/restApi.d.ts +1 -1
  52. package/dist/modules/restApi.d.ts.map +1 -1
  53. package/dist/modules/restApi.js +172 -31
  54. package/dist/modules/restApi.js.map +1 -1
  55. package/dist/modules/styles.d.ts +499 -13
  56. package/dist/modules/styles.d.ts.map +1 -1
  57. package/dist/modules/styles.js +555 -31
  58. package/dist/modules/styles.js.map +1 -1
  59. package/dist/modules/utils.d.ts +7 -15
  60. package/dist/modules/utils.d.ts.map +1 -1
  61. package/dist/modules/utils.js +45 -68
  62. package/dist/modules/utils.js.map +1 -1
  63. package/dist/servers/express.d.ts +5 -0
  64. package/dist/servers/express.d.ts.map +1 -1
  65. package/dist/servers/express.js +40 -1
  66. package/dist/servers/express.js.map +1 -1
  67. package/dist/spa/index.html +1 -1
  68. package/dist/spa/package-lock.json +1208 -708
  69. package/dist/spa/package.json +34 -34
  70. package/dist/spa/src/App.vue +59 -174
  71. package/dist/spa/src/adminforth.ts +42 -18
  72. package/dist/spa/src/afcl/AreaChart.vue +0 -1
  73. package/dist/spa/src/afcl/BarChart.vue +2 -2
  74. package/dist/spa/src/afcl/Button.vue +6 -6
  75. package/dist/spa/src/afcl/ButtonGroup.vue +91 -0
  76. package/dist/spa/src/afcl/Card.vue +25 -0
  77. package/dist/spa/src/afcl/Checkbox.vue +21 -13
  78. package/dist/spa/src/afcl/CountryFlag.vue +4 -1
  79. package/dist/spa/src/{components/CustomDatePicker.vue → afcl/DatePicker.vue} +95 -9
  80. package/dist/spa/src/afcl/Dialog.vue +47 -27
  81. package/dist/spa/src/afcl/Dropzone.vue +127 -48
  82. package/dist/spa/src/afcl/Input.vue +14 -6
  83. package/dist/spa/src/afcl/JsonViewer.vue +25 -0
  84. package/dist/spa/src/afcl/LinkButton.vue +3 -3
  85. package/dist/spa/src/afcl/PieChart.vue +5 -5
  86. package/dist/spa/src/afcl/ProgressBar.vue +7 -7
  87. package/dist/spa/src/afcl/Select.vue +82 -34
  88. package/dist/spa/src/afcl/Skeleton.vue +6 -6
  89. package/dist/spa/src/afcl/Table.vue +315 -73
  90. package/dist/spa/src/afcl/Textarea.vue +31 -0
  91. package/dist/spa/src/afcl/Toggle.vue +32 -0
  92. package/dist/spa/src/afcl/Tooltip.vue +28 -18
  93. package/dist/spa/src/afcl/VerticalTabs.vue +16 -7
  94. package/dist/spa/src/afcl/index.ts +6 -3
  95. package/dist/spa/src/components/AcceptModal.vue +48 -14
  96. package/dist/spa/src/components/Breadcrumbs.vue +5 -5
  97. package/dist/spa/src/components/CallActionWrapper.vue +15 -0
  98. package/dist/spa/src/components/ColumnValueInput.vue +38 -18
  99. package/dist/spa/src/components/ColumnValueInputWrapper.vue +4 -3
  100. package/dist/spa/src/components/CustomDateRangePicker.vue +9 -8
  101. package/dist/spa/src/components/CustomRangePicker.vue +37 -21
  102. package/dist/spa/src/components/ErrorMessage.vue +21 -0
  103. package/dist/spa/src/components/Filters.vue +195 -132
  104. package/dist/spa/src/components/GroupsTable.vue +9 -8
  105. package/dist/spa/src/components/MenuLink.vue +90 -23
  106. package/dist/spa/src/components/ResourceForm.vue +94 -51
  107. package/dist/spa/src/components/ResourceListTable.vue +115 -85
  108. package/dist/spa/src/components/ResourceListTableVirtual.vue +114 -80
  109. package/dist/spa/src/components/ShowTable.vue +21 -15
  110. package/dist/spa/src/components/Sidebar.vue +470 -0
  111. package/dist/spa/src/components/SingleSkeletLoader.vue +6 -6
  112. package/dist/spa/src/components/SkeleteLoader.vue +3 -3
  113. package/dist/spa/src/components/ThreeDotsMenu.vue +84 -15
  114. package/dist/spa/src/components/Toast.vue +40 -29
  115. package/dist/spa/src/components/UserMenuSettingsButton.vue +69 -0
  116. package/dist/spa/src/components/ValueRenderer.vue +44 -17
  117. package/dist/spa/src/controls/BoolToggle.vue +34 -0
  118. package/dist/spa/src/i18n.ts +5 -3
  119. package/dist/spa/src/main.ts +1 -1
  120. package/dist/spa/src/renderers/CompactField.vue +1 -1
  121. package/dist/spa/src/renderers/CompactUUID.vue +1 -1
  122. package/dist/spa/src/router/index.ts +8 -0
  123. package/dist/spa/src/shims-vue.d.ts +5 -0
  124. package/dist/spa/src/spa_types/core.ts +13 -1
  125. package/dist/spa/src/stores/core.ts +13 -1
  126. package/dist/spa/src/stores/filters.ts +33 -2
  127. package/dist/spa/src/stores/modal.ts +6 -1
  128. package/dist/spa/src/stores/toast.ts +22 -3
  129. package/dist/spa/src/types/Back.ts +163 -23
  130. package/dist/spa/src/types/Common.ts +91 -32
  131. package/dist/spa/src/types/FrontendAPI.ts +31 -5
  132. package/dist/spa/src/types/adapters/CaptchaAdapter.ts +34 -0
  133. package/dist/spa/src/types/adapters/EmailAdapter.ts +2 -2
  134. package/dist/spa/src/types/adapters/ImageVisionAdapter.ts +30 -0
  135. package/dist/spa/src/types/adapters/KeyValueAdapter.ts +16 -0
  136. package/dist/spa/src/types/adapters/index.ts +8 -0
  137. package/dist/spa/src/utils.ts +291 -11
  138. package/dist/spa/src/views/CreateView.vue +63 -21
  139. package/dist/spa/src/views/EditView.vue +55 -22
  140. package/dist/spa/src/views/ListView.vue +144 -87
  141. package/dist/spa/src/views/LoginView.vue +26 -35
  142. package/dist/spa/src/views/ResourceParent.vue +2 -2
  143. package/dist/spa/src/views/SettingsView.vue +121 -0
  144. package/dist/spa/src/views/ShowView.vue +83 -53
  145. package/dist/spa/src/websocket.ts +6 -1
  146. package/dist/spa/tsconfig.app.json +1 -1
  147. package/dist/spa/vite.config.ts +45 -2
  148. package/dist/types/Back.d.ts +146 -14
  149. package/dist/types/Back.d.ts.map +1 -1
  150. package/dist/types/Back.js +15 -0
  151. package/dist/types/Back.js.map +1 -1
  152. package/dist/types/Common.d.ts +106 -29
  153. package/dist/types/Common.d.ts.map +1 -1
  154. package/dist/types/Common.js.map +1 -1
  155. package/dist/types/FrontendAPI.d.ts +31 -3
  156. package/dist/types/FrontendAPI.d.ts.map +1 -1
  157. package/dist/types/FrontendAPI.js.map +1 -1
  158. package/dist/types/adapters/CaptchaAdapter.d.ts +30 -0
  159. package/dist/types/adapters/CaptchaAdapter.d.ts.map +1 -0
  160. package/dist/types/adapters/CaptchaAdapter.js +5 -0
  161. package/dist/types/adapters/CaptchaAdapter.js.map +1 -0
  162. package/dist/types/adapters/EmailAdapter.d.ts +1 -1
  163. package/dist/types/adapters/ImageVisionAdapter.d.ts +25 -0
  164. package/dist/types/adapters/ImageVisionAdapter.d.ts.map +1 -0
  165. package/dist/types/adapters/ImageVisionAdapter.js +2 -0
  166. package/dist/types/adapters/ImageVisionAdapter.js.map +1 -0
  167. package/dist/types/adapters/KeyValueAdapter.d.ts +10 -0
  168. package/dist/types/adapters/KeyValueAdapter.d.ts.map +1 -0
  169. package/dist/types/adapters/KeyValueAdapter.js +2 -0
  170. package/dist/types/adapters/KeyValueAdapter.js.map +1 -0
  171. package/dist/types/adapters/index.d.ts +9 -0
  172. package/dist/types/adapters/index.d.ts.map +1 -0
  173. package/dist/types/adapters/index.js +2 -0
  174. package/dist/types/adapters/index.js.map +1 -0
  175. package/package.json +4 -2
  176. package/dist/spa/src/types/adapters/index.js +0 -5
@@ -1,46 +1,43 @@
1
1
  <template>
2
2
 
3
3
 
4
- <div class="flex items-center w-full p-4 text-gray-500 rounded-lg shadow-lg dark:text-gray-400 dark:bg-gray-800 bg-white"
4
+ <div class="afcl-toast flex items-center w-full p-4 rounded-lg shadow-lg dark:text-darkToastText dark:bg-darkToastBackground bg-lightToastBackground text-lightToastText border-l-4"
5
+ :class="toast.variant == 'info' ? 'border-lightPrimary dark:border-darkPrimary' : toast.variant == 'danger' ? 'border-red-500 dark:border-red-800' : toast.variant == 'warning' ? 'border-orange-500 dark:border-orange-700' : 'border-green-500 dark:border-green-800'"
5
6
  role="alert"
6
- :class="
7
- {
8
- 'danger': 'bg-red-100',
9
- }[toast.variant]
10
- "
11
7
  >
12
- <div v-if="toast.variant == 'info'" class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-lightPrimary dark:text-darkPrimary bg-lightPrimaryOpacity rounded-lg dark:bg-blue-800 dark:text-blue-200">
13
- <svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
14
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.147 15.085a7.159 7.159 0 0 1-6.189 3.307A6.713 6.713 0 0 1 3.1 15.444c-2.679-4.513.287-8.737.888-9.548A4.373 4.373 0 0 0 5 1.608c1.287.953 6.445 3.218 5.537 10.5 1.5-1.122 2.706-3.01 2.853-6.14 1.433 1.049 3.993 5.395 1.757 9.117Z"/>
15
- </svg>
16
- <span class="sr-only">{{ $t('Fire icon') }}</span>
8
+ <div v-if="toast.variant == 'info'" class="af-toast-icon inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-lightPrimary dark:text-darkPrimary bg-lightPrimaryOpacity rounded-lg dark:bg-darkPrimary dark:!text-blue-100">
9
+ <IconInfoCircleSolid class="w-5 h-5" aria-hidden="true" />
17
10
  </div>
18
- <div v-else-if="toast.variant == 'danger'" class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-red-500 bg-red-100 rounded-lg dark:bg-red-800 dark:text-red-200">
19
- <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
20
- <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 11.793a1 1 0 1 1-1.414 1.414L10 11.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L8.586 10 6.293 7.707a1 1 0 0 1 1.414-1.414L10 8.586l2.293-2.293a1 1 0 0 1 1.414 1.414L11.414 10l2.293 2.293Z"/>
21
- </svg>
22
- <span class="sr-only">{{ $t('Error icon') }}</span>
11
+ <div v-else-if="toast.variant == 'danger'" class="af-toast-icon inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-red-500 bg-red-100 rounded-lg dark:bg-red-800 dark:text-red-200">
12
+ <IconCloseCircleSolid class="w-5 h-5" aria-hidden="true" />
23
13
  </div>
24
- <div v-else-if="toast.variant == 'warning'"class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-orange-500 bg-orange-100 rounded-lg dark:bg-orange-700 dark:text-orange-200">
25
- <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
26
- <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM10 15a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-4a1 1 0 0 1-2 0V6a1 1 0 0 1 2 0v5Z"/>
27
- </svg>
28
- <span class="sr-only">{{ $t('Warning icon') }}</span>
14
+ <div v-else-if="toast.variant == 'warning'" class="af-toast-icon inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-orange-500 bg-orange-100 rounded-lg dark:bg-orange-700 dark:text-orange-200">
15
+ <IconExclamationCircleSolid class="w-5 h-5" aria-hidden="true" />
16
+
29
17
  </div>
30
- <div v-else class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg dark:bg-green-800 dark:text-green-200">
31
- <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
32
- <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z"/>
33
- </svg>
34
- <span class="sr-only">{{ $t('Check icon') }}</span>
18
+ <div v-else class="af-toast-icon inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg dark:bg-green-800 dark:text-green-200">
19
+ <IconCheckCircleSolid class="w-5 h-5" aria-hidden="true" />
35
20
  </div>
36
21
 
37
22
  <div class="ms-3 text-sm font-normal max-w-xs pr-2" v-if="toast.messageHtml" v-html="toast.messageHtml"></div>
38
- <div class="ms-3 text-sm font-normal max-w-xs pr-2" v-else>{{toast.message}}</div>
39
- <button @click="closeToast" type="button" class="ms-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" >
23
+ <div class="ms-3 text-sm font-normal max-w-xs pr-2" v-else>
24
+ <div class="flex flex-col items-center justify-center">
25
+ {{toast.message}}
26
+ <div v-if="toast.buttons" class="flex justify-center mt-2 gap-2">
27
+ <div v-for="button in toast.buttons" class="af-toast-button rounded-md bg-lightButtonsBackground hover:bg-lightButtonsHover text-lightButtonsText dark:bg-darkPrimary dark:hover:bg-darkButtonsBackground dark:text-darkButtonsText">
28
+ <button @click="onButtonClick(button.value)" class="px-2 py-1 rounded hover:bg-black/5 dark:hover:bg-white/10">
29
+ {{ button.label }}
30
+ </button>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ <button @click="closeToast" type="button" class="ms-auto -mx-1.5 -my-1.5 bg-lightToastCloseIconBackground text-lightToastCloseIcon hover:text-lightToastCloseIconHover rounded-lg focus:ring-2 focus:ring-lightToastCloseIconFocusRing p-1.5 hover:bg-lightToastCloseIconBackgroundHover inline-flex items-center justify-center h-8 w-8 dark:text-darkToastCloseIcon dark:hover:text-darkToastCloseIconHover dark:bg-darkToastCloseIconBackground dark:hover:bg-darkToastCloseIconBackgroundHover dark:focus:ring-darkToastCloseIconFocusRing" >
40
36
  <svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
41
37
  <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"/>
42
38
  </svg>
43
39
  </button>
40
+ <!-- <div class="h-full ml-3 w-1 rounded-r-lg" :class="toast.variant == 'info' ? 'bg-lightPrimary dark:bg-darkPrimary' : toast.variant == 'danger' ? 'bg-red-500 dark:bg-red-800' : toast.variant == 'warning' ? 'bg-orange-500 dark:bg-orange-700' : 'bg-green-500 dark:bg-green-800'"></div> -->
44
41
  </div>
45
42
 
46
43
 
@@ -49,6 +46,8 @@
49
46
  <script setup lang="ts">
50
47
  import { onMounted } from 'vue';
51
48
  import { useToastStore } from '@/stores/toast';
49
+ import { IconInfoCircleSolid, IconCloseCircleSolid, IconExclamationCircleSolid, IconCheckCircleSolid } from '@iconify-prerendered/vue-flowbite';
50
+
52
51
  const toastStore = useToastStore();
53
52
  const emit = defineEmits(['close']);
54
53
  const props = defineProps<{
@@ -58,16 +57,28 @@ const props = defineProps<{
58
57
  variant: string;
59
58
  id: string;
60
59
  timeout?: number|'unlimited';
60
+ buttons?: { value: any; label: string }[];
61
61
  }
62
62
  }>();
63
63
  function closeToast() {
64
+ // resolve with undefined on close (X button)
65
+ toastStore.resolveToast(props.toast.id);
66
+ emit('close');
67
+ }
68
+
69
+ function onButtonClick(value: any) {
70
+ toastStore.resolveToast(props.toast.id, value);
64
71
  emit('close');
65
72
  }
66
73
 
67
74
  onMounted(() => {
68
75
  if (props.toast.timeout === 'unlimited') return;
69
76
  else {
70
- setTimeout(() => {emit('close');}, (props.toast.timeout || 10) * 1e3 );
77
+ setTimeout(() => {
78
+ // resolve with undefined on auto-timeout
79
+ toastStore.resolveToast(props.toast.id);
80
+ emit('close');
81
+ }, (props.toast.timeout || 10) * 1e3 );
71
82
  }
72
83
  });
73
84
 
@@ -0,0 +1,69 @@
1
+ <template>
2
+ <div class="min-w-40">
3
+ <div class="cursor-pointer flex items-center justify-between gap-1 block px-4 py-2 text-sm
4
+ bg-lightUserMenuItemBackground hover:bg-lightUserMenuItemBackgroundHover text-lightUserMenuItemText
5
+ hover:text-lightUserMenuItemText dark:bg-darkUserMenuItemBackground dark:hover:bg-darkUserMenuItemBackgroundHover
6
+ dark:text-darkUserMenuItemText dark:hover:darkUserMenuItemTextHover
7
+ w-full select-none "
8
+ @click="showDropdown = !showDropdown"
9
+ >
10
+ <span>{{ $t('Settings') }}</span>
11
+ <IconCaretDownSolid class="h-5 w-5 text-lightPrimary dark:text-gray-400 opacity-50 transition duration-150 ease-in"
12
+ :class="{ 'transform rotate-180': showDropdown }"
13
+ />
14
+ </div>
15
+
16
+ <div v-if="showDropdown" >
17
+
18
+ <router-link class="cursor-pointer flex items-center gap-1 block px-4 py-1 text-sm
19
+ bg-lightUserMenuItemBackground hover:bg-lightUserMenuItemBackgroundHover text-lightUserMenuItemText
20
+ hover:text-lightUserMenuItemText dark:bg-darkUserMenuItemBackground dark:hover:bg-darkUserMenuItemBackgroundHover
21
+ dark:text-darkUserMenuItemText dark:hover:darkUserMenuItemTextHover
22
+ w-full text-select-none pl-5 select-none"
23
+ v-for="option in options"
24
+ :to="getRoute(option)"
25
+ >
26
+ <span class="mr-1">
27
+ <component v-if="option.icon" :is="getIcon(option.icon)" class="w-5 h-5 transition duration-75" ></component>
28
+ </span>
29
+ <span>{{ option.pageLabel }}</span>
30
+ </router-link>
31
+ </div>
32
+
33
+
34
+ </div>
35
+ </template>
36
+
37
+ <script setup lang="ts">
38
+ import { IconCaretDownSolid } from '@iconify-prerendered/vue-flowbite';
39
+ import { computed, ref, onMounted, watch } from 'vue';
40
+ import { useCoreStore } from '@/stores/core';
41
+ import { getIcon } from '@/utils';
42
+ import { useRouter } from 'vue-router';
43
+ import { useI18n } from 'vue-i18n';
44
+
45
+ const router = useRouter();
46
+ const coreStore = useCoreStore();
47
+ const { t } = useI18n();
48
+
49
+ const showDropdown = ref(false);
50
+ const props = defineProps(['meta', 'resource']);
51
+
52
+ const options = computed(() => {
53
+ return coreStore.config?.settingPages?.map((page) => {
54
+ return {
55
+ pageLabel: page.pageLabel,
56
+ slug: page.slug || null,
57
+ icon: page.icon || null,
58
+ };
59
+ });
60
+ });
61
+
62
+ function getRoute(option: { slug?: string | null, pageLabel: string }) {
63
+ return {
64
+ name: 'settings',
65
+ params: { page: option.slug }
66
+ }
67
+ }
68
+
69
+ </script>
@@ -12,13 +12,40 @@
12
12
  >
13
13
  <RouterLink
14
14
  class="font-medium text-lightSidebarText dark:text-darkSidebarText hover:brightness-110 whitespace-nowrap"
15
- :to="{ name: 'resource-show', params: { primaryKey: foreignResource.pk, resourceId: column.foreignResource.resourceId || column.foreignResource.polymorphicResources.find((pr) => pr.whenValue === record[column.foreignResource.polymorphicOn]).resourceId } }"
15
+ :to="{
16
+ name: 'resource-show',
17
+ params: {
18
+ primaryKey: foreignResource.pk,
19
+ resourceId: column.foreignResource
20
+ ? (
21
+ column.foreignResource.resourceId
22
+ || column.foreignResource.polymorphicResources?.find(
23
+ (pr: any) => pr.whenValue === record[column.foreignResource?.polymorphicOn!]
24
+ )?.resourceId
25
+ )
26
+ : undefined
27
+ }
28
+ }"
16
29
  >
17
30
  {{ foreignResource.label }}
18
31
  </RouterLink>
19
32
  </span>
20
33
  <RouterLink v-else-if="record[column.name]" class="font-medium text-lightPrimary dark:text-darkPrimary hover:brightness-110 whitespace-nowrap"
21
- :to="{ name: 'resource-show', params: { primaryKey: record[column.name].pk, resourceId: column.foreignResource.resourceId || column.foreignResource.polymorphicResources.find((pr) => pr.whenValue === record[column.foreignResource.polymorphicOn]).resourceId } }">
34
+ :to="{
35
+ name: 'resource-show',
36
+ params: {
37
+ primaryKey: record[column.name].pk,
38
+ resourceId: column.foreignResource
39
+ ? (
40
+ column.foreignResource.resourceId
41
+ || column.foreignResource.polymorphicResources?.find(
42
+ (pr: any) => pr.whenValue === record[column.foreignResource?.polymorphicOn!]
43
+ )?.resourceId
44
+ )
45
+ : undefined
46
+ }
47
+ }"
48
+ >
22
49
  {{ record[column.name].label }}
23
50
  </RouterLink>
24
51
  <div v-else>
@@ -27,8 +54,8 @@
27
54
  </span>
28
55
 
29
56
  <span v-else-if="column.type === 'boolean'">
30
- <span v-if="record[column.name] === true" class="bg-green-100 whitespace-nowrap text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-green-400 border border-green-400">{{ $t('Yes') }}</span>
31
- <span v-else-if="record[column.name] === false" class="bg-red-100 whitespace-nowrap text-red-800gg text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400">{{ $t('No') }}</span>
57
+ <span v-if="record[column.name] === true" class="af-true-value-icon bg-green-100 whitespace-nowrap text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-green-400 border border-green-400">{{ $t('Yes') }}</span>
58
+ <span v-else-if="record[column.name] === false" class="af-false-value-icon bg-red-100 whitespace-nowrap text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400">{{ $t('No') }}</span>
32
59
  <span v-else class="bg-gray-100 whitespace-nowrap text-gray-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-gray-400 border border-gray-400">{{ $t('Unset') }}</span>
33
60
  </span>
34
61
  <span
@@ -38,14 +65,14 @@
38
65
  <template v-for="(arrayItem, arrayItemIndex) in record[column.name]">
39
66
  <span
40
67
  v-if="column.isArray.itemType === 'boolean' && arrayItem"
41
- :key="`${column.name}-${arrayItemIndex}`"
42
- class="bg-green-100 whitespace-nowrap text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-green-400 border border-green-400">
68
+ :key="`${column.name}-${arrayItemIndex}-true`"
69
+ class="af-true-value-icon bg-green-100 whitespace-nowrap text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-green-400 border border-green-400">
43
70
  {{ $t('Yes') }}
44
71
  </span>
45
72
  <span
46
73
  v-else-if="column.isArray.itemType === 'boolean'"
47
- :key="`${column.name}-${arrayItemIndex}`"
48
- class="bg-red-100 whitespace-nowrap text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400">
74
+ :key="`${column.name}-${arrayItemIndex}-false`"
75
+ class="af-false-value-icon bg-red-100 whitespace-nowrap text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400">
49
76
  {{ $t('No') }}
50
77
  </span>
51
78
  <span
@@ -53,30 +80,30 @@
53
80
  :key="`${column.name}-${arrayItemIndex}`"
54
81
  class="rounded-md m-0.5 bg-lightAnnouncementBG dark:bg-darkAnnouncementBG text-lightAnnouncementText dark:text-darkAnnouncementText py-0.5 px-2.5 text-sm"
55
82
  >
56
- {{ checkEmptyValues(getArrayItemDisplayValue(arrayItem, column), route.meta.type) }}
83
+ {{ checkEmptyValues(getArrayItemDisplayValue(arrayItem, column), route.meta.type as "show" | "list") }}
57
84
  </span>
58
85
  </template>
59
86
  </span>
60
87
  <span v-else-if="column.enum">
61
- {{ checkEmptyValues(column.enum.find(e => e.value === record[column.name])?.label || record[column.name], route.meta.type) }}
88
+ {{ checkEmptyValues(column.enum.find(e => e.value === record[column.name])?.label || record[column.name], route.meta.type as "show" | "list") }}
62
89
  </span>
63
90
  <span v-else-if="column.type === 'datetime'" class="whitespace-nowrap">
64
- {{ checkEmptyValues(formatDateTime(record[column.name]), route.meta.type) }}
91
+ {{ checkEmptyValues(formatDateTime(record[column.name]), route.meta.type as "show" | "list") }}
65
92
  </span>
66
93
  <span v-else-if="column.type === 'date'" class="whitespace-nowrap">
67
- {{ checkEmptyValues(formatDate(record[column.name]), route.meta.type) }}
94
+ {{ checkEmptyValues(formatDate(record[column.name]), route.meta.type as "show" | "list") }}
68
95
  </span>
69
96
  <span v-else-if="column.type === 'time'" class="whitespace-nowrap">
70
- {{ checkEmptyValues(formatTime(record[column.name]), route.meta.type) }}
97
+ {{ checkEmptyValues(formatTime(record[column.name]), route.meta.type as "show" | "list") }}
71
98
  </span>
72
99
  <span v-else-if="column.type === 'decimal'">
73
- {{ checkEmptyValues(record[column.name] && parseFloat(record[column.name]), route.meta.type) }}
100
+ {{ checkEmptyValues(record[column.name] && parseFloat(record[column.name]), route.meta.type as "show" | "list") }}
74
101
  </span>
75
102
  <span v-else-if="column.type === 'json'">
76
103
  <JsonViewer class="min-w-[6rem]" :value="record[column.name]" :expandDepth="column.extra?.jsonCollapsedLevel" copyable sort :theme="coreStore.theme"/>
77
104
  </span>
78
105
  <span v-else>
79
- {{ checkEmptyValues(record[column.name],route.meta.type) }}
106
+ {{ checkEmptyValues(record[column.name], route.meta.type as "show" | "list") }}
80
107
  </span>
81
108
  </div>
82
109
  </template>
@@ -90,7 +117,7 @@ import timezone from 'dayjs/plugin/timezone';
90
117
  import {checkEmptyValues} from '@/utils';
91
118
  import { useRoute, useRouter } from 'vue-router';
92
119
  import { JsonViewer } from "vue3-json-viewer";
93
- import "vue3-json-viewer/dist/index.css";
120
+ import "vue3-json-viewer/dist/vue3-json-viewer.css";
94
121
  import type { AdminForthResourceColumnCommon } from '@/types/Common';
95
122
 
96
123
  import { useCoreStore } from '@/stores/core';
@@ -122,7 +149,7 @@ function formatTime(time: string) {
122
149
  return dayjs(`0000-00-00 ${time}`).format(coreStore.config?.timeFormat || 'HH:mm:ss');
123
150
  }
124
151
 
125
- function getArrayItemDisplayValue(value, column) {
152
+ function getArrayItemDisplayValue(value: any, column: AdminForthResourceColumnCommon) {
126
153
  if (column.isArray?.itemType === 'datetime') {
127
154
  return formatDateTime(value);
128
155
  } else if (column.isArray?.itemType === 'date') {
@@ -0,0 +1,34 @@
1
+ <template>
2
+ <Toggle
3
+ :disabled="readonly"
4
+ @update:modelValue="$emit('update:value', $event)"
5
+ :modelValue="valueFromRecord"
6
+ >
7
+ <p>{{text}}</p>
8
+ </Toggle>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ import Toggle from '@/afcl/Toggle.vue';
13
+ import type {
14
+ AdminForthResourceColumnCommon,
15
+ AdminForthResourceCommon,
16
+ AdminUser,
17
+ } from "@/types/Common";
18
+
19
+ const props = defineProps<{
20
+ value: boolean,
21
+ text: string,
22
+ column: AdminForthResourceColumnCommon,
23
+ record: any,
24
+ meta: any,
25
+ resource: AdminForthResourceCommon,
26
+ adminUser: AdminUser,
27
+ readonly: boolean
28
+ }>();
29
+ console.log(JSON.stringify(props));
30
+ console.log("Current mode:", props.meta?.mode)
31
+ defineEmits(['update:value']);
32
+ const valueFromRecord = props.record[props.column.name]
33
+ const editReadOnly = props.column.editReadonly;
34
+ </script>
@@ -3,11 +3,11 @@ import { createApp } from 'vue';
3
3
 
4
4
 
5
5
  // taken from here https://vue-i18n.intlify.dev/guide/essentials/pluralization.html#custom-pluralization
6
- function slavicPluralRule(choice, choicesLength, orgRule) {
6
+ function slavicPluralRule(choice: number, choicesLength: number, orgRule: any) {
7
7
  if (choice === 0) {
8
8
  return 0
9
9
  }
10
-
10
+
11
11
  const teen = choice > 10 && choice < 20
12
12
  const endsWithOne = choice % 10 === 1
13
13
 
@@ -21,6 +21,8 @@ function slavicPluralRule(choice, choicesLength, orgRule) {
21
21
  return choicesLength < 4 ? 2 : 3
22
22
  }
23
23
 
24
+ export let i18nInstance: ReturnType<typeof createI18n> | null = null
25
+
24
26
  export function initI18n(app: ReturnType<typeof createApp>) {
25
27
  const i18n = createI18n({
26
28
  legacy: false,
@@ -48,7 +50,7 @@ export function initI18n(app: ReturnType<typeof createApp>) {
48
50
  return key + ' ';
49
51
  },
50
52
  });
51
-
52
53
  app.use(i18n);
54
+ i18nInstance = i18n
53
55
  return i18n
54
56
  }
@@ -14,7 +14,7 @@ app.use(createPinia())
14
14
  app.use(router)
15
15
 
16
16
  // get access to i18n instance outside components
17
- window.i18n = initI18n(app);
17
+ initI18n(app);
18
18
 
19
19
 
20
20
  /* IMPORTANT:ADMINFORTH CUSTOM USES */
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <Tooltip>
3
3
  <span class="flex items-center">
4
- {{ visualValue }} <IconFileCopyAltSolid @click.stop="copyToCB" class="w-5 h-5 text-lightPrimary dark:text-darkPrimary" v-if="visualValue"/>
4
+ {{ visualValue }} <IconFileCopyAltSolid @click.stop="copyToCB" class="min-w-5 min-h-5 text-lightPrimary dark:text-darkPrimary" v-if="visualValue"/>
5
5
  </span>
6
6
  <template #tooltip v-if="visualValue">
7
7
  {{ props.record[props.column.name] }}
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <Tooltip>
3
3
  <span class="flex items-center">
4
- {{ visualValue }} <IconFileCopyAltSolid @click.stop="copyToCB" class="w-5 h-5 text-lightPrimary dark:text-darkPrimary" v-if="visualValue"/>
4
+ {{ visualValue }} <IconFileCopyAltSolid @click.stop="copyToCB" class="min-w-5 min-h-5 text-lightPrimary dark:text-darkPrimary" v-if="visualValue"/>
5
5
  </span>
6
6
  <template #tooltip v-if="visualValue">
7
7
  {{ props.record[props.column.name] }}
@@ -62,6 +62,14 @@ const router = createRouter({
62
62
  },
63
63
  ]
64
64
  },
65
+ {
66
+ path: '/settings/:page?',
67
+ name: 'settings',
68
+ component: () => import('@/views/SettingsView.vue'),
69
+ meta: {
70
+ title: 'Settings',
71
+ },
72
+ },
65
73
  /* IMPORTANT:ADMINFORTH ROUTES */
66
74
  { path: "/:pathMatch(.*)*", component: PageNotFound },
67
75
  ]
@@ -0,0 +1,5 @@
1
+ declare module '*.vue' {
2
+ import type { DefineComponent } from 'vue';
3
+ const component: DefineComponent<{}, {}, any>;
4
+ export default component;
5
+ }
@@ -1,4 +1,4 @@
1
- import type { AdminForthResource, AdminForthResourceColumn } from '../types/AdminForthConfig';
1
+ import type { AdminForthResource, AdminForthResourceColumn } from '../types/Back.js';
2
2
 
3
3
  export type resourceById = {
4
4
  [key: string]: AdminForthResource;
@@ -21,7 +21,12 @@ export type ResourceColumns = {
21
21
 
22
22
  export type CoreConfig = {
23
23
  brandName: string,
24
+ singleTheme?: 'light' | 'dark',
24
25
  brandLogo: string,
26
+ iconOnlySidebar: {
27
+ logo?: string,
28
+ enabled?: boolean,
29
+ },
25
30
  title: string,
26
31
  datesFormat: string,
27
32
  timeFormat: string,
@@ -33,12 +38,19 @@ export type CoreConfig = {
33
38
  passwordHashField: string,
34
39
  loginBackgroundImage: string,
35
40
  loginBackgroundPosition: string,
41
+ removeBackgroundBlendMode: boolean,
36
42
  userFullnameField: string,
37
43
  },
38
44
  emptyFieldPlaceholder?: {
39
45
  show?: string,
40
46
  list?: string,
41
47
  } | string,
48
+
49
+ customHeadItems?: {
50
+ tagName: string;
51
+ attributes: { [key: string]: string | boolean };
52
+ innerCode?: string;
53
+ }[],
42
54
  }
43
55
 
44
56
 
@@ -16,6 +16,7 @@ export const useCoreStore = defineStore('core', () => {
16
16
  const record: Ref<any | null> = ref({});
17
17
  const resource: Ref<AdminForthResourceCommon | null> = ref(null);
18
18
  const userData: Ref<UserData | null> = ref(null);
19
+ const isResourceFetching = ref(false);
19
20
 
20
21
  const resourceColumnsWithFilters = computed(() => {
21
22
  if (!resource.value) {
@@ -118,7 +119,7 @@ export const useCoreStore = defineStore('core', () => {
118
119
  item.badge = badge;
119
120
  }
120
121
  });
121
-
122
+ websocket.unsubscribeAll();
122
123
  subscribeToMenuBadges();
123
124
 
124
125
  }
@@ -172,6 +173,7 @@ export const useCoreStore = defineStore('core', () => {
172
173
  // already fetched
173
174
  return;
174
175
  }
176
+ isResourceFetching.value = true;
175
177
  resourceColumnsId.value = resourceId;
176
178
  resourceColumnsError.value = '';
177
179
  const res = await callAdminForthApi({
@@ -188,6 +190,7 @@ export const useCoreStore = defineStore('core', () => {
188
190
  resource.value = res.resource;
189
191
  resourceOptions.value = res.resource.options;
190
192
  }
193
+ isResourceFetching.value = false;
191
194
  }
192
195
 
193
196
  async function getPublicConfig() {
@@ -198,6 +201,13 @@ export const useCoreStore = defineStore('core', () => {
198
201
  config.value = {...config.value, ...res};
199
202
  }
200
203
 
204
+ async function getLoginFormConfig() {
205
+ const res = await callAdminForthApi({
206
+ path: '/get_login_form_config',
207
+ method: 'GET',
208
+ });
209
+ config.value = {...config.value, ...res};
210
+ }
201
211
 
202
212
  const username = computed(() => {
203
213
  const usernameField = config.value?.usernameField;
@@ -218,6 +228,7 @@ export const useCoreStore = defineStore('core', () => {
218
228
  userFullname,
219
229
  getPublicConfig,
220
230
  fetchMenuAndResource,
231
+ getLoginFormConfig,
221
232
  fetchRecord,
222
233
  record,
223
234
  fetchResourceFull,
@@ -231,5 +242,6 @@ export const useCoreStore = defineStore('core', () => {
231
242
  fetchMenuBadges,
232
243
  resetAdminUser,
233
244
  resetResource,
245
+ isResourceFetching,
234
246
  }
235
247
  })
@@ -1,9 +1,11 @@
1
- import { ref, type Ref } from 'vue';
1
+ import { ref, computed, type Ref } from 'vue';
2
2
  import { defineStore } from 'pinia';
3
+ import { useCoreStore } from './core';
3
4
 
4
5
  export const useFiltersStore = defineStore('filters', () => {
5
6
  const filters: Ref<any[]> = ref([]);
6
7
  const sort: Ref<any> = ref({});
8
+ const coreStore = useCoreStore();
7
9
 
8
10
  const setSort = (s: any) => {
9
11
  sort.value = s;
@@ -20,8 +22,37 @@ export const useFiltersStore = defineStore('filters', () => {
20
22
  const getFilters = () => {
21
23
  return filters.value;
22
24
  }
25
+ const clearFilter = (fieldName: string) => {
26
+ filters.value = filters.value.filter(f => f.field !== fieldName);
27
+ }
23
28
  const clearFilters = () => {
24
29
  filters.value = [];
25
30
  }
26
- return {setFilter, getFilters, clearFilters, filters, setFilters, setSort, getSort}
31
+
32
+ const shouldFilterBeHidden = (fieldName: string) => {
33
+ if (coreStore.resource?.columns) {
34
+ const column = coreStore.resource.columns.find((col: any) => col.name === fieldName);
35
+ if (column?.showIn?.filter !== true) {
36
+ return true;
37
+ }
38
+ }
39
+ return false;
40
+ }
41
+
42
+ const visibleFiltersCount = computed(() => {
43
+ return filters.value.filter(f => !shouldFilterBeHidden(f.field)).length;
44
+ });
45
+
46
+ return {
47
+ setFilter,
48
+ getFilters,
49
+ clearFilters,
50
+ filters,
51
+ setFilters,
52
+ setSort,
53
+ getSort,
54
+ visibleFiltersCount,
55
+ shouldFilterBeHidden,
56
+ clearFilter
57
+ }
27
58
  })
@@ -29,7 +29,12 @@ export const useModalStore = defineStore('modal', () => {
29
29
  onCancelFunction.value = func;
30
30
  }
31
31
  function setModalContent(content: ModalContentType) {
32
- modalContent.value = content;
32
+ modalContent.value = {
33
+ title: content.title || 'title',
34
+ content: content.content || 'content',
35
+ acceptText: content.acceptText || 'acceptText',
36
+ cancelText: content.cancelText || 'cancelText',
37
+ };
33
38
  }
34
39
  function resetmodalState() {
35
40
  isOpened.value = false;
@@ -12,19 +12,38 @@ export const useToastStore = defineStore('toast', () => {
12
12
  watch(route, () => {
13
13
  // on route change clear all toasts older then 5 seconds
14
14
  const now = +new Date();
15
- toasts.value = toasts.value.filter((t) => now - t.createdAt < 5000);
15
+ toasts.value = toasts.value.filter((t) => t?.timeout === 'unlimited' || now - t.createdAt < 5000);
16
16
  });
17
17
 
18
- const addToast = (toast: { message: string; variant: string }) => {
18
+ const addToast = (toast: {
19
+ message?: string;
20
+ messageHtml?: string;
21
+ variant: string;
22
+ timeout?: number | 'unlimited';
23
+ buttons?: { value: any; label: string }[];
24
+ onResolve?: (value?: any) => void;
25
+ }): string => {
19
26
  const toastId = uuid();
20
27
  toasts.value.push({
21
28
  ...toast,
22
29
  id: toastId,
23
30
  createdAt: +new Date(),
24
31
  });
32
+ return toastId;
25
33
  };
26
34
  const removeToast = (toast: { id: string }) => {
27
35
  toasts.value = toasts.value.filter((t) => t.id !== toast.id);
28
36
  };
29
- return { toasts, addToast, removeToast };
37
+
38
+ const resolveToast = (toastId: string, value?: any) => {
39
+ const t = toasts.value.find((x) => x.id === toastId);
40
+ try {
41
+ t?.onResolve?.(value);
42
+ } catch {
43
+ // no-op
44
+ }
45
+ toasts.value = toasts.value.filter((x) => x.id !== toastId);
46
+ };
47
+
48
+ return { toasts, addToast, removeToast, resolveToast };
30
49
  });