fix(routing): split stack detail URL from compose editor URL (#1605)

Sidebar opens /stacks/:name (anatomy). Monaco uses /compose|/env|/files.
Refresh of a detail URL no longer opens the editor, and editor deep links
keep a hydration shell instead of flashing the dashboard.
This commit is contained in:
Anso
2026-07-09 12:05:36 -04:00
committed by GitHub
parent 7517a4f49c
commit 296ddff2a0
11 changed files with 216 additions and 28 deletions
@@ -55,6 +55,8 @@ function makeOpts(over: Partial<UseUrlSyncOptions> = {}): UseUrlSyncOptions {
isFileLoading: false,
activeTab: 'compose',
setActiveTab: vi.fn(),
editingCompose: false,
setEditingCompose: vi.fn(),
selectedEnvFile: '',
envFiles: [],
loadFileForRoute: vi.fn().mockResolvedValue({ ok: true, envFiles: [] }),
@@ -494,4 +496,86 @@ describe('useUrlSync', () => {
expect(result.current.routeDetailError).toBeNull();
});
it('hydrates tabless stack URL as detail without opening Monaco', async () => {
const loadFileForRoute = vi.fn().mockResolvedValue({ ok: true, envFiles: [] });
const applyEditorRouteState = vi.fn();
const setEditingCompose = vi.fn();
window.history.replaceState({ senchoIdx: 0 }, '', '/nodes/local/stacks/radarr');
renderHook(
(props) => useUrlSync(props),
{
initialProps: makeOpts({
activeView: 'editor',
files: ['radarr'],
selectedFile: null,
envFiles: [],
loadFileForRoute,
applyEditorRouteState,
setEditingCompose,
}),
},
);
await act(async () => {
await Promise.resolve();
});
expect(loadFileForRoute).toHaveBeenCalledWith('radarr');
expect(applyEditorRouteState).not.toHaveBeenCalled();
expect(setEditingCompose).toHaveBeenCalledWith(false);
});
it('writes tabless stack URL when detail is open', () => {
const pushSpy = vi.spyOn(window.history, 'pushState');
const { rerender } = renderHook(
(props) => useUrlSync(props),
{ initialProps: makeOpts({ activeView: 'dashboard' }) },
);
act(() => {
rerender(makeOpts({
activeView: 'editor',
selectedFile: 'radarr',
editingCompose: false,
activeTab: 'compose',
}));
});
const pushed = pushSpy.mock.calls.map((call) => String(call[2] ?? ''));
expect(pushed.some((p) => p === '/nodes/local/stacks/radarr')).toBe(true);
expect(pushed.some((p) => p.includes('/compose'))).toBe(false);
pushSpy.mockRestore();
});
it('opens Monaco when hydrating /compose deep link', async () => {
const loadFileForRoute = vi.fn().mockResolvedValue({ ok: true, envFiles: [] });
const applyEditorRouteState = vi.fn();
window.history.replaceState({ senchoIdx: 0 }, '', '/nodes/local/stacks/radarr/compose');
renderHook(
(props) => useUrlSync(props),
{
initialProps: makeOpts({
activeView: 'editor',
files: ['radarr'],
selectedFile: null,
loadFileForRoute,
applyEditorRouteState,
}),
},
);
await act(async () => {
await Promise.resolve();
});
expect(loadFileForRoute).toHaveBeenCalledWith('radarr');
expect(applyEditorRouteState).toHaveBeenCalledWith('compose');
});
});