From 210cebdfc33f93bc01597564df3dac570e5ef79f Mon Sep 17 00:00:00 2001 From: Tale Agent Date: Wed, 24 Jun 2026 15:44:32 +0000 Subject: [PATCH 1/2] fix: rename profile "Display name" to "Name" and auto-derive name from email (#1941) --- docs/de/platform/member/preferences.md | 6 +- docs/en/platform/member/preferences.md | 6 +- docs/fr/platform/member/preferences.md | 6 +- .../onboarding/steps/account-step.tsx | 40 ++++++- .../account/components/account-form.tsx | 40 ++++--- .../components/member-add-dialog.tsx | 38 +++++-- .../lib/utils/derive-name-from-email.test.ts | 102 ++++++++++++++++++ .../lib/utils/derive-name-from-email.ts | 78 ++++++++++++++ services/platform/messages/de.json | 7 +- services/platform/messages/en.json | 7 +- services/platform/messages/fr.json | 7 +- services/platform/tests/manual/responsive.md | 18 ++-- services/platform/tests/manual/settings.md | 2 +- 13 files changed, 303 insertions(+), 54 deletions(-) create mode 100644 services/platform/lib/utils/derive-name-from-email.test.ts create mode 100644 services/platform/lib/utils/derive-name-from-email.ts diff --git a/docs/de/platform/member/preferences.md b/docs/de/platform/member/preferences.md index f2a4e6c24..bd7da5779 100644 --- a/docs/de/platform/member/preferences.md +++ b/docs/de/platform/member/preferences.md @@ -1,9 +1,9 @@ --- title: Einstellungen -description: Die Mitglieder-Einstellungen, die dir über Organisationen und Chats hinweg folgen — Anzeigename und Passwort unter Konto, Theme und Locale im Profilmenü, eigene Anweisungen und Erinnerungen unter Personalisierung, und Abmelden. +description: Die Mitglieder-Einstellungen, die dir über Organisationen und Chats hinweg folgen — Name und Passwort unter Konto, Theme und Locale im Profilmenü, eigene Anweisungen und Erinnerungen unter Personalisierung, und Abmelden. --- -Einstellungen sind die Schrauben, die dir gehören, nicht der Organisation. Dein Anzeigename ist der Name, den Agents und Teamkolleginnen in Chats und Genehmigungen sehen. Deine Locale und dein Theme folgen dir zwischen Geräten. Deine eigenen Anweisungen und Erinnerungen prägen, wie Agents speziell dir antworten — getrennt von allem, was Admin oder Redakteur auf Organisationsebene gesetzt hat. Diese Seite zeigt, wo jeder Hebel sitzt und was er ändert. +Einstellungen sind die Schrauben, die dir gehören, nicht der Organisation. Dein Name ist das, was Agents und Teamkolleginnen in Chats und Genehmigungen sehen. Deine Locale und dein Theme folgen dir zwischen Geräten. Deine eigenen Anweisungen und Erinnerungen prägen, wie Agents speziell dir antworten — getrennt von allem, was Admin oder Redakteur auf Organisationsebene gesetzt hat. Diese Seite zeigt, wo jeder Hebel sitzt und was er ändert. Die Form ist bewusst zweischichtig: das Profilmenü (überall, einen Klick vom Avatar entfernt) trägt die schnellen Schalter; **Einstellungen > Konto** und **Einstellungen > Personalisierung** tragen die tieferen Kontofelder. Alles hier gehört dir — nichts davon lecken zu anderen Mitgliedern oder anderen Organisationen durch. @@ -17,7 +17,7 @@ Das Menü trägt außerdem einen Organisationswechsler, wenn du zu mehr als eine Öffne **Einstellungen > Konto**. Drei Abschnitte sitzen auf der Seite: **Profil**, **Sicherheit** und **Zwei-Faktor-Authentifizierung**. -Der Profil-Abschnitt hält deinen **Anzeigenamen** und deine **E-Mail**. Der Anzeigename ist inline bearbeitbar; die Änderung speichert beim Verlassen des Felds und schlägt beim nächsten Render in jedem Chat und jeder Genehmigung durch. Die E-Mail ist schreibgeschützt — sie ist das, womit du dich angemeldet hast, und ein Wechsel läuft über den Support. Es gibt kein Avatar-Feld auf der Seite; Tale leitet einen Avatar aus den Initialen deines Anzeigenamens ab. +Der Profil-Abschnitt zeigt zuerst deine **E-Mail**, dann deinen **Namen** — die E-Mail legt den Namen nahe, den Tale vorschlägt und den du frei bearbeiten kannst. Der Name ist inline bearbeitbar; die Änderung speichert und schlägt beim nächsten Render in jedem Chat und jeder Genehmigung durch. Die E-Mail ist schreibgeschützt — sie ist das, womit du dich angemeldet hast, und ein Wechsel läuft über den Support. Es gibt kein Avatar-Feld auf der Seite; Tale leitet einen Avatar aus den Initialen deines Namens ab. Der Sicherheits-Abschnitt hält einen einzelnen Knopf: **Passwort ändern**, wenn du dich mit E-Mail und Passwort registriert hast, **Passwort setzen**, wenn dein Konto über SSO föderiert ist und du ein Passwort als Rückfall hinzufügen willst. Beide Abläufe erzwingen die Passwort-Richtlinie der Organisation und zeigen die Regeln live, während du tippst. Der Zwei-Faktor-Abschnitt paart das Konto mit einer TOTP-App oder einem Hardware-Schlüssel und zeigt die Backup-Codes einmal bei der Einrichtung. diff --git a/docs/en/platform/member/preferences.md b/docs/en/platform/member/preferences.md index 1362a9c41..ee89583db 100644 --- a/docs/en/platform/member/preferences.md +++ b/docs/en/platform/member/preferences.md @@ -1,9 +1,9 @@ --- title: Preferences -description: The member-level settings that follow you across orgs and chats — display name and password under Account, theme and locale in the profile menu, custom instructions and memories under Personalization, and sign-out. +description: The member-level settings that follow you across orgs and chats — name and password under Account, theme and locale in the profile menu, custom instructions and memories under Personalization, and sign-out. --- -Preferences are the dials that belong to you rather than to the org. Your display name is the name agents and teammates see in chats and approvals. Your locale and theme follow you between devices. Your custom instructions and memories shape how agents reply to you specifically — separately from anything the Admin or Editor has set at the org level. This page maps where each lever lives and what it changes. +Preferences are the dials that belong to you rather than to the org. Your name is what agents and teammates see in chats and approvals. Your locale and theme follow you between devices. Your custom instructions and memories shape how agents reply to you specifically — separately from anything the Admin or Editor has set at the org level. This page maps where each lever lives and what it changes. The shape is intentionally two-layered: the profile menu (everywhere, one click from the avatar) carries the quick toggles; **Settings > Account** and **Settings > Personalization** carry the deeper account fields. Everything here is yours — none of it leaks to other members or other orgs. @@ -17,7 +17,7 @@ The menu also carries an organisation switcher when you belong to more than one Open **Settings > Account**. Three sections sit on the page: **Profile**, **Security**, and **Two-factor authentication**. -The Profile section holds your **display name** and your **email**. The display name is editable inline; the change saves on blur and propagates to every chat and approval the next time they render. Email is read-only — it is what you signed in with, and changing it goes through support. There is no avatar field on the page; Tale derives an avatar from your display name initials. +The Profile section shows your **email** first, then your **name** — the email implies the name Tale suggests, which you can edit freely. The name is editable inline; the change saves and propagates to every chat and approval the next time they render. Email is read-only — it is what you signed in with, and changing it goes through support. There is no avatar field on the page; Tale derives an avatar from your name's initials. The Security section holds a single button: **Change password** if you signed up with email and password, **Set password** if your account is federated through SSO and you want to add a password as a fallback. Both flows enforce the org's password policy and surface the rules live as you type. The Two-factor section pairs the account with a TOTP app or a hardware key and shows the backup codes once at enrolment. diff --git a/docs/fr/platform/member/preferences.md b/docs/fr/platform/member/preferences.md index 03cb11ba0..3276d6446 100644 --- a/docs/fr/platform/member/preferences.md +++ b/docs/fr/platform/member/preferences.md @@ -1,9 +1,9 @@ --- title: Préférences -description: Les réglages au niveau membre qui te suivent entre orgs et chats — nom d'affichage et mot de passe sous Compte, thème et langue dans le menu de profil, instructions personnalisées et mémoires sous Personnalisation, et déconnexion. +description: Les réglages au niveau membre qui te suivent entre orgs et chats — nom et mot de passe sous Compte, thème et langue dans le menu de profil, instructions personnalisées et mémoires sous Personnalisation, et déconnexion. --- -Les préférences sont les molettes qui t'appartiennent plutôt qu'à l'org. Ton nom d'affichage est le nom que voient agents et coéquipiers dans les chats et les approbations. Ta langue et ton thème te suivent entre les appareils. Tes instructions personnalisées et tes mémoires façonnent la manière dont les agents te répondent spécifiquement — séparément de tout ce que l'Administrateur ou l'Éditeur a posé au niveau de l'org. Cette page cartographie où vit chaque levier et ce qu'il change. +Les préférences sont les molettes qui t'appartiennent plutôt qu'à l'org. Ton nom est ce que voient agents et coéquipiers dans les chats et les approbations. Ta langue et ton thème te suivent entre les appareils. Tes instructions personnalisées et tes mémoires façonnent la manière dont les agents te répondent spécifiquement — séparément de tout ce que l'Administrateur ou l'Éditeur a posé au niveau de l'org. Cette page cartographie où vit chaque levier et ce qu'il change. La forme est volontairement à deux couches : le menu de profil (partout, à un clic de l'avatar) porte les bascules rapides ; **Paramètres > Compte** et **Paramètres > Personnalisation** portent les champs de compte plus profonds. Tout ici t'appartient — rien ne fuite vers d'autres membres ou d'autres orgs. @@ -17,7 +17,7 @@ Le menu porte aussi un sélecteur d'organisation quand tu appartiens à plus d'u Ouvre **Paramètres > Compte**. Trois sections siègent sur la page : **Profil**, **Sécurité** et **Authentification à deux facteurs**. -La section Profil tient ton **nom d'affichage** et ton **e-mail**. Le nom d'affichage s'édite en ligne ; la modification s'enregistre à la sortie du champ et se propage dans chaque chat et chaque approbation au prochain rendu. L'e-mail est en lecture seule — c'est avec lui que tu t'es connecté, et un changement passe par le support. Il n'y a pas de champ avatar sur la page ; Tale dérive un avatar à partir des initiales de ton nom d'affichage. +La section Profil affiche d'abord ton **e-mail**, puis ton **nom** — l'e-mail suggère le nom que Tale propose, que tu peux modifier librement. Le nom s'édite en ligne ; la modification s'enregistre et se propage dans chaque chat et chaque approbation au prochain rendu. L'e-mail est en lecture seule — c'est avec lui que tu t'es connecté, et un changement passe par le support. Il n'y a pas de champ avatar sur la page ; Tale dérive un avatar à partir des initiales de ton nom. La section Sécurité tient un seul bouton : **Changer le mot de passe** si tu t'es inscrit avec e-mail et mot de passe, **Définir le mot de passe** si ton compte est fédéré via SSO et que tu veux ajouter un mot de passe comme repli. Les deux flux imposent la politique de mot de passe de l'org et affichent les règles en direct pendant que tu tapes. La section Deux-facteurs apparie le compte à une app TOTP ou à une clé matérielle et affiche les codes de secours une fois à l'enrôlement. diff --git a/services/platform/app/features/organization/components/onboarding/steps/account-step.tsx b/services/platform/app/features/organization/components/onboarding/steps/account-step.tsx index 40a06c93e..e15a6a8bf 100644 --- a/services/platform/app/features/organization/components/onboarding/steps/account-step.tsx +++ b/services/platform/app/features/organization/components/onboarding/steps/account-step.tsx @@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { Button } from '@tale/ui/button'; import { Stack } from '@tale/ui/layout'; import { Separator } from '@tale/ui/separator'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -21,9 +21,11 @@ import { getEnv } from '@/lib/env'; import { useT } from '@/lib/i18n/client'; import { DEFAULT_PASSWORD_POLICY } from '@/lib/shared/schemas/governance'; import { createPasswordSchema } from '@/lib/shared/schemas/password'; +import { deriveNameFromEmail } from '@/lib/utils/derive-name-from-email'; type AccountFormData = { email: string; + name: string; password: string; }; @@ -47,6 +49,7 @@ export function AccountStep() { .string() .min(1, t('validation.emailRequired')) .email(tCommon('validation.email')), + name: z.string().trim().min(1, t('validation.nameRequired')), password: createPasswordSchema({ minLength: t('validation.passwordMinLength', { n: DEFAULT_PASSWORD_POLICY.minLength, @@ -63,23 +66,37 @@ export function AccountStep() { const form = useForm({ resolver: zodResolver(schema), mode: 'onChange', - defaultValues: { email: '', password: '' }, + defaultValues: { email: '', name: '', password: '' }, }); const { isValid, errors } = form.formState; + const email = form.watch('email'); const password = form.watch('password'); const passwordValidationItems = usePasswordValidation(password); + // Pre-fill Name with a suggestion derived from the email (#1941), kept as an + // editable value. Once the user edits Name themselves we stop overwriting it. + const nameEditedRef = useRef(false); + const nameField = form.register('name'); + useEffect(() => { + if (nameEditedRef.current) return; + form.setValue('name', deriveNameFromEmail(email), { shouldValidate: true }); + }, [email, form]); + const createAccount = useCallback(async (): Promise => { form.clearErrors(['email', 'password']); const ok = await form.trigger(); if (!ok) return false; - const { email, password: pw } = form.getValues(); + const { + email: emailValue, + name: nameValue, + password: pw, + } = form.getValues(); try { const result = await authClient.signUp.email({ - name: email, - email, + name: nameValue.trim() || emailValue, + email: emailValue, password: pw, }); @@ -129,6 +146,19 @@ export function AccountStep() { {...form.register('email')} /> + { + nameEditedRef.current = true; + return nameField.onChange(event); + }} + /> + ( - () => (user ? { name: user.name ?? '' } : undefined), - [user], - ); + const data = useMemo(() => { + if (!user) return undefined; + const email = user.email ?? ''; + const savedName = user.name?.trim() ?? ''; + // Legacy owner accounts were created with `name === email` (see the + // onboarding account step); treat that — and an empty name — as "no real + // name yet" and offer an editable suggestion derived from the email. + const name = + savedName && savedName !== email ? savedName : deriveNameFromEmail(email); + return { name }; + }, [user]); const save = useCallback( async (values: ProfileFormData) => { @@ -162,6 +170,18 @@ function ProfileSection() { disabled={editor.isLoading} className="divide-border divide-y" > + {/* Email first, then Name — the email implies the suggested name + (#1941), so it reads top-to-bottom as cause then effect. */} + +
+ +
+
+
- - -
- -
-
diff --git a/services/platform/app/features/settings/organization/components/member-add-dialog.tsx b/services/platform/app/features/settings/organization/components/member-add-dialog.tsx index 1811c1372..542085c34 100644 --- a/services/platform/app/features/settings/organization/components/member-add-dialog.tsx +++ b/services/platform/app/features/settings/organization/components/member-add-dialog.tsx @@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { Button } from '@tale/ui/button'; import { Stack } from '@tale/ui/layout'; import { ConvexError } from 'convex/values'; -import { useState, useMemo } from 'react'; +import { useEffect, useRef, useState, useMemo } from 'react'; import { useForm } from 'react-hook-form'; import * as z from 'zod'; @@ -20,6 +20,7 @@ import { usePasswordValidation } from '@/app/hooks/use-password-validation'; import { useToast } from '@/app/hooks/use-toast'; import { useT } from '@/lib/i18n/client'; import { createOptionalPasswordSchema } from '@/lib/shared/schemas/password'; +import { deriveNameFromEmail } from '@/lib/utils/derive-name-from-email'; import { narrowStringUnion } from '@/lib/utils/type-utils'; import { useCreateMember } from '../hooks/mutations'; @@ -108,10 +109,20 @@ export function AddMemberDialog({ formState, } = form; const selectedRole = watch('role'); + const email = watch('email'); const password = watch('password') ?? ''; const passwordValidationItems = usePasswordValidation(password, policy); + // Pre-fill Name with a suggestion derived from the email (#1941), kept as an + // editable value. Once the admin edits Name themselves we stop overwriting it. + const nameEditedRef = useRef(false); + const displayNameField = register('displayName'); + useEffect(() => { + if (nameEditedRef.current) return; + setValue('displayName', deriveNameFromEmail(email)); + }, [email, setValue]); + const onSubmit = async (data: AddMemberFormData) => { try { const result = await createMember({ @@ -137,6 +148,7 @@ export function AddMemberDialog({ setCredentials({ email: data.email, password: data.password }); setShowCredentials(true); } else { + nameEditedRef.current = false; reset(); onOpenChange(false); } @@ -164,12 +176,14 @@ export function AddMemberDialog({ setShowCredentials(false); setIsExistingUser(false); setCredentials(null); + nameEditedRef.current = false; reset(); onOpenChange(false); }; const handleOpenChange = (isOpen: boolean) => { if (!isOpen && !showCredentials) { + nameEditedRef.current = false; reset(); } onOpenChange(isOpen); @@ -187,14 +201,8 @@ export function AddMemberDialog({ isValid={formState.isValid} onSubmit={handleSubmit(onSubmit)} > - - + {/* Email first, then Name — the email implies the suggested name + (#1941), which we pre-fill below as an editable value. */} + { + nameEditedRef.current = true; + return displayNameField.onChange(event); + }} + className="w-full" + /> +