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.
- package/commands/createApp/templates/api.ts.hbs +10 -0
- package/commands/createApp/templates/index.ts.hbs +4 -1
- package/commands/createApp/utils.js +5 -0
- package/commands/createCustomComponent/main.js +12 -7
- package/commands/createCustomComponent/templates/customCrud/saveButton.vue.hbs +28 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +6 -0
- package/dist/auth.js.map +1 -1
- package/dist/dataConnectors/baseConnector.d.ts +1 -1
- package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
- package/dist/dataConnectors/baseConnector.js +23 -2
- package/dist/dataConnectors/baseConnector.js.map +1 -1
- package/dist/dataConnectors/postgres.d.ts.map +1 -1
- package/dist/dataConnectors/postgres.js +32 -14
- package/dist/dataConnectors/postgres.js.map +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -12
- package/dist/index.js.map +1 -1
- package/dist/modules/codeInjector.d.ts.map +1 -1
- package/dist/modules/codeInjector.js +12 -0
- package/dist/modules/codeInjector.js.map +1 -1
- package/dist/modules/configValidator.d.ts.map +1 -1
- package/dist/modules/configValidator.js +66 -10
- package/dist/modules/configValidator.js.map +1 -1
- package/dist/modules/restApi.d.ts +1 -1
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +18 -5
- package/dist/modules/restApi.js.map +1 -1
- package/dist/modules/styles.d.ts +0 -18
- package/dist/modules/styles.d.ts.map +1 -1
- package/dist/modules/styles.js +1 -19
- package/dist/modules/styles.js.map +1 -1
- package/dist/servers/express.d.ts +5 -0
- package/dist/servers/express.d.ts.map +1 -1
- package/dist/servers/express.js +26 -1
- package/dist/servers/express.js.map +1 -1
- package/dist/spa/package.json +1 -1
- package/dist/spa/src/App.vue +1 -1
- package/dist/spa/src/afcl/Input.vue +10 -2
- package/dist/spa/src/afcl/Select.vue +17 -3
- package/dist/spa/src/afcl/Table.vue +1 -1
- package/dist/spa/src/afcl/Tooltip.vue +4 -2
- package/dist/spa/src/components/AcceptModal.vue +43 -9
- package/dist/spa/src/components/CallActionWrapper.vue +15 -0
- package/dist/spa/src/components/ColumnValueInput.vue +1 -1
- package/dist/spa/src/components/CustomRangePicker.vue +3 -16
- package/dist/spa/src/components/Filters.vue +129 -112
- package/dist/spa/src/components/ResourceForm.vue +2 -2
- package/dist/spa/src/components/ResourceListTable.vue +45 -13
- package/dist/spa/src/components/ResourceListTableVirtual.vue +52 -16
- package/dist/spa/src/components/ShowTable.vue +5 -4
- package/dist/spa/src/components/Sidebar.vue +27 -5
- package/dist/spa/src/components/ThreeDotsMenu.vue +27 -17
- package/dist/spa/src/components/Toast.vue +15 -22
- package/dist/spa/src/components/UserMenuSettingsButton.vue +9 -10
- package/dist/spa/src/i18n.ts +4 -2
- package/dist/spa/src/main.ts +1 -1
- package/dist/spa/src/stores/core.ts +12 -0
- package/dist/spa/src/stores/filters.ts +5 -1
- package/dist/spa/src/types/Back.ts +22 -1
- package/dist/spa/src/types/Common.ts +24 -0
- package/dist/spa/src/utils.ts +69 -2
- package/dist/spa/src/views/CreateView.vue +47 -4
- package/dist/spa/src/views/EditView.vue +30 -3
- package/dist/spa/src/views/ListView.vue +6 -2
- package/dist/spa/src/views/LoginView.vue +4 -13
- package/dist/spa/src/views/SettingsView.vue +1 -1
- package/dist/spa/src/views/ShowView.vue +27 -17
- package/dist/types/Back.d.ts +27 -0
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +47 -0
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js.map +1 -1
- 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
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
142
|
-
@click="clear"
|
|
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-
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
<component
|
|
184
|
-
|
|
185
|
-
|
|
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="
|
|
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="
|
|
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-
|
|
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="
|
|
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
|
-
|
|
189
|
-
<Tooltip
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
194
|
-
|
|
195
|
-
|
|
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-
|
|
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
|
|
18
|
-
<img
|
|
19
|
-
<img
|
|
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>
|