@drax/crud-vue 3.18.1 → 3.20.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 CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "3.18.1",
6
+ "version": "3.20.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.11.0",
28
- "@drax/crud-front": "^3.11.0",
29
- "@drax/crud-share": "^3.17.0",
30
- "@drax/media-vue": "^3.18.1"
27
+ "@drax/common-front": "^3.19.0",
28
+ "@drax/crud-front": "^3.20.0",
29
+ "@drax/crud-share": "^3.20.0",
30
+ "@drax/media-vue": "^3.20.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": "241f90094f420bdbe8c998d0ccb89aff433db022"
53
+ "gitHead": "6d4aea4d05133be679166e398ec6a3ae61503d9e"
54
54
  }
@@ -17,9 +17,10 @@ const {entity} = defineProps({
17
17
  })
18
18
 
19
19
  const {
20
- onView, onCreate, onEdit, onDelete, resetCrudStore,
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="onEdit"
74
- @delete="onDelete"
74
+ @edit="onEditAt"
75
+ @delete="onDeleteAt"
75
76
  @export="doExport"
76
77
  @import="doImport"
77
- @view="onView"
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"
@@ -82,7 +82,6 @@ const inputErrors = computed(() => {
82
82
 
83
83
  defineEmits(['updateValue'])
84
84
 
85
-
86
85
  const hasHideDetails = computed(() => {
87
86
  if (readonly) {
88
87
  return true
@@ -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>
@@ -21,6 +21,7 @@ import {useCrudColumns} from "../composables/UseCrudColumns";
21
21
  import CrudFiltersDynamic from "./CrudFiltersDynamic.vue";
22
22
  import CrudFiltersAction from "./CrudFiltersAction.vue";
23
23
  import CrudFilterButton from "./buttons/CrudFilterButton.vue";
24
+ import CrudSavedQueriesButton from "./buttons/CrudSavedQueriesButton.vue";
24
25
 
25
26
  const {t, te} = useI18n()
26
27
  const {hasPermission} = useAuth()
@@ -89,6 +90,11 @@ onMounted(() => {
89
90
  :entity="entity"
90
91
  />
91
92
 
93
+ <crud-saved-queries-button
94
+ v-if="entity.isSavedQueriesEnabled"
95
+ :entity="entity"
96
+ />
97
+
92
98
  <slot name="toolbar">
93
99
  </slot>
94
100
 
@@ -199,7 +205,7 @@ onMounted(() => {
199
205
 
200
206
  <!-- GALLERY GRIDS -->
201
207
  <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"
208
+ <v-col v-for="(item, index) in items" :key="item.id || item.uuid || item.name || Math.random()" cols="12" sm="6" md="4"
203
209
  xl="3">
204
210
 
205
211
 
@@ -231,22 +237,22 @@ onMounted(() => {
231
237
  <v-divider></v-divider>
232
238
 
233
239
  <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}">
240
+ <slot name="item.actions" v-bind="{item, index}">
235
241
  </slot>
236
242
 
237
243
  <crud-view-button
238
244
  v-if="entity.isViewable && hasPermission(entity.permissions.view)"
239
- @click="$emit('view', item)"
245
+ @click="$emit('view', item, index)"
240
246
  />
241
247
 
242
248
  <crud-update-button
243
249
  v-if="entity.isEditable && entity.isItemEditable(item) && hasPermission(entity.permissions?.update)"
244
- @click="$emit('edit', item)"
250
+ @click="$emit('edit', item, index)"
245
251
  />
246
252
 
247
253
  <crud-delete-button
248
254
  v-if="entity.isDeletable && hasPermission(entity.permissions?.delete)"
249
- @click="$emit('delete', item)"
255
+ @click="$emit('delete', item, index)"
250
256
  />
251
257
  </v-card-actions>
252
258
  </v-card>
@@ -20,6 +20,7 @@ import { useCrudColumns } from "../composables/UseCrudColumns";
20
20
  import CrudFiltersDynamic from "./CrudFiltersDynamic.vue";
21
21
  import CrudFiltersAction from "./CrudFiltersAction.vue";
22
22
  import CrudFilterButton from "./buttons/CrudFilterButton.vue";
23
+ import CrudSavedQueriesButton from "./buttons/CrudSavedQueriesButton.vue";
23
24
 
24
25
  const {t, te} = useI18n()
25
26
  const {hasPermission} = useAuth()
@@ -96,6 +97,12 @@ defineEmits(['import', 'export', 'create', 'update', 'delete', 'view', 'edit'])
96
97
 
97
98
  </slot>
98
99
 
100
+ <crud-saved-queries-button
101
+ v-if="entity.isSavedQueriesEnabled"
102
+ :entity="entity"
103
+ />
104
+
105
+
99
106
  <crud-import-button
100
107
  :entity="entity"
101
108
  @import="(file:any, format:any) => $emit('import', file, format)"
@@ -119,6 +126,7 @@ defineEmits(['import', 'export', 'create', 'update', 'delete', 'view', 'edit'])
119
126
  :entity="entity"
120
127
  />
121
128
 
129
+
122
130
  <slot name="toolbar">
123
131
 
124
132
  </slot>
@@ -218,24 +226,24 @@ defineEmits(['import', 'export', 'create', 'update', 'delete', 'view', 'edit'])
218
226
  </template>
219
227
 
220
228
 
221
- <template v-slot:item.actions="{item}">
229
+ <template v-slot:item.actions="{item, index}">
222
230
 
223
- <slot name="item.actions" v-bind="{item}">
231
+ <slot name="item.actions" v-bind="{item, index}">
224
232
  </slot>
225
233
 
226
234
  <crud-view-button
227
235
  v-if="entity.isViewable && hasPermission(entity.permissions.view)"
228
- @click="$emit('view', item)"
236
+ @click="$emit('view', item, index)"
229
237
  />
230
238
 
231
239
  <crud-update-button
232
240
  v-if="entity.isEditable && entity.isItemEditable(item) && hasPermission(entity.permissions?.update)"
233
- @click="$emit('edit', item)"
241
+ @click="$emit('edit', item, index)"
234
242
  />
235
243
 
236
244
  <crud-delete-button
237
245
  v-if="entity.isDeletable && hasPermission(entity.permissions?.delete)"
238
- @click="$emit('delete', item)"
246
+ @click="$emit('delete', item, index)"
239
247
  />
240
248
 
241
249
  </template>
@@ -0,0 +1,346 @@
1
+ <script setup lang="ts">
2
+ import {computed, ref, type PropType} from "vue";
3
+ import type {ICrudSavedQuery, IEntityCrud, IEntityCrudFilter, IDraxFieldFilter} from "@drax/crud-share";
4
+ import {useI18n} from "vue-i18n";
5
+ import {useCrudStore} from "../../stores/UseCrudStore";
6
+ import {useCrud} from "../../composables/UseCrud";
7
+ import {useCrudColumns} from "../../composables/UseCrudColumns";
8
+ import {CrudSavedQueryProvider} from "@drax/crud-front";
9
+ import {useAuth, useAuthStore} from "@drax/identity-vue";
10
+ import {createCrudFilterValue} from "../../helpers/CrudRangeFilters";
11
+
12
+ const {t, te} = useI18n();
13
+ const {hasPermission} = useAuth();
14
+ const authStore = useAuthStore();
15
+
16
+ const props = defineProps({
17
+ entity: {type: Object as PropType<IEntityCrud>, required: true},
18
+ });
19
+
20
+ const store = useCrudStore(props.entity.name);
21
+ const {doPaginate} = useCrud(props.entity);
22
+ const {setVisibleColumns} = useCrudColumns(props.entity);
23
+
24
+ const menu = ref(false);
25
+ const saveDialog = ref(false);
26
+ const deleteDialog = ref(false);
27
+ const loading = ref(false);
28
+ const saving = ref(false);
29
+ const deleting = ref(false);
30
+ const savedQueries = ref<ICrudSavedQuery[]>([]);
31
+ const queryToDelete = ref<ICrudSavedQuery | null>(null);
32
+ const form = ref({
33
+ name: "",
34
+ shared: false,
35
+ });
36
+
37
+ type UserIdLike = {
38
+ id?: string;
39
+ _id?: string;
40
+ };
41
+
42
+ const title = computed(() => {
43
+ const key = "crud.savedQueries.title";
44
+ return te(key) ? t(key) : "Saved queries";
45
+ });
46
+
47
+ const saveTitle = computed(() => {
48
+ const key = "crud.savedQueries.save";
49
+ return te(key) ? t(key) : "Save query";
50
+ });
51
+
52
+ const noQueriesText = computed(() => {
53
+ const key = "crud.savedQueries.empty";
54
+ return te(key) ? t(key) : "No saved queries";
55
+ });
56
+
57
+ const deleteTitle = computed(() => {
58
+ const key = "crud.savedQueries.delete";
59
+ return te(key) ? t(key) : "Delete saved query";
60
+ });
61
+
62
+ const deleteConfirmText = computed(() => {
63
+ const key = "crud.savedQueries.deleteConfirm";
64
+ return te(key) ? t(key, {name: queryToDelete.value?.name || ""}) : `Delete "${queryToDelete.value?.name || ""}"?`;
65
+ });
66
+
67
+ const canViewSavedQueries = computed(() => hasPermission("crudSavedQuery:view"));
68
+ const canCreateSavedQueries = computed(() => hasPermission("crudSavedQuery:create"));
69
+ const canDeleteOwnSavedQueries = computed(() => hasPermission("crudSavedQuery:delete"));
70
+ const canDeleteAllSavedQueries = computed(() => hasPermission("crudSavedQuery:all") || hasPermission("crudSavedQuery:deleteAll"));
71
+
72
+ function clone<T>(value: T): T {
73
+ return JSON.parse(JSON.stringify(value));
74
+ }
75
+
76
+ function entityFilter(): IDraxFieldFilter[] {
77
+ return [{field: "entity", operator: "eq", value: props.entity.name}];
78
+ }
79
+
80
+ function definedDynamicFilters(): IEntityCrudFilter[] {
81
+ return clone(store.dynamicFilters.filter((filter: IEntityCrudFilter) => filter.name));
82
+ }
83
+
84
+ function buildStaticFilters(savedFilters: IDraxFieldFilter[] = []): IDraxFieldFilter[] {
85
+ const savedByField = new Map(savedFilters.map(filter => [filter.field, filter]));
86
+
87
+ return props.entity.filters.map(filter => {
88
+ const savedFilter = savedByField.get(filter.name);
89
+ return {
90
+ field: filter.name,
91
+ operator: savedFilter?.operator || filter.operator || "eq",
92
+ value: savedFilter ? savedFilter.value : createCrudFilterValue(filter)
93
+ };
94
+ });
95
+ }
96
+
97
+ function queryUserId(query: ICrudSavedQuery): string | undefined {
98
+ const user = query.user;
99
+ if (!user) {
100
+ return undefined;
101
+ }
102
+ if (typeof user === "string") {
103
+ return user;
104
+ }
105
+ const id = user._id || user.id;
106
+ return id ? String(id) : undefined;
107
+ }
108
+
109
+ function currentUserId(): string | undefined {
110
+ const authUser = authStore.authUser as UserIdLike | null;
111
+ const id = authUser?.id || authUser?._id;
112
+ return id ? String(id) : undefined;
113
+ }
114
+
115
+ function canDeleteQuery(query: ICrudSavedQuery): boolean {
116
+ if (canDeleteAllSavedQueries.value) {
117
+ return true;
118
+ }
119
+ if (!canDeleteOwnSavedQueries.value) {
120
+ return false;
121
+ }
122
+
123
+ const ownerId = queryUserId(query);
124
+ return !ownerId || ownerId === currentUserId();
125
+ }
126
+
127
+ async function loadQueries() {
128
+ loading.value = true;
129
+ try {
130
+ savedQueries.value = await CrudSavedQueryProvider.instance.find({
131
+ limit: 100,
132
+ orderBy: "name",
133
+ order: "asc",
134
+ filters: entityFilter()
135
+ });
136
+ } catch (error) {
137
+ console.error("Error loading saved queries", error);
138
+ savedQueries.value = [];
139
+ } finally {
140
+ loading.value = false;
141
+ }
142
+ }
143
+
144
+ function openSaveDialog() {
145
+ form.value = {name: "", shared: false};
146
+ saveDialog.value = true;
147
+ }
148
+
149
+ async function saveQuery() {
150
+ if (!form.value.name.trim()) {
151
+ return;
152
+ }
153
+
154
+ saving.value = true;
155
+ try {
156
+ await CrudSavedQueryProvider.instance.create({
157
+ entity: props.entity.name,
158
+ name: form.value.name.trim(),
159
+ shared: form.value.shared,
160
+ columns: clone(store.visibleColumns),
161
+ staticFilters: clone(store.filters),
162
+ dynamicFilters: definedDynamicFilters(),
163
+ });
164
+ saveDialog.value = false;
165
+ await loadQueries();
166
+ } catch (error) {
167
+ console.error("Error saving query", error);
168
+ } finally {
169
+ saving.value = false;
170
+ }
171
+ }
172
+
173
+ function openDeleteDialog(query: ICrudSavedQuery) {
174
+ queryToDelete.value = query;
175
+ deleteDialog.value = true;
176
+ menu.value = false;
177
+ }
178
+
179
+ async function deleteQuery() {
180
+ if (!queryToDelete.value) {
181
+ return;
182
+ }
183
+
184
+ deleting.value = true;
185
+ try {
186
+ await CrudSavedQueryProvider.instance.delete(queryToDelete.value._id);
187
+ deleteDialog.value = false;
188
+ queryToDelete.value = null;
189
+ await loadQueries();
190
+ } catch (error) {
191
+ console.error("Error deleting saved query", error);
192
+ } finally {
193
+ deleting.value = false;
194
+ }
195
+ }
196
+
197
+ async function applyQuery(query: ICrudSavedQuery) {
198
+ setVisibleColumns(query.columns || []);
199
+ store.setFilters(buildStaticFilters(clone(query.staticFilters || [])));
200
+ const dynamicFilters = clone(query.dynamicFilters || []);
201
+ store.setDynamicFilters(dynamicFilters);
202
+ store.setDynamicFiltersEnable(dynamicFilters.length > 0);
203
+ store.setPage(1);
204
+ menu.value = false;
205
+ await doPaginate();
206
+ }
207
+
208
+ function onMenuUpdate(value: boolean) {
209
+ menu.value = value;
210
+ if (value) {
211
+ loadQueries();
212
+ }
213
+ }
214
+ </script>
215
+
216
+ <template>
217
+ <v-menu
218
+ v-if="canViewSavedQueries"
219
+ :model-value="menu"
220
+ offset-y
221
+ :close-on-content-click="false"
222
+ @update:model-value="onMenuUpdate"
223
+ >
224
+ <template #activator="{ props: activatorProps }">
225
+ <v-btn
226
+ v-bind="activatorProps"
227
+ icon
228
+ variant="text"
229
+ >
230
+ <v-icon>mdi-content-save-cog</v-icon>
231
+ <v-tooltip activator="parent" location="bottom">
232
+ {{ title }}
233
+ </v-tooltip>
234
+ </v-btn>
235
+ </template>
236
+
237
+ <v-list min-width="280">
238
+ <v-list-subheader>{{ title }}</v-list-subheader>
239
+
240
+ <v-list-item
241
+ v-if="canCreateSavedQueries"
242
+ @click="openSaveDialog"
243
+ >
244
+ <template #prepend>
245
+ <v-icon>mdi-content-save-outline</v-icon>
246
+ </template>
247
+ <v-list-item-title>{{ saveTitle }}</v-list-item-title>
248
+ </v-list-item>
249
+
250
+ <v-divider />
251
+
252
+ <v-list-item v-if="loading">
253
+ <v-progress-linear indeterminate />
254
+ </v-list-item>
255
+
256
+ <v-list-item v-else-if="savedQueries.length === 0">
257
+ <v-list-item-title class="text-medium-emphasis">{{ noQueriesText }}</v-list-item-title>
258
+ </v-list-item>
259
+
260
+ <v-list-item
261
+ v-for="query in savedQueries"
262
+ :key="query._id"
263
+ @click="applyQuery(query)"
264
+ >
265
+ <template #prepend>
266
+ <v-icon>{{ query.shared ? "mdi-account-group-outline" : "mdi-account-outline" }}</v-icon>
267
+ </template>
268
+ <v-list-item-title>{{ query.name }}</v-list-item-title>
269
+ <template #append>
270
+ <v-btn
271
+ v-if="canDeleteQuery(query)"
272
+ icon
273
+ variant="text"
274
+ color="red"
275
+ size="small"
276
+ @click.stop="openDeleteDialog(query)"
277
+ >
278
+ <v-icon>mdi-delete</v-icon>
279
+ <v-tooltip activator="parent" location="bottom">
280
+ {{ te('action.delete') ? t('action.delete') : 'Delete' }}
281
+ </v-tooltip>
282
+ </v-btn>
283
+ </template>
284
+ </v-list-item>
285
+ </v-list>
286
+ </v-menu>
287
+
288
+ <v-dialog v-model="saveDialog" max-width="460">
289
+ <v-card>
290
+ <v-card-title>{{ saveTitle }}</v-card-title>
291
+ <v-card-text>
292
+ <v-text-field
293
+ v-model="form.name"
294
+ :label="te('crud.savedQueries.name') ? t('crud.savedQueries.name') : 'Name'"
295
+ density="compact"
296
+ variant="outlined"
297
+ autofocus
298
+ />
299
+ <v-switch
300
+ v-model="form.shared"
301
+ :label="te('crud.savedQueries.shared') ? t('crud.savedQueries.shared') : 'Shared'"
302
+ color="primary"
303
+ hide-details
304
+ />
305
+ </v-card-text>
306
+ <v-card-actions>
307
+ <v-spacer />
308
+ <v-btn variant="text" @click="saveDialog = false">
309
+ {{ te('action.cancel') ? t('action.cancel') : 'Cancel' }}
310
+ </v-btn>
311
+ <v-btn
312
+ color="primary"
313
+ variant="flat"
314
+ :loading="saving"
315
+ :disabled="!form.name.trim()"
316
+ @click="saveQuery"
317
+ >
318
+ {{ te('action.save') ? t('action.save') : 'Save' }}
319
+ </v-btn>
320
+ </v-card-actions>
321
+ </v-card>
322
+ </v-dialog>
323
+
324
+ <v-dialog v-model="deleteDialog" max-width="460">
325
+ <v-card>
326
+ <v-card-title>{{ deleteTitle }}</v-card-title>
327
+ <v-card-text>
328
+ {{ deleteConfirmText }}
329
+ </v-card-text>
330
+ <v-card-actions>
331
+ <v-spacer />
332
+ <v-btn variant="text" @click="deleteDialog = false">
333
+ {{ te('action.cancel') ? t('action.cancel') : 'Cancel' }}
334
+ </v-btn>
335
+ <v-btn
336
+ color="red"
337
+ variant="flat"
338
+ :loading="deleting"
339
+ @click="deleteQuery"
340
+ >
341
+ {{ te('action.delete') ? t('action.delete') : 'Delete' }}
342
+ </v-btn>
343
+ </v-card-actions>
344
+ </v-card>
345
+ </v-dialog>
346
+ </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.default ? filter.default : null,
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,
@@ -158,6 +158,16 @@ export function useCrudColumns(entity: IEntityCrud) {
158
158
  saveColumnsToStorage(defaultColumns)
159
159
  }
160
160
 
161
+ const setVisibleColumns = (columns: string[]) => {
162
+ const availableHeaders = entity.headers
163
+ .filter(header => !header.permission || hasPermission(header.permission))
164
+ .map(header => header.key)
165
+
166
+ const validColumns = columns.filter(key => availableHeaders.includes(key))
167
+ crudStore.setVisibleColumns(validColumns)
168
+ saveColumnsToStorage(validColumns)
169
+ }
170
+
161
171
  return {
162
172
  visibleColumns,
163
173
  translatedHeaders,
@@ -170,5 +180,6 @@ export function useCrudColumns(entity: IEntityCrud) {
170
180
  selectAll,
171
181
  deselectAll,
172
182
  resetToDefault,
183
+ setVisibleColumns,
173
184
  }
174
185
  }
@@ -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
  }
@@ -24,6 +24,8 @@ export function useFilterIcon() {
24
24
  return 'mdi-not-equal'
25
25
  case 'like':
26
26
  return 'mdi-contain'
27
+ case 'range':
28
+ return 'mdi-arrow-expand-horizontal'
27
29
  default:
28
30
  return 'mdi-equal'
29
31
  }
@@ -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.default ? filter.default : null, operator: (filter.operator ? filter.operator : 'eq')})
142
+ ({field: filter.name, value: createCrudFilterValue(filter), operator: (filter.operator ? filter.operator : 'eq')})
142
143
  ) as IDraxFieldFilter[]
143
144
  }
144
145
 
@@ -224,6 +225,10 @@ class EntityCrud implements IEntityCrud {
224
225
  return true
225
226
  }
226
227
 
228
+ get isSavedQueriesEnabled(){
229
+ return false
230
+ }
231
+
227
232
  get isGroupable() {
228
233
  return false
229
234
  }
@@ -346,6 +351,10 @@ class EntityCrud implements IEntityCrud {
346
351
  return false
347
352
  }
348
353
 
354
+ get navigationOperations(): IEntityCrudOperation[] {
355
+ return ['view']
356
+ }
357
+
349
358
  }
350
359
 
351
360
  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";
@@ -11,6 +12,7 @@ import CrudFiltersAction from "./components/CrudFiltersAction.vue";
11
12
  import CrudNotify from "./components/CrudNotify.vue";
12
13
  import CrudSearch from "./components/CrudSearch.vue";
13
14
  import CrudAutocomplete from "./components/CrudAutocomplete.vue";
15
+ import CrudSavedQueriesButton from "./components/buttons/CrudSavedQueriesButton.vue";
14
16
  import EntityCombobox from "./components/combobox/EntityCombobox.vue";
15
17
  import {useCrudStore} from "./stores/UseCrudStore";
16
18
  import {useEntityStore} from "./stores/UseEntityStore";
@@ -26,6 +28,7 @@ export {
26
28
  CrudDialog,
27
29
  CrudForm,
28
30
  CrudFormField,
31
+ CrudFieldRange,
29
32
  CrudFormList,
30
33
  CrudList,
31
34
  CrudListTable,
@@ -33,6 +36,7 @@ export {
33
36
  CrudNotify,
34
37
  CrudSearch,
35
38
  CrudAutocomplete,
39
+ CrudSavedQueriesButton,
36
40
  CrudFilters,
37
41
  CrudFiltersAction,
38
42
  useCrud,
@@ -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
  },