// State of the reader: the document, the view (in force / previews), the // language the text is read in, and the panel (which unit, which version, // which tab) — the panel lives in the URL so a link reopens it, as does the // vote unfolded under an article (`?vote=`). import { api, ApiError } from './api'; import type { GroupView } from './group'; import type { DocView, Lang, PreviewUnit, Proposal, Translation, Unit } from './types'; export type View = 'inforce' | 'wellrated' | 'all'; /** The panel: the basket of the vote, the versions of an article, a new proposal, the details of a version, the formula. */ export type Tab = 'basket' | 'proposals' | 'propose' | 'details' | 'settings'; export interface PanelState { unitId: string | null; proposalId: string | null; tab: Tab; draftId?: string | null; /** The basket, opened on one section. */ section?: string | null; prefill?: { title: string; body: string } | null; /** A new proposal that answers this objection (ADR 0010). */ objectionId?: string | null; /** The details of one's own version, its edit form open: to reword it. */ edit?: boolean; } /** The tabs as the URL names them. */ export const TABS: Record = { panier: 'basket', propositions: 'proposals', proposer: 'propose', details: 'details', reglages: 'settings' }; export const TAB_NAMES: Record = { basket: 'panier', proposals: 'propositions', propose: 'proposer', details: 'details', settings: 'reglages' }; /** The query of a reader's URL: the unfolded vote and the panel. */ export function panelSearch(panel: PanelState | null, vote: string | null): string { const q = new URLSearchParams(); if (vote) q.set('vote', vote); if (panel) { q.set('onglet', TAB_NAMES[panel.tab]); if (panel.unitId) q.set('article', panel.unitId); if (panel.proposalId) q.set('version', panel.proposalId); if (panel.draftId) q.set('brouillon', panel.draftId); if (panel.section) q.set('section', panel.section); if (panel.objectionId) q.set('objection', panel.objectionId); if (panel.edit) q.set('modifier', '1'); } return q.toString(); } export interface Shown { title: string; body: string; /** The text is a translation (editorial). */ translated: boolean; stale: boolean; /** Asked for a translation that does not exist yet: the source is shown. */ missing: boolean; } export function shown( v: { title: string; body: string; translations: Partial> }, lang: Lang, source: Lang, ): Shown { if (lang === source) return { title: v.title, body: v.body, translated: false, stale: false, missing: false }; const tr = v.translations[lang]; if (!tr) return { title: v.title, body: v.body, translated: false, stale: false, missing: true }; return { title: tr.title, body: tr.body, translated: true, stale: tr.stale, missing: false }; } export class DocState { view = $state(null); error = $state(null); mode = $state('inforce'); preview = $state(null); original = $state(false); async load(group: string, slug: string) { try { const g = await api.get(`/groups/${group}`); const d = g.docs.find((x) => x.slug === slug); if (!d) { this.error = g.group.open ? 'not_found' : 'forbidden'; return; } this.view = await api.get(`/docs/${d.id}`); this.error = null; if (this.mode !== 'inforce') await this.loadPreview(); } catch (e) { this.error = e instanceof ApiError ? e.code : 'internal'; } } async reload() { if (!this.view) return; this.view = await api.get(`/docs/${this.view.doc.id}`); if (this.mode !== 'inforce') await this.loadPreview(); } async setMode(mode: View) { this.mode = mode; if (mode === 'inforce') this.preview = null; else await this.loadPreview(); } async loadPreview() { if (!this.view) return; const r = await api.get<{ units: PreviewUnit[] }>(`/docs/${this.view.doc.id}/preview?mode=${this.mode}`); this.preview = r.units; } unit(id: string | null): Unit | undefined { return this.view?.units.find((u) => u.id === id); } proposal(id: string | null): Proposal | undefined { if (!id || !this.view) return undefined; for (const u of this.view.units) { if (u.inForce?.id === id) return u.inForce; const p = u.proposals.find((x) => x.id === id); if (p) return p; } return undefined; } get inForce(): Unit[] { return (this.view?.units ?? []).filter((u) => u.inForce); } /** Open proposals, in document order; drafts apart. */ get open(): { unit: Unit; proposal: Proposal }[] { const out: { unit: Unit; proposal: Proposal }[] = []; for (const unit of this.view?.units ?? []) { for (const proposal of unit.proposals) if (proposal.status === 'open') out.push({ unit, proposal }); } return out; } get drafts(): { unit: Unit; proposal: Proposal }[] { const out: { unit: Unit; proposal: Proposal }[] = []; for (const unit of this.view?.units ?? []) { for (const proposal of unit.proposals) if (proposal.status === 'draft') out.push({ unit, proposal }); } return out; } }