From e70a70803468e5dadcefeb84093e952db7cc97fa Mon Sep 17 00:00:00 2001 From: Tale Agent Date: Wed, 24 Jun 2026 10:09:08 +0000 Subject: [PATCH 1/3] fix(platform): use icon buttons for inline table row actions and confirm passkey revoke (#1963) Convert non-dropdown table/list row actions to IconButtons (with tooltips via aria-label) and route the destructive passkey revoke through a DeleteDialog confirmation. - passkey-section: the ghost "Remove" text button is now a trash IconButton that opens a DeleteDialog before revoking (previously revoked on click with no confirmation). - moderation-mapping-list: row Edit text button -> IconButton. - chat-filter-config: row Edit/Delete buttons -> IconButton for consistency. - Added passkeys.revokeConfirm{Title,Description} and moderationProvider.editMappingAria keys to en/de/fr; updated the 2FA docs. Resolves #1963. --- .../admin/two-factor-authentication.md | 2 +- .../admin/two-factor-authentication.md | 2 +- .../admin/two-factor-authentication.md | 2 +- .../account/components/passkey-section.tsx | 42 ++++++++++++++++--- .../components/chat-filter-config.tsx | 10 ++--- .../components/moderation-mapping-list.tsx | 12 +++--- services/platform/messages/de.json | 3 ++ services/platform/messages/en.json | 3 ++ services/platform/messages/fr.json | 3 ++ 9 files changed, 60 insertions(+), 19 deletions(-) diff --git a/docs/de/platform/admin/two-factor-authentication.md b/docs/de/platform/admin/two-factor-authentication.md index 7e3363ba2..5a2bf1294 100644 --- a/docs/de/platform/admin/two-factor-authentication.md +++ b/docs/de/platform/admin/two-factor-authentication.md @@ -23,7 +23,7 @@ Behandle Backup-Codes wie Passwörter. Lege sie in einen Passwort-Manager oder d Ein Passkey ist ein WebAuthn-Credential — Face ID, Touch ID, Windows Hello oder ein Hardware-Security-Key —, das bei jeder Anmeldung eine Challenge signiert, statt einen getippten Code zu liefern. Das Credential ist an die Origin der Site gebunden; eine täuschend ähnliche Phishing-Domain bekommt nichts, was sie wiederverwenden könnte. Ein Passkey ist dadurch phishing-resistent auf eine Art, die TOTP nicht erreicht, und er erfüllt eine erzwungene Zwei-Faktor-Richtlinie genau wie TOTP. -Zum Registrieren öffne **Konto > Sicherheit** und klick auf **Passkey hinzufügen**. Gib dem Credential einen Namen, den du später wiedererkennst, und wähle den **Authenticator-Typ**: **Beliebig (empfohlen)** lässt den Browser alles anbieten, was verfügbar ist, **Dieses Gerät (Face ID, Touch ID, Windows Hello)** beschränkt die Zeremonie auf den eingebauten Authenticator, und **Security-Key oder Smartphone** auf einen externen. Den Rest erledigt der Browser mit der Registrierungszeremonie. Dieselbe Liste trägt **Entfernen** zum Widerrufen deiner eigenen Credentials. +Zum Registrieren öffne **Konto > Sicherheit** und klick auf **Passkey hinzufügen**. Gib dem Credential einen Namen, den du später wiedererkennst, und wähle den **Authenticator-Typ**: **Beliebig (empfohlen)** lässt den Browser alles anbieten, was verfügbar ist, **Dieses Gerät (Face ID, Touch ID, Windows Hello)** beschränkt die Zeremonie auf den eingebauten Authenticator, und **Security-Key oder Smartphone** auf einen externen. Den Rest erledigt der Browser mit der Registrierungszeremonie. Jeder Eintrag in derselben Liste trägt eine **Entfernen**-Schaltfläche (Symbol) zum Widerrufen deiner eigenen Credentials; sie fragt vor dem Entfernen des Passkeys nach einer Bestätigung. Ein registrierter Passkey funktioniert an drei Türen. Auf dem Login-Bildschirm meldet dich **Mit einem Passkey anmelden** ohne Passwort an — das Credential ist selbst ein starker Nachweis. Auf dem Bestätigungs-Bildschirm nach einem Passwort-Login ersetzt **Stattdessen einen Passkey verwenden** den sechsstelligen Code. Und auf dem Registrierungs-Bildschirm, zu dem eine erzwungene Richtlinie nicht registrierte Mitglieder leitet, sitzt **Stattdessen einen Passkey registrieren** neben der TOTP-Einrichtung — ein Mitglied, das nur einen Passkey registriert und nie TOTP, besteht die Richtlinie. diff --git a/docs/en/platform/admin/two-factor-authentication.md b/docs/en/platform/admin/two-factor-authentication.md index 090949dd5..d736c716b 100644 --- a/docs/en/platform/admin/two-factor-authentication.md +++ b/docs/en/platform/admin/two-factor-authentication.md @@ -23,7 +23,7 @@ Treat backup codes like passwords. Store them in a password manager or print the A passkey is a WebAuthn credential — Face ID, Touch ID, Windows Hello, or a hardware security key — that signs a per-login challenge instead of producing a typed code. The credential is bound to the site's origin, so a look-alike phishing domain gets nothing to replay; that makes a passkey phishing-resistant in a way TOTP is not, and it satisfies an enforced two-factor policy exactly like TOTP does. -To register one, open **Account > Security** and click **Add a passkey**. Give the credential a name you will recognise later, then pick the **Authenticator type**: **Any (recommended)** lets the browser offer everything available, **This device (Face ID, Touch ID, Windows Hello)** narrows the ceremony to the built-in authenticator, and **Security key or phone** narrows it to a roaming one. The browser runs the registration ceremony from there. The same list carries **Remove** for revoking your own credentials. +To register one, open **Account > Security** and click **Add a passkey**. Give the credential a name you will recognise later, then pick the **Authenticator type**: **Any (recommended)** lets the browser offer everything available, **This device (Face ID, Touch ID, Windows Hello)** narrows the ceremony to the built-in authenticator, and **Security key or phone** narrows it to a roaming one. The browser runs the registration ceremony from there. Each entry in the same list carries a **Remove** icon button for revoking your own credentials; it asks you to confirm before the passkey is removed. A registered passkey works at three doors. On the login screen, **Sign in with a passkey** signs you in without typing the password — the credential is itself strong proof. On the verification screen after a password login, **Use a passkey instead** replaces the six-digit code. And on the enrolment screen an enforced policy routes unenrolled members to, **Register a passkey instead** sits next to the TOTP setup — a member who registers only a passkey, never TOTP, passes the policy. diff --git a/docs/fr/platform/admin/two-factor-authentication.md b/docs/fr/platform/admin/two-factor-authentication.md index 03ce2040b..10719fb1f 100644 --- a/docs/fr/platform/admin/two-factor-authentication.md +++ b/docs/fr/platform/admin/two-factor-authentication.md @@ -23,7 +23,7 @@ Traite les codes de secours comme des mots de passe. Range-les dans un gestionna Un passkey est un identifiant WebAuthn — Face ID, Touch ID, Windows Hello ou une clé de sécurité matérielle — qui signe un défi à chaque connexion au lieu de produire un code à taper. L'identifiant est lié à l'origine du site : un domaine de phishing qui lui ressemble n'obtient rien à rejouer. Un passkey résiste donc au phishing d'une façon que TOTP n'atteint pas, et il satisfait une politique de deux facteurs appliquée exactement comme TOTP. -Pour en enregistrer un, ouvre **Compte > Sécurité** et clique sur **Ajouter un passkey**. Donne à l'identifiant un nom que tu reconnaîtras plus tard, puis choisis le **Type d'authentificateur** : **Indifférent (recommandé)** laisse le navigateur proposer tout ce qui est disponible, **Cet appareil (Face ID, Touch ID, Windows Hello)** restreint la cérémonie à l'authentificateur intégré, et **Clé de sécurité ou téléphone** à un authentificateur itinérant. Le navigateur déroule la cérémonie d'enregistrement à partir de là. La même liste porte **Supprimer** pour révoquer tes propres passkeys. +Pour en enregistrer un, ouvre **Compte > Sécurité** et clique sur **Ajouter un passkey**. Donne à l'identifiant un nom que tu reconnaîtras plus tard, puis choisis le **Type d'authentificateur** : **Indifférent (recommandé)** laisse le navigateur proposer tout ce qui est disponible, **Cet appareil (Face ID, Touch ID, Windows Hello)** restreint la cérémonie à l'authentificateur intégré, et **Clé de sécurité ou téléphone** à un authentificateur itinérant. Le navigateur déroule la cérémonie d'enregistrement à partir de là. Chaque entrée de la même liste porte un bouton-icône **Supprimer** pour révoquer tes propres passkeys ; il demande une confirmation avant de supprimer le passkey. Un passkey enregistré fonctionne à trois portes. Sur l'écran de connexion, **Se connecter avec un passkey** te connecte sans taper le mot de passe — l'identifiant est lui-même une preuve forte. Sur l'écran de vérification après une connexion par mot de passe, **Utiliser un passkey à la place** remplace le code à six chiffres. Et sur l'écran d'inscription vers lequel une politique appliquée route les membres non inscrits, **Enregistrer un passkey à la place** se trouve à côté de la configuration TOTP — un membre qui n'enregistre qu'un passkey, jamais TOTP, passe la politique. diff --git a/services/platform/app/features/settings/account/components/passkey-section.tsx b/services/platform/app/features/settings/account/components/passkey-section.tsx index 686116a95..f73466966 100644 --- a/services/platform/app/features/settings/account/components/passkey-section.tsx +++ b/services/platform/app/features/settings/account/components/passkey-section.tsx @@ -1,11 +1,14 @@ 'use client'; import { Button } from '@tale/ui/button'; +import { IconButton } from '@tale/ui/icon-button'; import { HStack, Stack, VStack } from '@tale/ui/layout'; import { Text } from '@tale/ui/text'; import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { Trash2 } from 'lucide-react'; import { useState } from 'react'; +import { DeleteDialog } from '@/app/components/ui/dialog/delete-dialog'; import { SettingsSection } from '@/app/features/settings/components/settings-section'; import { useConvexQuery } from '@/app/hooks/use-convex-query'; import { useToast } from '@/app/hooks/use-toast'; @@ -43,6 +46,11 @@ export function PasskeySection() { const { data: status } = useConvexQuery(api.two_factor.queries.getStatus, {}); const [registerDialogOpen, setRegisterDialogOpen] = useState(false); + // Revoking a passkey is destructive (it can drop the user's only + // phishing-resistant second factor), so it confirms via `DeleteDialog` + // rather than firing on the row's click. Track the row pending revocation. + const [revokeTarget, setRevokeTarget] = useState(null); + const [isRevoking, setIsRevoking] = useState(false); const { data: passkeys, isLoading } = useQuery({ queryKey: PASSKEYS_QUERY_KEY, @@ -62,9 +70,13 @@ export function PasskeySection() { void queryClient.invalidateQueries({ queryKey: PASSKEYS_QUERY_KEY }); } - async function revoke(id: string) { + async function confirmRevoke() { + if (!revokeTarget) return; + setIsRevoking(true); try { - const result = await authClient.passkey.deletePasskey({ id }); + const result = await authClient.passkey.deletePasskey({ + id: revokeTarget.id, + }); if (result?.error) { toast({ title: t('passkeys.errors.revokeFailed'), @@ -74,11 +86,14 @@ export function PasskeySection() { } toast({ title: t('passkeys.revoked'), variant: 'success' }); invalidate(); + setRevokeTarget(null); } catch { toast({ title: t('passkeys.errors.revokeFailed'), variant: 'destructive', }); + } finally { + setIsRevoking(false); } } @@ -107,9 +122,12 @@ export function PasskeySection() { {pk.name?.trim() || t('passkeys.unnamed')} - + setRevokeTarget(pk)} + /> ))} @@ -129,6 +147,20 @@ export function PasskeySection() { invalidate(); }} /> + + { + if (!open) setRevokeTarget(null); + }} + title={t('passkeys.revokeConfirmTitle')} + description={t('passkeys.revokeConfirmDescription', { + name: revokeTarget?.name?.trim() || t('passkeys.unnamed'), + })} + deleteText={t('passkeys.revokeButton')} + isDeleting={isRevoking} + onDelete={() => void confirmRevoke()} + /> ); } diff --git a/services/platform/app/features/settings/governance/components/chat-filter-config.tsx b/services/platform/app/features/settings/governance/components/chat-filter-config.tsx index 75324a8ee..d3d9cff82 100644 --- a/services/platform/app/features/settings/governance/components/chat-filter-config.tsx +++ b/services/platform/app/features/settings/governance/components/chat-filter-config.tsx @@ -2,6 +2,7 @@ import { Alert } from '@tale/ui/alert'; import { Button } from '@tale/ui/button'; +import { IconButton } from '@tale/ui/icon-button'; import { Row, Stack } from '@tale/ui/layout'; import { Skeletonize } from '@tale/ui/skeleton-context'; import { @@ -662,7 +663,6 @@ function CategoryList({ onToggleEnabled, }: CategoryListProps) { const { t } = useT('governance'); - const { t: tCommon } = useT('common'); return ( {categories.length === 0 ? ( @@ -700,7 +700,7 @@ function CategoryList({ {category.patterns.length} - - + /> ))} diff --git a/services/platform/messages/de.json b/services/platform/messages/de.json index e59e126c8..694c90559 100644 --- a/services/platform/messages/de.json +++ b/services/platform/messages/de.json @@ -3277,6 +3277,7 @@ "yes": "Ja", "no": "Nein", "addMapping": "Kategorie-Zuordnung hinzufügen", + "editMappingAria": "Zuordnung für {category} bearbeiten", "editMappingTitle": "Kategorie-Zuordnung bearbeiten", "addMappingTitle": "Kategorie-Zuordnung hinzufügen", "deleteMappingConfirmTitle": "Kategorie-Zuordnung löschen?", @@ -6925,6 +6926,8 @@ "description": "Melde dich mit einem Passkey an (Face ID, Touch ID, Windows Hello oder ein Security-Key). Ein Passkey ist phishing-resistent und erfüllt die Zwei-Faktor-Anforderung deiner Organisation.", "addButton": "Passkey hinzufügen", "revokeButton": "Entfernen", + "revokeConfirmTitle": "Passkey entfernen?", + "revokeConfirmDescription": "Dadurch wird „{name}\" entfernt und kann nicht mehr zur Anmeldung verwendet werden. War es dein einziger zweiter Faktor, wirst du möglicherweise erneut zur Einrichtung der Zwei-Faktor-Authentifizierung aufgefordert.", "empty": "Noch keine Passkeys registriert.", "unnamed": "Passkey", "registered": "Passkey registriert", diff --git a/services/platform/messages/en.json b/services/platform/messages/en.json index fc09ec3de..76c6de0ae 100644 --- a/services/platform/messages/en.json +++ b/services/platform/messages/en.json @@ -3277,6 +3277,7 @@ "yes": "Yes", "no": "No", "addMapping": "Add category mapping", + "editMappingAria": "Edit mapping for {category}", "editMappingTitle": "Edit category mapping", "addMappingTitle": "Add category mapping", "deleteMappingConfirmTitle": "Delete category mapping?", @@ -7187,6 +7188,8 @@ "description": "Sign in with a passkey (Face ID, Touch ID, Windows Hello, or a security key). A passkey is phishing-resistant and satisfies your organization's two-factor requirement.", "addButton": "Add a passkey", "revokeButton": "Remove", + "revokeConfirmTitle": "Remove passkey?", + "revokeConfirmDescription": "This removes \"{name}\" and it can no longer be used to sign in. If it was your only second factor, you may be prompted to set up two-factor authentication again.", "empty": "No passkeys registered yet.", "unnamed": "Passkey", "registered": "Passkey registered", diff --git a/services/platform/messages/fr.json b/services/platform/messages/fr.json index e3bd63e2f..1a0d07f19 100644 --- a/services/platform/messages/fr.json +++ b/services/platform/messages/fr.json @@ -3278,6 +3278,7 @@ "yes": "Oui", "no": "Non", "addMapping": "Ajouter une correspondance", + "editMappingAria": "Modifier le mappage pour {category}", "editMappingTitle": "Modifier la correspondance", "addMappingTitle": "Ajouter une correspondance", "deleteMappingConfirmTitle": "Supprimer la correspondance ?", @@ -6926,6 +6927,8 @@ "description": "Connecte-toi avec un passkey (Face ID, Touch ID, Windows Hello ou une clé de sécurité). Un passkey est résistant au phishing et satisfait l'exigence de double authentification de ton organisation.", "addButton": "Ajouter un passkey", "revokeButton": "Supprimer", + "revokeConfirmTitle": "Supprimer le passkey ?", + "revokeConfirmDescription": "Cela supprime « {name} » qui ne pourra plus être utilisé pour se connecter. S'il s'agissait de ton seul second facteur, il se peut que tu doives reconfigurer la double authentification.", "empty": "Aucun passkey enregistré pour l'instant.", "unnamed": "Passkey", "registered": "Passkey enregistré", From b627cc99661ffdf5a95604202558f749d5af0227 Mon Sep 17 00:00:00 2001 From: Tale Agent Date: Wed, 24 Jun 2026 12:57:50 +0000 Subject: [PATCH 2/3] fix(platform): use icon buttons for legal-hold row actions (#1963) --- .../legal-hold/active-holds-section.tsx | 11 ++++--- .../governance/legal-hold/matters-section.tsx | 20 ++++++------ .../legal-hold/release-requests-section.tsx | 31 ++++++++++--------- 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/services/platform/app/features/settings/governance/legal-hold/active-holds-section.tsx b/services/platform/app/features/settings/governance/legal-hold/active-holds-section.tsx index fbe71254d..0b32e19c3 100644 --- a/services/platform/app/features/settings/governance/legal-hold/active-holds-section.tsx +++ b/services/platform/app/features/settings/governance/legal-hold/active-holds-section.tsx @@ -2,10 +2,11 @@ import { Badge } from '@tale/ui/badge'; import { Button } from '@tale/ui/button'; +import { IconButton } from '@tale/ui/icon-button'; import { Row, Stack } from '@tale/ui/layout'; import { Text } from '@tale/ui/text'; import type { ColumnDef } from '@tanstack/react-table'; -import { Lock } from 'lucide-react'; +import { Lock, LockOpen } from 'lucide-react'; import { useMemo, useState } from 'react'; import { TableDateCell } from '@/app/components/ui/data-display/table-date-cell'; @@ -157,16 +158,16 @@ export function ActiveHoldsSection({ meta: { isAction: true, align: 'right' as const }, cell: ({ row }) => ( - + /> ), size: 140, diff --git a/services/platform/app/features/settings/governance/legal-hold/matters-section.tsx b/services/platform/app/features/settings/governance/legal-hold/matters-section.tsx index ff68dcf6e..3d6f2e612 100644 --- a/services/platform/app/features/settings/governance/legal-hold/matters-section.tsx +++ b/services/platform/app/features/settings/governance/legal-hold/matters-section.tsx @@ -2,9 +2,11 @@ import { Badge } from '@tale/ui/badge'; import { Button } from '@tale/ui/button'; +import { IconButton } from '@tale/ui/icon-button'; import { Row } from '@tale/ui/layout'; import { Text } from '@tale/ui/text'; import type { ColumnDef } from '@tanstack/react-table'; +import { Archive, Pencil } from 'lucide-react'; import { useMemo, useState } from 'react'; import { TableDateCell } from '@/app/components/ui/data-display/table-date-cell'; @@ -92,30 +94,30 @@ export function MattersSection({ organizationId }: MattersSectionProps) { header: t('legalHold.columns.actions'), meta: { isAction: true, align: 'right' as const }, cell: ({ row }) => ( - - + /> {row.original.status === 'open' && ( - + /> )} ), diff --git a/services/platform/app/features/settings/governance/legal-hold/release-requests-section.tsx b/services/platform/app/features/settings/governance/legal-hold/release-requests-section.tsx index 43f060c04..5fa963b09 100644 --- a/services/platform/app/features/settings/governance/legal-hold/release-requests-section.tsx +++ b/services/platform/app/features/settings/governance/legal-hold/release-requests-section.tsx @@ -1,10 +1,11 @@ 'use client'; import { Badge } from '@tale/ui/badge'; -import { Button } from '@tale/ui/button'; +import { IconButton } from '@tale/ui/icon-button'; import { Row, Stack } from '@tale/ui/layout'; import { Text } from '@tale/ui/text'; import type { ColumnDef } from '@tanstack/react-table'; +import { Check, X } from 'lucide-react'; import { useMemo, useState } from 'react'; import { TableDateCell } from '@/app/components/ui/data-display/table-date-cell'; @@ -99,34 +100,34 @@ export function ReleaseRequestsSection({ cell: ({ row }) => { const isSelf = currentUser?.userId === row.original.requestedBy; return ( - - - + /> ); }, From 5752dff3564bc280dfd263d8ed3346a5f5f55163 Mon Sep 17 00:00:00 2001 From: Tale Agent Date: Wed, 24 Jun 2026 14:32:04 +0000 Subject: [PATCH 3/3] fix(platform): use icon button for run-list watch-live action (#1963) --- .../features/apps/registry/connected/run-list.tsx | 15 ++++++++------- .../legal-hold/active-holds-section.tsx | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/services/platform/app/features/apps/registry/connected/run-list.tsx b/services/platform/app/features/apps/registry/connected/run-list.tsx index 26c2e9f04..c9349da3e 100644 --- a/services/platform/app/features/apps/registry/connected/run-list.tsx +++ b/services/platform/app/features/apps/registry/connected/run-list.tsx @@ -7,7 +7,7 @@ * per-step live panels), reusing it rather than rebuilding a live view here. */ import { Badge } from '@tale/ui/badge'; -import { Button } from '@tale/ui/button'; +import { IconButton } from '@tale/ui/icon-button'; import { SkeletonText } from '@tale/ui/skeleton'; import { Table, @@ -19,7 +19,7 @@ import { } from '@tale/ui/table'; import { Text } from '@tale/ui/text'; import { useNavigate } from '@tanstack/react-router'; -import { Activity } from 'lucide-react'; +import { Activity, Eye } from 'lucide-react'; import { useMemo } from 'react'; import { useT } from '@/lib/i18n/client'; @@ -106,8 +106,11 @@ export function RunList({ title, workflowSlug }: RunListProps) { {fmt(run.startedAt)} - + /> ); diff --git a/services/platform/app/features/settings/governance/legal-hold/active-holds-section.tsx b/services/platform/app/features/settings/governance/legal-hold/active-holds-section.tsx index 0b32e19c3..d90ea99dc 100644 --- a/services/platform/app/features/settings/governance/legal-hold/active-holds-section.tsx +++ b/services/platform/app/features/settings/governance/legal-hold/active-holds-section.tsx @@ -161,6 +161,7 @@ export function ActiveHoldsSection({ {