Skip to content

Commit 89ce3ef

Browse files
fix(mosaic): let account section controllers own row pending state
Connected accounts now disables account menus while a connection starts, so removal can no longer begin mid-connect, and removal shares the section lock. Enterprise accounts moves onto the shared pending action, so a connect error clears when any connection starts.
1 parent 6571ef3 commit 89ce3ef

11 files changed

Lines changed: 55 additions & 31 deletions

‎.changeset/goofy-parents-trade.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

‎packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.connect.feature.test.tsx‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,27 @@ describe('connected accounts', () => {
158158
request.fail();
159159
});
160160

161+
it('holds connected account menus while a connection starts', async () => {
162+
await renderSection([google]);
163+
const request = holdRequests('post', '/v1/me/external_accounts');
164+
const user = userEvent.setup();
165+
166+
try {
167+
await user.click(screen.getByRole('button', { name: 'Connect GitHub' }));
168+
await waitFor(() => expect(request.requests).toHaveLength(1));
169+
const manage = screen.getByRole('button', { name: 'Manage Google' });
170+
expect(manage).toHaveAttribute('aria-disabled', 'true');
171+
await user.click(manage);
172+
expect(screen.queryByRole('menu')).toBeNull();
173+
} finally {
174+
request.fail();
175+
}
176+
177+
await waitFor(() =>
178+
expect(screen.getByRole('button', { name: 'Manage Google' })).not.toHaveAttribute('aria-disabled', 'true'),
179+
);
180+
});
181+
161182
it('verifies the session before connecting a provider, then continues to it', async () => {
162183
serveFapi(signedIn([], { verification: { secrets: { password: 'hunter2' } } }));
163184
const view = await renderWithClerk(<UserProfileConnectedAccountsSection />);

‎packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-account-row.view.tsx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,11 @@ import type {
1919

2020
export function UserProfileConnectProviderRowView({
2121
provider,
22-
isPending,
2322
isDisabled,
2423
onConnect,
2524
triggerRef,
2625
}: {
2726
provider: UserProfileConnectionProvider;
28-
isPending: boolean;
2927
isDisabled: boolean;
3028
onConnect: (id: string) => void;
3129
triggerRef?: Ref<HTMLButtonElement>;
@@ -58,8 +56,8 @@ export function UserProfileConnectProviderRowView({
5856
variant='outline'
5957
color='neutral'
6058
aria-label={fill(m.connectLabel, { provider: provider.provider })}
61-
isPending={isPending}
62-
disabled={isDisabled && !isPending}
59+
isPending={provider.isPending}
60+
disabled={isDisabled && !provider.isPending}
6361
onClick={() => onConnect(provider.id)}
6462
>
6563
{m.connect}
@@ -91,7 +89,7 @@ export function UserProfileConnectedAccountRowView({
9189
}) {
9290
const m = useMessages('userProfile').connectedAccounts;
9391
const actions: ActionMenuAction[] = [];
94-
if ((account.status === 'reconnect' || account.status === 'error') && onReconnect && !isDisabled) {
92+
if ((account.status === 'reconnect' || account.status === 'error') && onReconnect) {
9593
actions.push({
9694
label: account.status === 'reconnect' ? m.reconnect : m.tryAgain,
9795
onClick: () => onReconnect(account.id),
@@ -131,6 +129,8 @@ export function UserProfileConnectedAccountRowView({
131129
{actions.length > 0 ? (
132130
<Section.Actions>
133131
<ActionMenu
132+
disabled={isDisabled}
133+
focusableWhenDisabled
134134
triggerRef={triggerRef}
135135
label={fill(m.manageLabel, { provider: account.provider })}
136136
actions={actions}

‎packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.controller.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export interface UserProfileConnectedAccountsControllerOptions {
3030
export interface UserProfileConnectedAccountsController {
3131
accounts: UserProfileConnectedAccount[];
3232
availableProviders: UserProfileConnectionProvider[];
33-
pendingId: string | undefined;
33+
isDisabled: boolean;
3434
onConnect: (id: string) => void;
3535
onReconnect: (id: string) => void;
3636
sectionRef: Ref<HTMLDivElement>;
@@ -81,15 +81,17 @@ export function useUserProfileConnectedAccountsController({
8181
accounts: accounts.map(account =>
8282
actions.errorKey === account.id ? { ...account, reconnectError: actions.error } : account,
8383
),
84-
availableProviders: availableProviders.map(provider =>
85-
actions.errorKey === provider.id ? { ...provider, connectError: actions.error } : provider,
86-
),
87-
pendingId: actions.pendingKey,
84+
availableProviders: availableProviders.map(provider => ({
85+
...provider,
86+
isPending: actions.pendingKey === provider.id,
87+
connectError: actions.errorKey === provider.id ? actions.error : undefined,
88+
})),
89+
isDisabled: actions.isPending,
8890
onConnect: id => run(id, onConnect),
8991
onReconnect: id => run(id, onReconnect),
9092
sectionRef,
9193
registerTrigger: removalFocus.registerTrigger,
92-
remove: onRemove ? remove : undefined,
94+
remove: onRemove ? actions.guard(remove) : undefined,
9395
finalFocus: removalFocus.finalFocus,
9496
reconnectReverification,
9597
reconnectFinalFocus: () => (actions.lastKey ? removalFocus.trigger(actions.lastKey) : null),

‎packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.types.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface ConnectedAccountProviderDisplay {
1616
export interface UserProfileConnectionProvider extends ConnectedAccountProviderDisplay {
1717
id: string;
1818
connectError?: string;
19+
isPending?: boolean;
1920
}
2021

2122
export interface UserProfileConnectedAccount extends ConnectedAccountProviderDisplay {
@@ -30,7 +31,7 @@ export interface UserProfileConnectedAccountsSectionViewProps {
3031
accounts: UserProfileConnectedAccount[];
3132
sectionRef?: Ref<HTMLDivElement>;
3233
availableProviders?: UserProfileConnectionProvider[];
33-
pendingId?: string;
34+
isDisabled?: boolean;
3435
onConnect?: (id: string) => void;
3536
onReconnect?: (id: string) => void;
3637
registerTrigger: (id: string) => Ref<HTMLButtonElement>;

‎packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.view.tsx‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function UserProfileConnectedAccountsSectionView({
1818
accounts,
1919
sectionRef,
2020
availableProviders = [],
21-
pendingId,
21+
isDisabled = false,
2222
onConnect,
2323
onReconnect,
2424
registerTrigger,
@@ -31,7 +31,6 @@ export function UserProfileConnectedAccountsSectionView({
3131
}: UserProfileConnectedAccountsSectionViewProps) {
3232
const m = useMessages('userProfile').connectedAccounts;
3333
const hasRows = accounts.length > 0 || (availableProviders.length > 0 && Boolean(onConnect));
34-
const isBusy = pendingId !== undefined;
3534

3635
return (
3736
<>
@@ -52,7 +51,7 @@ export function UserProfileConnectedAccountsSectionView({
5251
key={account.id}
5352
account={account}
5453
triggerRef={registerTrigger(account.id)}
55-
isDisabled={isBusy}
54+
isDisabled={isDisabled}
5655
onReconnect={onReconnect}
5756
onRemove={remove ? account => remove.open(account) : undefined}
5857
/>
@@ -63,8 +62,7 @@ export function UserProfileConnectedAccountsSectionView({
6362
key={provider.id}
6463
provider={provider}
6564
triggerRef={registerTrigger(provider.id)}
66-
isPending={pendingId === provider.id}
67-
isDisabled={isBusy}
65+
isDisabled={isDisabled}
6866
onConnect={onConnect}
6967
/>
7068
))

‎packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-account-row.view.tsx‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,10 @@ import type { UserProfileEnterpriseAccount } from './user-profile-enterprise-acc
1414
export function UserProfileEnterpriseAccountRowView({
1515
account,
1616
onConnect,
17-
isPending,
1817
disabled,
1918
}: {
2019
account: UserProfileEnterpriseAccount;
2120
onConnect?: (id: string) => void;
22-
isPending?: boolean;
2321
disabled?: boolean;
2422
}) {
2523
const m = useMessages('userProfile').enterpriseAccounts;
@@ -58,7 +56,7 @@ export function UserProfileEnterpriseAccountRowView({
5856
size='sm'
5957
variant='outline'
6058
aria-label={fill(m.connectProvider, { provider: account.name })}
61-
isPending={isPending}
59+
isPending={account.isPending}
6260
pendingLabel={m.connecting}
6361
disabled={disabled}
6462
onClick={() => onConnect(account.id)}

‎packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.controller.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export interface UserProfileEnterpriseAccountsControllerOptions {
1818
export interface UserProfileEnterpriseAccountsController {
1919
accounts: UserProfileEnterpriseAccount[];
2020
connections: UserProfileEnterpriseConnection[];
21-
pendingId: string | undefined;
21+
isDisabled: boolean;
2222
onConnect: (connectionId: string) => void;
2323
connectReverification: ReverificationController;
2424
}
@@ -34,10 +34,12 @@ export function useUserProfileEnterpriseAccountsController({
3434

3535
return {
3636
accounts,
37-
connections: connections.map(connection =>
38-
actions.errorKey === connection.id ? { ...connection, connectError: actions.error } : connection,
39-
),
40-
pendingId: actions.pendingKey,
37+
connections: connections.map(connection => ({
38+
...connection,
39+
isPending: actions.pendingKey === connection.id,
40+
connectError: actions.errorKey === connection.id ? actions.error : undefined,
41+
})),
42+
isDisabled: actions.isPending,
4143
onConnect: id =>
4244
void actions.run(id, async () => {
4345
if ((await onConnect(id)) === 'redirecting') {

‎packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.types.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export interface UserProfileEnterpriseConnection {
44
iconUrl?: string;
55
monochromeIcon?: boolean;
66
connectError?: string;
7+
isPending?: boolean;
78
}
89

910
export interface UserProfileEnterpriseAccount extends UserProfileEnterpriseConnection {

‎packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.view.tsx‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ import type {
1111
export interface UserProfileEnterpriseAccountsSectionViewProps {
1212
accounts: UserProfileEnterpriseAccount[];
1313
connections?: UserProfileEnterpriseConnection[];
14-
pendingId?: string;
14+
isDisabled?: boolean;
1515
onConnect?: (id: string) => void;
1616
connectReverification?: ReverificationController;
1717
}
1818

1919
export function UserProfileEnterpriseAccountsSectionView({
2020
accounts,
2121
connections = [],
22-
pendingId,
22+
isDisabled = false,
2323
onConnect,
2424
connectReverification,
2525
}: UserProfileEnterpriseAccountsSectionViewProps) {
@@ -49,8 +49,7 @@ export function UserProfileEnterpriseAccountsSectionView({
4949
key={connection.id}
5050
account={connection}
5151
onConnect={onConnect}
52-
isPending={pendingId === connection.id}
53-
disabled={pendingId !== undefined}
52+
disabled={isDisabled}
5453
/>
5554
))
5655
: null}

0 commit comments

Comments
 (0)