@drax/crud-vue 3.19.0 → 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.19.0",
6
+ "version": "3.20.0",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "module": "./src/index.ts",
@@ -25,9 +25,9 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@drax/common-front": "^3.19.0",
28
- "@drax/crud-front": "^3.11.0",
29
- "@drax/crud-share": "^3.19.0",
30
- "@drax/media-vue": "^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": "bf445c10758ee014b45c2d76d6fddd5c578c738f"
53
+ "gitHead": "6d4aea4d05133be679166e398ec6a3ae61503d9e"
54
54
  }
@@ -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
 
@@ -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>
@@ -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>
@@ -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
  }
@@ -225,6 +225,10 @@ class EntityCrud implements IEntityCrud {
225
225
  return true
226
226
  }
227
227
 
228
+ get isSavedQueriesEnabled(){
229
+ return false
230
+ }
231
+
228
232
  get isGroupable() {
229
233
  return false
230
234
  }
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ import CrudFiltersAction from "./components/CrudFiltersAction.vue";
12
12
  import CrudNotify from "./components/CrudNotify.vue";
13
13
  import CrudSearch from "./components/CrudSearch.vue";
14
14
  import CrudAutocomplete from "./components/CrudAutocomplete.vue";
15
+ import CrudSavedQueriesButton from "./components/buttons/CrudSavedQueriesButton.vue";
15
16
  import EntityCombobox from "./components/combobox/EntityCombobox.vue";
16
17
  import {useCrudStore} from "./stores/UseCrudStore";
17
18
  import {useEntityStore} from "./stores/UseEntityStore";
@@ -35,6 +36,7 @@ export {
35
36
  CrudNotify,
36
37
  CrudSearch,
37
38
  CrudAutocomplete,
39
+ CrudSavedQueriesButton,
38
40
  CrudFilters,
39
41
  CrudFiltersAction,
40
42
  useCrud,