@drax/crud-vue 3.18.1 → 3.19.0
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/package.json +5 -5
- package/src/components/Crud.vue +24 -7
- package/src/components/CrudActiveFilters.vue +19 -0
- package/src/components/CrudFieldRange.vue +212 -0
- package/src/components/CrudFilters.vue +19 -1
- package/src/components/CrudFormField.vue +0 -1
- package/src/components/CrudList.vue +5 -5
- package/src/components/CrudListGallery.vue +5 -5
- package/src/components/CrudListTable.vue +5 -5
- package/src/composables/UseCrud.ts +103 -5
- package/src/composables/UseDynamicFilters.ts +3 -0
- package/src/composables/UseFilterIcon.ts +2 -0
- package/src/cruds/EntityCrud.ts +7 -2
- package/src/helpers/CrudRangeFilters.test.ts +52 -0
- package/src/helpers/CrudRangeFilters.ts +84 -0
- package/src/index.ts +2 -0
- package/src/stores/UseCrudStore.ts +4 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "3.
|
|
6
|
+
"version": "3.19.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./src/index.ts",
|
|
9
9
|
"module": "./src/index.ts",
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
"format": "prettier --write src/"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@drax/common-front": "^3.
|
|
27
|
+
"@drax/common-front": "^3.19.0",
|
|
28
28
|
"@drax/crud-front": "^3.11.0",
|
|
29
|
-
"@drax/crud-share": "^3.
|
|
30
|
-
"@drax/media-vue": "^3.
|
|
29
|
+
"@drax/crud-share": "^3.19.0",
|
|
30
|
+
"@drax/media-vue": "^3.19.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"pinia": "^3.0.4",
|
|
@@ -50,5 +50,5 @@
|
|
|
50
50
|
"vue-tsc": "^3.2.4",
|
|
51
51
|
"vuetify": "^3.11.8"
|
|
52
52
|
},
|
|
53
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "bf445c10758ee014b45c2d76d6fddd5c578c738f"
|
|
54
54
|
}
|
package/src/components/Crud.vue
CHANGED
|
@@ -17,9 +17,10 @@ const {entity} = defineProps({
|
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
const {
|
|
20
|
-
|
|
20
|
+
onCreate, onEditAt, onDeleteAt, resetCrudStore,
|
|
21
21
|
operation, dialog, notify, message, doExport, doImport,
|
|
22
|
-
prepareFilters, prepareSort, form
|
|
22
|
+
prepareFilters, prepareSort, form,
|
|
23
|
+
onViewAt, navigateView, canNavigateItems, canNavigatePrev, canNavigateNext
|
|
23
24
|
} = useCrud(entity);
|
|
24
25
|
|
|
25
26
|
const {hasPermission} = useAuth()
|
|
@@ -70,11 +71,11 @@ watch(dialog, (value) => {
|
|
|
70
71
|
:is="listComponent"
|
|
71
72
|
:entity="entity"
|
|
72
73
|
@create="onCreate"
|
|
73
|
-
@edit="
|
|
74
|
-
@delete="
|
|
74
|
+
@edit="onEditAt"
|
|
75
|
+
@delete="onDeleteAt"
|
|
75
76
|
@export="doExport"
|
|
76
77
|
@import="doImport"
|
|
77
|
-
@view="
|
|
78
|
+
@view="onViewAt"
|
|
78
79
|
>
|
|
79
80
|
|
|
80
81
|
<template v-slot:toolbar-left>
|
|
@@ -120,8 +121,8 @@ watch(dialog, (value) => {
|
|
|
120
121
|
</template>
|
|
121
122
|
|
|
122
123
|
|
|
123
|
-
<template v-slot:item.actions="{item}">
|
|
124
|
-
<slot name="item.actions" v-bind="{item}">
|
|
124
|
+
<template v-slot:item.actions="{item, index}">
|
|
125
|
+
<slot name="item.actions" v-bind="{item, index}">
|
|
125
126
|
</slot>
|
|
126
127
|
</template>
|
|
127
128
|
|
|
@@ -144,6 +145,22 @@ watch(dialog, (value) => {
|
|
|
144
145
|
:operation="operation"
|
|
145
146
|
>
|
|
146
147
|
<template #toolbar-actions>
|
|
148
|
+
<v-btn
|
|
149
|
+
v-if="canNavigateItems"
|
|
150
|
+
icon="mdi-chevron-left"
|
|
151
|
+
variant="text"
|
|
152
|
+
:disabled="!canNavigatePrev"
|
|
153
|
+
@click="navigateView(-1)"
|
|
154
|
+
/>
|
|
155
|
+
|
|
156
|
+
<v-btn
|
|
157
|
+
v-if="canNavigateItems"
|
|
158
|
+
icon="mdi-chevron-right"
|
|
159
|
+
variant="text"
|
|
160
|
+
:disabled="!canNavigateNext"
|
|
161
|
+
@click="navigateView(1)"
|
|
162
|
+
/>
|
|
163
|
+
|
|
147
164
|
<crud-ai-button
|
|
148
165
|
v-if="entity.isAiAssistable && ['create', 'edit'].includes(operation) && hasPermission('ai:promptCrud')"
|
|
149
166
|
:entity="entity"
|
|
@@ -6,6 +6,7 @@ import { useI18n } from 'vue-i18n'
|
|
|
6
6
|
import { useFilterIcon } from '../composables/UseFilterIcon'
|
|
7
7
|
import CrudRefDisplay from "./CrudRefDisplay.vue";
|
|
8
8
|
import {formatDate} from "@drax/common-front"
|
|
9
|
+
import {isRangeOperator, normalizeDateRangeValue} from "../helpers/CrudRangeFilters";
|
|
9
10
|
|
|
10
11
|
const { t} = useI18n()
|
|
11
12
|
|
|
@@ -36,6 +37,13 @@ const activeFilters = computed<any[]>(() => {
|
|
|
36
37
|
return null
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
if (isRangeOperator(filterDef)) {
|
|
41
|
+
const rangeValue = normalizeDateRangeValue(filter.value)
|
|
42
|
+
if (!rangeValue.from && !rangeValue.to) {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
return {
|
|
40
48
|
...filterDef,
|
|
41
49
|
value: filter.value,
|
|
@@ -50,9 +58,20 @@ const getFilterLabel = (filter: any) => {
|
|
|
50
58
|
return label
|
|
51
59
|
}
|
|
52
60
|
|
|
61
|
+
const formatRangeDate = (value: string | Date) => {
|
|
62
|
+
return formatDate(value instanceof Date ? value.toISOString() : value)
|
|
63
|
+
}
|
|
64
|
+
|
|
53
65
|
|
|
54
66
|
|
|
55
67
|
const getFilterValue = (filter: any) => {
|
|
68
|
+
if (isRangeOperator(filter)) {
|
|
69
|
+
const rangeValue = normalizeDateRangeValue(filter.value)
|
|
70
|
+
const from = rangeValue.from ? formatRangeDate(rangeValue.from) : t('crud.from')
|
|
71
|
+
const to = rangeValue.to ? formatRangeDate(rangeValue.to) : t('crud.to')
|
|
72
|
+
return `${from} - ${to}`
|
|
73
|
+
}
|
|
74
|
+
|
|
56
75
|
switch (filter.type) {
|
|
57
76
|
case 'date':
|
|
58
77
|
return formatDate(filter.value)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {computed, type PropType} from "vue";
|
|
3
|
+
import type {ValidationRule} from "vuetify";
|
|
4
|
+
import {VDateInput} from "vuetify/labs/VDateInput";
|
|
5
|
+
import type {IEntityCrud, IEntityCrudFilter} from "@drax/crud-share";
|
|
6
|
+
import {useI18n} from "vue-i18n";
|
|
7
|
+
import {normalizeDateRangeValue} from "../helpers/CrudRangeFilters";
|
|
8
|
+
|
|
9
|
+
const {t} = useI18n()
|
|
10
|
+
|
|
11
|
+
const valueModel = defineModel<any>({type: [Object], default: null})
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
name,
|
|
15
|
+
label,
|
|
16
|
+
field,
|
|
17
|
+
readonly,
|
|
18
|
+
errorMessages,
|
|
19
|
+
rules,
|
|
20
|
+
density,
|
|
21
|
+
variant,
|
|
22
|
+
clearable,
|
|
23
|
+
hideDetails,
|
|
24
|
+
singleLine,
|
|
25
|
+
hint,
|
|
26
|
+
persistentHint,
|
|
27
|
+
placeholder,
|
|
28
|
+
persistentPlaceholder,
|
|
29
|
+
prependIcon,
|
|
30
|
+
appendIcon,
|
|
31
|
+
prependInnerIcon,
|
|
32
|
+
appendInnerIcon,
|
|
33
|
+
onInput
|
|
34
|
+
} = defineProps({
|
|
35
|
+
name: {type: String, required: true},
|
|
36
|
+
label: {type: String, required: true},
|
|
37
|
+
entity: {type: Object as PropType<IEntityCrud>, required: true},
|
|
38
|
+
field: {type: Object as PropType<IEntityCrudFilter>, required: true},
|
|
39
|
+
readonly: {type: Boolean, default: false},
|
|
40
|
+
hideDetails: {type: Boolean, default: false},
|
|
41
|
+
hint: {type: String, required: false},
|
|
42
|
+
persistentHint: {type: Boolean, default: false},
|
|
43
|
+
placeholder: {type: String, required: false},
|
|
44
|
+
persistentPlaceholder: {type: Boolean, default: false},
|
|
45
|
+
errorMessages: {type: Array as PropType<string[]>, default: null, required: false},
|
|
46
|
+
onInput: {type: Function as PropType<Function>, required: false},
|
|
47
|
+
rules: {type: Array as PropType<ValidationRule[]>, required: false},
|
|
48
|
+
density: {type: String as PropType<'comfortable' | 'compact' | 'default'>, default: 'default'},
|
|
49
|
+
variant: {
|
|
50
|
+
type: String as PropType<'underlined' | 'outlined' | 'filled' | 'solo' | 'solo-inverted' | 'solo-filled' | 'plain'>,
|
|
51
|
+
default: 'filled'
|
|
52
|
+
},
|
|
53
|
+
singleLine: {type: Boolean, default: false},
|
|
54
|
+
clearable: {type: Boolean, default: false},
|
|
55
|
+
prependIcon: {type: String, default: ''},
|
|
56
|
+
prependInnerIcon: {type: String, default: ''},
|
|
57
|
+
appendIcon: {type: String, default: ''},
|
|
58
|
+
appendInnerIcon: {type: String, default: ''},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const emit = defineEmits(['updateValue'])
|
|
62
|
+
|
|
63
|
+
const rangeValueModel = computed({
|
|
64
|
+
get() {
|
|
65
|
+
return normalizeDateRangeValue(valueModel.value)
|
|
66
|
+
},
|
|
67
|
+
set(value) {
|
|
68
|
+
valueModel.value = normalizeDateRangeValue(value)
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
function normalizeDateValue(value: any, endOfDay = false) {
|
|
73
|
+
if (!value) {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const date = value instanceof Date ? new Date(value) : new Date(value)
|
|
78
|
+
|
|
79
|
+
if (Number.isNaN(date.getTime())) {
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (endOfDay) {
|
|
84
|
+
date.setHours(23, 59, 59, 0)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return date
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function updateRangeValue(bound: 'from' | 'to', rawValue: any) {
|
|
91
|
+
const nextValue = {
|
|
92
|
+
...normalizeDateRangeValue(rangeValueModel.value),
|
|
93
|
+
[bound]: normalizeDateValue(rawValue, bound === 'to' && !!field.endOfDay)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const fromDate = normalizeDateValue(nextValue.from)
|
|
97
|
+
const toDate = normalizeDateValue(nextValue.to)
|
|
98
|
+
|
|
99
|
+
if (fromDate && toDate && fromDate > toDate) {
|
|
100
|
+
if (bound === 'from') {
|
|
101
|
+
nextValue.to = normalizeDateValue(nextValue.from, !!field.endOfDay)
|
|
102
|
+
} else {
|
|
103
|
+
nextValue.from = normalizeDateValue(nextValue.to)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
rangeValueModel.value = nextValue
|
|
108
|
+
|
|
109
|
+
if(onInput && typeof onInput === 'function'){
|
|
110
|
+
onInput(nextValue)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
emit('updateValue')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const rangeFromModel = computed({
|
|
117
|
+
get() {
|
|
118
|
+
return rangeValueModel.value.from
|
|
119
|
+
},
|
|
120
|
+
set(value) {
|
|
121
|
+
updateRangeValue('from', value)
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const rangeToModel = computed({
|
|
126
|
+
get() {
|
|
127
|
+
return rangeValueModel.value.to
|
|
128
|
+
},
|
|
129
|
+
set(value) {
|
|
130
|
+
updateRangeValue('to', value)
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const rangeFromMax = computed(() => {
|
|
135
|
+
return rangeValueModel.value.to ?? field.max
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
const rangeToMin = computed(() => {
|
|
139
|
+
return rangeValueModel.value.from ?? undefined
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const fromInnerIcon = computed(() => {
|
|
143
|
+
return prependInnerIcon || 'mdi-calendar-start'
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const toInnerIcon = computed(() => {
|
|
147
|
+
return appendInnerIcon || 'mdi-calendar-end'
|
|
148
|
+
})
|
|
149
|
+
</script>
|
|
150
|
+
|
|
151
|
+
<template>
|
|
152
|
+
<v-row dense>
|
|
153
|
+
<v-col cols="12" sm="6">
|
|
154
|
+
<v-date-input
|
|
155
|
+
:name="`${name}_from`"
|
|
156
|
+
:label="`${label} ${t('crud.from')}`"
|
|
157
|
+
:hint="hint ?? field.hint"
|
|
158
|
+
:persistent-hint="persistentHint ?? field.persistentHint"
|
|
159
|
+
:placeholder="placeholder ?? field.placeholder"
|
|
160
|
+
:persistent-placeholder="persistentPlaceholder ?? field.persistentPlaceholder"
|
|
161
|
+
v-model="rangeFromModel"
|
|
162
|
+
:readonly="readonly"
|
|
163
|
+
:error-messages="errorMessages"
|
|
164
|
+
:rules="rules"
|
|
165
|
+
:density="density"
|
|
166
|
+
:variant="variant"
|
|
167
|
+
:clearable="clearable"
|
|
168
|
+
:hide-details="hideDetails"
|
|
169
|
+
:single-line="singleLine"
|
|
170
|
+
@click:clear="() => updateRangeValue('from', null)"
|
|
171
|
+
:prepend-icon="prependIcon"
|
|
172
|
+
:append-icon="appendIcon"
|
|
173
|
+
:prepend-inner-icon="fromInnerIcon"
|
|
174
|
+
:append-inner-icon="appendInnerIcon"
|
|
175
|
+
:max="rangeFromMax"
|
|
176
|
+
@input="onInput"
|
|
177
|
+
/>
|
|
178
|
+
</v-col>
|
|
179
|
+
|
|
180
|
+
<v-col cols="12" sm="6">
|
|
181
|
+
<v-date-input
|
|
182
|
+
:name="`${name}_to`"
|
|
183
|
+
:label="`${label} ${t('crud.to')}`"
|
|
184
|
+
:hint="hint ?? field.hint"
|
|
185
|
+
:persistent-hint="persistentHint ?? field.persistentHint"
|
|
186
|
+
:placeholder="placeholder ?? field.placeholder"
|
|
187
|
+
:persistent-placeholder="persistentPlaceholder ?? field.persistentPlaceholder"
|
|
188
|
+
v-model="rangeToModel"
|
|
189
|
+
:readonly="readonly"
|
|
190
|
+
:error-messages="errorMessages"
|
|
191
|
+
:rules="rules"
|
|
192
|
+
:density="density"
|
|
193
|
+
:variant="variant"
|
|
194
|
+
:clearable="clearable"
|
|
195
|
+
:hide-details="hideDetails"
|
|
196
|
+
:single-line="singleLine"
|
|
197
|
+
@click:clear="() => updateRangeValue('to', null)"
|
|
198
|
+
:prepend-icon="prependIcon"
|
|
199
|
+
:append-icon="appendIcon"
|
|
200
|
+
:prepend-inner-icon="toInnerIcon"
|
|
201
|
+
:append-inner-icon="appendInnerIcon"
|
|
202
|
+
:min="rangeToMin"
|
|
203
|
+
:max="field.max"
|
|
204
|
+
@input="onInput"
|
|
205
|
+
>
|
|
206
|
+
<template v-if="field.endOfDay && field.showEndOfDayChip !== false" v-slot:append-inner>
|
|
207
|
+
<v-chip size="small">23:59</v-chip>
|
|
208
|
+
</template>
|
|
209
|
+
</v-date-input>
|
|
210
|
+
</v-col>
|
|
211
|
+
</v-row>
|
|
212
|
+
</template>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import {computed, type PropType} from "vue";
|
|
3
3
|
import CrudFormField from "./CrudFormField.vue";
|
|
4
|
+
import CrudFieldRange from "./CrudFieldRange.vue";
|
|
4
5
|
import type {IEntityCrud, IEntityCrudFilter} from "@drax/crud-share";
|
|
5
6
|
import {useAuth} from "@drax/identity-vue";
|
|
6
7
|
import {useFilterIcon} from "../composables/UseFilterIcon";
|
|
@@ -18,6 +19,10 @@ const aFields = computed(() => {
|
|
|
18
19
|
return entity.filters.filter((field:IEntityCrudFilter) => !field.permission || hasPermission(field.permission))
|
|
19
20
|
})
|
|
20
21
|
|
|
22
|
+
function isRangeFilter(filter: IEntityCrudFilter) {
|
|
23
|
+
return filter.type === 'date' && filter.operator === 'range'
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
function filter() {
|
|
22
27
|
emit('applyFilter')
|
|
23
28
|
}
|
|
@@ -51,8 +56,21 @@ const emit = defineEmits(['applyFilter', 'clearFilter'])
|
|
|
51
56
|
>
|
|
52
57
|
|
|
53
58
|
<slot :name="`filter.${filter.name}`" v-bind="{filter, filterIndex: index}">
|
|
59
|
+
<crud-field-range
|
|
60
|
+
v-if="filter && valueModel[index] !== undefined && isRangeFilter(filter)"
|
|
61
|
+
:name="filter.name"
|
|
62
|
+
:label="filter.label"
|
|
63
|
+
:entity="entity"
|
|
64
|
+
:field="filter"
|
|
65
|
+
v-model="valueModel[index].value"
|
|
66
|
+
:clearable="true"
|
|
67
|
+
density="compact"
|
|
68
|
+
variant="outlined"
|
|
69
|
+
hide-details
|
|
70
|
+
@updateValue="onUpdateValue"
|
|
71
|
+
/>
|
|
54
72
|
<crud-form-field
|
|
55
|
-
v-if="filter && valueModel[index] !== undefined"
|
|
73
|
+
v-else-if="filter && valueModel[index] !== undefined"
|
|
56
74
|
:field="filter"
|
|
57
75
|
:entity="entity"
|
|
58
76
|
v-model="valueModel[index].value"
|
|
@@ -206,24 +206,24 @@ defineEmits(['import', 'export', 'create', 'update', 'delete', 'view', 'edit'])
|
|
|
206
206
|
</template>
|
|
207
207
|
|
|
208
208
|
|
|
209
|
-
<template v-slot:item.actions="{item}">
|
|
209
|
+
<template v-slot:item.actions="{item, index}">
|
|
210
210
|
|
|
211
|
-
<slot name="item.actions" v-bind="{item}">
|
|
211
|
+
<slot name="item.actions" v-bind="{item, index}">
|
|
212
212
|
</slot>
|
|
213
213
|
|
|
214
214
|
<crud-view-button
|
|
215
215
|
v-if="entity.isViewable && hasPermission(entity.permissions.view)"
|
|
216
|
-
@click="$emit('view', item)"
|
|
216
|
+
@click="$emit('view', item, index)"
|
|
217
217
|
/>
|
|
218
218
|
|
|
219
219
|
<crud-update-button
|
|
220
220
|
v-if="entity.isEditable && entity.isItemEditable(item) && hasPermission(entity.permissions?.update)"
|
|
221
|
-
@click="$emit('edit', item)"
|
|
221
|
+
@click="$emit('edit', item, index)"
|
|
222
222
|
/>
|
|
223
223
|
|
|
224
224
|
<crud-delete-button
|
|
225
225
|
v-if="entity.isDeletable && hasPermission(entity.permissions?.delete)"
|
|
226
|
-
@click="$emit('delete', item)"
|
|
226
|
+
@click="$emit('delete', item, index)"
|
|
227
227
|
/>
|
|
228
228
|
|
|
229
229
|
</template>
|
|
@@ -199,7 +199,7 @@ onMounted(() => {
|
|
|
199
199
|
|
|
200
200
|
<!-- GALLERY GRIDS -->
|
|
201
201
|
<v-row v-if="items.length > 0">
|
|
202
|
-
<v-col v-for="item in items" :key="item.id || item.uuid || item.name || Math.random()" cols="12" sm="6" md="4"
|
|
202
|
+
<v-col v-for="(item, index) in items" :key="item.id || item.uuid || item.name || Math.random()" cols="12" sm="6" md="4"
|
|
203
203
|
xl="3">
|
|
204
204
|
|
|
205
205
|
|
|
@@ -231,22 +231,22 @@ onMounted(() => {
|
|
|
231
231
|
<v-divider></v-divider>
|
|
232
232
|
|
|
233
233
|
<v-card-actions class="bg-grey-lighten-4 py-2 px-4 d-flex justify-end flex-wrap gap-2">
|
|
234
|
-
<slot name="item.actions" v-bind="{item}">
|
|
234
|
+
<slot name="item.actions" v-bind="{item, index}">
|
|
235
235
|
</slot>
|
|
236
236
|
|
|
237
237
|
<crud-view-button
|
|
238
238
|
v-if="entity.isViewable && hasPermission(entity.permissions.view)"
|
|
239
|
-
@click="$emit('view', item)"
|
|
239
|
+
@click="$emit('view', item, index)"
|
|
240
240
|
/>
|
|
241
241
|
|
|
242
242
|
<crud-update-button
|
|
243
243
|
v-if="entity.isEditable && entity.isItemEditable(item) && hasPermission(entity.permissions?.update)"
|
|
244
|
-
@click="$emit('edit', item)"
|
|
244
|
+
@click="$emit('edit', item, index)"
|
|
245
245
|
/>
|
|
246
246
|
|
|
247
247
|
<crud-delete-button
|
|
248
248
|
v-if="entity.isDeletable && hasPermission(entity.permissions?.delete)"
|
|
249
|
-
@click="$emit('delete', item)"
|
|
249
|
+
@click="$emit('delete', item, index)"
|
|
250
250
|
/>
|
|
251
251
|
</v-card-actions>
|
|
252
252
|
</v-card>
|
|
@@ -218,24 +218,24 @@ defineEmits(['import', 'export', 'create', 'update', 'delete', 'view', 'edit'])
|
|
|
218
218
|
</template>
|
|
219
219
|
|
|
220
220
|
|
|
221
|
-
<template v-slot:item.actions="{item}">
|
|
221
|
+
<template v-slot:item.actions="{item, index}">
|
|
222
222
|
|
|
223
|
-
<slot name="item.actions" v-bind="{item}">
|
|
223
|
+
<slot name="item.actions" v-bind="{item, index}">
|
|
224
224
|
</slot>
|
|
225
225
|
|
|
226
226
|
<crud-view-button
|
|
227
227
|
v-if="entity.isViewable && hasPermission(entity.permissions.view)"
|
|
228
|
-
@click="$emit('view', item)"
|
|
228
|
+
@click="$emit('view', item, index)"
|
|
229
229
|
/>
|
|
230
230
|
|
|
231
231
|
<crud-update-button
|
|
232
232
|
v-if="entity.isEditable && entity.isItemEditable(item) && hasPermission(entity.permissions?.update)"
|
|
233
|
-
@click="$emit('edit', item)"
|
|
233
|
+
@click="$emit('edit', item, index)"
|
|
234
234
|
/>
|
|
235
235
|
|
|
236
236
|
<crud-delete-button
|
|
237
237
|
v-if="entity.isDeletable && hasPermission(entity.permissions?.delete)"
|
|
238
|
-
@click="$emit('delete', item)"
|
|
238
|
+
@click="$emit('delete', item, index)"
|
|
239
239
|
/>
|
|
240
240
|
|
|
241
241
|
</template>
|
|
@@ -6,15 +6,19 @@ import type {
|
|
|
6
6
|
IEntityCrudOperation
|
|
7
7
|
} from "@drax/crud-share";
|
|
8
8
|
import {useCrudStore} from "../stores/UseCrudStore";
|
|
9
|
-
import {computed, nextTick, toRaw} from "vue";
|
|
9
|
+
import {computed, nextTick, toRaw, watch} from "vue";
|
|
10
10
|
import getItemId from "../helpers/getItemId";
|
|
11
11
|
import {useI18n} from "vue-i18n";
|
|
12
12
|
import {useRouter} from "vue-router";
|
|
13
|
+
import {createCrudFilterValue, expandRangeFilters} from "../helpers/CrudRangeFilters";
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
export function useCrud(entity: IEntityCrud) {
|
|
16
17
|
|
|
17
18
|
const store = useCrudStore(entity?.name)
|
|
19
|
+
const navigationOperations = (entity as IEntityCrud & {
|
|
20
|
+
navigationOperations?: Exclude<IEntityCrudOperation, null>[]
|
|
21
|
+
}).navigationOperations ?? ['view']
|
|
18
22
|
|
|
19
23
|
const router = useRouter();
|
|
20
24
|
|
|
@@ -48,6 +52,18 @@ export function useCrud(entity: IEntityCrud) {
|
|
|
48
52
|
}
|
|
49
53
|
})
|
|
50
54
|
|
|
55
|
+
const currentViewIndex = computed({
|
|
56
|
+
get() {
|
|
57
|
+
return store.currentViewIndex
|
|
58
|
+
}, set(value) {
|
|
59
|
+
store.setCurrentViewIndex(value)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const canNavigateItems = computed(() => {
|
|
64
|
+
return operation.value !== null && navigationOperations.includes(operation.value)
|
|
65
|
+
})
|
|
66
|
+
|
|
51
67
|
const operation = computed({
|
|
52
68
|
get() {
|
|
53
69
|
return store.operation
|
|
@@ -233,23 +249,97 @@ export function useCrud(entity: IEntityCrud) {
|
|
|
233
249
|
})
|
|
234
250
|
|
|
235
251
|
const prepareDynamicFilters = computed(() => {
|
|
236
|
-
return store.dynamicFilters.map((filter: IEntityCrudFilter) => {
|
|
252
|
+
return expandRangeFilters(store.dynamicFilters.map((filter: IEntityCrudFilter) => {
|
|
237
253
|
return {
|
|
238
254
|
field: filter.name,
|
|
239
255
|
operator: filter.operator,
|
|
240
256
|
value: filter.value
|
|
241
257
|
}
|
|
242
|
-
}) as IDraxFieldFilter[]
|
|
258
|
+
})) as IDraxFieldFilter[]
|
|
243
259
|
})
|
|
244
260
|
|
|
245
261
|
|
|
246
262
|
const getAllFilters = computed(() =>{
|
|
247
263
|
return [
|
|
248
|
-
...store.filters,
|
|
264
|
+
...expandRangeFilters(store.filters),
|
|
249
265
|
...prepareDynamicFilters.value
|
|
250
266
|
] as IDraxFieldFilter[]
|
|
251
267
|
})
|
|
252
268
|
|
|
269
|
+
function resolveViewIndex(item: Record<string, any>, index?: number | null) {
|
|
270
|
+
if (typeof index === 'number' && index >= 0) {
|
|
271
|
+
return index
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const itemIdentifier = entity.identifier ? item?.[entity.identifier] : getItemId(item)
|
|
275
|
+
|
|
276
|
+
if (itemIdentifier !== undefined && itemIdentifier !== null) {
|
|
277
|
+
return items.value.findIndex((currentItem: Record<string, any>) => {
|
|
278
|
+
const currentIdentifier = entity.identifier ? currentItem?.[entity.identifier] : getItemId(currentItem)
|
|
279
|
+
return currentIdentifier === itemIdentifier
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return items.value.findIndex((currentItem: Record<string, any>) => currentItem === item)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function openItemAt(operation: Exclude<IEntityCrudOperation, null>, item: Record<string, any>, index?: number) {
|
|
287
|
+
const resolvedIndex = resolveViewIndex(item, index)
|
|
288
|
+
currentViewIndex.value = resolvedIndex >= 0 ? resolvedIndex : null
|
|
289
|
+
|
|
290
|
+
switch (operation) {
|
|
291
|
+
case 'view':
|
|
292
|
+
onView(item)
|
|
293
|
+
return
|
|
294
|
+
case 'edit':
|
|
295
|
+
onEdit(item)
|
|
296
|
+
return
|
|
297
|
+
case 'delete':
|
|
298
|
+
onDelete(item)
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function onViewAt(item: Record<string, any>, index?: number) {
|
|
304
|
+
openItemAt('view', item, index)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function onEditAt(item: Record<string, any>, index?: number) {
|
|
308
|
+
openItemAt('edit', item, index)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function onDeleteAt(item: Record<string, any>, index?: number) {
|
|
312
|
+
openItemAt('delete', item, index)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const canNavigatePrev = computed(() => {
|
|
316
|
+
return canNavigateItems.value
|
|
317
|
+
&& currentViewIndex.value !== null
|
|
318
|
+
&& currentViewIndex.value > 0
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
const canNavigateNext = computed(() => {
|
|
322
|
+
return canNavigateItems.value
|
|
323
|
+
&& currentViewIndex.value !== null
|
|
324
|
+
&& currentViewIndex.value < items.value.length - 1
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
function navigateView(direction: -1 | 1) {
|
|
328
|
+
if (currentViewIndex.value === null || operation.value === null || !canNavigateItems.value) {
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const nextIndex = currentViewIndex.value + direction
|
|
333
|
+
const nextItem = items.value[nextIndex]
|
|
334
|
+
|
|
335
|
+
if (!nextItem) {
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
currentViewIndex.value = nextIndex
|
|
340
|
+
openItemAt(operation.value, nextItem, nextIndex)
|
|
341
|
+
}
|
|
342
|
+
|
|
253
343
|
|
|
254
344
|
async function doPaginate() {
|
|
255
345
|
store.setLoading(true)
|
|
@@ -524,7 +614,7 @@ export function useCrud(entity: IEntityCrud) {
|
|
|
524
614
|
(filter: IEntityCrudFilter) =>
|
|
525
615
|
({
|
|
526
616
|
field: filter.name,
|
|
527
|
-
value: filter
|
|
617
|
+
value: createCrudFilterValue(filter),
|
|
528
618
|
operator: (filter.operator ? filter.operator : 'eq')
|
|
529
619
|
})
|
|
530
620
|
) as IDraxFieldFilter[]
|
|
@@ -552,13 +642,21 @@ export function useCrud(entity: IEntityCrud) {
|
|
|
552
642
|
await doPaginate()
|
|
553
643
|
}
|
|
554
644
|
|
|
645
|
+
watch(dialog, (value) => {
|
|
646
|
+
if (!value) {
|
|
647
|
+
currentViewIndex.value = null
|
|
648
|
+
}
|
|
649
|
+
})
|
|
650
|
+
|
|
555
651
|
|
|
556
652
|
return {
|
|
557
653
|
doPaginate, doExport, doImport, doUpdate, doCreate, doDelete,
|
|
558
654
|
onView, onCreate, onEdit, onDelete, onCancel, onSubmit, resetCrudStore,
|
|
655
|
+
onViewAt, onEditAt, onDeleteAt, navigateView, resolveViewIndex, openItemAt,
|
|
559
656
|
dialog, notify, error, paginationError, message, formValid,
|
|
560
657
|
form, getForm, setForm,
|
|
561
658
|
operation, getOperation, setOperation,
|
|
659
|
+
currentViewIndex, canNavigateItems, canNavigatePrev, canNavigateNext,
|
|
562
660
|
loading, itemsPerPage, page, sortBy, search, totalItems, items,
|
|
563
661
|
prepareFilters, filters, clearFilters, applyFilters, prepareSort,
|
|
564
662
|
exportFiles, importFiles, exportLoading, importLoading, exportListVisible, importListVisible, exportError, importError,
|
|
@@ -171,6 +171,9 @@ export function useDynamicFilters(
|
|
|
171
171
|
if (['ref','array.ref'].includes(filter.type) && ['gt', 'gte', 'lt', 'lte', 'like'].includes(op.value)) {
|
|
172
172
|
return false
|
|
173
173
|
}
|
|
174
|
+
if (['date'].includes(filter.type) && ['in', 'nin', 'like'].includes(op.value)) {
|
|
175
|
+
return false
|
|
176
|
+
}
|
|
174
177
|
return true
|
|
175
178
|
})
|
|
176
179
|
}
|
package/src/cruds/EntityCrud.ts
CHANGED
|
@@ -2,8 +2,9 @@ import type {
|
|
|
2
2
|
IEntityCrud, IEntityCrudForm, IEntityCrudHeader, IEntityCrudRefs,
|
|
3
3
|
IEntityCrudRules, IEntityCrudField, IEntityCrudPermissions,
|
|
4
4
|
IDraxCrudProvider, IEntityCrudFilter, IEntityCrudFieldVariant, IDraxFieldFilter,
|
|
5
|
-
IEntityCrudOnInput
|
|
5
|
+
IEntityCrudOnInput, IEntityCrudOperation
|
|
6
6
|
} from "@drax/crud-share";
|
|
7
|
+
import {createCrudFilterValue} from "../helpers/CrudRangeFilters";
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
|
|
@@ -138,7 +139,7 @@ class EntityCrud implements IEntityCrud {
|
|
|
138
139
|
get formFilters(): IDraxFieldFilter[] {
|
|
139
140
|
return this.filters.map(
|
|
140
141
|
(filter: IEntityCrudFilter) =>
|
|
141
|
-
({field: filter.name, value: filter
|
|
142
|
+
({field: filter.name, value: createCrudFilterValue(filter), operator: (filter.operator ? filter.operator : 'eq')})
|
|
142
143
|
) as IDraxFieldFilter[]
|
|
143
144
|
}
|
|
144
145
|
|
|
@@ -346,6 +347,10 @@ class EntityCrud implements IEntityCrud {
|
|
|
346
347
|
return false
|
|
347
348
|
}
|
|
348
349
|
|
|
350
|
+
get navigationOperations(): IEntityCrudOperation[] {
|
|
351
|
+
return ['view']
|
|
352
|
+
}
|
|
353
|
+
|
|
349
354
|
}
|
|
350
355
|
|
|
351
356
|
export default EntityCrud;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {describe, expect, it} from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
createCrudFilterValue,
|
|
4
|
+
createEmptyDateRangeValue,
|
|
5
|
+
expandRangeFilters,
|
|
6
|
+
normalizeDateRangeValue
|
|
7
|
+
} from "./CrudRangeFilters";
|
|
8
|
+
|
|
9
|
+
describe('CrudRangeFilters', () => {
|
|
10
|
+
it('creates an empty range value by default', () => {
|
|
11
|
+
expect(createEmptyDateRangeValue()).toEqual({from: null, to: null})
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('normalizes invalid range values to an empty range', () => {
|
|
15
|
+
expect(normalizeDateRangeValue(null)).toEqual({from: null, to: null})
|
|
16
|
+
expect(normalizeDateRangeValue('2026-01-01')).toEqual({from: null, to: null})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('uses an empty object for range filters without default', () => {
|
|
20
|
+
expect(createCrudFilterValue({
|
|
21
|
+
name: 'birthdate',
|
|
22
|
+
type: 'date',
|
|
23
|
+
label: 'birthdate',
|
|
24
|
+
default: undefined,
|
|
25
|
+
operator: 'range'
|
|
26
|
+
})).toEqual({from: null, to: null})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('expands range filters into gte and lte filters', () => {
|
|
30
|
+
const from = new Date('2026-01-10T00:00:00.000Z')
|
|
31
|
+
const to = new Date('2026-01-20T00:00:00.000Z')
|
|
32
|
+
|
|
33
|
+
expect(expandRangeFilters([{
|
|
34
|
+
field: 'birthdate',
|
|
35
|
+
operator: 'range',
|
|
36
|
+
value: {from, to}
|
|
37
|
+
}])).toEqual([
|
|
38
|
+
{field: 'birthdate', operator: 'gte', value: from},
|
|
39
|
+
{field: 'birthdate', operator: 'lte', value: to}
|
|
40
|
+
])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('omits empty range bounds when expanding', () => {
|
|
44
|
+
expect(expandRangeFilters([{
|
|
45
|
+
field: 'birthdate',
|
|
46
|
+
operator: 'range',
|
|
47
|
+
value: {from: null, to: '2026-01-20'}
|
|
48
|
+
}])).toEqual([
|
|
49
|
+
{field: 'birthdate', operator: 'lte', value: '2026-01-20'}
|
|
50
|
+
])
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type {IDraxFieldFilter, IEntityCrudFilter} from "@drax/crud-share";
|
|
2
|
+
|
|
3
|
+
interface ICrudDateRangeValue {
|
|
4
|
+
from: Date | string | null
|
|
5
|
+
to: Date | string | null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function createEmptyDateRangeValue(): ICrudDateRangeValue {
|
|
9
|
+
return {
|
|
10
|
+
from: null,
|
|
11
|
+
to: null
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isRangeOperator(filter?: Pick<IEntityCrudFilter, 'operator'> | Pick<IDraxFieldFilter, 'operator'> | null): boolean {
|
|
16
|
+
return filter?.operator === 'range'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeDateRangeValue(value: any): ICrudDateRangeValue {
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
21
|
+
return createEmptyDateRangeValue()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
from: value.from ?? null,
|
|
26
|
+
to: value.to ?? null
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createCrudFilterValue(filter: IEntityCrudFilter) {
|
|
31
|
+
if (filter.operator === 'range') {
|
|
32
|
+
if (filter.default === undefined) {
|
|
33
|
+
return createEmptyDateRangeValue()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return normalizeDateRangeValue(filter.default)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return filter.default !== undefined ? filter.default : null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function expandRangeFilters(filters: Array<IDraxFieldFilter | IEntityCrudFilter>): IDraxFieldFilter[] {
|
|
43
|
+
return filters.flatMap((filter) => {
|
|
44
|
+
if (!isRangeOperator(filter)) {
|
|
45
|
+
return [{
|
|
46
|
+
field: 'field' in filter ? filter.field : filter.name,
|
|
47
|
+
operator: filter.operator ? filter.operator : 'eq',
|
|
48
|
+
value: filter.value
|
|
49
|
+
}]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const rangeValue = normalizeDateRangeValue(filter.value)
|
|
53
|
+
const fieldName = 'field' in filter ? filter.field : filter.name
|
|
54
|
+
const expandedFilters: IDraxFieldFilter[] = []
|
|
55
|
+
|
|
56
|
+
if (rangeValue.from !== null && rangeValue.from !== undefined && rangeValue.from !== '') {
|
|
57
|
+
expandedFilters.push({
|
|
58
|
+
field: fieldName,
|
|
59
|
+
operator: 'gte',
|
|
60
|
+
value: rangeValue.from
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (rangeValue.to !== null && rangeValue.to !== undefined && rangeValue.to !== '') {
|
|
65
|
+
expandedFilters.push({
|
|
66
|
+
field: fieldName,
|
|
67
|
+
operator: 'lte',
|
|
68
|
+
value: rangeValue.to
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return expandedFilters
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export {
|
|
77
|
+
createCrudFilterValue,
|
|
78
|
+
createEmptyDateRangeValue,
|
|
79
|
+
expandRangeFilters,
|
|
80
|
+
isRangeOperator,
|
|
81
|
+
normalizeDateRangeValue
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type {ICrudDateRangeValue}
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import Crud from "./components/Crud.vue";
|
|
|
2
2
|
import CrudDialog from "./components/CrudDialog.vue";
|
|
3
3
|
import CrudForm from "./components/CrudForm.vue";
|
|
4
4
|
import CrudFormField from "./components/CrudFormField.vue";
|
|
5
|
+
import CrudFieldRange from "./components/CrudFieldRange.vue";
|
|
5
6
|
import CrudFormList from "./components/CrudFormList.vue";
|
|
6
7
|
import CrudList from "./components/CrudList.vue";
|
|
7
8
|
import CrudListTable from "./components/CrudListTable.vue";
|
|
@@ -26,6 +27,7 @@ export {
|
|
|
26
27
|
CrudDialog,
|
|
27
28
|
CrudForm,
|
|
28
29
|
CrudFormField,
|
|
30
|
+
CrudFieldRange,
|
|
29
31
|
CrudFormList,
|
|
30
32
|
CrudList,
|
|
31
33
|
CrudListTable,
|
|
@@ -5,6 +5,7 @@ export const useCrudStore = (id: string = 'entity') => defineStore('CrudStore'+i
|
|
|
5
5
|
state: () => (
|
|
6
6
|
{
|
|
7
7
|
operation: null as IEntityCrudOperation,
|
|
8
|
+
currentViewIndex: null as number | null,
|
|
8
9
|
dialog: false as boolean,
|
|
9
10
|
form: {} as any,
|
|
10
11
|
formValid: {} as any,
|
|
@@ -79,6 +80,9 @@ export const useCrudStore = (id: string = 'entity') => defineStore('CrudStore'+i
|
|
|
79
80
|
setOperation(operation: IEntityCrudOperation) {
|
|
80
81
|
this.operation = operation
|
|
81
82
|
},
|
|
83
|
+
setCurrentViewIndex(index: number | null) {
|
|
84
|
+
this.currentViewIndex = index
|
|
85
|
+
},
|
|
82
86
|
setDialog(dialog: boolean) {
|
|
83
87
|
this.dialog = dialog
|
|
84
88
|
},
|