@afeefa/vue-app 0.0.343 → 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.
@@ -1 +1 @@
1
- 0.0.343
1
+ 0.0.345
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afeefa/vue-app",
3
- "version": "0.0.343",
3
+ "version": "0.0.345",
4
4
  "description": "",
5
5
  "author": "Afeefa Kollektiv <kollektiv@afeefa.de>",
6
6
  "license": "MIT",
@@ -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"
@@ -369,18 +368,17 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
369
368
  // Maßstab ist die GESAMTzahl der ungefilterten Liste (`totalCount`), nicht der
370
369
  // aktuelle Such-`count` und nicht `hasMore`: beide fallen während einer Suche
371
370
  // (0 Treffer → hasMore false) und flackern beim Reload. `totalCount` bleibt
372
- // stabil. Zwei Sonderfälle halten das Feld zusätzlich sichtbar:
371
+ // stabil. Sonderfälle:
372
+ // - Noch nichts geladen (`!loaded`): KEIN Feld — die Items erscheinen ohnehin
373
+ // erst mit der ersten Antwort; das Feld kommt dann (falls nötig) zusammen mit
374
+ // der Liste. Vorher kurz eins zu zeigen, das bei kurzen Listen gleich wieder
375
+ // verschwindet, wäre Flackern.
373
376
  // - Schon getippt (`search`): das Feld muss bleiben, sonst lässt sich die Suche
374
377
  // nicht mehr ändern/leeren.
375
- // - Noch nichts geladen (`!loaded`): `totalCount` ist erst nach der ersten Seite
376
- // bekannt. Bis dahin Feld zeigen, statt es nachträglich aufpoppen zu lassen.
377
378
  get showSearchField () {
378
- if (!this.isDynamic || !this.hasSearch) {
379
+ if (!this.isDynamic || !this.hasSearch || !this.loaded) {
379
380
  return false
380
381
  }
381
- if (!this.loaded) {
382
- return true
383
- }
384
382
  return !!this.search || this.totalCount > this.pageSize
385
383
  }
386
384
 
@@ -423,14 +421,14 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
423
421
  // wenn die ganze Liste auf einer Seite liegt — sonst (Pagination greift,
424
422
  // `hasMore`) würden sie zwischen den bisher geladenen Treffern und den noch
425
423
  // nachkommenden Seiten hängen, also faktisch mitten in der Liste. In dem Fall
426
- // ziehen wir alle Sonder-Items nach oben (sticky), damit sie sichtbar bleiben.
424
+ // ziehen wir alle Sonder-Items nach oben.
427
425
  get collapseSpecialToTop () {
428
426
  return this.isDynamic && this.hasMore
429
427
  }
430
428
 
431
- // Was die Liste anzeigt: Sonder-Items oben (vor den Treffern, bei dynamischer
432
- // Liste angepinnt), dann die Treffer, dann Sonder-Items unten — Letzteres nur,
433
- // solange die Liste auf einer Seite passt (§5, siehe collapseSpecialToTop).
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).
434
432
  get displayItems () {
435
433
  if (this.collapseSpecialToTop) {
436
434
  return [
@@ -446,15 +444,6 @@ export default class ASelect2 extends Mixins(ComponentWidthMixin, UsesPositionSe
446
444
  ]
447
445
  }
448
446
 
449
- // Anzahl der oben angepinnten Sonder-Items (für sticky-Rendering in Select2List).
450
- // Bei collapseSpecialToTop sind auch die unteren Sonder-Items mit oben.
451
- get topSpecialCount () {
452
- const top = this.matchingSpecialItems('top').length
453
- return this.collapseSpecialToTop
454
- ? top + this.matchingSpecialItems('bottom').length
455
- : top
456
- }
457
-
458
447
  // --- Laden (nur dynamisch) -----------------------------------------------
459
448
 
460
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>