forked from yvv/decision
v3 : liens d'équipe, publication sur git.open.us.org, huitième relecture
Liens d'équipe (Yvv, 2026-10-21) : un lien réutilisable jusqu'à une date
et pour un nombre de personnes, révocable. Chaque ouverture demande un nom
et crée sa propre identité d'invitation, jamais administratrice ; un nom
déjà pris est refusé ; une personne déjà connectée reçoit les groupes.
trustinWallet montre par quel lien chaque identité est venue, et donne un
nouveau lien de connexion à une personne invitée (autre appareil).
Migration additive 0002 (table team_links, colonne identities.team_link).
Publication : yvv/libredecision-textes sur git.open.us.org, pour
l'instant (un dépôt pour l'ouverture, puis le dépôt officiel, viendront).
Huitième relecture (c3d6bf8 : ok), points corrigés :
- une négation n'est jamais coupée ni formée par collage (« ja mais ») ;
- caractères invisibles : tout l'ensemble Unicode des ignorables et les
contrôles autres que saut de ligne et tabulation ;
- sauts de ligne et indentation font partie de la forme (listes) ;
- un titre tient sur une ligne.
Boucle : 65 tests Rust, svelte-check, 9 vitest, e2e 13/13 (C12 ajouté).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,16 +2,18 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { api, ApiError } from '$lib/api';
|
||||
import Guard from '$lib/components/Guard.svelte';
|
||||
import LinkReveal from '$lib/components/LinkReveal.svelte';
|
||||
import TeamLinks, { type TeamLink } from '$lib/components/TeamLinks.svelte';
|
||||
import { i18n, t, type Key } from '$lib/i18n/index.svelte';
|
||||
import { Remote } from '$lib/load.svelte';
|
||||
import { session } from '$lib/session.svelte';
|
||||
import { toasts } from '$lib/toast.svelte';
|
||||
|
||||
interface Overview {
|
||||
publication: { remote: string | null; publicKey: string | null };
|
||||
groups: { slug: string; name: string; policy: string; hidden: boolean; members: { identityId: string; grantedAt: string }[] }[];
|
||||
identities: { id: string; kind: 'g1' | 'invite' | 'dev'; displayName: string; g1Member: boolean; admin: boolean; lastSeenAt: string | null }[];
|
||||
identities: { id: string; kind: 'g1' | 'invite' | 'dev'; displayName: string; g1Member: boolean; admin: boolean; lastSeenAt: string | null; teamLink: string | null }[];
|
||||
invitations: { id: string; label: string; admin: boolean; groups: string[]; expiresAt: string }[];
|
||||
teamLinks: TeamLink[];
|
||||
}
|
||||
interface Feedback {
|
||||
feedback: { id: number; author: string | null; route: string; body: string; createdAt: string; status: 'new' | 'seen' | 'done' }[];
|
||||
@@ -29,6 +31,7 @@
|
||||
let groups = $state<string[]>([]);
|
||||
let admin = $state(false);
|
||||
let link = $state('');
|
||||
let relinked = $state<{ id: string; name: string; link: string } | null>(null);
|
||||
let error = $state('');
|
||||
|
||||
const nameOf = (id: string) => data.value?.identities.find((i) => i.id === id)?.displayName ?? id;
|
||||
@@ -54,9 +57,12 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function copy() {
|
||||
await navigator.clipboard.writeText(link);
|
||||
toasts.show(t('access.invite.copied'));
|
||||
// A new sign-in link for an invited person (another device, a lost session).
|
||||
async function relink(id: string, name: string) {
|
||||
await act(async () => {
|
||||
const r = await api.post<{ link: string }>('/access/invitations', { label: name, identityId: id });
|
||||
relinked = { id, name, link: r.link };
|
||||
});
|
||||
}
|
||||
|
||||
async function status(id: number, s: 'seen' | 'done') {
|
||||
@@ -118,13 +124,7 @@
|
||||
<label class="check"><input type="checkbox" bind:checked={admin} /> {t('access.invite.admin')}</label>
|
||||
</fieldset>
|
||||
<button class="btn primary small" type="submit">{t('access.invite.create')}</button>
|
||||
{#if link}
|
||||
<div class="link">
|
||||
<p class="small">{t('access.invite.link')}</p>
|
||||
<code>{link}</code>
|
||||
<button class="btn small" type="button" onclick={copy}>{t('access.invite.copy')}</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if link}<LinkReveal {link} note={t('access.invite.link')} />{/if}
|
||||
</form>
|
||||
{#if data.value.invitations.length}
|
||||
<ul class="list">
|
||||
@@ -138,6 +138,8 @@
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<TeamLinks groups={data.value.groups} links={data.value.teamLinks} {act} />
|
||||
|
||||
<section>
|
||||
<h2 class="eyebrow">{t('access.identities')}</h2>
|
||||
<ul class="list">
|
||||
@@ -147,9 +149,14 @@
|
||||
<b>{i.displayName}</b> · {t(`access.kind.${i.kind}`)}
|
||||
{#if i.g1Member} · {t('access.member')}{/if}
|
||||
{#if i.admin} · <span class="tag water">{t('access.admin')}</span>{/if}
|
||||
{#if i.teamLink} · <span class="faint small">{t('access.team.via', { label: i.teamLink })}</span>{/if}
|
||||
</span>
|
||||
<span class="row">
|
||||
{#if i.kind === 'invite'}<button class="btn quiet small" type="button" onclick={() => relink(i.id, i.displayName)}>{t('access.relink')}</button>{/if}
|
||||
<button class="btn quiet small" type="button" onclick={() => act(() => api.post(`/access/identities/${encodeURIComponent(i.id)}/admin`, { admin: !i.admin }))}>{i.admin ? t('access.admin.take') : t('access.admin.give')}</button>
|
||||
</span>
|
||||
<button class="btn quiet small" type="button" onclick={() => act(() => api.post(`/access/identities/${encodeURIComponent(i.id)}/admin`, { admin: !i.admin }))}>{i.admin ? t('access.admin.take') : t('access.admin.give')}</button>
|
||||
</li>
|
||||
{#if relinked?.id === i.id}<li class="reveal"><LinkReveal link={relinked.link} note={t('access.relink.link', { name: relinked.name })} /></li>{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
@@ -253,19 +260,8 @@
|
||||
.block > .btn {
|
||||
justify-self: start;
|
||||
}
|
||||
.link {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
padding: 0.8rem;
|
||||
border-radius: var(--r-input);
|
||||
background: var(--water-wash);
|
||||
}
|
||||
.link code {
|
||||
word-break: break-all;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.link .btn {
|
||||
justify-self: start;
|
||||
.list li.reveal {
|
||||
display: block;
|
||||
}
|
||||
.small {
|
||||
font-size: 0.84rem;
|
||||
|
||||
Reference in New Issue
Block a user