Phase 2 : filtrage server-side de /lignes (compte, client, type, signalement, écarts, recherche libre)
Remplace le "charge tout puis filtre côté client" (filteredRows()) par un nouvel endpoint /lignes/api/lignes (LedgerRowsController), qui pousse tous les filtres de la barre d'outils dans une seule requête Entity/Field Query API (pas de SQL brut) -- y compris à travers la relation field_repartition -> field_compte (paragraph -> taxonomie), confirmé fonctionner empiriquement avant d'écrire le contrôleur. `rows` ne contient donc plus que ce qui est à la fois dans la fenêtre de dates ET dans les filtres actifs ; la fenêtre glissante elle-même (loadOlder/loadNewer/ ensureScrollable) est inchangée, seul ce qui la peuple change. Ajouts : - Nouveau filtre "Libellé / Détails" (recherche plein texte sur field_notes ou le titre), débouncé côté client (350ms). - filterYear (l'"Année" dédiée) passe par le même endpoint via son paramètre "annee". - mergeChangedRows() (le merge du polling) tient maintenant compte des filtres actifs : une ligne qui ne correspond plus après un changement est retirée de `rows`, une ligne qui correspond nouvellement est ajoutée -- polling lui-même reste global/non filtré, seul le merge est filter-aware. JSON:API reste utilisé pour ce que l'endroit filtré ne couvre pas : le groupe entrée/sorties liées (fetchLignesByNids) et le polling (fetchChangedSince), tous deux indépendants d'une plage de dates+filtres. Testé en local : chaque filtre individuellement et combiné (compte+q, client+type), widening de fenêtre sous filtre restrictif, restauration combinée depuis le hash au reload, modale d'édition + reloadWindow après fermeture, polling sans erreur.
This commit is contained in:
@@ -2,20 +2,30 @@
|
||||
* @file
|
||||
* Progressive decoupling: Drupal renders the page shell (nav, auth via
|
||||
* session cookie, the "Ajouter une ligne" modal form); this Vue app fetches
|
||||
* JSON:API and renders a spreadsheet-like table of every ligne comptable,
|
||||
* with filters, month/year grouping, and per-row écart (répartition sum vs
|
||||
* montant HT) highlighting -- inconsistencies are shown, not hidden.
|
||||
* a spreadsheet-like table of every ligne comptable, with filters,
|
||||
* month/year grouping, and per-row écart (répartition sum vs montant HT)
|
||||
* highlighting -- inconsistencies are shown, not hidden.
|
||||
*
|
||||
* Sliding window: with 5+ years of migrated history (~1500+ lines), loading
|
||||
* everything up front took ~40s and made the tab briefly unresponsive.
|
||||
* Instead of one big fetch, `rows` only ever holds a date-range window
|
||||
* (initially the ~18 months around today), extended by ~6 months whenever
|
||||
* the user scrolls near the top or bottom edge (IntersectionObserver on two
|
||||
* sentinel rows), and trimmed from the far end once the window exceeds
|
||||
* MAX_LOADED_MONTHS so it stays a genuine sliding buffer, not an
|
||||
* ever-growing list. The "Année" filter and the totals footer can't be
|
||||
* computed from a partial window, so they're backed by their own small
|
||||
* server endpoints (LedgerStatsController) instead.
|
||||
* the user scrolls near the top or bottom edge, and trimmed from the far
|
||||
* end once the window exceeds MAX_LOADED_MONTHS so it stays a genuine
|
||||
* sliding buffer, not an ever-growing list. The "Année" filter and the
|
||||
* totals footer can't be computed from a partial window, so they're backed
|
||||
* by their own small server endpoints (LedgerStatsController) instead.
|
||||
*
|
||||
* Filtering: every toolbar filter (compte, client, type, signalement,
|
||||
* écarts, recherche libre) is applied server-side, folded straight into
|
||||
* whatever request populates the window -- see LedgerRowsController and
|
||||
* fetchFilteredLignes() below. `rows` therefore only ever holds rows that
|
||||
* both fall in the loaded date range AND match the active filters; there's
|
||||
* no separate "load everything, filter client-side" pass. JSON:API is
|
||||
* still used for a few things the filtered endpoint doesn't cover: the
|
||||
* entrée/sortie group drill-down (fetchLignesByNids()) and live-update
|
||||
* polling (fetchChangedSince()), both keyed on specific node ids or
|
||||
* "anything that changed" rather than a date range + filter set.
|
||||
*/
|
||||
(function (Drupal, Vue, jQuery) {
|
||||
'use strict';
|
||||
@@ -60,8 +70,7 @@
|
||||
// per year can search almost forever without ever surfacing more of
|
||||
// them. Comfortably wider than the whole migrated dataset (2021-today)
|
||||
// so a filter effectively gets the entire history to search, still
|
||||
// bounded (not literally unbounded memory) -- and DOM rendering cost
|
||||
// stays proportional to filteredRows, not this raw fetched count.
|
||||
// bounded (not literally unbounded memory).
|
||||
const MAX_LOADED_MONTHS_FILTERED = 96;
|
||||
// Hard cap on ensureScrollable()'s recursion (see below) -- comfortably
|
||||
// more than enough rounds to walk the entire MIN_LOADABLE_DATE..
|
||||
@@ -102,20 +111,39 @@
|
||||
return (db.getFullYear() - da.getFullYear()) * 12 + (db.getMonth() - da.getMonth());
|
||||
}
|
||||
|
||||
// Builds the JSON:API URL for a half-open date range [start, end).
|
||||
function rangeUrl(start, end) {
|
||||
const filter =
|
||||
'filter[dateRange][group][conjunction]=AND' +
|
||||
'&filter[gte][condition][path]=field_date_ligne' +
|
||||
'&filter[gte][condition][operator]=%3E%3D' +
|
||||
'&filter[gte][condition][value]=' + start +
|
||||
'&filter[gte][condition][memberOf]=dateRange' +
|
||||
'&filter[lt][condition][path]=field_date_ligne' +
|
||||
'&filter[lt][condition][operator]=%3C' +
|
||||
'&filter[lt][condition][value]=' + end +
|
||||
'&filter[lt][condition][memberOf]=dateRange';
|
||||
return API_BASE + '?include=field_repartition,field_repartition.field_compte,field_client,field_entree_liee,field_flag'
|
||||
+ '&page[limit]=50&sort=field_date_ligne,drupal_internal__nid&' + filter;
|
||||
// Builds the query string for LedgerRowsController::index() (GET
|
||||
// /lignes/api/lignes) -- the server-side-filtered replacement for
|
||||
// "fetch a JSON:API date range, then filter client-side": every
|
||||
// toolbar filter is folded straight into the same request that loads
|
||||
// the window, so `rows` only ever holds what's actually meant to be
|
||||
// visible right now, pre-shaped exactly like buildRows() below would
|
||||
// have produced from JSON:API (no separate row-building step needed
|
||||
// for this path). rangeOrAnnee is either {start, end} (the normal
|
||||
// sliding window, and jumpToYear()) or {annee} (filterYear's
|
||||
// enterYearMode) -- the controller accepts either.
|
||||
function filteredLignesUrl(rangeOrAnnee, filters) {
|
||||
const params = new URLSearchParams();
|
||||
if (rangeOrAnnee.annee) {
|
||||
params.set('annee', rangeOrAnnee.annee);
|
||||
} else {
|
||||
params.set('start', rangeOrAnnee.start);
|
||||
params.set('end', rangeOrAnnee.end);
|
||||
}
|
||||
if (filters.compte.length) params.set('compte', filters.compte.join(','));
|
||||
if (filters.client) params.set('client', filters.client);
|
||||
if (filters.type.length) params.set('type', filters.type.join(','));
|
||||
if (filters.flag.length) params.set('flag', filters.flag.join(','));
|
||||
if (filters.q) params.set('q', filters.q);
|
||||
if (filters.ecarts) params.set('ecarts', '1');
|
||||
if (filters.signale) params.set('signale', '1');
|
||||
return '/lignes/api/lignes?' + params.toString();
|
||||
}
|
||||
|
||||
async function fetchFilteredLignes(rangeOrAnnee, filters) {
|
||||
const res = await fetch(filteredLignesUrl(rangeOrAnnee, filters), { headers: { Accept: 'application/json' } });
|
||||
const json = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(json.error || ('/lignes/api/lignes a répondu ' + res.status));
|
||||
return json.rows;
|
||||
}
|
||||
|
||||
async function fetchLignes(url) {
|
||||
@@ -301,6 +329,7 @@
|
||||
filterType: params.get('type') ? params.get('type').split(',') : [],
|
||||
filterFlag: params.get('tag') ? params.get('tag').split(',') : [],
|
||||
filterYear: params.get('annee') || '',
|
||||
filterQ: params.get('q') || '',
|
||||
onlyErrors: params.get('ecarts') === '1',
|
||||
onlyFlagged: params.get('signale') === '1',
|
||||
aller: params.get('aller') || '',
|
||||
@@ -314,6 +343,7 @@
|
||||
if (state.filterType.length) params.set('type', state.filterType.join(','));
|
||||
if (state.filterFlag.length) params.set('tag', state.filterFlag.join(','));
|
||||
if (state.filterYear) params.set('annee', state.filterYear);
|
||||
if (state.filterQ) params.set('q', state.filterQ);
|
||||
if (state.onlyErrors) params.set('ecarts', '1');
|
||||
if (state.onlyFlagged) params.set('signale', '1');
|
||||
// Redundant/ambiguous alongside an active "Année" filter -- that
|
||||
@@ -417,6 +447,11 @@
|
||||
// means "has any tag at all".
|
||||
filterFlag: [],
|
||||
filterYear: '',
|
||||
// Free-text match against Libellé/Détail (field_notes, falling
|
||||
// back to the title) -- server-side (see LedgerRowsController),
|
||||
// debounced client-side (see the filterQ watcher) so every
|
||||
// keystroke doesn't fire its own request.
|
||||
filterQ: '',
|
||||
jumpYearValue: '',
|
||||
// Not a filter itself (jumpYearValue always resets to '' right
|
||||
// after firing) -- just remembers the last "Aller à" target so
|
||||
@@ -604,21 +639,6 @@
|
||||
if (!this.filterEntreeGroup) return [];
|
||||
return this.allComptes.filter((c) => this.filterEntreeGroup.some((r) => r.parCompte[c] !== undefined));
|
||||
},
|
||||
filteredRows() {
|
||||
return this.rows.filter((r) => {
|
||||
// Several selected comptes/types match with OR semantics ("any
|
||||
// of these") -- a line either touches one of the selected
|
||||
// comptes or it doesn't, same idea for type.
|
||||
if (this.filterCompte.length && !this.filterCompte.some((c) => r.parCompte[c] !== undefined)) return false;
|
||||
if (this.filterClient && r.client !== this.filterClient) return false;
|
||||
if (this.filterType.length && !this.filterType.includes(r.type)) return false;
|
||||
if (this.filterFlag.length && !this.filterFlag.some((f) => r.flags.includes(f))) return false;
|
||||
if (this.filterYear && (r.date || '').slice(0, 4) !== this.filterYear) return false;
|
||||
if (this.onlyErrors && !r.hasError) return false;
|
||||
if (this.onlyFlagged && !r.hasFlag) return false;
|
||||
return true;
|
||||
});
|
||||
},
|
||||
// Totals for the entrée + linked sorties drill-down modal (see
|
||||
// filterEntreeGroup above): the whole point of that view is "does
|
||||
// this entrée balance against what was paid out", so its own footer
|
||||
@@ -649,8 +669,12 @@
|
||||
par_compte: Object.fromEntries(Object.entries(parCompte).map(([c, v]) => [c, round(v)])),
|
||||
};
|
||||
},
|
||||
// `rows` no longer needs a separate client-side filtering pass --
|
||||
// every toolbar filter is already applied server-side by whatever
|
||||
// fetched the current window (see fetchFilteredLignes()), so this
|
||||
// only handles the month/year grouping on top.
|
||||
groupedRows() {
|
||||
const list = this.filteredRows;
|
||||
const list = this.rows;
|
||||
if (this.groupBy === 'none') return list;
|
||||
const groups = new Map();
|
||||
for (const r of list) {
|
||||
@@ -972,21 +996,45 @@
|
||||
// that fall inside the current window get appended -- that's the
|
||||
// one case that *does* need the array reassigned, since there's
|
||||
// no existing row object to mutate.
|
||||
//
|
||||
// `rows` is server-filtered (see fetchFilteredLignes()), unlike
|
||||
// before Phase 2 where it held the whole window and a computed
|
||||
// property filtered it for display -- pollForChanges() is
|
||||
// deliberately still unfiltered/global (see its own comment), so a
|
||||
// change reported here might have moved a row *out* of the active
|
||||
// filters (patch would leave a stale non-matching row visible) or
|
||||
// *into* them (a row not previously loaded now qualifies). A row
|
||||
// living in groupExtraRows instead is unaffected by the toolbar
|
||||
// filters at all (that's the drill-down modal's own concern), so
|
||||
// it's always just patched in place.
|
||||
mergeChangedRows(changedRows) {
|
||||
const isBeingEdited = (id) => this.editingTypeId === id || (this.editingCell && this.editingCell.id === id);
|
||||
const known = new Map([...this.rows, ...this.groupExtraRows].map((r) => [r.id, r]));
|
||||
const rowsById = new Map(this.rows.map((r) => [r.id, r]));
|
||||
const extraById = new Map(this.groupExtraRows.map((r) => [r.id, r]));
|
||||
const newOnes = [];
|
||||
const removeIds = new Set();
|
||||
for (const fresh of changedRows) {
|
||||
if (isBeingEdited(fresh.id)) continue;
|
||||
const existing = known.get(fresh.id);
|
||||
const inExtra = extraById.get(fresh.id);
|
||||
if (inExtra) {
|
||||
Object.assign(inExtra, fresh);
|
||||
continue;
|
||||
}
|
||||
const inWindow = fresh.date >= this.windowStart && fresh.date < this.windowEnd;
|
||||
const matches = inWindow && this.rowMatchesFilters(fresh);
|
||||
const existing = rowsById.get(fresh.id);
|
||||
if (existing) {
|
||||
Object.assign(existing, fresh);
|
||||
} else if (fresh.date >= this.windowStart && fresh.date < this.windowEnd) {
|
||||
if (matches) {
|
||||
Object.assign(existing, fresh);
|
||||
} else {
|
||||
removeIds.add(fresh.id);
|
||||
}
|
||||
} else if (matches) {
|
||||
newOnes.push(fresh);
|
||||
}
|
||||
}
|
||||
if (newOnes.length) {
|
||||
this.rows = [...this.rows, ...newOnes].sort((a, b) => (a.date || '').localeCompare(b.date || ''));
|
||||
if (newOnes.length || removeIds.size) {
|
||||
this.rows = [...this.rows.filter((r) => !removeIds.has(r.id)), ...newOnes].sort((a, b) => (a.date || '').localeCompare(b.date || ''));
|
||||
}
|
||||
},
|
||||
startEditType(item) {
|
||||
@@ -1190,8 +1238,7 @@
|
||||
const today = todayStr();
|
||||
this.windowStart = addMonths(today, -WINDOW_MONTHS);
|
||||
this.windowEnd = addMonths(today, WINDOW_MONTHS);
|
||||
const { data, includedMap } = await fetchLignes(rangeUrl(this.windowStart, this.windowEnd));
|
||||
this.rows = buildRows(data, includedMap);
|
||||
this.rows = await fetchFilteredLignes({ start: this.windowStart, end: this.windowEnd }, this.currentFilters());
|
||||
} catch (err) {
|
||||
this.error = err.message;
|
||||
} finally {
|
||||
@@ -1200,12 +1247,14 @@
|
||||
},
|
||||
// Post-edit refresh: re-fetch exactly the currently loaded window
|
||||
// (not a fresh "last 18 months" window) so editing an old line
|
||||
// doesn't silently reset how far the user had scrolled back.
|
||||
// doesn't silently reset how far the user had scrolled back. Also
|
||||
// the reload path every toolbar filter change routes through (see
|
||||
// onFilterChanged()) -- with filtering now server-side, changing a
|
||||
// filter has to re-fetch, not just re-scan what's already loaded.
|
||||
async reloadWindow() {
|
||||
if (!this.windowStart || !this.windowEnd) return;
|
||||
try {
|
||||
const { data, includedMap } = await fetchLignes(rangeUrl(this.windowStart, this.windowEnd));
|
||||
this.rows = buildRows(data, includedMap);
|
||||
this.rows = await fetchFilteredLignes({ start: this.windowStart, end: this.windowEnd }, this.currentFilters());
|
||||
} catch (err) {
|
||||
this.error = err.message;
|
||||
}
|
||||
@@ -1257,8 +1306,7 @@
|
||||
try {
|
||||
let newStart = addMonths(this.windowStart, -EXTEND_MONTHS);
|
||||
if (newStart < MIN_LOADABLE_DATE) newStart = MIN_LOADABLE_DATE;
|
||||
const { data, includedMap } = await fetchLignes(rangeUrl(newStart, this.windowStart));
|
||||
const newRows = buildRows(data, includedMap);
|
||||
const newRows = await fetchFilteredLignes({ start: newStart, end: this.windowStart }, this.currentFilters());
|
||||
this.windowStart = newStart;
|
||||
|
||||
if (newRows.length) {
|
||||
@@ -1298,8 +1346,7 @@
|
||||
try {
|
||||
let newEnd = addMonths(this.windowEnd, EXTEND_MONTHS);
|
||||
if (newEnd > MAX_LOADABLE_DATE) newEnd = MAX_LOADABLE_DATE;
|
||||
const { data, includedMap } = await fetchLignes(rangeUrl(this.windowEnd, newEnd));
|
||||
const newRows = buildRows(data, includedMap);
|
||||
const newRows = await fetchFilteredLignes({ start: this.windowEnd, end: newEnd }, this.currentFilters());
|
||||
this.windowEnd = newEnd;
|
||||
|
||||
if (newRows.length) {
|
||||
@@ -1359,8 +1406,9 @@
|
||||
if ((notScrollable || wrap.scrollTop < threshold) && !this.loadingOlder) this.loadOlder();
|
||||
if ((notScrollable || wrap.scrollTop + wrap.clientHeight > wrap.scrollHeight - threshold) && !this.loadingNewer) this.loadNewer();
|
||||
},
|
||||
// Whether compte/client/type/écarts thin filteredRows down from
|
||||
// whatever's actually loaded -- loadOlder()/loadNewer() use this to
|
||||
// Whether an active filter (server-side now, see
|
||||
// fetchFilteredLignes()) thins `rows` down to fewer matches than an
|
||||
// unfiltered window would have -- loadOlder()/loadNewer() use this to
|
||||
// widen the trim cap (MAX_LOADED_MONTHS_FILTERED instead of
|
||||
// MAX_LOADED_MONTHS), since the normal cap actively works against a
|
||||
// sparse filter: extending one end and immediately trimming the
|
||||
@@ -1368,7 +1416,54 @@
|
||||
// filter with only a few matches a year could search almost forever
|
||||
// without surfacing more of them.
|
||||
isFiltering() {
|
||||
return !!(this.filterCompte.length || this.filterClient || this.filterType.length || this.filterFlag.length || this.onlyErrors || this.onlyFlagged);
|
||||
return !!(this.filterCompte.length || this.filterClient || this.filterType.length || this.filterFlag.length || this.filterQ || this.onlyErrors || this.onlyFlagged);
|
||||
},
|
||||
// The non-date part of every toolbar filter, in the shape
|
||||
// fetchFilteredLignes()/filteredLignesUrl() expect -- built fresh
|
||||
// from current state each call rather than kept as its own piece
|
||||
// of reactive state, so there's only one source of truth for what
|
||||
// "the active filters" are.
|
||||
currentFilters() {
|
||||
return {
|
||||
compte: this.filterCompte,
|
||||
client: this.filterClient,
|
||||
type: this.filterType,
|
||||
flag: this.filterFlag,
|
||||
q: this.filterQ,
|
||||
ecarts: this.onlyErrors,
|
||||
signale: this.onlyFlagged,
|
||||
};
|
||||
},
|
||||
// Client-side mirror of the non-date conditions LedgerRowsController
|
||||
// applies server-side -- used only by mergeChangedRows() below, to
|
||||
// decide whether a row a poll just reported on still belongs in
|
||||
// `rows` (which, unlike before Phase 2, only ever holds matches for
|
||||
// the active filters, not the whole window). filterYear/the date
|
||||
// window itself is checked separately by the caller, since
|
||||
// windowStart/windowEnd already cover both the sliding-window and
|
||||
// year-mode cases identically.
|
||||
rowMatchesFilters(row) {
|
||||
if (this.filterCompte.length && !this.filterCompte.some((c) => row.parCompte[c] !== undefined)) return false;
|
||||
if (this.filterClient && row.client !== this.filterClient) return false;
|
||||
if (this.filterType.length && !this.filterType.includes(row.type)) return false;
|
||||
if (this.filterFlag.length && !this.filterFlag.some((f) => row.flags.includes(f))) return false;
|
||||
if (this.filterQ && !(row.libelle || '').toLowerCase().includes(this.filterQ.toLowerCase())) return false;
|
||||
if (this.onlyErrors && !row.hasError) return false;
|
||||
if (this.onlyFlagged && !row.hasFlag) return false;
|
||||
return true;
|
||||
},
|
||||
// Every toolbar filter change routes through here: with filtering
|
||||
// now server-side (see fetchFilteredLignes()), there's no more
|
||||
// "just recompute a client-side view" -- the currently loaded
|
||||
// window has to be re-fetched with the new filter applied. Queued
|
||||
// through the same chain as loadOlder()/loadNewer() (see
|
||||
// _queueWindowOp) since several filters can change in the same
|
||||
// tick (e.g. mounted() restoring them all from the URL hash at
|
||||
// once), and interleaving their fetches would race on `rows` the
|
||||
// same way parallel loadOlder()/loadNewer() calls used to.
|
||||
onFilterChanged() {
|
||||
this._queueWindowOp(() => this.reloadWindow().then(() => this.ensureScrollable()));
|
||||
this.syncHash();
|
||||
},
|
||||
// Keeps extending the window (both directions) as long as a filter
|
||||
// leaves too few matching rows to fill the viewport -- otherwise
|
||||
@@ -1504,8 +1599,7 @@
|
||||
try {
|
||||
const start = year + '-01-01';
|
||||
const end = (parseInt(year, 10) + 1) + '-01-01';
|
||||
const { data, includedMap } = await fetchLignes(rangeUrl(start, end));
|
||||
this.rows = buildRows(data, includedMap);
|
||||
this.rows = await fetchFilteredLignes({ annee: year }, this.currentFilters());
|
||||
this.windowStart = start;
|
||||
this.windowEnd = end;
|
||||
} catch (err) {
|
||||
@@ -1557,8 +1651,7 @@
|
||||
const yearStart = year + '-01-01';
|
||||
this.windowStart = yearStart;
|
||||
this.windowEnd = addMonths(yearStart, WINDOW_MONTHS * 2);
|
||||
const { data, includedMap } = await fetchLignes(rangeUrl(this.windowStart, this.windowEnd));
|
||||
this.rows = buildRows(data, includedMap);
|
||||
this.rows = await fetchFilteredLignes({ start: this.windowStart, end: this.windowEnd }, this.currentFilters());
|
||||
} catch (err) {
|
||||
this.error = err.message;
|
||||
} finally {
|
||||
@@ -1582,6 +1675,7 @@
|
||||
filterType: this.filterType,
|
||||
filterFlag: this.filterFlag,
|
||||
filterYear: this.filterYear,
|
||||
filterQ: this.filterQ,
|
||||
onlyErrors: this.onlyErrors,
|
||||
onlyFlagged: this.onlyFlagged,
|
||||
lastJumpYear: this.lastJumpYear,
|
||||
@@ -1612,49 +1706,54 @@
|
||||
}
|
||||
this.syncHash();
|
||||
},
|
||||
// Any of these can thin filteredRows enough to remove the
|
||||
// scrollbar the sliding window relies on to keep loading -- see
|
||||
// ensureScrollable(). filterYear is handled separately above (it
|
||||
// swaps the whole loading strategy, not just the visible subset).
|
||||
// Each also reflects into the URL hash so the filtered view is
|
||||
// reloadable/shareable -- see syncHash().
|
||||
// Any of these is now pushed server-side (see LedgerRowsController/
|
||||
// fetchFilteredLignes()) -- onFilterChanged() re-fetches the
|
||||
// currently loaded window with the new filter applied, then widens
|
||||
// it if that leaves too few rows to fill the viewport (see
|
||||
// ensureScrollable()). filterYear is handled separately above (it
|
||||
// swaps the whole loading strategy, not just which filter params
|
||||
// get sent). Each also reflects into the URL hash -- see syncHash().
|
||||
filterCompte() {
|
||||
this.ensureScrollable();
|
||||
this.syncHash();
|
||||
this.onFilterChanged();
|
||||
},
|
||||
filterClient() {
|
||||
this.ensureScrollable();
|
||||
this.syncHash();
|
||||
this.onFilterChanged();
|
||||
},
|
||||
filterType() {
|
||||
this.ensureScrollable();
|
||||
this.syncHash();
|
||||
this.onFilterChanged();
|
||||
},
|
||||
filterFlag() {
|
||||
this.ensureScrollable();
|
||||
this.syncHash();
|
||||
this.onFilterChanged();
|
||||
},
|
||||
// Debounced (unlike the other filters above) -- this one changes on
|
||||
// every keystroke, and each change is a network round-trip now,
|
||||
// not a free client-side recompute.
|
||||
filterQ() {
|
||||
clearTimeout(this._filterQDebounce);
|
||||
this._filterQDebounce = setTimeout(() => this.onFilterChanged(), 350);
|
||||
},
|
||||
onlyErrors() {
|
||||
this.ensureScrollable();
|
||||
this.syncHash();
|
||||
this.onFilterChanged();
|
||||
},
|
||||
onlyFlagged() {
|
||||
this.ensureScrollable();
|
||||
this.syncHash();
|
||||
this.onFilterChanged();
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
// Reproduce whatever the URL hash describes -- reload or a shared
|
||||
// link should land on the same filtered view. The four basic
|
||||
// filters just narrow filteredRows, so they're safe to set before
|
||||
// deciding how to load; filterYear/aller each own their loading
|
||||
// path (enterYearMode()/jumpToYear()), so at most one of those
|
||||
// runs instead of the default load().
|
||||
// link should land on the same filtered view. The basic filters are
|
||||
// all sent as query params on the very first load() below, so
|
||||
// setting them here first (before deciding how to load) means that
|
||||
// first fetch already reflects the restored view instead of
|
||||
// loading unfiltered and re-fetching a moment later; filterYear/
|
||||
// aller each own their loading path (enterYearMode()/jumpToYear()),
|
||||
// so at most one of those runs instead of the default load().
|
||||
const hashState = readHashState();
|
||||
this.filterCompte = hashState.filterCompte;
|
||||
this.filterClient = hashState.filterClient;
|
||||
this.filterType = hashState.filterType;
|
||||
this.filterFlag = hashState.filterFlag;
|
||||
this.filterQ = hashState.filterQ;
|
||||
this.onlyErrors = hashState.onlyErrors;
|
||||
this.onlyFlagged = hashState.onlyFlagged;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user