adminforth 2.11.18 → 2.12.1

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 (76) hide show
  1. package/commands/createApp/templates/api.ts.hbs +10 -0
  2. package/commands/createApp/templates/index.ts.hbs +4 -1
  3. package/commands/createApp/utils.js +5 -0
  4. package/commands/createCustomComponent/main.js +12 -7
  5. package/commands/createCustomComponent/templates/customCrud/saveButton.vue.hbs +28 -0
  6. package/dist/auth.d.ts.map +1 -1
  7. package/dist/auth.js +6 -0
  8. package/dist/auth.js.map +1 -1
  9. package/dist/dataConnectors/baseConnector.d.ts +1 -1
  10. package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  11. package/dist/dataConnectors/baseConnector.js +23 -2
  12. package/dist/dataConnectors/baseConnector.js.map +1 -1
  13. package/dist/dataConnectors/postgres.d.ts.map +1 -1
  14. package/dist/dataConnectors/postgres.js +32 -14
  15. package/dist/dataConnectors/postgres.js.map +1 -1
  16. package/dist/index.d.ts +10 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +19 -12
  19. package/dist/index.js.map +1 -1
  20. package/dist/modules/codeInjector.d.ts.map +1 -1
  21. package/dist/modules/codeInjector.js +12 -0
  22. package/dist/modules/codeInjector.js.map +1 -1
  23. package/dist/modules/configValidator.d.ts.map +1 -1
  24. package/dist/modules/configValidator.js +66 -10
  25. package/dist/modules/configValidator.js.map +1 -1
  26. package/dist/modules/restApi.d.ts +1 -1
  27. package/dist/modules/restApi.d.ts.map +1 -1
  28. package/dist/modules/restApi.js +18 -5
  29. package/dist/modules/restApi.js.map +1 -1
  30. package/dist/modules/styles.d.ts +0 -18
  31. package/dist/modules/styles.d.ts.map +1 -1
  32. package/dist/modules/styles.js +1 -19
  33. package/dist/modules/styles.js.map +1 -1
  34. package/dist/servers/express.d.ts +5 -0
  35. package/dist/servers/express.d.ts.map +1 -1
  36. package/dist/servers/express.js +26 -1
  37. package/dist/servers/express.js.map +1 -1
  38. package/dist/spa/package.json +1 -1
  39. package/dist/spa/src/App.vue +1 -1
  40. package/dist/spa/src/afcl/Input.vue +10 -2
  41. package/dist/spa/src/afcl/Select.vue +17 -3
  42. package/dist/spa/src/afcl/Table.vue +1 -1
  43. package/dist/spa/src/afcl/Tooltip.vue +4 -2
  44. package/dist/spa/src/components/AcceptModal.vue +43 -9
  45. package/dist/spa/src/components/CallActionWrapper.vue +15 -0
  46. package/dist/spa/src/components/ColumnValueInput.vue +1 -1
  47. package/dist/spa/src/components/CustomRangePicker.vue +3 -16
  48. package/dist/spa/src/components/Filters.vue +129 -112
  49. package/dist/spa/src/components/ResourceForm.vue +2 -2
  50. package/dist/spa/src/components/ResourceListTable.vue +45 -13
  51. package/dist/spa/src/components/ResourceListTableVirtual.vue +52 -16
  52. package/dist/spa/src/components/ShowTable.vue +5 -4
  53. package/dist/spa/src/components/Sidebar.vue +27 -5
  54. package/dist/spa/src/components/ThreeDotsMenu.vue +27 -17
  55. package/dist/spa/src/components/Toast.vue +15 -22
  56. package/dist/spa/src/components/UserMenuSettingsButton.vue +9 -10
  57. package/dist/spa/src/i18n.ts +4 -2
  58. package/dist/spa/src/main.ts +1 -1
  59. package/dist/spa/src/stores/core.ts +12 -0
  60. package/dist/spa/src/stores/filters.ts +5 -1
  61. package/dist/spa/src/types/Back.ts +22 -1
  62. package/dist/spa/src/types/Common.ts +24 -0
  63. package/dist/spa/src/utils.ts +69 -2
  64. package/dist/spa/src/views/CreateView.vue +47 -4
  65. package/dist/spa/src/views/EditView.vue +30 -3
  66. package/dist/spa/src/views/ListView.vue +6 -2
  67. package/dist/spa/src/views/LoginView.vue +4 -13
  68. package/dist/spa/src/views/SettingsView.vue +1 -1
  69. package/dist/spa/src/views/ShowView.vue +27 -17
  70. package/dist/types/Back.d.ts +27 -0
  71. package/dist/types/Back.d.ts.map +1 -1
  72. package/dist/types/Back.js.map +1 -1
  73. package/dist/types/Common.d.ts +47 -0
  74. package/dist/types/Common.d.ts.map +1 -1
  75. package/dist/types/Common.js.map +1 -1
  76. package/package.json +2 -1
@@ -20,116 +20,131 @@
20
20
  <div class="py-4 ">
21
21
  <ul class="space-y-3 font-medium">
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
- :searchDisabled="!c.foreignResource.searchableFields"
47
- @scroll-near-end="loadMoreOptions(c.name)"
48
- @search="(searchTerm) => {
49
- if (c.foreignResource.searchableFields && onSearchInput[c.name]) {
50
- onSearchInput[c.name](searchTerm);
51
- }
52
- }"
53
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event || undefined })"
54
- :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value || (c.filterOptions.multiselect ? [] : '')"
55
- >
56
- <template #extra-item v-if="columnLoadingState[c.name]?.loading">
57
- <div class="text-center text-gray-400 dark:text-gray-300 py-2 flex items-center justify-center gap-2">
58
- <Spinner class="w-4 h-4" />
59
- {{ $t('Loading...') }}
60
- </div>
61
- </template>
62
- </Select>
63
- <Select
64
- :multiple="c.filterOptions.multiselect"
65
- class="w-full"
66
- v-else-if="c.type === 'boolean'"
67
- :options="[
68
- { label: $t('Yes'), value: true },
69
- { label: $t('No'), value: false },
70
- // if field is not required, undefined might be there, and user might want to filter by it
71
- ...(c.required ? [] : [ { label: $t('Unset'), value: undefined } ])
72
- ]"
73
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event })"
74
- :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value !== undefined
75
- ? filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value
76
- : (c.filterOptions.multiselect ? [] : '')"
77
- />
78
-
79
- <Select
80
- :multiple="c.filterOptions.multiselect"
81
- class="w-full"
82
- v-else-if="c.enum"
83
- :options="c.enum"
84
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event || undefined })"
85
- :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value || (c.filterOptions.multiselect ? [] : '')"
86
- />
87
-
88
- <Input
89
- v-else-if="['string', 'text', 'json', 'richtext', 'unknown'].includes(c.type)"
90
- type="text"
91
- full-width
92
- :placeholder="$t('Search')"
93
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions?.substringSearch ? 'ilike' : 'eq', value: $event || undefined })"
94
- :modelValue="getFilterItem({ column: c, operator: c.filterOptions?.substringSearch ? 'ilike' : 'eq' })"
95
- />
96
-
97
- <CustomDateRangePicker
98
- v-else-if="['datetime', 'date', 'time'].includes(c.type)"
99
- :column="c"
100
- :valueStart="filtersStore.filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
101
- @update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: $event || undefined })"
102
- :valueEnd="filtersStore.filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
103
- @update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: $event || undefined })"
104
- />
105
-
106
- <CustomRangePicker
107
- v-else-if="['integer', 'decimal', 'float'].includes(c.type) && c.allowMinMaxQuery"
108
- :min="getFilterMinValue(c.name)"
109
- :max="getFilterMaxValue(c.name)"
110
- :valueStart="getFilterItem({ column: c, operator: 'gte' })"
111
- @update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? $event : undefined })"
112
- :valueEnd="getFilterItem({ column: c, operator: 'lte' })"
113
- @update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? $event : undefined })"
114
- />
115
-
116
- <div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
117
- <Input
118
- type="number"
119
- aria-describedby="helper-text-explanation"
120
- :placeholder="$t('From')"
121
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? $event : undefined })"
122
- :modelValue="getFilterItem({ column: c, operator: 'gte' })"
123
- />
124
- <Input
125
- type="number"
126
- aria-describedby="helper-text-explanation"
127
- :placeholder="$t('To')"
128
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? $event : undefined })"
129
- :modelValue="getFilterItem({ column: c, operator: 'lte' })"
130
- />
131
- </div>
132
-
23
+ <div class="flex flex-col">
24
+ <div class="flex justify-between items-center">
25
+ <p class="dark:text-gray-400 h-7 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>
133
148
  </li>
134
149
  </ul>
135
150
  </div>
@@ -138,8 +153,8 @@
138
153
  <button
139
154
  :disabled="!filtersStore.visibleFiltersCount"
140
155
  type="button"
141
- class="flex items-center py-1 px-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"
142
- @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>
143
158
 
144
159
  </div>
145
160
  </div>
@@ -162,6 +177,8 @@ import Input from '@/afcl/Input.vue';
162
177
  import Select from '@/afcl/Select.vue';
163
178
  import Spinner from '@/afcl/Spinner.vue';
164
179
  import debounce from 'debounce';
180
+ import { Tooltip } from '@/afcl';
181
+ import { IconCloseOutline } from '@iconify-prerendered/vue-flowbite';
165
182
 
166
183
  const filtersStore = useFiltersStore();
167
184
  const { t } = useI18n();
@@ -63,7 +63,7 @@
63
63
 
64
64
  <script setup lang="ts">
65
65
 
66
- import { applyRegexValidation, callAdminForthApi, loadMoreForeignOptions, searchForeignOptions, createSearchInputHandlers} from '@/utils';
66
+ import { applyRegexValidation, callAdminForthApi, loadMoreForeignOptions, searchForeignOptions, createSearchInputHandlers, checkShowIf } from '@/utils';
67
67
  import { computedAsync } from '@vueuse/core';
68
68
  import { computed, onMounted, reactive, ref, watch, provide, type Ref } from 'vue';
69
69
  import { useRouter, useRoute } from 'vue-router';
@@ -322,7 +322,7 @@ async function searchOptions(columnName: string, searchTerm: string) {
322
322
 
323
323
 
324
324
  const editableColumns = computed(() => {
325
- return props.resource?.columns?.filter(column => column.showIn?.[mode.value]);
325
+ return props.resource?.columns?.filter(column => column.showIn?.[mode.value] && (currentValues.value ? checkShowIf(column, currentValues.value) : true));
326
326
  });
327
327
 
328
328
  const isValid = computed(() => {
@@ -14,8 +14,8 @@
14
14
 
15
15
  <tbody>
16
16
  <!-- table header -->
17
- <tr class="t-header sticky z-10 top-0 text-xs text-lightListTableHeadingText bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-darkListTableHeadingText">
18
- <td scope="col" class="p-4">
17
+ <tr class="t-header sticky z-20 top-0 text-xs text-lightListTableHeadingText bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-darkListTableHeadingText">
18
+ <td scope="col" class="p-4 sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading">
19
19
  <Checkbox
20
20
  :modelValue="allFromThisPageChecked"
21
21
  :disabled="!rows || !rows.length"
@@ -25,7 +25,7 @@
25
25
  </Checkbox>
26
26
  </td>
27
27
 
28
- <td v-for="c in columnsListed" ref="headerRefs" scope="col" class="px-2 md:px-3 lg:px-6 py-3">
28
+ <td v-for="c in columnsListed" ref="headerRefs" scope="col" class="px-2 md:px-3 lg:px-6 py-3" :class="{'sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading': c.listSticky}">
29
29
 
30
30
  <div @click="(evt) => c.sortable && onSortButtonClick(evt, c.name)"
31
31
  class="flex items-center " :class="{'cursor-pointer':c.sortable}">
@@ -90,7 +90,7 @@
90
90
 
91
91
  :class="{'border-b': rowI !== rows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
92
92
  >
93
- <td class="w-4 p-4 cursor-default" @click="(e)=>e.stopPropagation()">
93
+ <td class="w-4 p-4 cursor-default sticky-column bg-lightListTable dark:bg-darkListTable" @click="(e)=>e.stopPropagation()">
94
94
  <Checkbox
95
95
  :model-value="checkboxesInternal.includes(row._primaryKeyValue)"
96
96
  @change="(e: any)=>{addToCheckedValues(row._primaryKeyValue)}"
@@ -100,7 +100,7 @@
100
100
  </Checkbox>
101
101
  </td>
102
102
 
103
- <td v-for="c in columnsListed" class="px-2 md:px-3 lg:px-6 py-4">
103
+ <td v-for="c in columnsListed" class="px-2 md:px-3 lg:px-6 py-4" :class="{'sticky-column bg-lightListTable dark:bg-darkListTable': c.listSticky}">
104
104
  <!-- if c.name in listComponentsPerColumn, render it. If not, render ValueRenderer -->
105
105
  <component
106
106
  :is="c?.components?.list ? getCustomComponent(typeof c.components.list === 'string' ? { file: c.components.list } : c.components.list) : ValueRenderer"
@@ -172,22 +172,43 @@
172
172
  :resource="coreStore.resource"
173
173
  :adminUser="coreStore.adminUser"
174
174
  :record="row"
175
+ :updateRecords="()=>emits('update:records', true)"
175
176
  />
176
177
  </template>
177
178
 
178
179
  <template v-if="resource.options?.actions">
179
- <Tooltip v-for="action in resource.options.actions.filter(a => a.showIn?.list)" :key="action.id">
180
- <button
181
- @click="startCustomAction(action.id, row)"
182
- >
183
- <component v-if="action.icon" :is="getIcon(action.icon)" class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"></component>
184
- </button>
185
- <template v-slot:tooltip>
180
+ <Tooltip
181
+ v-for="action in resource.options.actions.filter(a => a.showIn?.list)"
182
+ :key="action.id"
183
+ >
184
+ <component
185
+ :is="action.customComponent ? getCustomComponent(action.customComponent) : CallActionWrapper"
186
+ :meta="action.customComponent?.meta"
187
+ :row="row"
188
+ :resource="resource"
189
+ :adminUser="adminUser"
190
+ @callAction="(payload? : Object) => startCustomAction(action.id, payload ?? row)"
191
+ >
192
+ <button
193
+ type="button"
194
+ :disabled="rowActionLoadingStates?.[action.id]"
195
+ @click.stop.prevent
196
+ >
197
+ <component
198
+ v-if="action.icon"
199
+ :is="getIcon(action.icon)"
200
+ class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"
201
+ />
202
+ </button>
203
+ </component>
204
+
205
+ <template #tooltip>
186
206
  {{ action.name }}
187
207
  </template>
188
208
  </Tooltip>
189
209
  </template>
190
210
  </div>
211
+
191
212
  </td>
192
213
  </tr>
193
214
  </tbody>
@@ -250,7 +271,7 @@
250
271
  </div>
251
272
 
252
273
  <!-- Help text -->
253
- <span class="text-sm text-lightListTablePaginationHelpText dark:text-darkListTablePaginationHelpText">
274
+ <span class="ml-4 text-sm text-lightListTablePaginationHelpText dark:text-darkListTablePaginationHelpText">
254
275
  <span v-if="((((page || 1) - 1) * pageSize + 1 > totalRows) && totalRows > 0)">{{ $t('Wrong Page') }} </span>
255
276
  <template v-else-if="resource && totalRows > 0">
256
277
 
@@ -598,4 +619,15 @@ input[type="checkbox"][disabled] {
598
619
  input[type="checkbox"]:not([disabled]) {
599
620
  @apply cursor-pointer;
600
621
  }
622
+ td.sticky-column {
623
+ @apply sticky left-0 z-10;
624
+ &:not(:first-child) {
625
+ @apply left-[56px];
626
+ }
627
+ }
628
+ tr:not(:first-child):hover {
629
+ td.sticky-column {
630
+ @apply bg-lightListTableRowHover dark:bg-darkListTableRowHover;
631
+ }
632
+ }
601
633
  </style>
@@ -2,7 +2,7 @@
2
2
  <!-- table -->
3
3
  <div class="relative shadow-listTableShadow dark:shadow-darkListTableShadow overflow-auto "
4
4
  :class="{'rounded-default': !noRoundings}"
5
- :style="`height: ${containerHeight}px; will-change: transform;`"
5
+ :style="{ maxHeight: `${containerHeight}px` }"
6
6
  @scroll="handleScroll"
7
7
  ref="containerRef"
8
8
  >
@@ -14,12 +14,12 @@
14
14
  <div class="h-2 bg-lightListSkeletLoader rounded-full dark:bg-darkListSkeletLoader max-w-[360px]"></div>
15
15
  </div>
16
16
  </div>
17
- <table v-else class="h-full w-full text-sm text-left rtl:text-right text-lightListTableText dark:text-darkListTableText rounded-default">
17
+ <table v-else class="w-full text-sm text-left rtl:text-right text-lightListTableText dark:text-darkListTableText rounded-default">
18
18
 
19
19
  <tbody>
20
20
  <!-- table header -->
21
- <tr class="t-header sticky z-10 top-0 text-xs bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-gray-400">
22
- <td scope="col" class="p-4">
21
+ <tr class="t-header sticky z-20 top-0 text-xs bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-gray-400">
22
+ <td scope="col" class="p-4 sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading">
23
23
  <Checkbox
24
24
  :modelValue="allFromThisPageChecked"
25
25
  :disabled="!rows || !rows.length"
@@ -29,7 +29,7 @@
29
29
  </Checkbox>
30
30
  </td>
31
31
 
32
- <td v-for="c in columnsListed" ref="headerRefs" scope="col" class="px-2 md:px-3 lg:px-6 py-3">
32
+ <td v-for="c in columnsListed" ref="headerRefs" scope="col" class="px-2 md:px-3 lg:px-6 py-3" :class="{'sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading': c.listSticky}">
33
33
 
34
34
  <div @click="(evt) => c.sortable && onSortButtonClick(evt, c.name)"
35
35
  class="flex items-center " :class="{'cursor-pointer':c.sortable}">
@@ -74,7 +74,7 @@
74
74
  :column-widths="columnWidths"
75
75
  />
76
76
 
77
- <tr v-else-if="rows.length === 0" class="h-full bg-lightListTable dark:bg-darkListTable dark:border-darkListTableBorder">
77
+ <tr v-else-if="rows.length === 0" class="bg-lightListTable dark:bg-darkListTable dark:border-darkListTableBorder">
78
78
  <td :colspan="resource?.columns.length + 2">
79
79
 
80
80
  <div id="toast-simple"
@@ -101,7 +101,7 @@
101
101
  :class="{'border-b': rowI !== visibleRows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
102
102
  @mounted="(el: any) => updateRowHeight(`row_${row._primaryKeyValue}`, el.offsetHeight)"
103
103
  >
104
- <td class="w-4 p-4 cursor-default" @click="(e)=>e.stopPropagation()">
104
+ <td class="w-4 p-4 cursor-default sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading" @click="(e)=>e.stopPropagation()">
105
105
  <Checkbox
106
106
  :model-value="checkboxesInternal.includes(row._primaryKeyValue)"
107
107
  @change="(e: any)=>{addToCheckedValues(row._primaryKeyValue)}"
@@ -110,7 +110,7 @@
110
110
  <span class="sr-only">{{ $t('checkbox') }}</span>
111
111
  </Checkbox>
112
112
  </td>
113
- <td v-for="c in columnsListed" class="px-2 md:px-3 lg:px-6 py-4">
113
+ <td v-for="c in columnsListed" class="px-2 md:px-3 lg:px-6 py-4" :class="{'sticky-column bg-lightListTable dark:bg-darkListTable': c.listSticky}">
114
114
  <!-- if c.name in listComponentsPerColumn, render it. If not, render ValueRenderer -->
115
115
  <component
116
116
  :is="c?.components?.list ? getCustomComponent(typeof c.components.list === 'string' ? { file: c.components.list } : c.components.list) : ValueRenderer"
@@ -182,17 +182,42 @@
182
182
  :resource="coreStore.resource"
183
183
  :adminUser="coreStore.adminUser"
184
184
  :record="row"
185
+ :updateRecords="()=>emits('update:records', true)"
185
186
  />
186
187
  </template>
187
188
 
188
- <template v-if="resource.options?.actions">
189
- <Tooltip v-for="action in resource.options.actions.filter(a => a.showIn?.list)" :key="action.id">
190
- <button
191
- @click="startCustomAction(action.id, row)"
189
+ <template v-if="resource.options?.actions">
190
+ <Tooltip
191
+ v-for="action in resource.options.actions.filter(a => a.showIn?.list)"
192
+ :key="action.id"
193
+ >
194
+ <CallActionWrapper
195
+ :disabled="rowActionLoadingStates?.[action.id]"
196
+ @callAction="startCustomAction(action.id, row)"
192
197
  >
193
- <component v-if="action.icon" :is="getIcon(action.icon)" class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"></component>
194
- </button>
195
- <template v-slot:tooltip>
198
+ <component
199
+ :is="action.customComponent ? getCustomComponent(action.customComponent) : 'span'"
200
+ :meta="action.customComponent?.meta"
201
+ :row="row"
202
+ :resource="resource"
203
+ :adminUser="adminUser"
204
+ @callAction="(payload? : Object) => startCustomAction(action.id, payload ?? row)"
205
+ >
206
+ <button
207
+ type="button"
208
+ :disabled="rowActionLoadingStates?.[action.id]"
209
+ @click.stop.prevent
210
+ >
211
+ <component
212
+ v-if="action.icon"
213
+ :is="getIcon(action.icon)"
214
+ class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"
215
+ />
216
+ </button>
217
+ </component>
218
+ </CallActionWrapper>
219
+
220
+ <template #tooltip>
196
221
  {{ action.name }}
197
222
  </template>
198
223
  </Tooltip>
@@ -267,7 +292,7 @@
267
292
  </div>
268
293
 
269
294
  <!-- Help text -->
270
- <span class="text-sm text-lightListTablePaginationHelpText dark:text-darkListTablePaginationHelpText">
295
+ <span class="ml-4 text-sm text-lightListTablePaginationHelpText dark:text-darkListTablePaginationHelpText">
271
296
  <span v-if="((((page || 1) - 1) * pageSize + 1 > totalRows) && totalRows > 0)">{{ $t('Wrong Page') }} </span>
272
297
  <template v-else-if="resource && totalRows > 0">
273
298
 
@@ -733,4 +758,15 @@ input[type="checkbox"][disabled] {
733
758
  input[type="checkbox"]:not([disabled]) {
734
759
  @apply cursor-pointer;
735
760
  }
761
+ td.sticky-column {
762
+ @apply sticky left-0 z-10;
763
+ &:not(:first-child) {
764
+ @apply left-[56px];
765
+ }
766
+ }
767
+ tr:not(:first-child):hover {
768
+ td.sticky-column {
769
+ @apply bg-lightListTableRowHover dark:bg-darkListTableRowHover;
770
+ }
771
+ }
736
772
  </style>
@@ -24,14 +24,14 @@
24
24
  dark:bg-darkShowTablesBodyBackground dark:border-darkShowTableBodyBorder block md:table-row"
25
25
  >
26
26
  <component
27
- v-if="column.components?.showRow"
27
+ v-if="column.components?.showRow && checkShowIf(column, record)"
28
28
  :is="getCustomComponent(column.components.showRow)"
29
29
  :meta="column.components.showRow.meta"
30
30
  :column="column"
31
31
  :resource="coreStore.resource"
32
32
  :record="coreStore.record"
33
33
  />
34
- <template v-else>
34
+ <template v-else-if="checkShowIf(column, record)">
35
35
  <td class="px-6 py-4 relative block md:table-cell font-bold md:font-normal pb-0 md:pb-4">
36
36
  {{ column.label }}
37
37
  </td>
@@ -59,14 +59,15 @@
59
59
 
60
60
  <script setup lang="ts">
61
61
  import ValueRenderer from '@/components/ValueRenderer.vue';
62
- import { getCustomComponent } from '@/utils';
62
+ import { getCustomComponent, checkShowIf } from '@/utils';
63
63
  import { useCoreStore } from '@/stores/core';
64
64
  import { computed } from 'vue';
65
- import type { AdminForthResourceCommon } from '@/types/Common';
65
+ import type { AdminForthResourceCommon, AdminForthResourceColumnInputCommon } from '@/types/Common';
66
66
  const props = withDefaults(defineProps<{
67
67
  columns: Array<{
68
68
  name: string;
69
69
  label?: string;
70
+ showIf?: AdminForthResourceColumnInputCommon['showIf'];
70
71
  components?: {
71
72
  show?: {
72
73
  file: string;
@@ -4,7 +4,7 @@
4
4
  @mouseover="!isTogglingSidebar && (isSidebarHovering = true)"
5
5
  @mouseleave="!isTogglingSidebar && (isSidebarHovering = false)"
6
6
  id="logo-lightSidebar"
7
- class="sidebar-container fixed border-none top-0 left-0 z-30 h-screen transition-all duration-300 ease-in-out bg-lightSidebar dark:bg-darkSidebar border-r border-lightSidebarBorder sm:translate-x-0 dark:border-darkSidebarBorder"
7
+ class="sidebar-container fixed border-none top-0 left-0 z-40 h-screen transition-all duration-300 ease-in-out bg-lightSidebar dark:bg-darkSidebar border-r border-lightSidebarBorder sm:translate-x-0 dark:border-darkSidebarBorder"
8
8
  :class="{
9
9
  '-translate-x-full': !sideBarOpen,
10
10
  'transform-none': sideBarOpen,
@@ -13,10 +13,10 @@
13
13
  }"
14
14
  aria-label="Sidebar"
15
15
  >
16
- <div class="h-full px-3 pb-4 bg-lightSidebar dark:bg-darkSidebar border-r border-lightSidebarBorder dark:border-darkSidebarBorder" :class="{'sidebar-scroll':!isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering)}">
17
- <div class="af-logo-title-wrapper flex ms-2 relative transition-all duration-300 ease-in-out h-8 items-center" :class="{'my-4 ': isSidebarIconOnly && !isSidebarHovering, 'm-4': !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering)}">
18
- <img v-if="coreStore.config?.showBrandLogoInSidebar !== false && (!iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))" :src="loadFile(coreStore.config?.brandLogo || '@/assets/logo.svg')" :alt="`${ coreStore.config?.brandName } Logo`" class="af-logo h-8 me-3" />
19
- <img v-if="coreStore.config?.showBrandLogoInSidebar !== false && coreStore.config?.iconOnlySidebar?.logo && iconOnlySidebarEnabled && isSidebarIconOnly && !isSidebarHovering" :src="loadFile(coreStore.config?.iconOnlySidebar?.logo || '')" :alt="`${ coreStore.config?.brandName } Logo`" class="af-sidebar-icon-only-logo h-8 me-3" />
16
+ <div class="h-full px-3 pb-20 md:pb-4 bg-lightSidebar dark:bg-darkSidebar border-r border-lightSidebarBorder dark:border-darkSidebarBorder" :class="{'sidebar-scroll':!isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering)}">
17
+ <div class="af-logo-title-wrapper flex relative transition-all duration-300 ease-in-out h-8 items-center" :class="{'my-4 ': isSidebarIconOnly && !isSidebarHovering, 'm-4': !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering)}">
18
+ <img :src="loadFile(coreStore.config?.brandLogo || '@/assets/logo.svg')" :alt="`${ coreStore.config?.brandName } Logo`" class="af-logo h-8 me-3" :class="{ 'hidden': !(coreStore.config?.showBrandLogoInSidebar !== false && (!iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))) }" />
19
+ <img :src="loadFile(coreStore.config?.iconOnlySidebar?.logo || '')" :alt="`${ coreStore.config?.brandName } Logo`" class="af-sidebar-icon-only-logo h-8 me-3" :class="{ 'hidden': !(coreStore.config?.showBrandLogoInSidebar !== false && coreStore.config?.iconOnlySidebar?.logo && iconOnlySidebarEnabled && isSidebarIconOnly && !isSidebarHovering) }" />
20
20
  <span
21
21
  v-if="coreStore.config?.showBrandNameInSidebar && (!iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))"
22
22
  class="af-title self-center text-lightNavbarText-size font-semibold sm:text-lightNavbarText-size whitespace-nowrap dark:text-darkSidebarText text-lightSidebarText"
@@ -430,6 +430,11 @@ onMounted(() => {
430
430
 
431
431
  onUnmounted(() => {
432
432
  smQuery.removeEventListener('change', handleBreakpointChange);
433
+ if (isMobile.value && props.sideBarOpen) {
434
+ document.body.style.overflow = '';
435
+ document.body.style.position = '';
436
+ document.body.style.width = '';
437
+ }
433
438
  })
434
439
 
435
440
  watch(() => props.forceIconOnly, (force) => {
@@ -445,4 +450,21 @@ watch(() => props.forceIconOnly, (force) => {
445
450
  isSidebarIconOnly.value = false;
446
451
  }
447
452
  }, { immediate: true })
453
+
454
+ watch(() => props.sideBarOpen, (isOpen) => {
455
+ if (isMobile.value) {
456
+ if (isOpen) {
457
+ // Lock body scroll
458
+ document.body.style.overflow = 'hidden';
459
+ document.body.style.position = 'fixed';
460
+ document.body.style.width = '100%';
461
+ } else {
462
+ // Unlock body scroll
463
+ document.body.style.overflow = '';
464
+ document.body.style.position = '';
465
+ document.body.style.width = '';
466
+ }
467
+ }
468
+ }, { immediate: true })
469
+
448
470
  </script>