@afeefa/vue-app 0.0.344 → 0.0.345
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/.afeefa/package/release/version.txt +1 -1
- package/package.json +1 -1
- package/src/api-resources/ListAction.js +13 -1
- package/src/components/ASelect2.vue +4 -14
- package/src/components/select2/Select2List.vue +2 -16
- package/src/services/start-filter/StartFilterStorage.js +51 -0
- package/src-admin/components/index.js +2 -0
- package/src-admin/components/list/ListStartFilterButton.vue +123 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0.
|
|
1
|
+
0.0.345
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextRouteFilterSource } from '@a-vue/components/list/NextRouteFilterSource'
|
|
2
2
|
import { AlertEvent } from '@a-vue/events'
|
|
3
3
|
import { eventBus } from '@a-vue/plugins/event-bus/EventBus'
|
|
4
|
+
import { StartFilterStorage } from '@a-vue/services/start-filter/StartFilterStorage'
|
|
4
5
|
import { ListViewModel } from '@afeefa/api-resources-client'
|
|
5
6
|
|
|
6
7
|
import { ApiAction } from './ApiAction'
|
|
@@ -10,7 +11,18 @@ export class ListAction extends ApiAction {
|
|
|
10
11
|
return this.execute()
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
initFiltersForRoute (route) {
|
|
14
|
+
initFiltersForRoute (route, { startFilterKey } = {}) {
|
|
15
|
+
// Startfilter anwenden, wenn die URL keine Filter vorgibt: synthetisches
|
|
16
|
+
// Route-Objekt mit der gespeicherten Query (kein Router-Eingriff — die
|
|
17
|
+
// Liste pusht die Filter anschließend selbst in die URL). Bei vorhandener
|
|
18
|
+
// Session-History bleibt die injizierte Query wirkungslos (History gewinnt).
|
|
19
|
+
if (startFilterKey && !Object.keys(route.query).length) {
|
|
20
|
+
const startFilterQuery = StartFilterStorage.load(startFilterKey)
|
|
21
|
+
if (startFilterQuery) {
|
|
22
|
+
route = { ...route, query: startFilterQuery }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
const request = new ListViewModel(this)
|
|
15
27
|
// read from next route query string, but do not push
|
|
16
28
|
// list component will be init with used_filters
|
|
@@ -123,7 +123,6 @@
|
|
|
123
123
|
v-if="activeTab === 'search'"
|
|
124
124
|
ref="list"
|
|
125
125
|
:items="displayItems"
|
|
126
|
-
:stickyCount="isDynamic ? topSpecialCount : 0"
|
|
127
126
|
:selection="activeSelection"
|
|
128
127
|
:getTitle="getTitle"
|
|
129
128
|
:getSubtitle="getSubtitle"
|
|
@@ -422,14 +421,14 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
422
421
|
// wenn die ganze Liste auf einer Seite liegt — sonst (Pagination greift,
|
|
423
422
|
// `hasMore`) würden sie zwischen den bisher geladenen Treffern und den noch
|
|
424
423
|
// nachkommenden Seiten hängen, also faktisch mitten in der Liste. In dem Fall
|
|
425
|
-
// ziehen wir alle Sonder-Items nach oben
|
|
424
|
+
// ziehen wir alle Sonder-Items nach oben.
|
|
426
425
|
get collapseSpecialToTop () {
|
|
427
426
|
return this.isDynamic && this.hasMore
|
|
428
427
|
}
|
|
429
428
|
|
|
430
|
-
// Was die Liste anzeigt: Sonder-Items oben (vor den Treffern,
|
|
431
|
-
//
|
|
432
|
-
//
|
|
429
|
+
// Was die Liste anzeigt: Sonder-Items oben (vor den Treffern), dann die
|
|
430
|
+
// Treffer, dann Sonder-Items unten — Letzteres nur, solange die Liste auf
|
|
431
|
+
// eine Seite passt (§5, siehe collapseSpecialToTop).
|
|
433
432
|
get displayItems () {
|
|
434
433
|
if (this.collapseSpecialToTop) {
|
|
435
434
|
return [
|
|
@@ -445,15 +444,6 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
|
|
|
445
444
|
]
|
|
446
445
|
}
|
|
447
446
|
|
|
448
|
-
// Anzahl der oben angepinnten Sonder-Items (für sticky-Rendering in Select2List).
|
|
449
|
-
// Bei collapseSpecialToTop sind auch die unteren Sonder-Items mit oben.
|
|
450
|
-
get topSpecialCount () {
|
|
451
|
-
const top = this.matchingSpecialItems('top').length
|
|
452
|
-
return this.collapseSpecialToTop
|
|
453
|
-
? top + this.matchingSpecialItems('bottom').length
|
|
454
|
-
: top
|
|
455
|
-
}
|
|
456
|
-
|
|
457
447
|
// --- Laden (nur dynamisch) -----------------------------------------------
|
|
458
448
|
|
|
459
449
|
async loadResults ({ reset = false } = {}) {
|
|
@@ -29,8 +29,7 @@
|
|
|
29
29
|
:class="['row', 'row-' + index, {
|
|
30
30
|
included: polarityOf(model) === 'include',
|
|
31
31
|
excluded: polarityOf(model) === 'exclude',
|
|
32
|
-
active: activeIndex === index
|
|
33
|
-
sticky: index < stickyCount
|
|
32
|
+
active: activeIndex === index
|
|
34
33
|
}]"
|
|
35
34
|
@click="onRowClick(model)"
|
|
36
35
|
>
|
|
@@ -144,10 +143,7 @@ import { Component, Vue, Watch } from '@a-vue'
|
|
|
144
143
|
// Zustands — die Polarität (include/exclude) zeigt weiter der Nicht-Button.
|
|
145
144
|
showCheckbox: false,
|
|
146
145
|
isLoading: false,
|
|
147
|
-
hasMore: false
|
|
148
|
-
// Anzahl der oben angepinnten Sonder-Items (§5): die ersten N Zeilen
|
|
149
|
-
// bleiben beim Scrollen sichtbar (position: sticky).
|
|
150
|
-
stickyCount: 0
|
|
146
|
+
hasMore: false
|
|
151
147
|
}
|
|
152
148
|
]
|
|
153
149
|
})
|
|
@@ -360,16 +356,6 @@ export default class Select2List extends Vue {
|
|
|
360
356
|
}
|
|
361
357
|
}
|
|
362
358
|
|
|
363
|
-
// Oben angepinnte Sonder-Items (§5): bleiben beim Scrollen sichtbar. Weißer
|
|
364
|
-
// Hintergrund, damit durchscrollende Treffer nicht durchscheinen.
|
|
365
|
-
|
|
366
|
-
:deep(.a-table-row.sticky) {
|
|
367
|
-
position: sticky;
|
|
368
|
-
top: 0;
|
|
369
|
-
z-index: 1;
|
|
370
|
-
background: white;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
359
|
// Endlos-Scroll-Ziel: braucht Höhe, damit der IntersectionObserver feuert.
|
|
374
360
|
|
|
375
361
|
.sentinel {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Speicherung des persönlichen Startfilters einer Liste im localStorage.
|
|
2
|
+
//
|
|
3
|
+
// Gespeichert wird die Filterbelegung in Query-Form (wie sie auch in der URL
|
|
4
|
+
// stünde), pro Liste über deren storageKey. Genutzt vom ListStartFilterButton
|
|
5
|
+
// (speichern/löschen/anwenden) und von ListAction.initFiltersForRoute
|
|
6
|
+
// (Anwenden beim Listen-Start).
|
|
7
|
+
|
|
8
|
+
const KEY_PREFIX = 'start-filters-'
|
|
9
|
+
|
|
10
|
+
export const StartFilterStorage = {
|
|
11
|
+
load (storageKey) {
|
|
12
|
+
if (!storageKey) {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const storageItem = localStorage.getItem(KEY_PREFIX + storageKey)
|
|
17
|
+
if (storageItem) {
|
|
18
|
+
return JSON.parse(storageItem)
|
|
19
|
+
}
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.warn('Failed to load start filter from localStorage:', error)
|
|
22
|
+
}
|
|
23
|
+
return null
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
save (storageKey, query) {
|
|
27
|
+
if (!storageKey) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
localStorage.setItem(KEY_PREFIX + storageKey, JSON.stringify(query))
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.warn('Failed to save start filter to localStorage:', error)
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
clear (storageKey) {
|
|
38
|
+
if (!storageKey) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
localStorage.removeItem(KEY_PREFIX + storageKey)
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.warn('Failed to clear start filter from localStorage:', error)
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
has (storageKey) {
|
|
49
|
+
return this.load(storageKey) !== null
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -19,6 +19,7 @@ import HSeparator from './HSeparator'
|
|
|
19
19
|
import ListColumnHeader from './list/ListColumnHeader'
|
|
20
20
|
import ListColumnSelector from './list/ListColumnSelector'
|
|
21
21
|
import ListConfig from './list/ListConfig'
|
|
22
|
+
import ListStartFilterButton from './list/ListStartFilterButton'
|
|
22
23
|
import ListView from './list/ListView'
|
|
23
24
|
import ModelCount from './model/ModelCount'
|
|
24
25
|
import ModelIcon from './model/ModelIcon'
|
|
@@ -32,6 +33,7 @@ Vue.component('ListColumnHeader', ListColumnHeader)
|
|
|
32
33
|
Vue.component('ListConfig', ListConfig)
|
|
33
34
|
Vue.component('ListView', ListView)
|
|
34
35
|
Vue.component('ListColumnSelector', ListColumnSelector)
|
|
36
|
+
Vue.component('ListStartFilterButton', ListStartFilterButton)
|
|
35
37
|
|
|
36
38
|
Vue.component('EditPage', EditPage)
|
|
37
39
|
Vue.component('EditFormButtons', EditFormButtons)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<!-- ohne gespeicherten Startfilter: Klick speichert direkt -->
|
|
4
|
+
<v-btn
|
|
5
|
+
v-if="!hasStartFilter"
|
|
6
|
+
class="startFilterButton"
|
|
7
|
+
title="Aktuelle Filter als Startfilter speichern"
|
|
8
|
+
small
|
|
9
|
+
@click="saveStartFilter"
|
|
10
|
+
>
|
|
11
|
+
<v-icon
|
|
12
|
+
color="#999999"
|
|
13
|
+
size="1rem"
|
|
14
|
+
class="mr-1"
|
|
15
|
+
>
|
|
16
|
+
{{ mdiPinOutline }}
|
|
17
|
+
</v-icon>
|
|
18
|
+
Filter
|
|
19
|
+
</v-btn>
|
|
20
|
+
|
|
21
|
+
<!-- mit gespeichertem Startfilter: Kontext-Menü -->
|
|
22
|
+
<a-context-menu v-else>
|
|
23
|
+
<template #activator>
|
|
24
|
+
<v-btn
|
|
25
|
+
class="startFilterButton"
|
|
26
|
+
title="Startfilter gespeichert"
|
|
27
|
+
small
|
|
28
|
+
>
|
|
29
|
+
<v-icon
|
|
30
|
+
color="#999999"
|
|
31
|
+
size="1rem"
|
|
32
|
+
class="mr-1"
|
|
33
|
+
>
|
|
34
|
+
{{ mdiPin }}
|
|
35
|
+
</v-icon>
|
|
36
|
+
Filter
|
|
37
|
+
</v-btn>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<a-context-menu-item @click="applyStartFilter">
|
|
41
|
+
<v-icon>
|
|
42
|
+
{{ mdiPin }}
|
|
43
|
+
</v-icon>
|
|
44
|
+
Liste auf Startfilter setzen
|
|
45
|
+
</a-context-menu-item>
|
|
46
|
+
|
|
47
|
+
<a-context-menu-item @click="saveStartFilter">
|
|
48
|
+
<v-icon>
|
|
49
|
+
{{ mdiContentSaveOutline }}
|
|
50
|
+
</v-icon>
|
|
51
|
+
Startfilter ersetzen
|
|
52
|
+
</a-context-menu-item>
|
|
53
|
+
|
|
54
|
+
<a-context-menu-item @click="removeStartFilter">
|
|
55
|
+
<v-icon>
|
|
56
|
+
{{ mdiTrashCanOutline }}
|
|
57
|
+
</v-icon>
|
|
58
|
+
Startfilter löschen
|
|
59
|
+
</a-context-menu-item>
|
|
60
|
+
</a-context-menu>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<script>
|
|
65
|
+
import { Component, Vue } from '@a-vue'
|
|
66
|
+
import { StartFilterStorage } from '@a-vue/services/start-filter/StartFilterStorage'
|
|
67
|
+
import { mdiContentSaveOutline, mdiPin, mdiPinOutline, mdiTrashCanOutline } from '@mdi/js'
|
|
68
|
+
|
|
69
|
+
// Filter, die nicht zum Startfilter gehören:
|
|
70
|
+
// Seitenzahl und Suchfeld (zählen auch in der Filterleiste nicht als gesetzte Filter)
|
|
71
|
+
const EXCLUDED_FILTERS = ['page', 'q', 'qfield']
|
|
72
|
+
|
|
73
|
+
@Component({
|
|
74
|
+
props: ['filters', 'storageKey']
|
|
75
|
+
})
|
|
76
|
+
export default class ListStartFilterButton extends Vue {
|
|
77
|
+
mdiPin = mdiPin
|
|
78
|
+
mdiPinOutline = mdiPinOutline
|
|
79
|
+
mdiContentSaveOutline = mdiContentSaveOutline
|
|
80
|
+
mdiTrashCanOutline = mdiTrashCanOutline
|
|
81
|
+
|
|
82
|
+
hasStartFilter = false
|
|
83
|
+
|
|
84
|
+
created () {
|
|
85
|
+
this.hasStartFilter = StartFilterStorage.has(this.storageKey)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
saveStartFilter () {
|
|
89
|
+
StartFilterStorage.save(this.storageKey, this.getCurrentFilterQuery())
|
|
90
|
+
this.hasStartFilter = true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Wendet den gespeicherten Startfilter auf die lebende Ansicht an:
|
|
95
|
+
* Query pushen, der $route.query-Watcher der Liste übernimmt den Rest
|
|
96
|
+
* (Filter ohne Query-Wert fallen auf Default zurück, inkl. Suche/Seite).
|
|
97
|
+
*/
|
|
98
|
+
applyStartFilter () {
|
|
99
|
+
const query = StartFilterStorage.load(this.storageKey) || {}
|
|
100
|
+
// Duplicate-Push vermeiden (JSON-Vergleich ist key-Reihenfolge-sensitiv,
|
|
101
|
+
// daher zusätzlich catch gegen NavigationDuplicated)
|
|
102
|
+
if (JSON.stringify(this.$router.currentRoute.query) !== JSON.stringify(query)) {
|
|
103
|
+
this.$router.push({ query }).catch(() => {})
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
removeStartFilter () {
|
|
108
|
+
StartFilterStorage.clear(this.storageKey)
|
|
109
|
+
this.hasStartFilter = false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getCurrentFilterQuery () {
|
|
113
|
+
let query = {}
|
|
114
|
+
for (const filter of Object.values(this.filters)) {
|
|
115
|
+
if (EXCLUDED_FILTERS.includes(filter.name)) {
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
query = { ...query, ...filter.toQuerySource() }
|
|
119
|
+
}
|
|
120
|
+
return query
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
</script>
|