adminforth 2.4.0-next.29 → 2.4.0-next.290
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/callTsProxy.js +14 -4
- package/commands/cli.js +2 -1
- package/commands/createApp/templates/api.ts.hbs +10 -0
- package/commands/createApp/templates/custom/tsconfig.json.hbs +2 -3
- package/commands/createApp/templates/index.ts.hbs +12 -1
- package/commands/createApp/utils.js +6 -1
- package/commands/createCustomComponent/configLoader.js +17 -4
- package/commands/createCustomComponent/main.js +13 -7
- package/commands/createCustomComponent/templates/customCrud/beforeActionButtons.vue.hbs +38 -0
- package/commands/createCustomComponent/templates/customCrud/saveButton.vue.hbs +28 -0
- package/commands/createPlugin/templates/custom/tsconfig.json.hbs +2 -5
- package/commands/createPlugin/templates/package.json.hbs +1 -1
- package/commands/generateModels.js +30 -22
- package/dist/auth.d.ts +9 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +21 -2
- 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 +69 -17
- package/dist/dataConnectors/baseConnector.js.map +1 -1
- package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
- package/dist/dataConnectors/clickhouse.js +15 -0
- package/dist/dataConnectors/clickhouse.js.map +1 -1
- package/dist/dataConnectors/mongo.d.ts.map +1 -1
- package/dist/dataConnectors/mongo.js +50 -15
- package/dist/dataConnectors/mongo.js.map +1 -1
- package/dist/dataConnectors/mysql.d.ts.map +1 -1
- package/dist/dataConnectors/mysql.js +11 -0
- package/dist/dataConnectors/mysql.js.map +1 -1
- package/dist/dataConnectors/postgres.d.ts.map +1 -1
- package/dist/dataConnectors/postgres.js +43 -14
- package/dist/dataConnectors/postgres.js.map +1 -1
- package/dist/dataConnectors/sqlite.d.ts.map +1 -1
- package/dist/dataConnectors/sqlite.js +11 -0
- package/dist/dataConnectors/sqlite.js.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -21
- package/dist/index.js.map +1 -1
- package/dist/modules/codeInjector.d.ts +2 -0
- package/dist/modules/codeInjector.d.ts.map +1 -1
- package/dist/modules/codeInjector.js +62 -6
- package/dist/modules/codeInjector.js.map +1 -1
- package/dist/modules/configValidator.d.ts +6 -0
- package/dist/modules/configValidator.d.ts.map +1 -1
- package/dist/modules/configValidator.js +202 -25
- 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 +172 -31
- package/dist/modules/restApi.js.map +1 -1
- package/dist/modules/styles.d.ts +499 -13
- package/dist/modules/styles.d.ts.map +1 -1
- package/dist/modules/styles.js +555 -31
- package/dist/modules/styles.js.map +1 -1
- package/dist/modules/utils.d.ts +7 -15
- package/dist/modules/utils.d.ts.map +1 -1
- package/dist/modules/utils.js +45 -68
- package/dist/modules/utils.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 +40 -1
- package/dist/servers/express.js.map +1 -1
- package/dist/spa/index.html +1 -1
- package/dist/spa/package-lock.json +5 -4
- package/dist/spa/package.json +2 -2
- package/dist/spa/src/App.vue +58 -173
- package/dist/spa/src/adminforth.ts +42 -18
- package/dist/spa/src/afcl/BarChart.vue +2 -2
- package/dist/spa/src/afcl/Button.vue +6 -6
- package/dist/spa/src/afcl/ButtonGroup.vue +91 -0
- package/dist/spa/src/afcl/Card.vue +25 -0
- package/dist/spa/src/afcl/Checkbox.vue +21 -13
- package/dist/spa/src/afcl/CountryFlag.vue +4 -1
- package/dist/spa/src/{components/CustomDatePicker.vue → afcl/DatePicker.vue} +95 -9
- package/dist/spa/src/afcl/Dialog.vue +47 -27
- package/dist/spa/src/afcl/Dropzone.vue +12 -12
- package/dist/spa/src/afcl/Input.vue +14 -6
- package/dist/spa/src/afcl/JsonViewer.vue +25 -0
- package/dist/spa/src/afcl/LinkButton.vue +3 -3
- package/dist/spa/src/afcl/PieChart.vue +5 -5
- package/dist/spa/src/afcl/ProgressBar.vue +7 -7
- package/dist/spa/src/afcl/Select.vue +68 -34
- package/dist/spa/src/afcl/Skeleton.vue +6 -6
- package/dist/spa/src/afcl/Table.vue +213 -74
- package/dist/spa/src/afcl/Textarea.vue +31 -0
- package/dist/spa/src/afcl/Toggle.vue +32 -0
- package/dist/spa/src/afcl/Tooltip.vue +28 -18
- package/dist/spa/src/afcl/VerticalTabs.vue +16 -7
- package/dist/spa/src/afcl/index.ts +6 -3
- package/dist/spa/src/components/AcceptModal.vue +48 -14
- package/dist/spa/src/components/Breadcrumbs.vue +5 -5
- package/dist/spa/src/components/CallActionWrapper.vue +15 -0
- package/dist/spa/src/components/ColumnValueInput.vue +38 -18
- package/dist/spa/src/components/ColumnValueInputWrapper.vue +4 -3
- package/dist/spa/src/components/CustomDateRangePicker.vue +9 -8
- package/dist/spa/src/components/CustomRangePicker.vue +37 -21
- package/dist/spa/src/components/ErrorMessage.vue +21 -0
- package/dist/spa/src/components/Filters.vue +194 -131
- package/dist/spa/src/components/GroupsTable.vue +9 -8
- package/dist/spa/src/components/MenuLink.vue +90 -23
- package/dist/spa/src/components/ResourceForm.vue +94 -51
- package/dist/spa/src/components/ResourceListTable.vue +115 -85
- package/dist/spa/src/components/ResourceListTableVirtual.vue +113 -79
- package/dist/spa/src/components/ShowTable.vue +21 -15
- package/dist/spa/src/components/Sidebar.vue +470 -0
- package/dist/spa/src/components/SingleSkeletLoader.vue +6 -6
- package/dist/spa/src/components/SkeleteLoader.vue +3 -3
- package/dist/spa/src/components/ThreeDotsMenu.vue +84 -15
- package/dist/spa/src/components/Toast.vue +37 -26
- package/dist/spa/src/components/UserMenuSettingsButton.vue +69 -0
- package/dist/spa/src/components/ValueRenderer.vue +43 -16
- package/dist/spa/src/controls/BoolToggle.vue +34 -0
- package/dist/spa/src/i18n.ts +5 -3
- package/dist/spa/src/main.ts +1 -1
- package/dist/spa/src/renderers/CompactField.vue +1 -1
- package/dist/spa/src/renderers/CompactUUID.vue +1 -1
- package/dist/spa/src/router/index.ts +8 -0
- package/dist/spa/src/shims-vue.d.ts +5 -0
- package/dist/spa/src/spa_types/core.ts +13 -1
- package/dist/spa/src/stores/core.ts +13 -1
- package/dist/spa/src/stores/filters.ts +33 -2
- package/dist/spa/src/stores/modal.ts +6 -1
- package/dist/spa/src/stores/toast.ts +22 -3
- package/dist/spa/src/types/Back.ts +162 -23
- package/dist/spa/src/types/Common.ts +91 -32
- package/dist/spa/src/types/FrontendAPI.ts +31 -5
- package/dist/spa/src/types/adapters/CaptchaAdapter.ts +34 -0
- package/dist/spa/src/types/adapters/EmailAdapter.ts +2 -2
- package/dist/spa/src/types/adapters/ImageVisionAdapter.ts +30 -0
- package/dist/spa/src/types/adapters/KeyValueAdapter.ts +16 -0
- package/dist/spa/src/types/adapters/index.ts +8 -0
- package/dist/spa/src/utils.ts +291 -11
- package/dist/spa/src/views/CreateView.vue +63 -21
- package/dist/spa/src/views/EditView.vue +55 -22
- package/dist/spa/src/views/ListView.vue +144 -87
- package/dist/spa/src/views/LoginView.vue +26 -35
- package/dist/spa/src/views/ResourceParent.vue +2 -2
- package/dist/spa/src/views/SettingsView.vue +121 -0
- package/dist/spa/src/views/ShowView.vue +83 -53
- package/dist/spa/src/websocket.ts +6 -1
- package/dist/spa/tsconfig.app.json +1 -1
- package/dist/spa/vite.config.ts +45 -2
- package/dist/types/Back.d.ts +145 -14
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js +15 -0
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +106 -29
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js.map +1 -1
- package/dist/types/FrontendAPI.d.ts +31 -3
- package/dist/types/FrontendAPI.d.ts.map +1 -1
- package/dist/types/FrontendAPI.js.map +1 -1
- package/dist/types/adapters/CaptchaAdapter.d.ts +30 -0
- package/dist/types/adapters/CaptchaAdapter.d.ts.map +1 -0
- package/dist/types/adapters/CaptchaAdapter.js +5 -0
- package/dist/types/adapters/CaptchaAdapter.js.map +1 -0
- package/dist/types/adapters/EmailAdapter.d.ts +1 -1
- package/dist/types/adapters/ImageVisionAdapter.d.ts +25 -0
- package/dist/types/adapters/ImageVisionAdapter.d.ts.map +1 -0
- package/dist/types/adapters/ImageVisionAdapter.js +2 -0
- package/dist/types/adapters/ImageVisionAdapter.js.map +1 -0
- package/dist/types/adapters/KeyValueAdapter.d.ts +10 -0
- package/dist/types/adapters/KeyValueAdapter.d.ts.map +1 -0
- package/dist/types/adapters/KeyValueAdapter.js +2 -0
- package/dist/types/adapters/KeyValueAdapter.js.map +1 -0
- package/dist/types/adapters/index.d.ts +9 -0
- package/dist/types/adapters/index.d.ts.map +1 -0
- package/dist/types/adapters/index.js +2 -0
- package/dist/types/adapters/index.js.map +1 -0
- package/package.json +4 -2
- package/dist/spa/src/types/adapters/index.js +0 -5
|
@@ -4,27 +4,20 @@
|
|
|
4
4
|
:min="minFormatted"
|
|
5
5
|
:max="maxFormatted"
|
|
6
6
|
type="number" aria-describedby="helper-text-explanation"
|
|
7
|
-
class="bg-
|
|
7
|
+
class="flex-1 bg-lightRangePickerInputBackground border border-lightRangePickerInputBorder text-lightRangePickerInputText placeholder-lightRangePickerInputPlaceholder text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-20 p-2.5 dark:bg-darkRangePickerInputBackground dark:border-darkRangePickerInputBorder dark:placeholder-darkRangePickerInputPlaceholder dark:text-darkRangePickerInputText dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
8
8
|
:placeholder="$t('From')"
|
|
9
9
|
v-model="start"
|
|
10
10
|
>
|
|
11
|
-
|
|
11
|
+
<p>_</p>
|
|
12
12
|
<input
|
|
13
13
|
:min="minFormatted"
|
|
14
14
|
:max="maxFormatted"
|
|
15
15
|
type="number" aria-describedby="helper-text-explanation"
|
|
16
|
-
class="bg-
|
|
16
|
+
class="flex-1 bg-lightRangePickerInputBackground border border-lightRangePickerInputBorder text-lightRangePickerInputText placeholder-lightRangePickerInputPlaceholder text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-20 p-2.5 dark:bg-darkRangePickerInputBackground dark:border-darkRangePickerInputBorder dark:placeholder-darkRangePickerInputPlaceholder dark:text-darkRangePickerInputText dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
17
17
|
:placeholder="$t('To')"
|
|
18
18
|
v-model="end"
|
|
19
19
|
>
|
|
20
20
|
|
|
21
|
-
<button
|
|
22
|
-
v-if="isChanged"
|
|
23
|
-
type="button"
|
|
24
|
-
class="flex items-center p-0.5 ml-auto 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"
|
|
25
|
-
@click="clear">Clear
|
|
26
|
-
</button>
|
|
27
|
-
|
|
28
21
|
<div v-if="min && max" class="w-full px-2.5">
|
|
29
22
|
<vue-slider
|
|
30
23
|
class="custom-slider"
|
|
@@ -57,15 +50,15 @@ const props = defineProps({
|
|
|
57
50
|
|
|
58
51
|
const emit = defineEmits(['update:valueStart', 'update:valueEnd']);
|
|
59
52
|
|
|
60
|
-
const minFormatted = computed(() => Math.floor(props.min));
|
|
61
|
-
const maxFormatted = computed(() => Math.ceil(props.max));
|
|
53
|
+
const minFormatted = computed(() => Math.floor(<number>props.min));
|
|
54
|
+
const maxFormatted = computed(() => Math.ceil(<number>props.max));
|
|
62
55
|
|
|
63
56
|
const isChanged = computed(() => {
|
|
64
57
|
return start.value && start.value !== minFormatted.value || end.value && end.value !== maxFormatted.value;
|
|
65
58
|
});
|
|
66
59
|
|
|
67
|
-
const start = ref(props.valueStart);
|
|
68
|
-
const end = ref(props.valueEnd);
|
|
60
|
+
const start = ref<string | number>(props.valueStart);
|
|
61
|
+
const end = ref<string | number>(props.valueEnd);
|
|
69
62
|
|
|
70
63
|
const sliderValue = ref([minFormatted.value, maxFormatted.value]);
|
|
71
64
|
|
|
@@ -112,13 +105,7 @@ watch([minFormatted,maxFormatted], () => {
|
|
|
112
105
|
setSliderValues(minFormatted.value, maxFormatted.value)
|
|
113
106
|
})
|
|
114
107
|
|
|
115
|
-
|
|
116
|
-
start.value = ''
|
|
117
|
-
end.value = ''
|
|
118
|
-
setSliderValues('', '')
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function setSliderValues(start, end) {
|
|
108
|
+
function setSliderValues(start: any, end: any) {
|
|
122
109
|
sliderValue.value = [start || minFormatted.value, end || maxFormatted.value];
|
|
123
110
|
}
|
|
124
111
|
</script>
|
|
@@ -153,4 +140,33 @@ function setSliderValues(start, end) {
|
|
|
153
140
|
@apply bg-lightPrimaryOpacity;
|
|
154
141
|
}
|
|
155
142
|
}
|
|
143
|
+
|
|
144
|
+
.dark .custom-slider {
|
|
145
|
+
&:deep(.vue-slider-rail) {
|
|
146
|
+
background-color: rgb(55 65 81); // gray-700
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
&:deep(.vue-slider-dot-handle) {
|
|
150
|
+
@apply bg-darkPrimary;
|
|
151
|
+
border: none;
|
|
152
|
+
box-shadow: none;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
&:deep(.vue-slider-dot-handle:hover) {
|
|
156
|
+
@apply bg-darkPrimary;
|
|
157
|
+
filter: brightness(1.1);
|
|
158
|
+
border: none;
|
|
159
|
+
box-shadow: none;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
&:deep(.vue-slider-process) {
|
|
163
|
+
@apply bg-darkPrimaryOpacity;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
&:deep(.vue-slider-process:hover) {
|
|
167
|
+
filter: brightness(1.1);
|
|
168
|
+
@apply bg-darkPrimaryOpacity;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
156
172
|
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="error" class="af-login-modal-error flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
|
|
3
|
+
<svg class="flex-shrink-0 inline w-4 h-4 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
|
4
|
+
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
|
|
5
|
+
</svg>
|
|
6
|
+
<span class="sr-only">{{ $t('Info') }}</span>
|
|
7
|
+
<div>
|
|
8
|
+
{{ error }}
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup>
|
|
14
|
+
|
|
15
|
+
defineProps({
|
|
16
|
+
error: {
|
|
17
|
+
type: String,
|
|
18
|
+
default: null
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
</script>
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
<!-- drawer component -->
|
|
3
3
|
<div id="drawer-navigation"
|
|
4
4
|
|
|
5
|
-
class="fixed
|
|
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-
|
|
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-
|
|
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>
|
|
@@ -20,112 +20,141 @@
|
|
|
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
|
-
|
|
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>
|
|
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.
|
|
154
|
+
:disabled="!filtersStore.visibleFiltersCount"
|
|
126
155
|
type="button"
|
|
127
|
-
class="flex items-center py-1
|
|
128
|
-
@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>
|
|
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 =
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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-
|
|
7
|
-
<thead v-if="!allColumnsHaveCustomComponent" class="text-xs text-
|
|
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-
|
|
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-
|
|
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-
|
|
59
|
-
<div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-
|
|
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
|
});
|