feat(vue-fiches): fiche= dans l'URL, fiche à fiche, historique push/replace/back (B12-M2/M3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGNSEFZdoKvbdsGyfSnu5S
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
55027ba482
commit
fe1043e321
@@ -17,6 +17,7 @@ import type { MiniFiche } from '~/composables/useFicheAdapter'
|
|||||||
import {
|
import {
|
||||||
lireQuery, ecrireQuery, filtresValides, filtrerFiches, compterEtiquettes,
|
lireQuery, ecrireQuery, filtresValides, filtrerFiches, compterEtiquettes,
|
||||||
triEffectif, trierFiches, regrouper, ordreFiches, nbFiltresActifs,
|
triEffectif, trierFiches, regrouper, ordreFiches, nbFiltresActifs,
|
||||||
|
lireFiche, queryAvecFiche, positionFiche, modeOuverture, modeFermeture, empreinteListe,
|
||||||
type ConfigVueFiches, type EtatFiches, type EtatVue, type TriFiches,
|
type ConfigVueFiches, type EtatFiches, type EtatVue, type TriFiches,
|
||||||
} from '~/utils/vueFiches'
|
} from '~/utils/vueFiches'
|
||||||
|
|
||||||
@@ -50,11 +51,29 @@ export function useVueFiches(
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// ── Écriture ──
|
// ── Écriture ──
|
||||||
|
// La navigation est asynchrone : une query écrite n'est dans `route.query`
|
||||||
|
// qu'au tick suivant. Deux écritures du même tick (« Voir sur la carte » :
|
||||||
|
// Mixte, puis la sous-vue de la page) partent donc de la dernière query
|
||||||
|
// écrite, sinon la seconde efface la première.
|
||||||
|
let enAttente: Record<string, string> | null = null
|
||||||
|
watch(() => route.query, () => { enAttente = null })
|
||||||
|
const queryBase = () => enAttente ?? route.query
|
||||||
|
function naviguer(query: Record<string, string>, mode: 'push' | 'replace' = 'replace') {
|
||||||
|
enAttente = query
|
||||||
|
if (mode === 'push') router.push({ query })
|
||||||
|
else router.replace({ query })
|
||||||
|
}
|
||||||
|
function etatDepuis(base: Record<string, unknown>): EtatFiches {
|
||||||
|
const b = lireQuery(base, cfg.value)
|
||||||
|
return { ...b, q: q.value, filtres: filtresValides(b.filtres, cfg.value) }
|
||||||
|
}
|
||||||
|
|
||||||
function ecrire(partiel: Partial<EtatFiches>) {
|
function ecrire(partiel: Partial<EtatFiches>) {
|
||||||
const nouvel = { ...etat.value, ...partiel }
|
const base = queryBase()
|
||||||
|
const nouvel = { ...etatDepuis(base), ...partiel }
|
||||||
qEcrit = nouvel.q
|
qEcrit = nouvel.q
|
||||||
if (minuteurQ && partiel.q === undefined) { clearTimeout(minuteurQ); minuteurQ = null }
|
if (minuteurQ && partiel.q === undefined) { clearTimeout(minuteurQ); minuteurQ = null }
|
||||||
router.replace({ query: ecrireQuery(nouvel, cfg.value, route.query) })
|
naviguer(ecrireQuery(nouvel, cfg.value, base))
|
||||||
}
|
}
|
||||||
|
|
||||||
function setQ(v: string) {
|
function setQ(v: string) {
|
||||||
@@ -155,11 +174,78 @@ export function useVueFiches(
|
|||||||
/** Fiche survolée (liste ↔ épingle) ; la carte de la page s'en sert, et peut l'écrire. */
|
/** Fiche survolée (liste ↔ épingle) ; la carte de la page s'en sert, et peut l'écrire. */
|
||||||
const survolId = ref<string | number | null>(null)
|
const survolId = ref<string | number | null>(null)
|
||||||
|
|
||||||
|
// ── Fiche ouverte (B12) : `fiche=` dans l'URL, lu aussi au rendu serveur ──
|
||||||
|
const ficheId = computed(() => lireFiche(route.query))
|
||||||
|
/** La fiche ouverte, cherchée dans TOUTES les fiches (elle peut être hors filtres). null = pas encore chargée ou inconnue. */
|
||||||
|
const ficheOuverte = computed<MiniFiche | null>(() => {
|
||||||
|
const id = ficheId.value
|
||||||
|
return id == null ? null : toValue(fiches).find((f) => String(f.id) === id) ?? null
|
||||||
|
})
|
||||||
|
/** Rang dans l'ordre courant ; null = hors de vos filtres (ou pas de fiche). */
|
||||||
|
const position = computed(() => positionFiche(ordre.value, ficheId.value))
|
||||||
|
/**
|
||||||
|
* Empreinte de la liste au moment où CETTE page a ajouté l'entrée
|
||||||
|
* d'historique de la fiche ; null = fiche venue d'un lien partagé.
|
||||||
|
*/
|
||||||
|
let listeALOuverture: string | null = null
|
||||||
|
|
||||||
|
function ouvrirFiche(id: string | number) {
|
||||||
|
if (String(id) === lireFiche(queryBase())) return
|
||||||
|
const base = queryBase()
|
||||||
|
const mode = modeOuverture(lireFiche(base))
|
||||||
|
if (mode === 'push') listeALOuverture = empreinteListe(base)
|
||||||
|
naviguer(queryAvecFiche(base, id), mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
function fermerFiche() {
|
||||||
|
if (lireFiche(queryBase()) == null) return
|
||||||
|
const mode = modeFermeture(listeALOuverture != null, listeALOuverture === empreinteListe(queryBase()))
|
||||||
|
listeALOuverture = null
|
||||||
|
if (mode === 'back') { enAttente = null; router.back() }
|
||||||
|
else naviguer(queryAvecFiche(queryBase(), null))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Étiquette cliquée dans le détail : ajoute le filtre ; `fermer` (téléphone) referme la fiche dans le même geste. */
|
||||||
|
function filtrerDepuisFiche(dim: string, valeur: string, fermer: boolean) {
|
||||||
|
const base = queryBase()
|
||||||
|
const e = etatDepuis(base)
|
||||||
|
const vals = e.filtres[dim] ?? []
|
||||||
|
const filtres = vals.includes(valeur) ? e.filtres : { ...e.filtres, [dim]: [...vals, valeur] }
|
||||||
|
const query = ecrireQuery({ ...e, filtres }, cfg.value, base)
|
||||||
|
if (fermer) listeALOuverture = null
|
||||||
|
naviguer(fermer ? queryAvecFiche(query, null) : query)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retour arrière du navigateur (ou lien interne) : l'URL a perdu `fiche=`.
|
||||||
|
watch(ficheId, (id) => { if (id == null) listeALOuverture = null })
|
||||||
|
|
||||||
|
const fichePrecedente = () => { if (position.value?.precedente) ouvrirFiche(position.value.precedente.id) }
|
||||||
|
const ficheSuivante = () => { if (position.value?.suivante) ouvrirFiche(position.value.suivante.id) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* « Voir sur la carte » : ordinateur → Mixte, la fiche reste ouverte ;
|
||||||
|
* téléphone et tablette (pas de Mixte) → Carte, la fiche se ferme (elle
|
||||||
|
* couvrirait la carte). La page lit `centrage` et centre son épingle.
|
||||||
|
*/
|
||||||
|
const centrage = ref<{ id: string; n: number } | null>(null)
|
||||||
|
function voirSurCarte(id: string | number) {
|
||||||
|
centrage.value = { id: String(id), n: (centrage.value?.n ?? 0) + 1 }
|
||||||
|
if (largeur.value === 'ordinateur' && cfg.value.etats.includes('mixte')) {
|
||||||
|
setVue('mixte')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
listeALOuverture = null
|
||||||
|
const base = queryBase()
|
||||||
|
naviguer(queryAvecFiche(ecrireQuery({ ...etatDepuis(base), vue: 'carte' }, cfg.value, base), null))
|
||||||
|
}
|
||||||
|
|
||||||
return reactive({
|
return reactive({
|
||||||
cfg, etat, q, largeur, vueAffichee, tri, resultat, groupes, ordre, compteurs,
|
cfg, etat, q, largeur, vueAffichee, tri, resultat, groupes, ordre, compteurs,
|
||||||
idsResultat, nbSansAdresse, total, nbActifs, survolId,
|
idsResultat, nbSansAdresse, total, nbActifs, survolId,
|
||||||
|
ficheId, ficheOuverte, position, centrage,
|
||||||
setVue, setQ, toggle, ajouterFiltre, effacerTout, setTri, setGroupe, setSousVue,
|
setVue, setQ, toggle, ajouterFiltre, effacerTout, setTri, setGroupe, setSousVue,
|
||||||
setSansAdresse, voirSansAdresse,
|
setSansAdresse, voirSansAdresse,
|
||||||
|
ouvrirFiche, fermerFiche, fichePrecedente, ficheSuivante, voirSurCarte, filtrerDepuisFiche,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,5 +128,63 @@ ok(eq(gt.map((x) => x.libelle), ['Martinique', 'Métropole']), 'groupe territoir
|
|||||||
ok(V.regrouper(fiches, null, cfg).length === 1, 'sans regroupement : un seul bloc')
|
ok(V.regrouper(fiches, null, cfg).length === 1, 'sans regroupement : un seul bloc')
|
||||||
ok(eq(ids(V.ordreFiches(g)), [1, 4, 2, 3, 5]), 'ordre fiche à fiche = ordre affiché des groupes (B12)')
|
ok(eq(ids(V.ordreFiches(g)), [1, 4, 2, 3, 5]), 'ordre fiche à fiche = ordre affiché des groupes (B12)')
|
||||||
|
|
||||||
|
// ── Fiche ouverte : URL, fiche à fiche, historique (B12-M2/M3) ──
|
||||||
|
ok(V.lireFiche({ fiche: '412' }) === '412' && V.lireFiche({ fiche: 'f1-rotor' }) === 'f1-rotor', 'ouverture par URL : fiche= lu (id numérique ou texte)')
|
||||||
|
ok(V.lireFiche({}) === null && V.lireFiche({ fiche: '' }) === null && V.lireFiche({ fiche: ' ' }) === null, 'fiche= absent ou vide → pas de fiche')
|
||||||
|
ok(V.lireFiche({ fiche: ['7', '8'] }) === '7', 'fiche= répété → la première valeur')
|
||||||
|
const qListe = { vue: 'fiches', fonctions: 'Juridique,Formation', tri: 'nom' }
|
||||||
|
ok(eq(V.queryAvecFiche(qListe, 412), { ...qListe, fiche: '412' }), 'queryAvecFiche pose fiche= et garde filtres, tri, vue')
|
||||||
|
ok(eq(V.queryAvecFiche({ ...qListe, fiche: '412' }, null), qListe), 'queryAvecFiche(null) retire fiche= et rien d’autre')
|
||||||
|
const qEcrite = V.ecrireQuery({ ...V.lireQuery({ ...qListe, fiche: '412' }, cfg), tri: 'pertinence' }, cfg, { ...qListe, fiche: '412' })
|
||||||
|
ok(qEcrite.fiche === '412' && qEcrite.tri === 'pertinence', 'un changement de tri garde la fiche ouverte')
|
||||||
|
ok(V.empreinteListe({ ...qListe, fiche: '1' }) === V.empreinteListe({ fiche: '9', tri: 'nom', fonctions: 'Juridique,Formation', vue: 'fiches' }), 'empreinte de la liste : indépendante de fiche= et de l’ordre des clés')
|
||||||
|
|
||||||
|
const ordre = V.ordreFiches(g) // [1, 4, 2, 3, 5]
|
||||||
|
const p = V.positionFiche(ordre, '2')
|
||||||
|
ok(p && p.rang === 3 && p.total === 5 && p.precedente.id === 4 && p.suivante.id === 3, '« 3 / 5 » : précédente et suivante dans l’ordre courant')
|
||||||
|
ok(V.positionFiche(ordre, 1).precedente === null && V.positionFiche(ordre, 5).suivante === null, 'pas de bouclage aux extrémités')
|
||||||
|
const ordreFiltre = V.ordreFiches(V.regrouper(V.filtrerFiches(fiches, etat0({ filtres: { fonctions: ['Juridique'], echelle: [] } }), cfg), null, cfg))
|
||||||
|
ok(V.positionFiche(ordreFiltre, '4') === null, 'fiche hors filtres → position null (« hors de vos filtres », sans précédente/suivante)')
|
||||||
|
ok(V.positionFiche(ordre, null) === null && V.positionFiche(ordre, '999') === null, 'pas de fiche ou id inconnu → null')
|
||||||
|
|
||||||
|
// Historique simulé, avec les mêmes décisions que composables/useVueFiches.ts
|
||||||
|
function navigateur(depart) {
|
||||||
|
const pile = [depart]
|
||||||
|
let i = 0
|
||||||
|
let liste = null // listeALOuverture
|
||||||
|
const cur = () => pile[i]
|
||||||
|
return {
|
||||||
|
cur,
|
||||||
|
longueur: () => i + 1,
|
||||||
|
ouvrir(id) {
|
||||||
|
const mode = V.modeOuverture(V.lireFiche(cur()))
|
||||||
|
if (mode === 'push') { liste = V.empreinteListe(cur()); pile.splice(i + 1); pile.push(V.queryAvecFiche(cur(), id)); i++ }
|
||||||
|
else pile[i] = V.queryAvecFiche(cur(), id)
|
||||||
|
},
|
||||||
|
filtrer(q) { pile[i] = q },
|
||||||
|
fermer() {
|
||||||
|
const mode = V.modeFermeture(liste != null, liste === V.empreinteListe(cur()))
|
||||||
|
liste = null
|
||||||
|
if (mode === 'back') i--
|
||||||
|
else pile[i] = V.queryAvecFiche(cur(), null)
|
||||||
|
},
|
||||||
|
retour() { i--; if (V.lireFiche(cur()) == null) liste = null },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let nav = navigateur(qListe)
|
||||||
|
nav.ouvrir(1); nav.ouvrir(4); nav.ouvrir(2)
|
||||||
|
ok(nav.longueur() === 2 && nav.cur().fiche === '2', 'ouvrir puis deux fois « suivante » : une seule entrée d’historique ajoutée')
|
||||||
|
nav.retour()
|
||||||
|
ok(nav.longueur() === 1 && V.lireFiche(nav.cur()) === null && eq(nav.cur(), qListe), 'retour arrière (téléphone) : la fiche se ferme, la liste et ses filtres restent')
|
||||||
|
nav = navigateur(qListe)
|
||||||
|
nav.ouvrir(1); nav.fermer()
|
||||||
|
ok(nav.longueur() === 1 && eq(nav.cur(), qListe), 'fermer une fiche ouverte depuis la liste : retour en arrière, historique propre')
|
||||||
|
nav = navigateur({ ...qListe, fiche: '412' })
|
||||||
|
nav.fermer()
|
||||||
|
ok(nav.longueur() === 1 && eq(nav.cur(), qListe), 'lien partagé : fermer retire fiche= sur place, sans quitter la page')
|
||||||
|
nav = navigateur(qListe)
|
||||||
|
nav.ouvrir(1); nav.filtrer({ ...nav.cur(), echelle: 'National' }); nav.fermer()
|
||||||
|
ok(nav.cur().echelle === 'National' && V.lireFiche(nav.cur()) === null, 'filtre changé fiche ouverte : fermer garde le nouveau filtre (pas de retour arrière)')
|
||||||
|
|
||||||
console.log(`\n${n - fail}/${n}`)
|
console.log(`\n${n - fail}/${n}`)
|
||||||
process.exit(fail ? 1 : 0)
|
process.exit(fail ? 1 : 0)
|
||||||
|
|||||||
+71
-1
@@ -121,7 +121,8 @@ export function lireQuery(query: Query, cfg: ConfigVueFiches): EtatFiches {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Query à écrire pour un état. Les paramètres que la vue fiches ne gère pas
|
* Query à écrire pour un état. Les paramètres que la vue fiches ne gère pas
|
||||||
* (`territoire`, `random`, `fiche` de B12…) sont recopiés depuis `base`.
|
* (`territoire`, `random`…) sont recopiés depuis `base`, `fiche` aussi : un
|
||||||
|
* filtre changé garde la fiche ouverte (elle peut passer « hors de vos filtres »).
|
||||||
*/
|
*/
|
||||||
export function ecrireQuery(etat: EtatFiches, cfg: ConfigVueFiches, base: Query = {}): Record<string, string> {
|
export function ecrireQuery(etat: EtatFiches, cfg: ConfigVueFiches, base: Query = {}): Record<string, string> {
|
||||||
const geres = new Set(['vue', 'q', 'adresse', 'tri', 'groupe', ...cfg.dimensions.map((d) => d.cle)])
|
const geres = new Set(['vue', 'q', 'adresse', 'tri', 'groupe', ...cfg.dimensions.map((d) => d.cle)])
|
||||||
@@ -295,3 +296,72 @@ export function regrouper(fiches: MiniFiche[], def: GroupeDef | null, cfg: Confi
|
|||||||
export function ordreFiches(groupes: GroupeRendu[]): MiniFiche[] {
|
export function ordreFiches(groupes: GroupeRendu[]): MiniFiche[] {
|
||||||
return groupes.flatMap((g) => g.fiches)
|
return groupes.flatMap((g) => g.fiches)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Fiche ouverte (B12) ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** `fiche=` de l'URL ; null si absent ou vide. L'id reste une chaîne (Entraide : nombre, Réseaux : `f1-rotor`). */
|
||||||
|
export function lireFiche(query: Query): string | null {
|
||||||
|
const v = premiere(query.fiche).trim()
|
||||||
|
return v || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Query avec `fiche=` posé (id) ou retiré (null), tout le reste recopié tel quel. */
|
||||||
|
export function queryAvecFiche(query: Query, id: string | number | null): Record<string, string> {
|
||||||
|
const out: Record<string, string> = {}
|
||||||
|
for (const [k, v] of Object.entries(query)) {
|
||||||
|
if (k !== 'fiche' && premiere(v) !== '') out[k] = premiere(v)
|
||||||
|
}
|
||||||
|
if (id != null && String(id) !== '') out.fiche = String(id)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PositionFiche {
|
||||||
|
/** Rang dans l'ordre courant, de 1 à total. */
|
||||||
|
rang: number
|
||||||
|
total: number
|
||||||
|
precedente: MiniFiche | null
|
||||||
|
suivante: MiniFiche | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Place de la fiche dans l'ordre courant (filtres + tri + regroupement).
|
||||||
|
* null = fiche hors de l'ordre courant (« hors de vos filtres ») : pas de
|
||||||
|
* précédente ni de suivante. Pas de bouclage aux extrémités.
|
||||||
|
*/
|
||||||
|
export function positionFiche(ordre: MiniFiche[], id: string | number | null): PositionFiche | null {
|
||||||
|
if (id == null) return null
|
||||||
|
const i = ordre.findIndex((f) => String(f.id) === String(id))
|
||||||
|
if (i === -1) return null
|
||||||
|
return {
|
||||||
|
rang: i + 1,
|
||||||
|
total: ordre.length,
|
||||||
|
precedente: i > 0 ? ordre[i - 1] : null,
|
||||||
|
suivante: i < ordre.length - 1 ? ordre[i + 1] : null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Historique (SPEC §5) : OUVRIR une fiche depuis la liste ajoute une entrée
|
||||||
|
* (le retour du téléphone la referme) ; passer d'une fiche à l'autre la
|
||||||
|
* remplace (le retour ne rejoue pas les vingt fiches parcourues).
|
||||||
|
*/
|
||||||
|
export function modeOuverture(ficheCourante: string | null): 'push' | 'replace' {
|
||||||
|
return ficheCourante == null ? 'push' : 'replace'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FERMER : si l'entrée a été ajoutée par cette page ET que la liste derrière
|
||||||
|
* n'a pas changé depuis (mêmes filtres, tri, vue), on revient en arrière :
|
||||||
|
* l'historique reste propre. Sinon (lien partagé, rechargement, filtre changé
|
||||||
|
* fiche ouverte), on retire `fiche=` sur place : un retour arrière aurait
|
||||||
|
* défait les filtres choisis entre-temps.
|
||||||
|
*/
|
||||||
|
export function modeFermeture(ouverteParLaPage: boolean, listeInchangee: boolean): 'back' | 'replace' {
|
||||||
|
return ouverteParLaPage && listeInchangee ? 'back' : 'replace'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Empreinte de la liste derrière la fiche : la query sans `fiche=`, clés triées. */
|
||||||
|
export function empreinteListe(query: Query): string {
|
||||||
|
const q = queryAvecFiche(query, null)
|
||||||
|
return Object.keys(q).sort().map((k) => `${k}=${q[k]}`).join('&')
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user