Fusion de la tête du chantier (924a40d : lots B2 et F) dans le lot D

Les deux côtés gardés partout. Aux coutures :
- une objection (redit / contredit) porte sur deux articles, jamais sur un module ;
  un ordre ne se corrige pas, ni par son auteur ni par un autre ; une coquille du
  titre suit le chemin de F (l'auteur du titre, un administrateur pour le titre
  de départ, ou 3 jours), et documents.title suit à l'application ;
- votable() couvre les modules ; leurs totaux passent par shown_tally, à l'écran
  et dans document.json ;
- « Modules à voter » ne compte plus une version retenue par une objection (test
  vu rouge) ; la liste du groupe la compte toujours parmi les propositions en cours ;
- le panier : une fonction pour les articles et les modules, avec les versions
  retenues de F ; un nouvel article se propose dans une section, à sa fin, et peut
  répondre à une objection ;
- migrations 0007 (modules) puis 0008 (objections) ;
- C25 et C26 supportent un groupe d'essai où une voix suffit (dès 16 membres).

Boucle : fmt, clippy -D warnings, cargo test (227), svelte-check 0, vitest 55,
build, e2e 35 / 35 (C1–C33 et M1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-10-31 09:41:30 +01:00
co-authored by Claude Opus 5.5
82 changed files with 8285 additions and 677 deletions
@@ -11,10 +11,12 @@
import UnitBlock from '$lib/components/UnitBlock.svelte';
import VoteFold from '$lib/components/VoteFold.svelte';
import WaveRule from '$lib/components/WaveRule.svelte';
import { sections } from '$lib/basket';
import { DocState, shown, type PanelState, type Tab, type View } from '$lib/doc.svelte';
import { sections, votableIds } from '$lib/basket';
import { DocState, panelSearch, shown, TABS, type PanelState, type View } from '$lib/doc.svelte';
import { i18n, LANGS, t } from '$lib/i18n/index.svelte';
import { answerPanel } from '$lib/objections';
import { BasketState } from '$lib/picks.svelte';
import { session } from '$lib/session.svelte';
import type { Lang, Proposal, Unit } from '$lib/types';
import type { Line } from '$lib/vote';
import { articles, type Filter } from '$lib/versions';
@@ -26,23 +28,30 @@
$effect(() => {
void doc.load(group, slug);
});
// The basket of this document, kept for the tab; versions no longer open to vote leave it.
// The basket of this document, kept for the tab; versions no longer open to vote
// (or held back by an objection) leave it.
$effect(() => {
const v = doc.view;
if (!v) return;
const known = new Set(sections(v.units, v.modules).flatMap((s) => s.slots.flatMap((x) => [...x.versions, ...x.inertia].map((p) => p.id))));
basket.load(v.doc.id, known);
basket.load(v.doc.id, votableIds(sections(v.units, v.modules)));
});
// The panel and the unfolded vote live in the URL: a link (or a feedback report) reopens them.
const TABS: Record<string, Tab> = { panier: 'basket', propositions: 'proposals', proposer: 'propose', details: 'details', reglages: 'settings' };
const TAB_NAMES: Record<Tab, string> = { basket: 'panier', proposals: 'propositions', propose: 'proposer', details: 'details', settings: 'reglages' };
let prefill = $state<PanelState['prefill']>(null);
const panel = $derived.by((): PanelState | null => {
const q = page.url.searchParams;
const tab = TABS[q.get('onglet') ?? ''];
if (!tab) return null;
return { unitId: q.get('article'), proposalId: q.get('version'), tab, draftId: q.get('brouillon'), section: q.get('section'), prefill };
return {
unitId: q.get('article'),
proposalId: q.get('version'),
tab,
draftId: q.get('brouillon'),
section: q.get('section'),
prefill,
objectionId: q.get('objection'),
edit: q.get('modifier') === '1',
};
});
/** The version whose vote is unfolded under its article (`?vote=`; the old `onglet=vote` too). */
const vote = $derived.by(() => {
@@ -52,16 +61,7 @@
function go(nextPanel: PanelState | null, nextVote: string | null) {
prefill = nextPanel?.prefill ?? null;
const q = new URLSearchParams();
if (nextVote) q.set('vote', nextVote);
if (nextPanel) {
q.set('onglet', TAB_NAMES[nextPanel.tab]);
if (nextPanel.unitId) q.set('article', nextPanel.unitId);
if (nextPanel.proposalId) q.set('version', nextPanel.proposalId);
if (nextPanel.draftId) q.set('brouillon', nextPanel.draftId);
if (nextPanel.section) q.set('section', nextPanel.section);
}
const search = q.toString();
const search = panelSearch(nextPanel, nextVote);
void goto(`${page.url.pathname}${search ? `?${search}` : ''}`, {
replaceState: (!!panel && !!nextPanel) || (!nextPanel && !panel),
noScroll: true,
@@ -130,6 +130,7 @@
onopen={(id) => go(panel, id)}
onsettings={(id) => openPanel({ unitId: unit.id, proposalId: id, tab: 'settings' })}
ondetails={(id) => openPanel({ unitId: unit.id, proposalId: id, tab: 'details' })}
onanswer={(o) => openPanel(answerPanel(o, session.me?.id ?? null))}
onsign={sign}
/>
{/snippet}