diff --git a/web/src/i18n/messages/ar.json b/web/src/i18n/messages/ar.json index 7472c32a99..3d9ab38e49 100644 --- a/web/src/i18n/messages/ar.json +++ b/web/src/i18n/messages/ar.json @@ -2690,7 +2690,19 @@ "label": "إعادة المحاولة" }, "savedAccounts": { - "label": "{count, plural, one {حساب محفوظ واحد} other {# حسابات محفوظة}}" + "label": "{count, plural, one {حساب محفوظ واحد} other {# حسابات محفوظة}}", + "expandButton": { + "ariaLabel": "إظهار الحسابات المحفوظة" + }, + "collapseButton": { + "ariaLabel": "إخفاء الحسابات المحفوظة" + } + }, + "accountSummary": { + "newAccountSuffix": "(حساب جديد)", + "editButton": { + "label": "تعديل الحساب" + } }, "timeout": { "toast": "انتهت مهلة العملية بعد {seconds} ثانية. هل تتحقق من إعداداتك بحثًا عن أخطاء؟" diff --git a/web/src/i18n/messages/de.json b/web/src/i18n/messages/de.json index 4b0acda995..a1626ccbe1 100644 --- a/web/src/i18n/messages/de.json +++ b/web/src/i18n/messages/de.json @@ -2690,7 +2690,19 @@ "label": "Erneut versuchen" }, "savedAccounts": { - "label": "{count, plural, one {# gespeichertes Konto} other {# gespeicherte Konten}}" + "label": "{count, plural, one {# gespeichertes Konto} other {# gespeicherte Konten}}", + "expandButton": { + "ariaLabel": "Gespeicherte Konten anzeigen" + }, + "collapseButton": { + "ariaLabel": "Gespeicherte Konten ausblenden" + } + }, + "accountSummary": { + "newAccountSuffix": "(Neues Konto)", + "editButton": { + "label": "Konto bearbeiten" + } }, "timeout": { "toast": "Zeitüberschreitung des Vorgangs nach {seconds} Sekunden. Prüfen Sie Ihre Konfiguration auf Fehler." diff --git a/web/src/i18n/messages/en.json b/web/src/i18n/messages/en.json index a5294b343c..69fa5e5ae6 100644 --- a/web/src/i18n/messages/en.json +++ b/web/src/i18n/messages/en.json @@ -2690,7 +2690,19 @@ "label": "Retry" }, "savedAccounts": { - "label": "{count, plural, one {# Saved Account} other {# Saved Accounts}}" + "label": "{count, plural, one {# Saved Account} other {# Saved Accounts}}", + "expandButton": { + "ariaLabel": "Show saved accounts" + }, + "collapseButton": { + "ariaLabel": "Hide saved accounts" + } + }, + "accountSummary": { + "newAccountSuffix": "(New Account)", + "editButton": { + "label": "Edit Account" + } }, "timeout": { "toast": "Operation timed out after {seconds} seconds. Check your configuration for errors?" diff --git a/web/src/i18n/messages/es.json b/web/src/i18n/messages/es.json index 4c3ce6a05e..c0de77921e 100644 --- a/web/src/i18n/messages/es.json +++ b/web/src/i18n/messages/es.json @@ -2690,7 +2690,19 @@ "label": "Reintentar" }, "savedAccounts": { - "label": "{count, plural, one {# cuenta guardada} other {# cuentas guardadas}}" + "label": "{count, plural, one {# cuenta guardada} other {# cuentas guardadas}}", + "expandButton": { + "ariaLabel": "Mostrar cuentas guardadas" + }, + "collapseButton": { + "ariaLabel": "Ocultar cuentas guardadas" + } + }, + "accountSummary": { + "newAccountSuffix": "(Cuenta nueva)", + "editButton": { + "label": "Editar cuenta" + } }, "timeout": { "toast": "La operación agotó el tiempo de espera tras {seconds} segundos. ¿Quieres revisar tu configuración por si hay errores?" diff --git a/web/src/i18n/messages/fr.json b/web/src/i18n/messages/fr.json index ad9db064d3..2d853b1aa9 100644 --- a/web/src/i18n/messages/fr.json +++ b/web/src/i18n/messages/fr.json @@ -2690,7 +2690,19 @@ "label": "Réessayer" }, "savedAccounts": { - "label": "{count, plural, one {# compte enregistré} other {# comptes enregistrés}}" + "label": "{count, plural, one {# compte enregistré} other {# comptes enregistrés}}", + "expandButton": { + "ariaLabel": "Afficher les comptes enregistrés" + }, + "collapseButton": { + "ariaLabel": "Masquer les comptes enregistrés" + } + }, + "accountSummary": { + "newAccountSuffix": "(Nouveau compte)", + "editButton": { + "label": "Modifier le compte" + } }, "timeout": { "toast": "L'opération a expiré après {seconds} secondes. Vérifiez si votre configuration contient des erreurs ?" diff --git a/web/src/i18n/messages/ja.json b/web/src/i18n/messages/ja.json index 13d465bb01..7f8237712b 100644 --- a/web/src/i18n/messages/ja.json +++ b/web/src/i18n/messages/ja.json @@ -2690,7 +2690,19 @@ "label": "再試行" }, "savedAccounts": { - "label": "{count, plural, one {保存済みアカウント # 件} other {保存済みアカウント # 件}}" + "label": "{count, plural, one {保存済みアカウント # 件} other {保存済みアカウント # 件}}", + "expandButton": { + "ariaLabel": "保存済みアカウントを表示" + }, + "collapseButton": { + "ariaLabel": "保存済みアカウントを非表示" + } + }, + "accountSummary": { + "newAccountSuffix": "(新しいアカウント)", + "editButton": { + "label": "アカウントを編集" + } }, "timeout": { "toast": "{seconds} 秒後に操作がタイムアウトしました。設定に誤りがないか確認してください。" diff --git a/web/src/i18n/messages/ko.json b/web/src/i18n/messages/ko.json index 0e9e332ca4..89e1e84ab8 100644 --- a/web/src/i18n/messages/ko.json +++ b/web/src/i18n/messages/ko.json @@ -2690,7 +2690,19 @@ "label": "다시 시도" }, "savedAccounts": { - "label": "{count, plural, one {저장된 계정 #개} other {저장된 계정 #개}}" + "label": "{count, plural, one {저장된 계정 #개} other {저장된 계정 #개}}", + "expandButton": { + "ariaLabel": "저장된 계정 표시" + }, + "collapseButton": { + "ariaLabel": "저장된 계정 숨기기" + } + }, + "accountSummary": { + "newAccountSuffix": "(새 계정)", + "editButton": { + "label": "계정 편집" + } }, "timeout": { "toast": "{seconds}초 후 작업 시간이 초과되었습니다. 설정에 오류가 있는지 확인하세요." diff --git a/web/src/i18n/messages/pt.json b/web/src/i18n/messages/pt.json index 44f47cf1fe..8865bdc871 100644 --- a/web/src/i18n/messages/pt.json +++ b/web/src/i18n/messages/pt.json @@ -2690,7 +2690,19 @@ "label": "Tentar novamente" }, "savedAccounts": { - "label": "{count, plural, one {# conta salva} other {# contas salvas}}" + "label": "{count, plural, one {# conta salva} other {# contas salvas}}", + "expandButton": { + "ariaLabel": "Mostrar contas salvas" + }, + "collapseButton": { + "ariaLabel": "Ocultar contas salvas" + } + }, + "accountSummary": { + "newAccountSuffix": "(Nova conta)", + "editButton": { + "label": "Editar conta" + } }, "timeout": { "toast": "A operação expirou após {seconds} segundos. Verifique se há erros na sua configuração." diff --git a/web/src/i18n/messages/zh.json b/web/src/i18n/messages/zh.json index d58da4c6ac..e3d40a6d64 100644 --- a/web/src/i18n/messages/zh.json +++ b/web/src/i18n/messages/zh.json @@ -2690,7 +2690,19 @@ "label": "重试" }, "savedAccounts": { - "label": "{count, plural, one {# 个已保存账户} other {# 个已保存账户}}" + "label": "{count, plural, one {# 个已保存账户} other {# 个已保存账户}}", + "expandButton": { + "ariaLabel": "显示已保存的账户" + }, + "collapseButton": { + "ariaLabel": "隐藏已保存的账户" + } + }, + "accountSummary": { + "newAccountSuffix": "(新账户)", + "editButton": { + "label": "编辑账户" + } }, "timeout": { "toast": "操作在 {seconds} 秒后超时。请检查配置是否有误。" diff --git a/web/src/views/admin/connectors/AddConnectorPage/components/AccountSummary.tsx b/web/src/views/admin/connectors/AddConnectorPage/components/AccountSummary.tsx new file mode 100644 index 0000000000..eb20f2ca06 --- /dev/null +++ b/web/src/views/admin/connectors/AddConnectorPage/components/AccountSummary.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { Button } from "@opal/components"; +import { Content, Section } from "@opal/layouts"; +import { SvgPlusCircle, SvgUserEdit } from "@opal/icons"; +import type { DraftCredential } from "@/lib/credentials/types"; +import { useUser } from "@/providers/UserProvider"; + +interface AccountSummaryProps { + /** The new account typed into the form, which Create saves. */ + draft: DraftCredential; + /** The source's display name, for an account with no name to show. */ + sourceName: string; +} + +/** + * One row that names the new account: its display name, else the current + * user's email. The new-account card shows it as its closed header once the + * account is usable. + */ +export function AccountSummary({ draft, sourceName }: AccountSummaryProps) { + const t = useTranslations("admin.connectorsList.add"); + const { user } = useUser(); + // Named like a saved account: its display name, else its creator (the + // current user). + const title: string = + draft.sharing.name ?? + user?.email ?? + t("newAccountButton.label", { source: sourceName }); + + return ( + // The action sits level with the title line. +
+
+ +
+ {/* Repeats its card's own action, so it leaves the tab order. */} + +
+ ); +} diff --git a/web/src/views/admin/connectors/AddConnectorPage/components/AuthenticationAccountCard.tsx b/web/src/views/admin/connectors/AddConnectorPage/components/AuthenticationAccountCard.tsx index 6c29df1d6d..6e44fe6205 100644 --- a/web/src/views/admin/connectors/AddConnectorPage/components/AuthenticationAccountCard.tsx +++ b/web/src/views/admin/connectors/AddConnectorPage/components/AuthenticationAccountCard.tsx @@ -77,6 +77,11 @@ interface AuthenticationAccountCardProps { * One that cannot is disabled. */ selectable: boolean; + /** + * Marks the pick with the "Selected" label only, not the selected card + * style, as in the folded account section. + */ + quietSelection?: boolean; /** Called when the user picks this account. */ onSelect: () => void; /** Called when the user picks the selected account again, to drop it. */ @@ -99,6 +104,7 @@ export default function AuthenticationAccountCard({ sourceName, selected, selectable, + quietSelection = false, onSelect, onDeselect, onDelete, @@ -195,7 +201,7 @@ export default function AuthenticationAccountCard({ expandable expanded={expanded} expandableContentHeight="full" - state={selected ? "selected" : "filled"} + state={selected && !quietSelection ? "selected" : "filled"} border="solid" rounding={4} padding={2} diff --git a/web/src/views/admin/connectors/AddConnectorPage/components/CredentialChecksCard.tsx b/web/src/views/admin/connectors/AddConnectorPage/components/CredentialChecksCard.tsx index 3daa6ad220..a5f04b02af 100644 --- a/web/src/views/admin/connectors/AddConnectorPage/components/CredentialChecksCard.tsx +++ b/web/src/views/admin/connectors/AddConnectorPage/components/CredentialChecksCard.tsx @@ -453,11 +453,14 @@ export interface CredentialChecksCardProps { credential: CredentialRef | null; /** Locks the Start Checks prompt until the credential section is valid. */ locked: boolean; + /** Called when Start Checks starts the first run. */ + onStart?: () => void; } export default function CredentialChecksCard({ source, credential, locked, + onStart, }: CredentialChecksCardProps) { const promptT = useTranslations("admin.connectorChecks.prompt"); const { appName } = useSettings(); @@ -479,7 +482,14 @@ export default function CredentialChecksCard({ center padding={0} rightChildren={ - } diff --git a/web/src/views/admin/connectors/AddConnectorPage/sections/AuthenticationAccountSection.tsx b/web/src/views/admin/connectors/AddConnectorPage/sections/AuthenticationAccountSection.tsx index 63ce9ea1af..570dd6c2e9 100644 --- a/web/src/views/admin/connectors/AddConnectorPage/sections/AuthenticationAccountSection.tsx +++ b/web/src/views/admin/connectors/AddConnectorPage/sections/AuthenticationAccountSection.tsx @@ -19,7 +19,7 @@ import { } from "@opal/layouts"; import { Disabled } from "@opal/core"; import { FormikField } from "@/refresh-components/form/FormikField"; -import { SvgListTree, SvgPlusCircle } from "@opal/icons"; +import { SvgExpand, SvgFold, SvgListTree, SvgPlusCircle } from "@opal/icons"; import type { Credential, DraftCredential, @@ -48,6 +48,7 @@ import { CreateStdOAuthCredential } from "@/lib/credentials/components/CreateStd import { credentialMatchesBoundFields, getCredentialSpec, + isDraftCredential, realmFields, shouldRedirectToOAuth, toCredentialRef, @@ -58,6 +59,7 @@ import type { ConfigurableSources } from "@/lib/connectors/types/source"; import { credentialBoundFieldNames } from "@/lib/connectors/utils"; import AuthenticationAccountCard from "@/views/admin/connectors/AddConnectorPage/components/AuthenticationAccountCard"; import CredentialChecksCard from "@/views/admin/connectors/AddConnectorPage/components/CredentialChecksCard"; +import { AccountSummary } from "@/views/admin/connectors/AddConnectorPage/components/AccountSummary"; interface AuthenticationAccountSectionProps { /** The source being set up. */ @@ -115,7 +117,14 @@ export default function AuthenticationAccountSection({ isAuthorizing, } = useCredentialSetup(connector); const checkReports = useCredentialCheckReports(connector); - const [showSavedAccounts, setShowSavedAccounts] = useState(true); + // Expanded, the section lists every account. Folded, only the chosen one + // (or the new-account card when nothing is chosen) stays. + const [accountsExpanded, setAccountsExpanded] = useState(true); + /** Folds the section. The new-account card closes too; expanding leaves it closed. */ + function foldAccounts() { + close(); + setAccountsExpanded(false); + } // The create card's one label, whatever the number of routes; the tabs // inside it name the routes. @@ -324,8 +333,15 @@ export default function AuthenticationAccountSection({ sourceName={displayName} selected={credential.id === selectedSavedId} selectable={isSelectable(credential)} + // Folded, the pick is the only card shown: it keeps a light look, + // and a click opens the section rather than dropping the pick. + quietSelection={!accountsExpanded} onSelect={() => onSwap(credential)} - onDeselect={() => onCredentialChange(null)} + onDeselect={() => + accountsExpanded + ? onCredentialChange(null) + : setAccountsExpanded(true) + } onDelete={() => onDeleteCredential(credential)} checkReport={checkReports.reportFor(credential.id)} onRerunChecks={() => checkReports.rerun(credential.id)} @@ -333,6 +349,15 @@ export default function AuthenticationAccountSection({ ); } + // The chosen account: the picked saved account, or the new account once it + // is usable and no saved account is picked. + const chosenDraft: DraftCredential | null = + selectedSavedId === null && + checkedCredential !== null && + isDraftCredential(checkedCredential) + ? checkedCredential + : null; + // One card creates a credential. Its header toggles it; the fold below is a // plain container, so a click in the open form cannot fold it away. The // routes into the source are tabs inside the fold. @@ -351,7 +376,8 @@ export default function AuthenticationAccountSection({ // interactive half is focusable, so a role here folds no other // control into that name. role="button" - aria-label={newAccountLabel} + // Showing a summary, the card is named by it (its visible text). + aria-label={!isCreating && chosenDraft ? undefined : newAccountLabel} aria-expanded={isCreating} tabIndex={0} expandedContent={ @@ -391,15 +417,20 @@ export default function AuthenticationAccountSection({ } onClick={() => (isCreating ? close() : selectMethod(defaultMethod))} > -
- -
+ {/* Closed, a usable new account shows its summary as the header. */} + {!isCreating && chosenDraft ? ( + + ) : ( +
+ +
+ )} ); @@ -414,18 +445,33 @@ export default function AuthenticationAccountSection({ variant="section" padding={0} rightChildren={ - +
+
+ +
+
} /> @@ -434,39 +480,55 @@ export default function AuthenticationAccountSection({ the types honest. */} {!credentials ? null : (
-
- {/* Hidden accounts animate away, then leave the page. The fold - holds the cards' gap, so a closed one leaves no space behind. A - div, as Section's own padding would override the bottom one. */} - 0}> -
- {credentials.map(renderSavedCard)} -
-
-
- {canAuthorize && ( -
- -
- )} + {/* One list in a fixed order. Folded, each card that is not the + chosen one fades, then its height collapses, so the chosen card + moves up on its own. Each item holds its own spacing: a closed fold + takes no height, but a gap around it would stay. */} +
+ {credentials.map((credential) => ( + +
{renderSavedCard(credential)}
+
+ ))} + {canAuthorize && ( + +
+
+ +
+
+
+ )} + {/* Shown unless a saved account is the pick. Its own fold (the + form) is independent of the section's. */} + {newAccountCard} -
-
+ +
)}