Skip to content

Commit a2bef26

Browse files
committed
fix(workspace): refresh dashboard input on switch
1 parent 4f662dd commit a2bef26

3 files changed

Lines changed: 202 additions & 37 deletions

File tree

apps/web/src/components/HomeView.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,7 @@ export function HomeView({
472472
const [fallbackProjectMetadata, setFallbackProjectMetadata] =
473473
useState<ProjectMetadata | null>(null);
474474
const [active, setActive] = useState<ActivePlugin | null>(null);
475+
const previousWorkspaceNameRef = useRef<string | null>(null);
475476
// A placeholder-carousel scenario the user submitted on an empty composer.
476477
// We seed the prompt + bind the template synchronously, then let an effect
477478
// fire submit() once both have committed (submit() reads state, not args).
@@ -962,6 +963,53 @@ export function HomeView({
962963
[designSystemId, designSystemPickerSystems, t],
963964
);
964965

966+
// A preset can bind while one Workspace is selected, then remain mounted as
967+
// this tab switches to another Workspace. `usePlugin` seeds workspace_name
968+
// at bind time, but that snapshot must not outlive the request-local
969+
// Workspace context. Refresh only a missing value or the value previously
970+
// supplied by context, preserving an explicit plugin input when one exists.
971+
// This reads the exact context selected for this tab; it never consults or
972+
// writes Vela/daemon account-level active-workspace state. That model cannot
973+
// represent two clients of one account open in different Workspaces.
974+
useEffect(() => {
975+
const nextWorkspaceName = workspaceContext?.workspaceName?.trim() || null;
976+
const previousWorkspaceName = previousWorkspaceNameRef.current;
977+
previousWorkspaceNameRef.current = nextWorkspaceName;
978+
979+
setActive((currentActive) => {
980+
if (!currentActive) return currentActive;
981+
const workspaceField = currentActive.inputFields.find(
982+
(field) => field.name === 'workspace_name',
983+
);
984+
if (!workspaceField || workspaceField.default !== undefined) return currentActive;
985+
986+
const currentValue = currentActive.inputs.workspace_name;
987+
const currentWorkspaceName =
988+
currentValue === undefined || currentValue === null
989+
? ''
990+
: String(currentValue).trim();
991+
const contextOwnsCurrentValue =
992+
currentWorkspaceName.length === 0
993+
|| (previousWorkspaceName !== null
994+
&& currentWorkspaceName === previousWorkspaceName);
995+
if (!contextOwnsCurrentValue || currentWorkspaceName === (nextWorkspaceName ?? '')) {
996+
return currentActive;
997+
}
998+
999+
const inputs = { ...currentActive.inputs };
1000+
if (nextWorkspaceName) inputs.workspace_name = nextWorkspaceName;
1001+
else delete inputs.workspace_name;
1002+
return {
1003+
...currentActive,
1004+
inputs,
1005+
inputsValid: pluginInputsAreValid(currentActive.inputFields, inputs),
1006+
// The pinned apply snapshot belongs to the old inputs. Force submit to
1007+
// resolve a new snapshot for the newly selected Workspace.
1008+
result: null,
1009+
};
1010+
});
1011+
}, [workspaceContext?.workspaceName]);
1012+
9651013
function focusPromptAtEnd() {
9661014
requestAnimationFrame(() => {
9671015
inputRef.current?.focusEnd();

apps/web/tests/components/HomeView.template-use-send-enabled.test.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,72 @@ describe('community template Use lands a sendable composer', () => {
450450
});
451451
expect(screen.queryByRole('alert')).toBeNull();
452452
});
453+
454+
it('refreshes live-dashboard workspace_name after the tab switches Workspaces', async () => {
455+
writeHomeGuideStage('done');
456+
workspaceContextForTest = {
457+
workspaceId: 'ws-personal',
458+
workspaceType: 'personal',
459+
workspaceMemberId: 'wm-personal',
460+
role: 'owner',
461+
memberStatus: 'active',
462+
lifecycleState: 'active',
463+
billingState: 'active',
464+
planId: null,
465+
providerMode: 'platform_credits',
466+
seatSummary: {
467+
seatLimit: 1,
468+
usedSeats: 1,
469+
availableSeats: 0,
470+
isSeatFull: true,
471+
},
472+
permissions: {
473+
canInviteMembers: false,
474+
canManageMembers: true,
475+
canManageBilling: true,
476+
canManageAutoRecharge: true,
477+
canShareProjects: true,
478+
canWriteSyncedFiles: true,
479+
canViewWorkspaceSettings: true,
480+
canManageSharedResources: true,
481+
},
482+
workspaceName: 'Personal Workspace',
483+
};
484+
stubFetch();
485+
486+
const submittedPluginInputs: Record<string, unknown>[] = [];
487+
const tree = () => (
488+
<I18nProvider initial="en">
489+
<HomeView
490+
projects={[]}
491+
onSubmit={(payload) => {
492+
submittedPluginInputs.push(payload.pluginInputs ?? {});
493+
}}
494+
onOpenProject={() => undefined}
495+
onViewAllProjects={() => undefined}
496+
promptHandoff={createPluginUseHandoff(1, LIVE_DASHBOARD.id, { action: 'use-with-query' })}
497+
/>
498+
</I18nProvider>
499+
);
500+
const view = render(tree());
501+
const submit = await boundSubmit();
502+
503+
workspaceContextForTest = {
504+
...workspaceContextForTest,
505+
workspaceId: 'ws-team',
506+
workspaceType: 'team',
507+
workspaceMemberId: 'wm-team',
508+
workspaceName: 'Design Team',
509+
};
510+
view.rerender(tree());
511+
fireEvent.click(submit);
512+
513+
await waitFor(() => {
514+
expect(submittedPluginInputs).toContainEqual(
515+
expect.objectContaining({ workspace_name: 'Design Team' }),
516+
);
517+
});
518+
});
453519
});
454520

455521
describe('required-input gate survives where the user can still fill it', () => {

e2e/ui/home-hero-rail.test.ts

Lines changed: 88 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1541,64 +1541,115 @@ test('[P1] home hero example presets update the composer input for prototype and
15411541
});
15421542

15431543
test('[P1] live dashboard preset sends the active workspace name to plugin apply', async ({ page }) => {
1544+
const personalWorkspace = {
1545+
workspaceId: 'ws-personal',
1546+
workspaceName: 'Personal Workspace',
1547+
workspaceType: 'personal',
1548+
workspaceMemberId: 'wm-personal',
1549+
role: 'owner',
1550+
memberStatus: 'active',
1551+
lifecycleState: 'active',
1552+
} as const;
1553+
const teamWorkspace = {
1554+
workspaceId: 'ws-qa',
1555+
workspaceName: 'QA Team',
1556+
workspaceType: 'team',
1557+
workspaceMemberId: 'wm-qa',
1558+
role: 'member',
1559+
memberStatus: 'active',
1560+
lifecycleState: 'active',
1561+
} as const;
1562+
const workspaceContext = (
1563+
selected: typeof personalWorkspace | typeof teamWorkspace,
1564+
includeWorkspaceName = false,
1565+
) => ({
1566+
workspaceId: selected.workspaceId,
1567+
workspaceType: selected.workspaceType,
1568+
workspaceMemberId: selected.workspaceMemberId,
1569+
role: selected.role,
1570+
memberStatus: 'active' as const,
1571+
lifecycleState: 'active' as const,
1572+
billingState: 'active' as const,
1573+
planId: selected.workspaceType === 'team' ? 'team' : null,
1574+
providerMode: 'platform_credits' as const,
1575+
seatSummary: {
1576+
seatLimit: 10,
1577+
usedSeats: 1,
1578+
availableSeats: 9,
1579+
isSeatFull: false,
1580+
},
1581+
permissions: {
1582+
canInviteMembers: false,
1583+
canManageMembers: false,
1584+
canManageBilling: false,
1585+
canManageAutoRecharge: false,
1586+
canShareProjects: true,
1587+
canWriteSyncedFiles: true,
1588+
canViewWorkspaceSettings: true,
1589+
canManageSharedResources: false,
1590+
},
1591+
...(includeWorkspaceName ? { workspaceName: selected.workspaceName } : {}),
1592+
});
15441593
await page.route('**/api/workspace/directory', async (route) => {
15451594
await route.fulfill({
15461595
json: {
1547-
items: [{
1548-
workspaceId: 'ws-qa',
1549-
workspaceName: 'QA Team',
1550-
workspaceType: 'team',
1551-
workspaceMemberId: 'wm-qa',
1552-
role: 'member',
1553-
memberStatus: 'active',
1554-
lifecycleState: 'active',
1555-
}],
1596+
items: [personalWorkspace, teamWorkspace],
15561597
activeWorkspaceId: null,
15571598
},
15581599
});
15591600
});
15601601
await page.route('**/api/workspace/context', async (route) => {
1602+
const workspaceId = route.request().headers()['x-od-workspace-id'];
1603+
const selected = workspaceId === teamWorkspace.workspaceId
1604+
? teamWorkspace
1605+
: personalWorkspace;
15611606
await route.fulfill({
15621607
json: {
1563-
context: {
1564-
workspaceId: 'ws-qa',
1565-
workspaceType: 'team',
1566-
workspaceMemberId: 'wm-qa',
1567-
role: 'member',
1568-
memberStatus: 'active',
1569-
lifecycleState: 'active',
1570-
billingState: 'active',
1571-
planId: 'team',
1572-
providerMode: 'platform_credits',
1573-
seatSummary: {
1574-
seatLimit: 10,
1575-
usedSeats: 1,
1576-
availableSeats: 9,
1577-
isSeatFull: false,
1578-
},
1579-
permissions: {
1580-
canInviteMembers: false,
1581-
canManageMembers: false,
1582-
canManageBilling: false,
1583-
canManageAutoRecharge: false,
1584-
canShareProjects: true,
1585-
canWriteSyncedFiles: true,
1586-
canViewWorkspaceSettings: true,
1587-
canManageSharedResources: false,
1588-
},
1589-
},
1608+
context: workspaceContext(selected),
1609+
},
1610+
});
1611+
});
1612+
await page.route('**/api/workspace/active', async (route) => {
1613+
if (route.request().method() !== 'PUT') {
1614+
await route.fallback();
1615+
return;
1616+
}
1617+
const body = route.request().postDataJSON() as {
1618+
workspaceId?: unknown;
1619+
workspaceMemberId?: unknown;
1620+
};
1621+
const selected = [personalWorkspace, teamWorkspace].find(
1622+
(item) =>
1623+
item.workspaceId === body.workspaceId
1624+
&& item.workspaceMemberId === body.workspaceMemberId,
1625+
);
1626+
if (!selected) {
1627+
await route.fulfill({ status: 400, json: { error: 'exact_workspace_scope_required' } });
1628+
return;
1629+
}
1630+
await route.fulfill({
1631+
json: {
1632+
activeWorkspaceId: selected.workspaceId,
1633+
context: workspaceContext(selected, true),
15901634
},
15911635
});
15921636
});
15931637

15941638
await gotoEntryHome(page);
15951639
await page.getByTestId('workspace-home-rail-toggle').click();
1596-
await expect(page.getByText('QA Team', { exact: true })).toBeVisible();
1640+
await expect(page.getByTestId('workspace-switcher')).toContainText('Personal Workspace');
15971641

15981642
await pickHomeTemplate(page, 'live-artifact');
15991643
await usePreset(page, 'example-live-dashboard');
16001644
await expect(page.getByTestId('home-hero-submit')).toBeEnabled();
16011645

1646+
// The preset is already bound to Personal. Switching the request-local tab
1647+
// context must replace that context-owned input and invalidate the old apply
1648+
// snapshot before Send — no account-level active Workspace participates.
1649+
await page.getByTestId('workspace-switcher').click();
1650+
await page.getByRole('menuitem', { name: 'QA Team' }).click();
1651+
await expect(page.getByTestId('workspace-switcher')).toContainText('QA Team');
1652+
16021653
const applyRequestPromise = page.waitForRequest((request) =>
16031654
request.method() === 'POST'
16041655
&& request.url().includes('/api/plugins/example-live-dashboard/apply'),

0 commit comments

Comments
 (0)