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
@@ -2,130 +2,159 @@
2
2
  <!-- drawer component -->
3
3
  <div id="drawer-navigation"
4
4
 
5
- class="fixed right-0 z-50 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl dark:shadow-gray-900"
5
+ class="af-filters-sidebar fixed right-0 z-50 p-4 overflow-y-auto transition-transform translate-x-full bg-lightFiltersBackgroung w-80 dark:bg-darkFiltersBackgroung shadow-xl dark:shadow-gray-900"
6
6
 
7
7
  :class="show ? 'top-0 transform-none' : ''"
8
8
  tabindex="-1" aria-labelledby="drawer-navigation-label"
9
9
  :style="{ height: `calc(100dvh ` }"
10
10
  >
11
- <h5 id="drawer-navigation-label" class="text-base font-semibold text-gray-500 uppercase dark:text-gray-400">
11
+ <h5 id="drawer-navigation-label" class="text-base font-semibold text-lightFiltersHeaderText uppercase dark:text-darkFiltersHeaderText">
12
12
  {{ $t('Filters') }}
13
13
 
14
- <button type="button" @click="$emit('hide')" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 absolute end-2.5 inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white" >
14
+ <button type="button" @click="$emit('hide')" class="text-lightFiltersCloseIcon bg-transparent hover:bg-lightFiltersCloseIconHoverBackground hover:text-lightFiltersCloseIconHover rounded-lg text-sm p-1.5 absolute end-2.5 inline-flex items-center dark:text-darkFiltersCloseIcon dark:hover:bg-darkFiltersCloseIconHoverBackground dark:hover:text-darkFiltersCloseIconHover" >
15
15
  <svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
16
16
  <span class="sr-only">{{ $t('Close menu') }}</span>
17
17
  </button>
18
18
  </h5>
19
19
 
20
20
  <div class="py-4 ">
21
- <ul class="space-y-3 font-medium">
21
+ <ul class="space-y-3 font-medium text-sm">
22
22
  <li v-for="c in columnsWithFilter" :key="c">
23
- <p class="dark:text-gray-400">{{ c.label }}</p>
24
- <component
25
- v-if="c.components?.filter"
26
- :is="getCustomComponent(c.components.filter)"
27
- :meta="c?.components?.list?.meta"
28
- :column="c"
29
- class="w-full"
30
- @update:modelValue="(filtersArray) => {
31
- filtersStore.filters = filtersStore.filters.filter(f => f.field !== c.name);
32
-
33
- for (const f of filtersArray) {
34
- filtersStore.filters.push({ field: c.name, ...f });
35
- }
36
- console.log('filtersStore.filters', filtersStore.filters);
37
- emits('update:filters', [...filtersStore.filters]);
38
- }"
39
- :modelValue="filtersStore.filters.filter(f => f.field === c.name)"
40
- />
41
- <Select
42
- v-else-if="c.foreignResource"
43
- :multiple="c.filterOptions.multiselect"
44
- class="w-full"
45
- :options="columnOptions[c.name] || []"
46
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event || undefined })"
47
- :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value || (c.filterOptions.multiselect ? [] : '')"
48
- />
49
- <Select
50
- :multiple="c.filterOptions.multiselect"
51
- class="w-full"
52
- v-else-if="c.type === 'boolean'"
53
- :options="[
54
- { label: $t('Yes'), value: true },
55
- { label: $t('No'), value: false },
56
- // if field is not required, undefined might be there, and user might want to filter by it
57
- ...(c.required ? [] : [ { label: $t('Unset'), value: undefined } ])
58
- ]"
59
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event })"
60
- :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value !== undefined
61
- ? filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value
62
- : (c.filterOptions.multiselect ? [] : '')"
63
- />
64
-
65
- <Select
66
- :multiple="c.filterOptions.multiselect"
67
- class="w-full"
68
- v-else-if="c.enum"
69
- :options="c.enum"
70
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event || undefined })"
71
- :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value || (c.filterOptions.multiselect ? [] : '')"
72
- />
73
-
74
- <Input
75
- v-else-if="['string', 'text', 'json', 'richtext', 'unknown'].includes(c.type)"
76
- type="text"
77
- full-width
78
- :placeholder="$t('Search')"
79
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions?.substringSearch ? 'ilike' : 'eq', value: $event || undefined })"
80
- :modelValue="getFilterItem({ column: c, operator: c.filterOptions?.substringSearch ? 'ilike' : 'eq' })"
81
- />
82
-
83
- <CustomDateRangePicker
84
- v-else-if="['datetime', 'date', 'time'].includes(c.type)"
85
- :column="c"
86
- :valueStart="filtersStore.filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
87
- @update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: $event || undefined })"
88
- :valueEnd="filtersStore.filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
89
- @update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: $event || undefined })"
90
- />
91
-
92
- <CustomRangePicker
93
- v-else-if="['integer', 'decimal', 'float'].includes(c.type) && c.allowMinMaxQuery"
94
- :min="getFilterMinValue(c.name)"
95
- :max="getFilterMaxValue(c.name)"
96
- :valueStart="getFilterItem({ column: c, operator: 'gte' })"
97
- @update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: $event || undefined })"
98
- :valueEnd="getFilterItem({ column: c, operator: 'lte' })"
99
- @update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: $event || undefined })"
100
- />
101
-
102
- <div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
103
- <Input
104
- type="number"
105
- aria-describedby="helper-text-explanation"
106
- :placeholder="$t('From')"
107
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'gte', value: $event || undefined })"
108
- :modelValue="getFilterItem({ column: c, operator: 'gte' })"
109
- />
110
- <Input
111
- type="number"
112
- aria-describedby="helper-text-explanation"
113
- :placeholder="$t('To')"
114
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'lte', value: $event|| undefined })"
115
- :modelValue="getFilterItem({ column: c, operator: 'lte' })"
116
- />
117
- </div>
118
-
23
+ <div class="flex flex-col">
24
+ <div class="flex justify-between items-center">
25
+ <p class="dark:text-gray-400 my-1">{{ c.label }}</p>
26
+ <Tooltip v-if="filtersStore.filters.find(f => f.field === c.name)">
27
+ <button
28
+ class=" flex items-center justify-center w-7 h-7 my-1 hover:border rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
29
+ :disabled="!filtersStore.filters.find(f => f.field === c.name)"
30
+ @click="filtersStore.clearFilter(c.name);"
31
+ >
32
+ <IconCloseOutline />
33
+ </button>
34
+ <template #tooltip>
35
+ Clear filter
36
+ </template>
37
+ </Tooltip>
38
+ </div>
39
+ <component
40
+ v-if="c.components?.filter"
41
+ :is="getCustomComponent(c.components.filter)"
42
+ :meta="c?.components?.list?.meta"
43
+ :column="c"
44
+ class="w-full"
45
+ @update:modelValue="(filtersArray) => {
46
+ filtersStore.filters = filtersStore.filters.filter(f => f.field !== c.name);
47
+
48
+ for (const f of filtersArray) {
49
+ filtersStore.filters.push({ field: c.name, ...f });
50
+ }
51
+ console.log('filtersStore.filters', filtersStore.filters);
52
+ emits('update:filters', [...filtersStore.filters]);
53
+ }"
54
+ :modelValue="filtersStore.filters.filter(f => f.field === c.name)"
55
+ />
56
+ <Select
57
+ v-else-if="c.foreignResource"
58
+ :multiple="c.filterOptions.multiselect"
59
+ class="w-full"
60
+ :options="columnOptions[c.name] || []"
61
+ :searchDisabled="!c.foreignResource.searchableFields"
62
+ @scroll-near-end="loadMoreOptions(c.name)"
63
+ @search="(searchTerm) => {
64
+ if (c.foreignResource.searchableFields && onSearchInput[c.name]) {
65
+ onSearchInput[c.name](searchTerm);
66
+ }
67
+ }"
68
+ @update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event || undefined })"
69
+ :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value || (c.filterOptions.multiselect ? [] : '')"
70
+ >
71
+ <template #extra-item v-if="columnLoadingState[c.name]?.loading">
72
+ <div class="text-center text-gray-400 dark:text-gray-300 py-2 flex items-center justify-center gap-2">
73
+ <Spinner class="w-4 h-4" />
74
+ {{ $t('Loading...') }}
75
+ </div>
76
+ </template>
77
+ </Select>
78
+ <Select
79
+ :multiple="c.filterOptions.multiselect"
80
+ class="w-full"
81
+ v-else-if="c.type === 'boolean'"
82
+ :options="[
83
+ { label: $t('Yes'), value: true },
84
+ { label: $t('No'), value: false },
85
+ // if field is not required, undefined might be there, and user might want to filter by it
86
+ ...(c.required ? [] : [ { label: $t('Unset'), value: undefined } ])
87
+ ]"
88
+ @update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event })"
89
+ :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value !== undefined
90
+ ? filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value
91
+ : (c.filterOptions.multiselect ? [] : '')"
92
+ />
93
+
94
+ <Select
95
+ :multiple="c.filterOptions.multiselect"
96
+ class="w-full"
97
+ v-else-if="c.enum"
98
+ :options="c.enum"
99
+ @update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event || undefined })"
100
+ :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value || (c.filterOptions.multiselect ? [] : '')"
101
+ />
102
+
103
+ <Input
104
+ v-else-if="['string', 'text', 'json', 'richtext', 'unknown'].includes(c.type)"
105
+ type="text"
106
+ full-width
107
+ :placeholder="$t('Search')"
108
+ @update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions?.substringSearch ? 'ilike' : 'eq', value: $event || undefined })"
109
+ :modelValue="getFilterItem({ column: c, operator: c.filterOptions?.substringSearch ? 'ilike' : 'eq' })"
110
+ />
111
+
112
+ <CustomDateRangePicker
113
+ v-else-if="['datetime', 'date', 'time'].includes(c.type)"
114
+ :column="c"
115
+ :valueStart="filtersStore.filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
116
+ @update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: $event || undefined })"
117
+ :valueEnd="filtersStore.filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
118
+ @update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: $event || undefined })"
119
+ />
120
+
121
+ <CustomRangePicker
122
+ v-else-if="['integer', 'decimal', 'float'].includes(c.type) && c.allowMinMaxQuery"
123
+ :min="getFilterMinValue(c.name)"
124
+ :max="getFilterMaxValue(c.name)"
125
+ :valueStart="getFilterItem({ column: c, operator: 'gte' })"
126
+ @update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? $event : undefined })"
127
+ :valueEnd="getFilterItem({ column: c, operator: 'lte' })"
128
+ @update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? $event : undefined })"
129
+ />
130
+
131
+ <div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
132
+ <Input
133
+ type="number"
134
+ aria-describedby="helper-text-explanation"
135
+ :placeholder="$t('From')"
136
+ @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? $event : undefined })"
137
+ :modelValue="getFilterItem({ column: c, operator: 'gte' })"
138
+ />
139
+ <Input
140
+ type="number"
141
+ aria-describedby="helper-text-explanation"
142
+ :placeholder="$t('To')"
143
+ @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? $event : undefined })"
144
+ :modelValue="getFilterItem({ column: c, operator: 'lte' })"
145
+ />
146
+ </div>
147
+ </div>
119
148
  </li>
120
149
  </ul>
121
150
  </div>
122
151
 
123
152
  <div class="flex justify-end gap-2">
124
153
  <button
125
- :disabled="!filtersStore.filters.length"
154
+ :disabled="!filtersStore.visibleFiltersCount"
126
155
  type="button"
127
- 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 disabled:opacity-50 disabled:cursor-not-allowed"
128
- @click="clear">{{ $t('Clear all') }}</button>
156
+ class="flex gap-1 items-center py-1 pr-3 text-sm font-medium text-lightFiltersClearAllButtonText focus:outline-none bg-lightFiltersClearAllButtonBackground rounded border border-lightFiltersClearAllButtonBorder hover:bg-lightFiltersClearAllButtonBackgroundHover hover:text-lightFiltersClearAllButtonTextHover focus:z-10 focus:ring-4 focus:ring-lightFiltersClearAllButtonFocus dark:focus:ring-darkFiltersClearAllButtonFocus dark:bg-darkFiltersClearAllButtonBackground dark:text-darkFiltersClearAllButtonText dark:border-darkFiltersClearAllButtonBorder dark:hover:text-darkFiltersClearAllButtonTextHover dark:hover:bg-darkFiltersClearAllButtonBackgroundHover disabled:opacity-50 disabled:cursor-not-allowed"
157
+ @click="clear"><IconCloseOutline class="ml-3"/> {{ $t('Clear all') }}</button>
129
158
 
130
159
  </div>
131
160
  </div>
@@ -136,18 +165,20 @@
136
165
  </template>
137
166
 
138
167
  <script setup>
139
- import { watch, computed } from 'vue';
168
+ import { watch, computed, ref, reactive } from 'vue';
140
169
  import { useI18n } from 'vue-i18n';
141
170
  import CustomDateRangePicker from '@/components/CustomDateRangePicker.vue';
142
- import { callAdminForthApi } from '@/utils';
171
+ import { callAdminForthApi, loadMoreForeignOptions, searchForeignOptions, createSearchInputHandlers } from '@/utils';
143
172
  import { useRouter } from 'vue-router';
144
- import { computedAsync } from '@vueuse/core'
145
173
  import CustomRangePicker from "@/components/CustomRangePicker.vue";
146
174
  import { useFiltersStore } from '@/stores/filters';
147
175
  import { getCustomComponent } from '@/utils';
148
176
  import Input from '@/afcl/Input.vue';
149
177
  import Select from '@/afcl/Select.vue';
178
+ import Spinner from '@/afcl/Spinner.vue';
150
179
  import debounce from 'debounce';
180
+ import { Tooltip } from '@/afcl';
181
+ import { IconCloseOutline } from '@iconify-prerendered/vue-flowbite';
151
182
 
152
183
  const filtersStore = useFiltersStore();
153
184
  const { t } = useI18n();
@@ -165,31 +196,54 @@ const columnsWithFilter = computed(
165
196
  () => props.columns?.filter(column => column.showIn.filter) || []
166
197
  );
167
198
 
168
- const columnOptions = computedAsync(async () => {
169
- const ret = {};
170
- if (!props.columns) {
171
- return ret;
172
- }
173
- await Promise.all(
174
- Object.values(props.columns).map(async (column) => {
175
- if (column.foreignResource) {
176
- const list = await callAdminForthApi({
177
- method: 'POST',
178
- path: `/get_resource_foreign_data`,
179
- body: {
180
- resourceId: router.currentRoute.value.params.resourceId,
181
- column: column.name,
182
- limit: 10000,
183
- offset: 0,
184
- },
185
- });
186
- ret[column.name] = list.items;
199
+ const columnOptions = ref({});
200
+ const columnLoadingState = reactive({});
201
+ const columnOffsets = reactive({});
202
+ const columnEmptyResultsCount = reactive({});
203
+
204
+ watch(() => props.columns, async (newColumns) => {
205
+ if (!newColumns) return;
206
+
207
+ for (const column of newColumns) {
208
+ if (column.foreignResource) {
209
+ if (!columnOptions.value[column.name]) {
210
+ columnOptions.value[column.name] = [];
211
+ columnLoadingState[column.name] = { loading: false, hasMore: true };
212
+ columnOffsets[column.name] = 0;
213
+ columnEmptyResultsCount[column.name] = 0;
214
+
215
+ await loadMoreOptions(column.name);
187
216
  }
188
- })
189
- );
217
+ }
218
+ }
219
+ }, { immediate: true });
220
+
221
+ // Function to load more options for a specific column
222
+ async function loadMoreOptions(columnName, searchTerm = '') {
223
+ return loadMoreForeignOptions({
224
+ columnName,
225
+ searchTerm,
226
+ columns: props.columns,
227
+ resourceId: router.currentRoute.value.params.resourceId,
228
+ columnOptions,
229
+ columnLoadingState,
230
+ columnOffsets,
231
+ columnEmptyResultsCount
232
+ });
233
+ }
190
234
 
191
- return ret;
192
- }, {});
235
+ async function searchOptions(columnName, searchTerm) {
236
+ return searchForeignOptions({
237
+ columnName,
238
+ searchTerm,
239
+ columns: props.columns,
240
+ resourceId: router.currentRoute.value.params.resourceId,
241
+ columnOptions,
242
+ columnLoadingState,
243
+ columnOffsets,
244
+ columnEmptyResultsCount
245
+ });
246
+ }
193
247
 
194
248
 
195
249
  // sync 'body' class 'overflow-hidden' with show prop show
@@ -221,10 +275,18 @@ const onFilterInput = computed(() => {
221
275
  }, {});
222
276
  });
223
277
 
278
+ const onSearchInput = computed(() => {
279
+ return createSearchInputHandlers(
280
+ props.columns,
281
+ searchOptions,
282
+ (column) => column.filterOptions?.debounceTimeMs || 300
283
+ );
284
+ });
285
+
224
286
  function setFilterItem({ column, operator, value }) {
225
287
 
226
288
  const index = filtersStore.filters.findIndex(f => f.field === column.name && f.operator === operator);
227
- if (value === undefined) {
289
+ if (value === undefined || value === '' || value === null) {
228
290
  if (index !== -1) {
229
291
  filtersStore.filters.splice(index, 1);
230
292
  }
@@ -239,11 +301,12 @@ function setFilterItem({ column, operator, value }) {
239
301
  }
240
302
 
241
303
  function getFilterItem({ column, operator }) {
242
- return filtersStore.filters.find(f => f.field === column.name && f.operator === operator)?.value || '';
304
+ const filterValue = filtersStore.filters.find(f => f.field === column.name && f.operator === operator)?.value;
305
+ return filterValue !== undefined ? filterValue : '';
243
306
  }
244
307
 
245
308
  async function clear() {
246
- filtersStore.clearFilters();
309
+ filtersStore.filters = [...filtersStore.filters.filter(f => filtersStore.shouldFilterBeHidden(f.field))];
247
310
  emits('update:filters', [...filtersStore.filters]);
248
311
  }
249
312
 
@@ -1,10 +1,10 @@
1
1
  <template>
2
2
  <div class="rounded-lg shadow-resourseFormShadow dark:shadow-darkResourseFormShadow dark:shadow-2xl">
3
- <div v-if="group.groupName && !group.noTitle" class="text-md font-semibold px-6 py-3 flex flex-1 items-center dark:border-gray-600 text-gray-700 bg-lightFormHeading dark:bg-gray-700 dark:text-gray-400 rounded-t-lg">
3
+ <div v-if="group.groupName && !group.noTitle" class="text-md font-semibold px-6 py-3 flex flex-1 items-center dark:border-darkFormBorder text-lightListTableHeadingText bg-lightFormHeading dark:bg-darkFormHeading dark:text-darkListTableHeadingText rounded-t-lg">
4
4
  {{ group.groupName }}
5
5
  </div>
6
- <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
7
- <thead v-if="!allColumnsHaveCustomComponent" class="text-xs text-gray-700 uppercase dark:text-gray-400 bg-lightFormHeading dark:bg-gray-700 block md:table-row-group ">
6
+ <table class="w-full text-sm text-left rtl:text-right text-lightFormFieldTextColor dark:text-darkFormFieldTextColor">
7
+ <thead v-if="!allColumnsHaveCustomComponent" class="text-xs text-lightListTableHeadingText uppercase dark:text-darkListTableHeadingText bg-lightFormHeading dark:bg-darkFormHeading block md:table-row-group ">
8
8
  <tr>
9
9
  <th scope="col" :class="{'rounded-tl-lg': !group.groupName}" class="px-6 py-3 hidden md:w-52 md:table-cell">
10
10
  {{ $t('Field') }}
@@ -19,7 +19,7 @@
19
19
  v-for="(column, i) in group.columns"
20
20
  :key="column.name"
21
21
  v-if="currentValues !== null"
22
- class="bg-ligftForm dark:bg-gray-800 dark:border-gray-700 block md:table-row"
22
+ class="bg-lightForm dark:bg-darkForm dark:border-darkFormBorder block md:table-row"
23
23
  :class="{ 'border-b': i !== group.columns.length - 1}"
24
24
  >
25
25
  <td class="px-6 py-4 flex items-center block md:table-cell pb-0 md:pb-4"
@@ -29,7 +29,7 @@
29
29
  <Tooltip v-if="column.required[mode]">
30
30
 
31
31
  <IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
32
- :class="(columnError(column) && validating) ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'"
32
+ :class="(columnError(column) && validating) ? 'text-lightInputErrorColor dark:text-darkInputErrorColor' : 'text-lightRequiredIconColor dark:text-darkRequiredIconColor'"
33
33
  />
34
34
 
35
35
  <template #tooltip>
@@ -55,8 +55,8 @@
55
55
  @update:emptiness="customComponentsEmptiness[$event.name] = $event.value"
56
56
  :readonly="readonlyColumns?.includes(column.name)"
57
57
  />
58
- <div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
59
- <div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
58
+ <div v-if="columnError(column) && validating" class="mt-1 text-xs text-lightInputErrorColor dark:text-darkInputErrorColor">{{ columnError(column) }}</div>
59
+ <div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-lightFormFieldTextColor dark:text-darkFormFieldTextColor">{{ column.editingNote[mode] }}</div>
60
60
  </td>
61
61
  </tr>
62
62
  </tbody>
@@ -70,6 +70,7 @@
70
70
  import { ref, computed, watch, nextTick, type Ref } from 'vue';
71
71
  import { useI18n } from 'vue-i18n';
72
72
  import ColumnValueInputWrapper from "@/components/ColumnValueInputWrapper.vue";
73
+ import type { AdminForthResourceColumnInputCommon } from '@/types/Common';
73
74
 
74
75
  const { t } = useI18n();
75
76
 
@@ -89,7 +90,7 @@
89
90
  const customComponentsInValidity: Ref<Record<string, boolean>> = ref({});
90
91
  const customComponentsEmptiness: Ref<Record<string, boolean>> = ref({});
91
92
  const allColumnsHaveCustomComponent = computed(() => {
92
- return props.group.columns.every(column => {
93
+ return props.group.columns.every((column: AdminForthResourceColumnInputCommon) => {
93
94
  const componentKey = `${props.source}Row` as keyof typeof column.components;
94
95
  return column.components?.[componentKey];
95
96
  });
@@ -1,42 +1,109 @@
1
1
  <template>
2
2
  <RouterLink
3
3
  :to="{name: item.resourceId ? 'resource-list' : item.path, params: item.resourceId ? { resourceId: item.resourceId }: {}}"
4
- class="flex group items-center py-2 text-lightSidebarText dark:text-darkSidebarText rounded-default hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextHover active:bg-lightSidebarActive dark:active:bg-darkSidebarHover" role="menuitem"
4
+ class="af-menu-link flex group relative items-center w-full py-2 text-lightSidebarText dark:text-darkSidebarText rounded-default transition-all duration-200 ease-in-out"
5
5
  :class="{
6
- 'px-4': isChild,
7
- 'px-2': !isChild,
6
+ 'hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextHover active:bg-lightSidebarActive dark:active:bg-darkSidebarHover': !['divider', 'gap', 'heading'].includes(item.type),
7
+ 'pl-6 pr-3.5': (isChild && !isSidebarIconOnly && !isSidebarHovering) || (isChild && isSidebarIconOnly && isSidebarHovering),
8
+ 'px-3.5 ': !isChild || (isSidebarIconOnly && !isSidebarHovering),
9
+ 'max-w-12': isSidebarIconOnly && !isSidebarHovering,
8
10
  'bg-lightSidebarItemActive dark:bg-darkSidebarItemActive': item.resourceId ?
9
11
  ($route.params.resourceId === item.resourceId && $route.name === 'resource-list') :
10
12
  ($route.name === item.path)
11
13
  }"
12
14
  >
13
- <component v-if="item.icon" :is="getIcon(item.icon)" class="w-5 h-5 text-lightSidebarIcons dark:text-darkSidebarIcons transition duration-75 group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover" ></component>
14
- <span class="text-ellipsis overflow-hidden ms-3">{{ item.label }}</span>
15
- <span v-if="item.badge"
15
+ <component v-if="item.icon" :is="getIcon(item.icon)"
16
+ class="min-w-5 min-h-5 text-lightSidebarIcons dark:text-darkSidebarIcons group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover transition-all duration-200 ease-in-out"
16
17
  >
17
-
18
- <Tooltip v-if="item.badgeTooltip">
19
- <div class="inline-flex items-center justify-center h-3 py-3 px-1 ms-3 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
20
- fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent min-w-[1.5rem] max-w-[3rem]">{{ item.badge }}</div>
21
-
22
- <template #tooltip>
23
- {{ item.badgeTooltip }}
24
- </template>
25
- </Tooltip>
26
- <template v-else>
27
- <div class="inline-flex items-center justify-center h-3 py-3 px-1 ms-3 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
28
- fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent min-w-[1.5rem] max-w-[3rem]">{{ item.badge }}</div>
29
- </template>
30
-
18
+ </component>
19
+ <div
20
+ class="overflow-hidden block ms-3 pr-4 text-left rtl:text-right transition-all duration-200 ease-in-out"
21
+ :class="{
22
+ 'opacity-0 ms-0 translate-x-4 flex-none': isSidebarIconOnly && !isSidebarHovering,
23
+ 'opacity-100 ms-3 translate-x-0 flex-1': !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering),
24
+ }"
25
+ :style="isSidebarIconOnly ? {
26
+ minWidth: isChild
27
+ ? 'calc(16.5rem - 0.75rem*2 - 1.5rem*2 - 1.25rem - 0.75rem)'
28
+ : 'calc(16.5rem - 0.75rem*2 - 0.875rem*2 - 1.25rem - 0.75rem)',
29
+ width: isChild
30
+ ? 'calc(16.5rem - 0.75rem*2 - 1.5rem*2 - 1.25rem - 0.75rem)'
31
+ : 'calc(16.5rem - 0.75rem*2 - 0.875rem*2 - 1.25rem - 0.75rem)'
32
+ } : {}"
33
+ >
34
+ {{ item.label }}
35
+ </div>
36
+ <span class="absolute flex items-center justify-center right-1 top-1/2 -translate-y-1/2" v-if="item.badge && showExpandedBadge">
37
+ <Tooltip v-if="item.badgeTooltip">
38
+ <div class="af-badge inline-flex items-center justify-center h-3 py-2.5 px-1 ms-3 text-xs font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
39
+ fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent min-w-[1.5rem] max-w-[3rem]">{{ item.badge }}</div>
40
+ <template #tooltip>
41
+ {{ item.badgeTooltip }}
42
+ </template>
43
+ </Tooltip>
44
+ <template v-else>
45
+ <div class="af-badge inline-flex items-center justify-center h-3 py-2.5 px-1 ms-3 text-xs font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
46
+ fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent min-w-[1.5rem] max-w-[3rem]">{{ item.badge }}</div>
47
+ </template>
31
48
  </span>
32
-
49
+ <div v-if="item.badge && isSidebarIconOnly && !isSidebarHovering" class="af-badge absolute right-0.5 bottom-1 -translate-y-1/2 inline-flex items-center justify-center h-2 w-2 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
50
+ fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent">
51
+ </div>
33
52
  </RouterLink>
34
53
  </template>
35
54
 
36
- <script setup lang="ts">
55
+ <script setup lang="ts">
37
56
  import { getIcon } from '@/utils';
38
57
  import { Tooltip } from '@/afcl';
39
- const props = defineProps(['item', 'isChild']);
58
+ import { ref, watch } from 'vue';
59
+
60
+ const props = defineProps(['item', 'isChild', 'isSidebarIconOnly', 'isSidebarHovering']);
61
+
62
+ const BADGE_SHOW_DELAY_MS = 200;
63
+ const showExpandedBadge = ref(false);
64
+ let showBadgeTimer: ReturnType<typeof setTimeout> | null = null;
65
+
66
+ function cancelShowBadgeTimer() {
67
+ if (showBadgeTimer) {
68
+ clearTimeout(showBadgeTimer);
69
+ showBadgeTimer = null;
70
+ }
71
+ }
72
+
73
+ function showBadgeImmediately() {
74
+ cancelShowBadgeTimer();
75
+ showExpandedBadge.value = true;
76
+ }
77
+
78
+ function hideBadgeImmediately() {
79
+ cancelShowBadgeTimer();
80
+ showExpandedBadge.value = false;
81
+ }
82
+
83
+ function showBadgeAfterDelay() {
84
+ cancelShowBadgeTimer();
85
+ showBadgeTimer = setTimeout(() => {
86
+ showExpandedBadge.value = true;
87
+ showBadgeTimer = null;
88
+ }, BADGE_SHOW_DELAY_MS);
89
+ }
90
+
91
+ watch(
92
+ [() => props.isSidebarIconOnly, () => props.isSidebarHovering],
93
+ ([isIconOnly, isHovering]) => {
94
+ if (!isIconOnly) {
95
+ showBadgeImmediately();
96
+ return;
97
+ }
98
+
99
+ if (isHovering) {
100
+ showBadgeAfterDelay();
101
+ return;
102
+ }
40
103
 
104
+ hideBadgeImmediately();
105
+ },
106
+ { immediate: true }
107
+ );
41
108
 
42
109
  </script>