mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-10-19 01:50:51 -05:00
feat(connectors): animated account-section fold with a new-account summary (#15875)
This commit is contained in:
1 parent
b2edc253e6
commit
e04e8b2df8
13 files changed
+305
-63
No files matched your search
@@ -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} ثانية. هل تتحقق من إعداداتك بحثًا عن أخطاء؟"
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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?"
|
||||
|
||||
@@ -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?"
|
||||
|
||||
@@ -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 ?"
|
||||
|
||||
@@ -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} 秒後に操作がタイムアウトしました。設定に誤りがないか確認してください。"
|
||||
|
||||
@@ -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}초 후 작업 시간이 초과되었습니다. 설정에 오류가 있는지 확인하세요."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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} 秒后超时。请检查配置是否有误。"
|
||||
|
||||
@@ -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.
|
||||
<Section
|
||||
flexDirection="row"
|
||||
alignItems="start"
|
||||
justifyContent="between"
|
||||
gap={4}
|
||||
width="full"
|
||||
>
|
||||
<Section alignItems="start" padding={2} width="full">
|
||||
<Content
|
||||
icon={SvgPlusCircle}
|
||||
title={title}
|
||||
suffix={t("accountSummary.newAccountSuffix")}
|
||||
sizePreset="main-ui"
|
||||
variant="section"
|
||||
/>
|
||||
</Section>
|
||||
{/* Repeats its card's own action, so it leaves the tab order. */}
|
||||
<Button prominence="tertiary" rightIcon={SvgUserEdit} tabIndex={-1}>
|
||||
{t("accountSummary.editButton.label")}
|
||||
</Button>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
+7
-1
@@ -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}
|
||||
|
||||
+11
-1
@@ -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={
|
||||
<Button icon={SvgPlay} disabled={locked} onClick={checks.begin}>
|
||||
<Button
|
||||
icon={SvgPlay}
|
||||
disabled={locked}
|
||||
onClick={() => {
|
||||
checks.begin();
|
||||
onStart?.();
|
||||
}}
|
||||
>
|
||||
{promptT("startButton.label")}
|
||||
</Button>
|
||||
}
|
||||
|
||||
+114
-52
@@ -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<boolean>(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<boolean>(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))}
|
||||
>
|
||||
<Section padding={2} width="full">
|
||||
<Content
|
||||
icon={SvgPlusCircle}
|
||||
title={newAccountLabel}
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
color={isCreating ? "interactive" : "muted"}
|
||||
/>
|
||||
</Section>
|
||||
{/* Closed, a usable new account shows its summary as the header. */}
|
||||
{!isCreating && chosenDraft ? (
|
||||
<AccountSummary draft={chosenDraft} sourceName={displayName} />
|
||||
) : (
|
||||
<Section padding={2} width="full">
|
||||
<Content
|
||||
icon={SvgPlusCircle}
|
||||
title={newAccountLabel}
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
color={isCreating ? "interactive" : "muted"}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
</SelectCard>
|
||||
);
|
||||
|
||||
@@ -414,18 +445,33 @@ export default function AuthenticationAccountSection({
|
||||
variant="section"
|
||||
padding={0}
|
||||
rightChildren={
|
||||
<Button
|
||||
icon={SvgListTree}
|
||||
prominence="internal"
|
||||
// Held in its hover look while the list is open.
|
||||
interaction={showSavedAccounts ? "hover" : "rest"}
|
||||
aria-expanded={showSavedAccounts}
|
||||
onClick={() => setShowSavedAccounts((shown) => !shown)}
|
||||
>
|
||||
{t("add.savedAccounts.label", {
|
||||
count: credentials?.length ?? 0,
|
||||
})}
|
||||
</Button>
|
||||
<Section flexDirection="row" alignItems="center" gap={0} width="fit">
|
||||
<Section padding={2} width="fit">
|
||||
<Content
|
||||
icon={SvgListTree}
|
||||
title={t("add.savedAccounts.label", {
|
||||
count: credentials?.length ?? 0,
|
||||
})}
|
||||
sizePreset="main-ui"
|
||||
variant="body"
|
||||
color="muted"
|
||||
width="fit"
|
||||
/>
|
||||
</Section>
|
||||
<Button
|
||||
icon={accountsExpanded ? SvgFold : SvgExpand}
|
||||
prominence="tertiary"
|
||||
aria-label={
|
||||
accountsExpanded
|
||||
? t("add.savedAccounts.collapseButton.ariaLabel")
|
||||
: t("add.savedAccounts.expandButton.ariaLabel")
|
||||
}
|
||||
aria-expanded={accountsExpanded}
|
||||
onClick={() =>
|
||||
accountsExpanded ? foldAccounts() : setAccountsExpanded(true)
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -434,39 +480,55 @@ export default function AuthenticationAccountSection({
|
||||
the types honest. */}
|
||||
{!credentials ? null : (
|
||||
<Section gap={6} alignItems="stretch" width="full">
|
||||
<Section gap={0} alignItems="stretch" width="full" height="fit">
|
||||
{/* 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. */}
|
||||
<Fold open={showSavedAccounts && credentials.length > 0}>
|
||||
<div className="flex flex-col gap-2 pb-2">
|
||||
{credentials.map(renderSavedCard)}
|
||||
</div>
|
||||
</Fold>
|
||||
<Section gap={2} alignItems="stretch" width="full">
|
||||
{canAuthorize && (
|
||||
<Section flexDirection="row" justifyContent="start" gap={1}>
|
||||
<Button
|
||||
disabled={isAuthorizing}
|
||||
variant="action"
|
||||
onClick={handleAuthorize}
|
||||
>
|
||||
{isAuthorizing
|
||||
? t("add.authorizeButton.pendingLabel")
|
||||
: t("add.authorizeButton.label", {
|
||||
source: displayName,
|
||||
})}
|
||||
</Button>
|
||||
</Section>
|
||||
)}
|
||||
{/* 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. */}
|
||||
<div className="flex flex-col">
|
||||
{credentials.map((credential) => (
|
||||
<Fold
|
||||
key={credential.id}
|
||||
open={accountsExpanded || credential.id === selectedSavedId}
|
||||
keepMounted
|
||||
>
|
||||
<div className="pb-2">{renderSavedCard(credential)}</div>
|
||||
</Fold>
|
||||
))}
|
||||
{canAuthorize && (
|
||||
<Fold open={accountsExpanded} keepMounted>
|
||||
<div className="pb-2">
|
||||
<Section flexDirection="row" justifyContent="start" gap={1}>
|
||||
<Button
|
||||
disabled={isAuthorizing}
|
||||
variant="action"
|
||||
onClick={handleAuthorize}
|
||||
>
|
||||
{isAuthorizing
|
||||
? t("add.authorizeButton.pendingLabel")
|
||||
: t("add.authorizeButton.label", {
|
||||
source: displayName,
|
||||
})}
|
||||
</Button>
|
||||
</Section>
|
||||
</div>
|
||||
</Fold>
|
||||
)}
|
||||
{/* Shown unless a saved account is the pick. Its own fold (the
|
||||
form) is independent of the section's. */}
|
||||
<Fold
|
||||
open={accountsExpanded || selectedSavedId === null}
|
||||
keepMounted
|
||||
>
|
||||
{newAccountCard}
|
||||
</Section>
|
||||
</Section>
|
||||
</Fold>
|
||||
</div>
|
||||
|
||||
<CredentialChecksCard
|
||||
source={connector}
|
||||
credential={toCredentialRef(checkedCredential)}
|
||||
locked={checksLocked || !checkedCredential}
|
||||
// Starting the checks completes the account step, so it folds.
|
||||
onStart={foldAccounts}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user