From f62347cc6b2887c04035bb83f5f48584df9364f6 Mon Sep 17 00:00:00 2001 From: Yvv Date: Tue, 3 Nov 2026 05:49:57 +0100 Subject: [PATCH] =?UTF-8?q?Lot=20V,=20interface=20:=20les=20types=20du=20c?= =?UTF-8?q?ontrat=20(tallyPending,=20tallySimulated,=20myVoteState,=20vote?= =?UTF-8?q?.simulated,=20mode=20des=20bulletins,=20publication.waitsFor,?= =?UTF-8?q?=20messages=20=C2=AB=20ballot=20=C2=BB)=20;=20i18n=20des=20?= =?UTF-8?q?=C3=A9tats=20(simul=C3=A9,=20=C3=A9cart=C3=A9=20par=20une=20obj?= =?UTF-8?q?ection,=20messages=20d'un=20vote=20=C3=A9cart=C3=A9)=20en=20fr,?= =?UTF-8?q?=20en,=20es=20;=20la=20bo=C3=AEte=20=C2=AB=20Messages=20=C2=BB?= =?UTF-8?q?=20dit=20les=20votes=20=C3=A9cart=C3=A9s=20;=20=C2=AB=20Tes=20b?= =?UTF-8?q?ulletins=20=C2=BB=20marque=20un=20vote=20simul=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- web/src/lib/i18n/en.ts | 12 ++++++- web/src/lib/i18n/es.ts | 12 ++++++- web/src/lib/i18n/fr.ts | 12 ++++++- web/src/lib/types.ts | 52 ++++++++++++++++++++++++--- web/src/routes/bulletins/+page.svelte | 7 ++-- web/src/routes/messages/+page.svelte | 24 +++++++++++-- web/tests/unit/basket.test.ts | 4 +-- web/tests/unit/versions.test.ts | 4 +-- 8 files changed, 111 insertions(+), 16 deletions(-) diff --git a/web/src/lib/i18n/en.ts b/web/src/lib/i18n/en.ts index 7094e24..1d7e944 100644 --- a/web/src/lib/i18n/en.ts +++ b/web/src/lib/i18n/en.ts @@ -255,6 +255,10 @@ export const en: Messages = { 'vote.mine.chain': 'recorded on the Ğ1 chain', 'vote.mine.saved': 'saved', 'vote.mine.pending': 'Waiting for the Ğ1 chain: {choice}.', + 'vote.mine.simulated': 'simulated', + 'vote.simulated.note': 'The Ğ1 chain is not open yet: your vote is simulated. It counts as a vote, marked “simulated”, until it is erased when the chain opens.', + 'vote.tally.simulated.one': 'of which {n} simulated', + 'vote.tally.simulated.other': 'of which {n} simulated', 'vote.mine.refused': 'Your last vote was not counted: {reason}', 'vote.mine.expired': 'Your last vote was not seen on the Ğ1 chain in time: it does not count.', 'vote.mine.receipts': 'Your ballots and receipts', @@ -269,6 +273,7 @@ export const en: Messages = { 'ballot.reason.not_member': 'the wallet that signed is not that of a member of the Ğ1 web of trust.', 'ballot.reason.no_identity': 'the wallet that signed is linked to no Ğ1 identity.', 'ballot.reason.transfer': 'the transfer to the vote account is missing, or its amount is too low.', + 'ballot.reason.objection': 'an objection was holding this version back when the Ğ1 chain confirmed your vote; your earlier vote stands.', 'basket.title': 'Voting basket', 'basket.intro': 'Choose article by article, a whole section or the whole document: it all goes in a single gesture.', 'basket.doc': 'The whole document', @@ -336,6 +341,7 @@ export const en: Messages = { 'ballots.lines.other': '{n} votes', 'ballots.status.pending': 'Waiting for the Ğ1 chain', 'ballots.status.confirmed': 'Counted for the Ğ1 identity “{name}”', + 'ballots.status.simulated': 'Simulated: counted for “{name}” until the Ğ1 chain opens', 'ballots.status.refused': 'Not counted: {reason}', 'ballots.status.expired': 'Not seen on the Ğ1 chain in time: it does not count', 'ballots.block': 'block {n}', @@ -475,7 +481,7 @@ export const en: Messages = { 'objection.respond.contredit': 'Reword', 'objection.linked': 'This proposal answers an objection: once submitted, the person who raised it lifts it or reiterates it.', 'messages.title': 'Messages', - 'messages.intro': 'What waits for a gesture from you: an objection to answer, a typo fix to validate. Each message carries its deadline; a message dealt with asks nothing more.', + 'messages.intro': 'What waits for a gesture from you: an objection to answer, a typo fix to validate. Each message carries its deadline; a message dealt with asks nothing more. You also read here what became of a vote that was not counted.', 'messages.none': 'No messages for now.', 'messages.readAll': 'Mark all as read', 'messages.read': 'Mark as read', @@ -484,12 +490,16 @@ export const en: Messages = { 'messages.open': 'See it in the text', 'messages.topic.objection': 'Objection', 'messages.topic.typo': 'Typo', + 'messages.topic.ballot': 'Vote', 'messages.objection_raised.redit': '{name} raises an objection: your version “{held}” repeats “{other}”. Until it is lifted, nobody can vote for it any more. To answer, change the article it repeats.', 'messages.objection_raised.contredit': '{name} raises an objection: your version “{held}” contradicts “{other}”. Until it is lifted, nobody can vote for it any more. To answer, reword it.', 'messages.objection_reiterated': '{name} reiterates their objection: nobody can vote for “{held}” yet. Answer again.', 'messages.objection_answered': 'Your objection on “{held}” has been answered. Lift it if the answer suits you, or reiterate it. With no answer from you, it is lifted.', 'messages.typo_pending.author': '{name} suggests a typo fix in your version “{title}”.', 'messages.typo_pending.admin': '{name} suggests a typo fix in “{title}”, a seed text: an administrator validates it.', + 'messages.ballot_expired': 'Your vote of {date} ({articles}) was not seen on the Ğ1 chain within 24 hours: it is set aside. You can vote again.', + 'messages.ballot_refused': 'Your vote of {date} ({articles}) was not counted: {reason} You can vote again.', + 'messages.ballot_objection': 'Your “for” on {articles} is set aside: an objection was holding this version back when the Ğ1 chain confirmed your vote. Your earlier vote stands.', 'messages.respond': 'Answer', 'messages.outcome.answered': 'answered', 'messages.outcome.reiterated': 'objection reiterated', diff --git a/web/src/lib/i18n/es.ts b/web/src/lib/i18n/es.ts index 507d7b3..11e1102 100644 --- a/web/src/lib/i18n/es.ts +++ b/web/src/lib/i18n/es.ts @@ -255,6 +255,10 @@ export const es: Messages = { 'vote.mine.chain': 'inscrito en la cadena Ğ1', 'vote.mine.saved': 'registrado', 'vote.mine.pending': 'Esperando a la cadena Ğ1: {choice}.', + 'vote.mine.simulated': 'simulado', + 'vote.simulated.note': 'La cadena Ğ1 aún no está abierta: tu voto es simulado. Cuenta como un voto, marcado «simulado», hasta que se borre al abrirse la cadena.', + 'vote.tally.simulated.one': 'de ellos {n} simulado', + 'vote.tally.simulated.other': 'de ellos {n} simulados', 'vote.mine.refused': 'Tu último voto no se contó: {reason}', 'vote.mine.expired': 'Tu último voto no se vio a tiempo en la cadena Ğ1: no cuenta.', 'vote.mine.receipts': 'Tus papeletas y recibos', @@ -269,6 +273,7 @@ export const es: Messages = { 'ballot.reason.not_member': 'el monedero que firmó no es el de un miembro de la red de confianza Ğ1.', 'ballot.reason.no_identity': 'el monedero que firmó no está vinculado a ninguna identidad Ğ1.', 'ballot.reason.transfer': 'falta la transferencia a la cuenta de los votos, o su importe es demasiado bajo.', + 'ballot.reason.objection': 'una objeción retenía esta versión cuando la cadena Ğ1 confirmó tu voto; tu voto anterior se mantiene.', 'basket.title': 'Cesta de votación', 'basket.intro': 'Elige artículo por artículo, una sección entera o todo el documento: todo sale en un solo gesto.', 'basket.doc': 'Todo el documento', @@ -336,6 +341,7 @@ export const es: Messages = { 'ballots.lines.other': '{n} votos', 'ballots.status.pending': 'Esperando a la cadena Ğ1', 'ballots.status.confirmed': 'Contado para la identidad Ğ1 «{name}»', + 'ballots.status.simulated': 'Simulado: contado para «{name}» hasta que se abra la cadena Ğ1', 'ballots.status.refused': 'No contado: {reason}', 'ballots.status.expired': 'No visto a tiempo en la cadena Ğ1: no cuenta', 'ballots.block': 'bloque {n}', @@ -475,7 +481,7 @@ export const es: Messages = { 'objection.respond.contredit': 'Reformular', 'objection.linked': 'Esta propuesta responde a una objeción: una vez enviada, la persona que la hizo la levanta o la reitera.', 'messages.title': 'Mensajes', - 'messages.intro': 'Lo que espera un gesto tuyo: una objeción que responder, una corrección de errata que validar. Cada mensaje lleva su plazo; un mensaje tratado ya no pide nada.', + 'messages.intro': 'Lo que espera un gesto tuyo: una objeción que responder, una corrección de errata que validar. Cada mensaje lleva su plazo; un mensaje tratado ya no pide nada. Aquí lees también qué fue de un voto que no se contó.', 'messages.none': 'Ningún mensaje por ahora.', 'messages.readAll': 'Marcar todo como leído', 'messages.read': 'Marcar como leído', @@ -484,12 +490,16 @@ export const es: Messages = { 'messages.open': 'Verlo en el texto', 'messages.topic.objection': 'Objeción', 'messages.topic.typo': 'Errata', + 'messages.topic.ballot': 'Voto', 'messages.objection_raised.redit': '{name} hace una objeción: tu versión «{held}» repite «{other}». Hasta que se levante, nadie puede votar a favor. Para responder, modifica el artículo que repite.', 'messages.objection_raised.contredit': '{name} hace una objeción: tu versión «{held}» contradice «{other}». Hasta que se levante, nadie puede votar a favor. Para responder, reformúlala.', 'messages.objection_reiterated': '{name} reitera su objeción: nadie puede votar aún a favor de «{held}». Responde de nuevo.', 'messages.objection_answered': 'Tu objeción sobre «{held}» recibió una respuesta. Levántala si te convence, o reitérala. Sin respuesta tuya, se levanta.', 'messages.typo_pending.author': '{name} propone una corrección de errata en tu versión «{title}».', 'messages.typo_pending.admin': '{name} propone una corrección de errata en «{title}», un texto del seed: un administrador la valida.', + 'messages.ballot_expired': 'Tu voto del {date} ({articles}) no se vio en la cadena Ğ1 en 24 horas: queda descartado. Puedes votar de nuevo.', + 'messages.ballot_refused': 'Tu voto del {date} ({articles}) no se contó: {reason} Puedes votar de nuevo.', + 'messages.ballot_objection': 'Tu «a favor» sobre {articles} queda descartado: una objeción retenía esta versión cuando la cadena Ğ1 confirmó tu voto. Tu voto anterior se mantiene.', 'messages.respond': 'Responder', 'messages.outcome.answered': 'respuesta dada', 'messages.outcome.reiterated': 'objeción reiterada', diff --git a/web/src/lib/i18n/fr.ts b/web/src/lib/i18n/fr.ts index db80759..bfa4b0b 100644 --- a/web/src/lib/i18n/fr.ts +++ b/web/src/lib/i18n/fr.ts @@ -259,6 +259,10 @@ export const fr = { 'vote.mine.chain': 'inscrit sur la chaîne Ğ1', 'vote.mine.saved': 'enregistré', 'vote.mine.pending': 'En attente de la chaîne Ğ1 : {choice}.', + 'vote.mine.simulated': 'simulé', + 'vote.simulated.note': "La chaîne Ğ1 n'est pas encore ouverte : ton vote est simulé. Il compte comme un vote, marqué « simulé », jusqu'à son effacement à l'ouverture de la chaîne.", + 'vote.tally.simulated.one': 'dont {n} simulé', + 'vote.tally.simulated.other': 'dont {n} simulés', 'vote.mine.refused': "Ton dernier vote n'a pas été compté : {reason}", 'vote.mine.expired': "Ton dernier vote n'a pas été vu sur la chaîne Ğ1 à temps : il ne compte pas.", 'vote.mine.receipts': 'Tes bulletins et reçus', @@ -273,6 +277,7 @@ export const fr = { 'ballot.reason.not_member': "le portefeuille qui a signé n'est pas celui d'un membre de la toile de confiance Ğ1.", 'ballot.reason.no_identity': "le portefeuille qui a signé n'est rattaché à aucune identité Ğ1.", 'ballot.reason.transfer': 'le virement vers le compte des votes manque, ou son montant est trop faible.', + 'ballot.reason.objection': "une objection retenait cette version quand la chaîne Ğ1 a confirmé ton vote ; ton vote d'avant reste.", 'basket.title': 'Panier de vote', 'basket.intro': 'Choisis article par article, une section entière ou tout le document : tout part en un seul geste.', 'basket.doc': 'Tout le document', @@ -340,6 +345,7 @@ export const fr = { 'ballots.lines.other': '{n} votes', 'ballots.status.pending': 'En attente de la chaîne Ğ1', 'ballots.status.confirmed': "Compté pour l'identité Ğ1 « {name} »", + 'ballots.status.simulated': "Simulé : compté pour « {name} » jusqu'à l'ouverture de la chaîne Ğ1", 'ballots.status.refused': 'Pas compté : {reason}', 'ballots.status.expired': 'Pas vu sur la chaîne Ğ1 à temps : il ne compte pas', 'ballots.block': 'bloc {n}', @@ -479,7 +485,7 @@ export const fr = { 'objection.respond.contredit': 'Reformuler', 'objection.linked': "Cette proposition répond à une objection : une fois soumise, la personne qui l'a faite la lève ou la réitère.", 'messages.title': 'Messages', - 'messages.intro': 'Ce qui attend un geste de toi : une objection à laquelle répondre, une correction de coquille à valider. Chaque message porte son échéance ; un message traité ne demande plus rien.', + 'messages.intro': "Ce qui attend un geste de toi : une objection à laquelle répondre, une correction de coquille à valider. Chaque message porte son échéance ; un message traité ne demande plus rien. Tu y lis aussi ce qu'il advient d'un vote qui n'a pas été compté.", 'messages.none': "Aucun message pour l'instant.", 'messages.readAll': 'Tout marquer comme lu', 'messages.read': 'Marquer comme lu', @@ -488,12 +494,16 @@ export const fr = { 'messages.open': 'Voir dans le texte', 'messages.topic.objection': 'Objection', 'messages.topic.typo': 'Coquille', + 'messages.topic.ballot': 'Vote', 'messages.objection_raised.redit': "{name} fait une objection : ta version « {held} » redit « {other} ». Jusqu'à sa levée, personne ne peut plus voter pour elle. Pour répondre, modifie l'article visé.", 'messages.objection_raised.contredit': "{name} fait une objection : ta version « {held} » contredit « {other} ». Jusqu'à sa levée, personne ne peut plus voter pour elle. Pour répondre, reformule-la.", 'messages.objection_reiterated': '{name} réitère son objection : personne ne peut encore voter pour « {held} ». Réponds à nouveau.', 'messages.objection_answered': 'Ton objection sur « {held} » a reçu une réponse. Lève-la si elle te convient, ou réitère-la. Sans réponse de ta part, elle est levée.', 'messages.typo_pending.author': '{name} propose une correction de coquille dans ta version « {title} ».', 'messages.typo_pending.admin': '{name} propose une correction de coquille dans « {title} », un texte du seed : un administrateur la valide.', + 'messages.ballot_expired': "Ton vote du {date} ({articles}) n'a pas été vu sur la chaîne Ğ1 en 24 heures : il est écarté. Tu peux voter à nouveau.", + 'messages.ballot_refused': "Ton vote du {date} ({articles}) n'a pas été compté : {reason} Tu peux voter à nouveau.", + 'messages.ballot_objection': "Ton « pour » sur {articles} est écarté : une objection retenait cette version quand la chaîne Ğ1 a confirmé ton vote. Ton vote d'avant reste.", 'messages.respond': 'Répondre', 'messages.outcome.answered': 'réponse donnée', 'messages.outcome.reiterated': 'objection réitérée', diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 92f6cd1..6f5def8 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -117,7 +117,14 @@ export interface Proposal { seedMinimal: boolean; /** A seed proposal (ADR 0011): open, put forward by the seed to start the vote, not in force. */ seedProposal?: boolean; + /** The totals shown: recorded, pending and simulated votes, each intention in place of + * the vote its session sees as its own (ADR 0012). */ tally: Tally; + /** The part of `tally` still without proof (an intention pending on the chain). */ + tallyPending: Tally; + /** The part of `tally` that comes from the simulation (ADR 0012). */ + tallySimulated: Tally; + /** On the totals shown (`tally`), the quorum included (ADR 0013). */ evaluation: Evaluation | null; votedAtInertia: Inertia; /** Held back by a standing objection: the latest arrival of the two (never a text in force). */ @@ -126,11 +133,17 @@ export interface Proposal { votable: boolean; /** Standing objections around this version. */ objections: number; + /** What the person sees as her vote now: her latest gesture, an intention pending included. */ myVote: 'for' | 'against' | null; + /** Where « ton vote » stands (ADR 0012): `recorded` — proven by the chain, or a vote of + * the server in a managed group; `pending` — an intention waiting for the chain; + * `simulated` — a vote of the simulation, while the chain is closed. */ + myVoteState: 'recorded' | 'pending' | 'simulated' | null; myVoteStale: boolean; /** « Ton vote » was cast on the Ğ1 chain (confirmed). */ myVoteChain: boolean; - /** A ballot of this session not counted yet: pending on the chain, or refused, or let go. */ + /** A ballot of this session not counted yet: pending on the chain, or refused, or let go — + * or its *for* set aside by an objection (`refused`, reason `objection`, ADR 0012). */ myBallot: MyBallot | null; translations: Partial>; /** An order version: the order it gives its section now. */ @@ -142,7 +155,7 @@ export type Choice = 'for' | 'against' | 'none'; export interface MyBallot { id: string; status: 'pending' | 'refused' | 'expired'; - reason: 'not_member' | 'no_identity' | 'transfer' | null; + reason: 'not_member' | 'no_identity' | 'transfer' | 'objection' | null; choice: Choice; } @@ -162,8 +175,12 @@ export interface ChainSettings { /** How this session votes in the group of the document, and whether it may now. */ export interface VoteAccess { + /** `server` too while the chain is closed: the vote is then simulated (`simulated`). */ way: 'server' | 'chain'; open: boolean; + /** The chain is closed: the vote leaves by a write of the server, marked as simulated, + * and counts until an act « supprime » sets it aside (ADR 0012). */ + simulated: boolean; reason: 'members_only' | 'chain_off' | null; chain: ChainSettings | null; } @@ -181,6 +198,8 @@ export interface Ballot { voter: string | null; entered: Entered[]; expiresAt: string; + /** `simulated`: a ballot of the simulation, proven by the server, never on the chain. */ + mode: 'chain' | 'simulated'; } /** A ballot of the person's history (`GET /api/ballots`): never anyone else's. */ @@ -195,6 +214,7 @@ export interface BallotEntry { lines: number; createdAt: string; settledAt: string | null; + mode: Ballot['mode']; } export interface Unit { @@ -234,6 +254,9 @@ export interface Publication { doneAt?: string | null; commit?: string | null; attempts?: number; + /** Why it is suspended: no article in force yet (`text`, ADR 0011), or something in force + * entered by simulated votes that the proven votes do not carry yet (`proven`, ADR 0012). */ + waitsFor?: 'text' | 'proven' | null; } export interface DocView { @@ -345,11 +368,29 @@ export interface TypoFix { version?: { id: string; title: string; status: Proposal['status']; unitId: string; code: string | null }; } -/** A message of one's box: about an objection or a typo fix, with its gestures now. */ +/** A ballot set aside, as a message tells it (ADR 0012): line by line, what became of it. */ +export interface BallotNotice { + id: string; + status: Ballot['status']; + reason: MyBallot['reason']; + mode: Ballot['mode']; + createdAt: string; + settledAt: string | null; + lines: { proposalId: string; unitId: string; code: string; title: string; choice: Choice; outcome: 'counted' | 'superseded' | 'objection' | null }[]; +} + +/** A message of one's box: about an objection, a typo fix or a ballot set aside, with its gestures now. */ export interface Message { id: string; - topic: 'objection' | 'typo'; - kind: 'objection_raised' | 'objection_answered' | 'objection_reiterated' | 'typo_pending'; + topic: 'objection' | 'typo' | 'ballot'; + kind: + | 'objection_raised' + | 'objection_answered' + | 'objection_reiterated' + | 'typo_pending' + | 'ballot_expired' + | 'ballot_refused' + | 'ballot_objection'; cycle: number; createdAt: string; dueAt: string | null; @@ -361,4 +402,5 @@ export interface Message { doc: { id: string; groupSlug: string; slug: string; title: LangMap }; objection?: Objection; typo?: TypoFix; + ballot?: BallotNotice; } diff --git a/web/src/routes/bulletins/+page.svelte b/web/src/routes/bulletins/+page.svelte index 5c8220d..f08f170 100644 --- a/web/src/routes/bulletins/+page.svelte +++ b/web/src/routes/bulletins/+page.svelte @@ -27,6 +27,8 @@ function standing(b: BallotEntry): string { if (b.status === 'pending') return t('ballots.status.pending'); + // A vote of the simulation (ADR 0012): counted for the session, never on the chain. + if (b.status === 'confirmed' && b.mode === 'simulated') return t('ballots.status.simulated', { name: b.voter ?? '' }); if (b.status === 'confirmed') return t('ballots.status.confirmed', { name: b.voter ?? '' }); if (b.status === 'refused') return t('ballots.status.refused', { reason: t(`ballot.reason.${b.reason ?? 'transfer'}`) }); return t('ballots.status.expired'); @@ -62,7 +64,7 @@ {#each data.value.ballots as b (b.id)}
  • {i18n.date(b.createdAt)} · {titleOf(b.doc.title)} · {i18n.plural('ballots.lines', b.lines)}

    -

    +

    {standing(b)}{#if b.block !== null} · {t('ballots.block', { n: i18n.number(b.block) })}{/if}

    {b.comment} @@ -159,7 +161,8 @@ .state.confirmed { color: var(--moss); } - .state.pending { + .state.pending, + .state.confirmed.simulated { color: var(--water); } .state.refused, diff --git a/web/src/routes/messages/+page.svelte b/web/src/routes/messages/+page.svelte index e0bb3b0..7ba8692 100644 --- a/web/src/routes/messages/+page.svelte +++ b/web/src/routes/messages/+page.svelte @@ -2,7 +2,8 @@ // The box « Messages » of trustinWallet, in the application (2026-10-31, lot F, // ADR 0010): what waits for a gesture of the person — an objection to answer, // to lift or to reiterate, a typo fix to validate or refuse —, each with its - // deadline. A message dealt with offers no gesture any more. No mail. + // deadline. A message dealt with offers no gesture any more. No mail. A ballot set + // aside (ADR 0012) is told here too, without gesture: one votes again in the text. import { onMount } from 'svelte'; import { goto } from '$app/navigation'; import { api } from '$lib/api'; @@ -44,10 +45,27 @@ await reload(); } + /** The lines of a ballot a message is about: those set aside by an objection, or all. */ + function linesOf(m: Message) { + const lines = m.ballot?.lines ?? []; + const held = lines.filter((l) => l.outcome === 'objection'); + return m.kind === 'ballot_objection' && held.length ? held : lines; + } + /** The text of a message, in the language of the reader, from its subject. */ function textOf(m: Message): string { const o = m.objection; const fix = m.typo; + const b = m.ballot; + if (b) { + const articles = linesOf(m) + .map((l) => l.code) + .join(', '); + const date = i18n.date(b.createdAt); + if (m.kind === 'ballot_expired') return t('messages.ballot_expired', { date, articles }); + if (m.kind === 'ballot_refused') return t('messages.ballot_refused', { date, articles, reason: t(`ballot.reason.${b.reason ?? 'transfer'}`) }); + return t('messages.ballot_objection', { articles }); + } if (o) { const held = o.heldVersion?.title ?? ''; if (m.kind === 'objection_raised') return t(`messages.objection_raised.${o.kind}`, { name: o.objector ?? '', held, other: o.other.title ?? '' }); @@ -73,6 +91,8 @@ function hrefOf(m: Message): string { const v = m.objection?.heldVersion ?? m.typo?.version ?? null; const base = `/g/${m.doc.groupSlug}/docs/${m.doc.slug}`; + const line = linesOf(m)[0]; + if (line) return `${base}?${panelSearch({ tab: 'details', unitId: line.unitId, proposalId: line.proposalId }, null)}`; if (!v) return base; return `${base}?${panelSearch({ tab: 'details', unitId: v.unitId ?? null, proposalId: v.id }, null)}`; } @@ -149,7 +169,7 @@

    {textOf(m)}

    {#if m.objection?.reason && m.kind !== 'objection_answered'}

    {m.objection.reason}

    {/if} {#if m.typo}{/if} - {#if m.done} + {#if m.done && m.topic !== 'ballot'}

    {outcomeOf(m)}

    {:else if m.dueAt}

    {t('messages.due', { date: i18n.date(m.dueAt) })}

    diff --git a/web/tests/unit/basket.test.ts b/web/tests/unit/basket.test.ts index f1abf4a..9d3991e 100644 --- a/web/tests/unit/basket.test.ts +++ b/web/tests/unit/basket.test.ts @@ -6,8 +6,8 @@ function prop(id: string, unitId: string, over: Partial = {}): Proposa return { id, unitId, kind: 'text', inertiaTarget: null, title: unitId, body: `texte ${id}`, status: 'open', origin: 'seed', label: null, author: null, mine: false, revision: 1, createdAt: '', updatedAt: '', submittedAt: null, enteredAt: null, - confirmedAt: null, seedMinimal: false, tally: { for: 0, against: 0 }, evaluation: null, votedAtInertia: 3, - blocked: false, votable: true, objections: 0, myVote: null, myVoteStale: false, myVoteChain: false, myBallot: null, translations: {}, + confirmedAt: null, seedMinimal: false, tally: { for: 0, against: 0 }, tallyPending: { for: 0, against: 0 }, tallySimulated: { for: 0, against: 0 }, evaluation: null, votedAtInertia: 3, + blocked: false, votable: true, objections: 0, myVote: null, myVoteState: null, myVoteStale: false, myVoteChain: false, myBallot: null, translations: {}, ...over, }; } diff --git a/web/tests/unit/versions.test.ts b/web/tests/unit/versions.test.ts index 383086a..1f904f7 100644 --- a/web/tests/unit/versions.test.ts +++ b/web/tests/unit/versions.test.ts @@ -6,9 +6,9 @@ function prop(id: string, unitId: string, over: Partial = {}): Proposa return { id, unitId, kind: 'text', inertiaTarget: null, title: unitId, body: `texte ${id}`, status: 'open', origin: 'seed', label: null, author: null, mine: false, revision: 1, createdAt: '', updatedAt: '', submittedAt: null, enteredAt: null, - confirmedAt: null, seedMinimal: false, tally: { for: 0, against: 0 }, + confirmedAt: null, seedMinimal: false, tally: { for: 0, against: 0 }, tallyPending: { for: 0, against: 0 }, tallySimulated: { for: 0, against: 0 }, evaluation: { cast: 0, required: 50, reached: false, missing: 50, requiredRatio: 1, wellRated: false }, - votedAtInertia: 3, blocked: false, votable: true, objections: 0, myVote: null, myVoteStale: false, myVoteChain: false, myBallot: null, translations: {}, + votedAtInertia: 3, blocked: false, votable: true, objections: 0, myVote: null, myVoteState: null, myVoteStale: false, myVoteChain: false, myBallot: null, translations: {}, ...over, }; }