v3 : dixième relecture — mention « sur invitation », noms attendus protégés
- Tout auteur venu par invitation porte « sur invitation » à côté de son nom (propositions, révisions, signalements) : un nom pris avant l'arrivée de son titulaire Ğ1, ou une lettre sosie, ne passe pas pour un membre de la toile. - Un lien d'équipe refuse aussi le nom qu'une invitation attend encore. Boucle : 68 tests Rust, svelte-check, 9 vitest, e2e 13/13. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -22,7 +22,7 @@ pas encore en ligne : les points des septième, huitième et neuvième relecture
|
|||||||
- Relectures indépendantes avant mise en ligne, chacune traitée avant de livrer : `1f9c241 :
|
- Relectures indépendantes avant mise en ligne, chacune traitée avant de livrer : `1f9c241 :
|
||||||
ok`, `94907af : ok`, puis quatre « à reprendre » (`ac9c904`, `9203fd2`, `1eac6f8`, `6c0754a`),
|
ok`, `94907af : ok`, puis quatre « à reprendre » (`ac9c904`, `9203fd2`, `1eac6f8`, `6c0754a`),
|
||||||
toutes sur la même question : une « coquille » (sans vote, publiée) qui change un sens, puis
|
toutes sur la même question : une « coquille » (sans vote, publiée) qui change un sens, puis
|
||||||
`fa8e8dc : ok` (mis en ligne), `c3d6bf8 : ok`, `fd19dbf : ok`. La
|
`fa8e8dc : ok` (mis en ligne), `c3d6bf8 : ok`, `fd19dbf : ok`, `753b522 : ok`. La
|
||||||
règle est désormais simple et explicable : **une coquille touche les lettres et les espaces,
|
règle est désormais simple et explicable : **une coquille touche les lettres et les espaces,
|
||||||
rien d'autre** (CHANTIER.md, ADR 0003). Autres points traités : ordre des workflows, anciens
|
rien d'autre** (CHANTIER.md, ADR 0003). Autres points traités : ordre des workflows, anciens
|
||||||
conteneurs en `restart: always`, vote réservé aux membres de la toile dans Monnaie libre (une
|
conteneurs en `restart: always`, vote réservé aux membres de la toile dans Monnaie libre (une
|
||||||
|
|||||||
@@ -210,6 +210,9 @@ pub struct ProposalRow {
|
|||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
pub author_id: Option<String>,
|
pub author_id: Option<String>,
|
||||||
pub author_name: Option<String>,
|
pub author_name: Option<String>,
|
||||||
|
/// The author came by invitation, not with a Ğ1 key: shown next to the name,
|
||||||
|
/// so a name taken ahead of its owner, or a look-alike, does not pass for them.
|
||||||
|
pub author_invited: Option<bool>,
|
||||||
pub revision: i64,
|
pub revision: i64,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub updated_at: String,
|
pub updated_at: String,
|
||||||
@@ -220,7 +223,7 @@ pub struct ProposalRow {
|
|||||||
|
|
||||||
pub const PROPOSAL_SELECT: &str =
|
pub const PROPOSAL_SELECT: &str =
|
||||||
"SELECT p.id, p.unit_id, p.kind, p.inertia_target, p.title, p.body, p.status, p.origin,
|
"SELECT p.id, p.unit_id, p.kind, p.inertia_target, p.title, p.body, p.status, p.origin,
|
||||||
p.label, p.author_id, i.display_name AS author_name, p.revision, p.created_at, p.updated_at, p.submitted_at,
|
p.label, p.author_id, i.display_name AS author_name, i.kind = 'invite' AS author_invited, p.revision, p.created_at, p.updated_at, p.submitted_at,
|
||||||
p.entered_at, p.confirmed_at
|
p.entered_at, p.confirmed_at
|
||||||
FROM proposals p LEFT JOIN identities i ON i.id = p.author_id";
|
FROM proposals p LEFT JOIN identities i ON i.id = p.author_id";
|
||||||
|
|
||||||
@@ -466,7 +469,9 @@ fn proposal_json(
|
|||||||
json!({
|
json!({
|
||||||
"id": p.id, "unitId": p.unit_id, "kind": kind, "inertiaTarget": target,
|
"id": p.id, "unitId": p.unit_id, "kind": kind, "inertiaTarget": target,
|
||||||
"title": p.title, "body": p.body, "status": p.status, "origin": p.origin, "label": p.label,
|
"title": p.title, "body": p.body, "status": p.status, "origin": p.origin, "label": p.label,
|
||||||
"author": p.author_id.as_ref().map(|id| json!({ "id": id, "name": p.author_name })),
|
"author": p.author_id.as_ref().map(|id| json!({
|
||||||
|
"id": id, "name": p.author_name, "invited": p.author_invited.unwrap_or(false),
|
||||||
|
})),
|
||||||
"mine": p.author_id.as_deref() == Some(me),
|
"mine": p.author_id.as_deref() == Some(me),
|
||||||
"revision": p.revision, "createdAt": p.created_at, "updatedAt": p.updated_at,
|
"revision": p.revision, "createdAt": p.created_at, "updatedAt": p.updated_at,
|
||||||
"submittedAt": p.submitted_at, "enteredAt": p.entered_at, "confirmedAt": p.confirmed_at,
|
"submittedAt": p.submitted_at, "enteredAt": p.entered_at, "confirmedAt": p.confirmed_at,
|
||||||
|
|||||||
@@ -551,10 +551,12 @@ pub async fn detail(
|
|||||||
title: String,
|
title: String,
|
||||||
body: String,
|
body: String,
|
||||||
author_name: Option<String>,
|
author_name: Option<String>,
|
||||||
|
author_invited: Option<bool>,
|
||||||
created_at: String,
|
created_at: String,
|
||||||
}
|
}
|
||||||
let revs = sqlx::query_as::<_, Rev>(
|
let revs = sqlx::query_as::<_, Rev>(
|
||||||
"SELECT r.revision, r.kind, r.title, r.body, i.display_name AS author_name, r.created_at
|
"SELECT r.revision, r.kind, r.title, r.body, i.display_name AS author_name,
|
||||||
|
i.kind = 'invite' AS author_invited, r.created_at
|
||||||
FROM revisions r LEFT JOIN identities i ON i.id = r.author_id WHERE r.proposal_id = ? ORDER BY r.id",
|
FROM revisions r LEFT JOIN identities i ON i.id = r.author_id WHERE r.proposal_id = ? ORDER BY r.id",
|
||||||
)
|
)
|
||||||
.bind(&id)
|
.bind(&id)
|
||||||
@@ -567,7 +569,8 @@ pub async fn detail(
|
|||||||
let diff = previous
|
let diff = previous
|
||||||
.map(|prev| edit::diff(&format!("{}\n{}", prev.title, prev.body), &format!("{}\n{}", r.title, r.body)));
|
.map(|prev| edit::diff(&format!("{}\n{}", prev.title, prev.body), &format!("{}\n{}", r.title, r.body)));
|
||||||
history.push(json!({
|
history.push(json!({
|
||||||
"revision": r.revision, "kind": r.kind, "author": r.author_name, "at": r.created_at, "diff": diff,
|
"revision": r.revision, "kind": r.kind, "author": r.author_name,
|
||||||
|
"authorInvited": r.author_invited.unwrap_or(false), "at": r.created_at, "diff": diff,
|
||||||
}));
|
}));
|
||||||
previous = Some(r);
|
previous = Some(r);
|
||||||
}
|
}
|
||||||
@@ -580,13 +583,14 @@ pub async fn detail(
|
|||||||
reason: String,
|
reason: String,
|
||||||
author_id: String,
|
author_id: String,
|
||||||
author_name: String,
|
author_name: String,
|
||||||
|
author_invited: bool,
|
||||||
created_at: String,
|
created_at: String,
|
||||||
lifted_at: Option<String>,
|
lifted_at: Option<String>,
|
||||||
lifted_by: Option<String>,
|
lifted_by: Option<String>,
|
||||||
}
|
}
|
||||||
let sigs = sqlx::query_as::<_, Sig>(
|
let sigs = sqlx::query_as::<_, Sig>(
|
||||||
"SELECT s.id, s.proposal_id, s.target_proposal_id, s.reason, s.author_id, i.display_name AS author_name,
|
"SELECT s.id, s.proposal_id, s.target_proposal_id, s.reason, s.author_id, i.display_name AS author_name,
|
||||||
s.created_at, s.lifted_at, s.lifted_by
|
i.kind = 'invite' AS author_invited, s.created_at, s.lifted_at, s.lifted_by
|
||||||
FROM signals s JOIN identities i ON i.id = s.author_id
|
FROM signals s JOIN identities i ON i.id = s.author_id
|
||||||
WHERE s.proposal_id = ? OR s.target_proposal_id = ? ORDER BY s.created_at DESC",
|
WHERE s.proposal_id = ? OR s.target_proposal_id = ? ORDER BY s.created_at DESC",
|
||||||
)
|
)
|
||||||
@@ -603,7 +607,8 @@ pub async fn detail(
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
signals.push(json!({
|
signals.push(json!({
|
||||||
"id": s.id, "reason": s.reason, "author": s.author_name, "mine": s.author_id == user.id,
|
"id": s.id, "reason": s.reason, "author": s.author_name, "authorInvited": s.author_invited,
|
||||||
|
"mine": s.author_id == user.id,
|
||||||
"at": s.created_at, "liftedAt": s.lifted_at, "liftedBy": s.lifted_by,
|
"at": s.created_at, "liftedAt": s.lifted_at, "liftedBy": s.lifted_by,
|
||||||
"other": other.map(|o| json!({ "id": o.id, "title": o.title, "status": o.status, "code": other_code })),
|
"other": other.map(|o| json!({ "id": o.id, "title": o.title, "status": o.status, "code": other_code })),
|
||||||
}));
|
}));
|
||||||
@@ -626,7 +631,8 @@ pub async fn detail(
|
|||||||
"proposal": {
|
"proposal": {
|
||||||
"id": p.id, "kind": p.kind, "inertiaTarget": p.inertia_target, "title": p.title, "body": p.body,
|
"id": p.id, "kind": p.kind, "inertiaTarget": p.inertia_target, "title": p.title, "body": p.body,
|
||||||
"status": p.status, "origin": p.origin, "label": p.label, "revision": p.revision,
|
"status": p.status, "origin": p.origin, "label": p.label, "revision": p.revision,
|
||||||
"author": p.author_name, "mine": p.author_id.as_deref() == Some(user.id.as_str()),
|
"author": p.author_name, "authorInvited": p.author_invited.unwrap_or(false),
|
||||||
|
"mine": p.author_id.as_deref() == Some(user.id.as_str()),
|
||||||
"submittedAt": p.submitted_at, "enteredAt": p.entered_at, "confirmedAt": p.confirmed_at,
|
"submittedAt": p.submitted_at, "enteredAt": p.entered_at, "confirmedAt": p.confirmed_at,
|
||||||
"seedMinimal": p.status == "in_force" && p.entered_at.is_none() && p.confirmed_at.is_none() && p.origin == "seed",
|
"seedMinimal": p.status == "in_force" && p.entered_at.is_none() && p.confirmed_at.is_none() && p.origin == "seed",
|
||||||
"tally": st.tally(&p.id), "evaluation": eval, "votedAtInertia": voted_at.as_str(),
|
"tally": st.tally(&p.id), "evaluation": eval, "votedAtInertia": voted_at.as_str(),
|
||||||
|
|||||||
@@ -97,7 +97,16 @@ pub struct JoinBody {
|
|||||||
/// `name_key` settles two openings at the same instant.
|
/// `name_key` settles two openings at the same instant.
|
||||||
async fn free_name(state: &AppState, raw: &str) -> AppResult<names::Name> {
|
async fn free_name(state: &AppState, raw: &str) -> AppResult<names::Name> {
|
||||||
let name = names::check(raw).ok_or(AppError::Invalid("name"))?;
|
let name = names::check(raw).ok_or(AppError::Invalid("name"))?;
|
||||||
let taken: Vec<String> = sqlx::query_scalar("SELECT display_name FROM identities").fetch_all(&state.db).await?;
|
// People already there, and those an invitation still waits for.
|
||||||
|
let mut taken: Vec<String> = sqlx::query_scalar("SELECT display_name FROM identities").fetch_all(&state.db).await?;
|
||||||
|
taken.extend(
|
||||||
|
sqlx::query_scalar::<_, String>(
|
||||||
|
"SELECT label FROM invitations WHERE used_at IS NULL AND identity_id IS NULL AND expires_at > ?",
|
||||||
|
)
|
||||||
|
.bind(util::now())
|
||||||
|
.fetch_all(&state.db)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
if taken.iter().any(|t| names::key(t) == name.key) {
|
if taken.iter().any(|t| names::key(t) == name.key) {
|
||||||
return Err(AppError::Conflict("name_taken"));
|
return Err(AppError::Conflict("name_taken"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -504,6 +504,21 @@ async fn a_team_link_makes_one_identity_per_opening() {
|
|||||||
let luna = v["me"]["id"].as_str().expect("id").to_owned();
|
let luna = v["me"]["id"].as_str().expect("id").to_owned();
|
||||||
let (_, g) = first.call("GET", "/api/groups", None).await;
|
let (_, g) = first.call("GET", "/api/groups", None).await;
|
||||||
assert!(g["groups"].as_array().expect("g").iter().any(|g| g["slug"] == "essai" && g["open"] == true));
|
assert!(g["groups"].as_array().expect("g").iter().any(|g| g["slug"] == "essai" && g["open"] == true));
|
||||||
|
// What an invited person writes says so next to their name.
|
||||||
|
let essai = doc_id(&mut first, "essai").await;
|
||||||
|
let (_, doc) = first.call("GET", &format!("/api/docs/{essai}"), None).await;
|
||||||
|
let uid = unit(&doc, "E2")["id"].as_str().expect("u").to_owned();
|
||||||
|
let (_, v) = first
|
||||||
|
.call("POST", &format!("/api/docs/{essai}/proposals"), Some(json!({
|
||||||
|
"unitId": uid, "title": "Réciprocité", "body": "Je m'engage à ne certifier que des personnes connues.", "submit": true,
|
||||||
|
})))
|
||||||
|
.await;
|
||||||
|
let (_, d) = admin.call("GET", &format!("/api/proposals/{}", v["id"].as_str().expect("id")), None).await;
|
||||||
|
assert_eq!(
|
||||||
|
(d["proposal"]["author"].as_str(), d["proposal"]["authorInvited"].as_bool()),
|
||||||
|
(Some("Luna"), Some(true))
|
||||||
|
);
|
||||||
|
assert_eq!(d["history"][0]["authorInvited"], true);
|
||||||
|
|
||||||
// Nobody joins under a name already there; the refusal does not count as an opening.
|
// Nobody joins under a name already there; the refusal does not count as an opening.
|
||||||
let mut second = admin.with(None);
|
let mut second = admin.with(None);
|
||||||
@@ -511,6 +526,10 @@ async fn a_team_link_makes_one_identity_per_opening() {
|
|||||||
let (s, v) = second.call("POST", "/api/auth/team", Some(json!({ "token": token, "name": taken }))).await;
|
let (s, v) = second.call("POST", "/api/auth/team", Some(json!({ "token": token, "name": taken }))).await;
|
||||||
assert_eq!((s, v["error"].as_str()), (StatusCode::CONFLICT, Some("name_taken")), "{taken}");
|
assert_eq!((s, v["error"].as_str()), (StatusCode::CONFLICT, Some("name_taken")), "{taken}");
|
||||||
}
|
}
|
||||||
|
// Nor under the name an invitation still waits for.
|
||||||
|
admin.call("POST", "/api/access/invitations", Some(json!({ "label": "Chiara Rossi", "groups": ["essai"] }))).await;
|
||||||
|
let (s, v) = second.call("POST", "/api/auth/team", Some(json!({ "token": token, "name": "chiara rossi" }))).await;
|
||||||
|
assert_eq!((s, v["error"].as_str()), (StatusCode::CONFLICT, Some("name_taken")));
|
||||||
// Look-alike letters from other alphabets are not a name.
|
// Look-alike letters from other alphabets are not a name.
|
||||||
let (s, v) = second.call("POST", "/api/auth/team", Some(json!({ "token": token, "name": "Lunа" }))).await;
|
let (s, v) = second.call("POST", "/api/auth/team", Some(json!({ "token": token, "name": "Lunа" }))).await;
|
||||||
assert_eq!((s, v["error"].as_str()), (StatusCode::BAD_REQUEST, Some("name")));
|
assert_eq!((s, v["error"].as_str()), (StatusCode::BAD_REQUEST, Some("name")));
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ Duniter peut se loger et tout faire — sauf la gestion des accès », qui revie
|
|||||||
unique) ; un nom s'écrit en lettres latines, chiffres, espace, trait d'union, apostrophe ou
|
unique) ; un nom s'écrit en lettres latines, chiffres, espace, trait d'union, apostrophe ou
|
||||||
point (pas de sosie cyrillique, grec ou pleine chasse). Une personne déjà connectée garde son
|
point (pas de sosie cyrillique, grec ou pleine chasse). Une personne déjà connectée garde son
|
||||||
identité et reçoit les groupes ; elle ne prend une place que si le lien lui en ajoute un ; sur
|
identité et reçoit les groupes ; elle ne prend une place que si le lien lui en ajoute un ; sur
|
||||||
un navigateur partagé, « Ce n'est pas moi » la déconnecte.
|
un navigateur partagé, « Ce n'est pas moi » la déconnecte. Un nom qu'une invitation attend
|
||||||
|
est protégé aussi.
|
||||||
|
- Tout auteur venu par invitation porte la mention **« sur invitation »** à côté de son nom
|
||||||
|
(propositions, révisions, signalements) : un nom pris avant l'arrivée de son titulaire Ğ1, ou
|
||||||
|
une lettre sosie, ne passe jamais pour un membre de la toile.
|
||||||
trustinWallet montre par quel lien chaque identité est venue. Limite assumée : une même
|
trustinWallet montre par quel lien chaque identité est venue. Limite assumée : une même
|
||||||
personne qui l'ouvre deux fois, sous deux noms, a deux identités — sans effet dans Monnaie
|
personne qui l'ouvre deux fois, sous deux noms, a deux identités — sans effet dans Monnaie
|
||||||
libre (seule la toile vote), à surveiller dans le groupe d'essai.
|
libre (seule la toile vote), à surveiller dans le groupe d'essai.
|
||||||
|
|||||||
@@ -196,6 +196,9 @@ Les raisons longues sont dans `docs/adr/`.
|
|||||||
**et** sous sa clé Ğ1 aurait deux voix : c'est à l'administration de n'en admettre qu'une.
|
**et** sous sa clé Ğ1 aurait deux voix : c'est à l'administration de n'en admettre qu'une.
|
||||||
- Un administrateur peut créer un nouveau lien pour une identité d'invitation existante
|
- Un administrateur peut créer un nouveau lien pour une identité d'invitation existante
|
||||||
(pour la dépanner) — jamais pour une identité Ğ1.
|
(pour la dépanner) — jamais pour une identité Ğ1.
|
||||||
|
- Une identité d'invitation peut porter le nom d'un membre Ğ1 qui ne s'est pas encore
|
||||||
|
connecté (la connexion Ğ1 ne vérifie pas les noms) : la mention « sur invitation » à côté de
|
||||||
|
chaque auteur qui n'est pas venu avec sa clé Ğ1 les distingue.
|
||||||
- Des accents ou une ponctuation qui portent du sens restent des coquilles (« ou » / « où »,
|
- Des accents ou une ponctuation qui portent du sens restent des coquilles (« ou » / « où »,
|
||||||
« a » / « à », une virgule autour d'une relative) : le diff de chaque correction est visible
|
« a » / « à », une virgule autour d'une relative) : le diff de chaque correction est visible
|
||||||
dans l'historique de l'article.
|
dans l'historique de l'article.
|
||||||
|
|||||||
@@ -46,3 +46,4 @@ page, la langue et la taille de l'écran. Les retours se traitent dans trustinWa
|
|||||||
| 2026-10-21 | automate | local, après la septième relecture (corrections) | C1–C11, M1 | 12 / 12 verts |
|
| 2026-10-21 | automate | local, après la septième relecture (corrections) | C1–C11, M1 | 12 / 12 verts |
|
||||||
| 2026-10-21 | automate | local, liens d'équipe et huitième relecture | C1–C12, M1 | 13 / 13 verts |
|
| 2026-10-21 | automate | local, liens d'équipe et huitième relecture | C1–C12, M1 | 13 / 13 verts |
|
||||||
| 2026-10-21 | automate | local, neuvième relecture (noms, places, quantités) | C1–C12, M1 | 13 / 13 verts |
|
| 2026-10-21 | automate | local, neuvième relecture (noms, places, quantités) | C1–C12, M1 | 13 / 13 verts |
|
||||||
|
| 2026-10-21 | automate | local, mention « sur invitation » (dixième relecture) | C1–C12, M1 | 13 / 13 verts |
|
||||||
|
|||||||
@@ -25,13 +25,13 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
interface Detail {
|
interface Detail {
|
||||||
proposal: Proposal & { author: string | null };
|
proposal: Proposal & { author: string | null; authorInvited: boolean };
|
||||||
unit: { id: string; code: string; inertia: Inertia };
|
unit: { id: string; code: string; inertia: Inertia };
|
||||||
inForce: { id: string; title: string; body: string } | null;
|
inForce: { id: string; title: string; body: string } | null;
|
||||||
diff: DiffPart[] | null;
|
diff: DiffPart[] | null;
|
||||||
history: { revision: number; kind: 'initial' | 'typo' | 'substantive'; author: string | null; at: string; diff: DiffPart[] | null }[];
|
history: { revision: number; kind: 'initial' | 'typo' | 'substantive'; author: string | null; authorInvited: boolean; at: string; diff: DiffPart[] | null }[];
|
||||||
signals: {
|
signals: {
|
||||||
id: string; reason: string; author: string; mine: boolean; at: string; liftedAt: string | null; liftedBy: string | null;
|
id: string; reason: string; author: string; authorInvited: boolean; mine: boolean; at: string; liftedAt: string | null; liftedBy: string | null;
|
||||||
other: { id: string; title: string; status: string; code: string | null } | null;
|
other: { id: string; title: string; status: string; code: string | null } | null;
|
||||||
}[];
|
}[];
|
||||||
electorate: number;
|
electorate: number;
|
||||||
@@ -223,7 +223,7 @@
|
|||||||
{#if p.seedMinimal}<span class="tag water" title={t('doc.seed.long')}>{t('doc.seed')}</span>
|
{#if p.seedMinimal}<span class="tag water" title={t('doc.seed.long')}>{t('doc.seed')}</span>
|
||||||
{:else if p.confirmedAt}<span class="tag moss">{t('doc.confirmed')}</span>
|
{:else if p.confirmedAt}<span class="tag moss">{t('doc.confirmed')}</span>
|
||||||
{:else if p.enteredAt && p.status === 'in_force'}<span class="tag moss">{t('doc.adopted', { date: i18n.date(p.enteredAt) })}</span>{/if}
|
{:else if p.enteredAt && p.status === 'in_force'}<span class="tag moss">{t('doc.adopted', { date: i18n.date(p.enteredAt) })}</span>{/if}
|
||||||
{#if p.author}{t('panel.by', { name: p.author })}{:else if p.origin === 'seed'}{t('panel.seed.origin')}{/if}
|
{#if p.author}{t('panel.by', { name: p.author })}{#if p.authorInvited} · {t('author.invited')}{/if}{:else if p.origin === 'seed'}{t('panel.seed.origin')}{/if}
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -334,7 +334,7 @@
|
|||||||
<ol class="history">
|
<ol class="history">
|
||||||
{#each detail.history.slice(1) as h (h.at + h.revision)}
|
{#each detail.history.slice(1) as h (h.at + h.revision)}
|
||||||
<li>
|
<li>
|
||||||
<p class="small"><b>{t(`history.${h.kind}`)}</b> · {i18n.date(h.at)}{#if h.author} · {t('history.by', { name: h.author })}{/if}</p>
|
<p class="small"><b>{t(`history.${h.kind}`)}</b> · {i18n.date(h.at)}{#if h.author} · {t('history.by', { name: h.author })}{#if h.authorInvited} · {t('author.invited')}{/if}{/if}</p>
|
||||||
{#if h.diff}<DiffText parts={h.diff} />{/if}
|
{#if h.diff}<DiffText parts={h.diff} />{/if}
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -349,7 +349,7 @@
|
|||||||
<div class="signal" class:lifted={s.liftedAt}>
|
<div class="signal" class:lifted={s.liftedAt}>
|
||||||
<p class="small"><b>{s.other?.title ?? ''}</b> — {s.reason}</p>
|
<p class="small"><b>{s.other?.title ?? ''}</b> — {s.reason}</p>
|
||||||
<p class="faint small">
|
<p class="faint small">
|
||||||
{t('signal.by', { name: s.author })} · {i18n.date(s.at)}
|
{t('signal.by', { name: s.author })}{#if s.authorInvited} · {t('author.invited')}{/if} · {i18n.date(s.at)}
|
||||||
{#if s.liftedAt} · {s.liftedBy === 'revision' ? t('signal.lifted.revision') : t('signal.lifted')}{/if}
|
{#if s.liftedAt} · {s.liftedBy === 'revision' ? t('signal.lifted.revision') : t('signal.lifted')}{/if}
|
||||||
</p>
|
</p>
|
||||||
{#if s.mine && !s.liftedAt}<button class="btn quiet small" type="button" disabled={busy} onclick={() => lift(s.id)}>{t('signal.lift')}</button>{/if}
|
{#if s.mine && !s.liftedAt}<button class="btn quiet small" type="button" disabled={busy} onclick={() => lift(s.id)}>{t('signal.lift')}</button>{/if}
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ export const en: Messages = {
|
|||||||
'panel.inforce': 'In force',
|
'panel.inforce': 'In force',
|
||||||
'panel.proposal': 'Proposal',
|
'panel.proposal': 'Proposal',
|
||||||
'panel.by': 'proposed by {name}',
|
'panel.by': 'proposed by {name}',
|
||||||
|
'author.invited': 'by invitation',
|
||||||
'panel.seed.origin': 'text from the seed',
|
'panel.seed.origin': 'text from the seed',
|
||||||
'panel.changes': 'View changes',
|
'panel.changes': 'View changes',
|
||||||
'panel.text': 'View text',
|
'panel.text': 'View text',
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ export const es: Messages = {
|
|||||||
'panel.inforce': 'En vigor',
|
'panel.inforce': 'En vigor',
|
||||||
'panel.proposal': 'Propuesta',
|
'panel.proposal': 'Propuesta',
|
||||||
'panel.by': 'propuesto por {name}',
|
'panel.by': 'propuesto por {name}',
|
||||||
|
'author.invited': 'por invitación',
|
||||||
'panel.seed.origin': 'texto procedente del seed',
|
'panel.seed.origin': 'texto procedente del seed',
|
||||||
'panel.changes': 'Ver los cambios',
|
'panel.changes': 'Ver los cambios',
|
||||||
'panel.text': 'Ver el texto',
|
'panel.text': 'Ver el texto',
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ export const fr = {
|
|||||||
'panel.inforce': 'En vigueur',
|
'panel.inforce': 'En vigueur',
|
||||||
'panel.proposal': 'Proposition',
|
'panel.proposal': 'Proposition',
|
||||||
'panel.by': 'proposé par {name}',
|
'panel.by': 'proposé par {name}',
|
||||||
|
'author.invited': 'sur invitation',
|
||||||
'panel.seed.origin': 'texte issu du seed',
|
'panel.seed.origin': 'texte issu du seed',
|
||||||
'panel.changes': 'Voir les changements',
|
'panel.changes': 'Voir les changements',
|
||||||
'panel.text': 'Voir le texte',
|
'panel.text': 'Voir le texte',
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export interface Proposal {
|
|||||||
status: 'draft' | 'open' | 'in_force' | 'superseded' | 'applied' | 'withdrawn';
|
status: 'draft' | 'open' | 'in_force' | 'superseded' | 'applied' | 'withdrawn';
|
||||||
origin: 'seed' | 'member';
|
origin: 'seed' | 'member';
|
||||||
label: string | null;
|
label: string | null;
|
||||||
author: { id: string; name: string | null } | null;
|
author: { id: string; name: string | null; invited: boolean } | null;
|
||||||
mine: boolean;
|
mine: boolean;
|
||||||
revision: number;
|
revision: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user