diff --git a/src/workspaces/officerdev/src/apps/CodeEditor/CodeEditorPanelWrapper.tsx b/src/workspaces/officerdev/src/apps/CodeEditor/CodeEditorPanelWrapper.tsx index 9013ca81..c709343a 100644 --- a/src/workspaces/officerdev/src/apps/CodeEditor/CodeEditorPanelWrapper.tsx +++ b/src/workspaces/officerdev/src/apps/CodeEditor/CodeEditorPanelWrapper.tsx @@ -1,3 +1,18 @@ +import { useWorkspace, cwdToPath } from '../../components/Workspace'; import { CodeEditorView } from './CodeEditor'; -export const CodeEditorPanelWrapper = () => ; +/** + * The editor as a dashboard panel, rooted at the dashboard's folder. + * + * It ignored the workspace entirely and always opened the explorer at home, while `CodeEditorHeader` read + * `cwd` for itself and displayed the right folder — so a scoped dashboard showed `~/Tests/platform` in the + * header above a tree of the home directory. The two halves of the same panel disagreed because only one + * of them asked. + * + * No `urlState`: a dashboard can hold two editors and one `?file=` would drive both. Same reasoning as + * FileBrowserPanelWrapper, which is where this pattern came from and which had it right already. + */ +export const CodeEditorPanelWrapper = () => { + const { cwd } = useWorkspace(); + return ; +}; diff --git a/src/workspaces/officerdev/src/apps/Dashboards/DashboardListApp.tsx b/src/workspaces/officerdev/src/apps/Dashboards/DashboardListApp.tsx index cbf3452d..17b0f76d 100644 --- a/src/workspaces/officerdev/src/apps/Dashboards/DashboardListApp.tsx +++ b/src/workspaces/officerdev/src/apps/Dashboards/DashboardListApp.tsx @@ -25,6 +25,7 @@ import { NEW_DASH_TEMPLATE_KEY, } from './constants'; import { useNewDashboardDraft } from './useNewDashboardDraft'; +import { cwdToPath } from '../../components/Workspace'; export const DashboardListApp = () => { const location = useLocation(); @@ -74,7 +75,7 @@ export const DashboardListApp = () => { setName(ws.name); setDescription(ws.description ?? ''); setTemplateIdx(ws.templateIdx ?? 0); - const cwdPath = !ws.cwd || ws.cwd === '~' ? '/' : ws.cwd.replace(/^~\//, '/'); + const cwdPath = cwdToPath(ws.cwd); setFilePath(cwdPath); }; diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserPanelWrapper.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserPanelWrapper.tsx index 56a3da55..e6e19113 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserPanelWrapper.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserPanelWrapper.tsx @@ -1,8 +1,6 @@ -import { useWorkspace } from '../../../components/Workspace'; +import { useWorkspace, cwdToPath } from '../../../components/Workspace'; import { FileBrowserApp } from './FileBrowserApp'; -const cwdToPath = (cwd: string) => (cwd === '~' ? '/' : cwd.slice(1)); - export const FileBrowserPanelWrapper = () => { const { cwd, workspace } = useWorkspace(); const basePath = cwdToPath(cwd); diff --git a/src/workspaces/officerdev/src/components/Workspace/cwd-path.test.ts b/src/workspaces/officerdev/src/components/Workspace/cwd-path.test.ts new file mode 100644 index 00000000..848d8978 --- /dev/null +++ b/src/workspaces/officerdev/src/components/Workspace/cwd-path.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, test } from 'bun:test'; +import { cwdToPath } from './cwd-path'; + +// One conversion, because there were two and they disagreed — and a third was about to be written. + +describe('cwdToPath', () => { + test('the home shorthand is the root', () => { + expect(cwdToPath('~')).toBe('/'); + }); + + test('a scoped dashboard resolves to its folder', () => { + expect(cwdToPath('~/Tests/platform')).toBe('/Tests/platform'); + }); + + test('missing or empty is the root, not the empty string', () => { + // `''.slice(1)` gave `''`, which is not a path any endpoint accepts — it listed nothing rather than + // failing, which is the quiet kind of wrong. + expect(cwdToPath('')).toBe('/'); + expect(cwdToPath(undefined)).toBe('/'); + expect(cwdToPath(null)).toBe('/'); + }); + + test('an already-absolute path is left alone', () => { + // `'/abs'.slice(1)` gave `'abs'` — a relative path, silently resolved against somewhere else. + expect(cwdToPath('/already/abs')).toBe('/already/abs'); + }); + + test('spaces and punctuation are untouched — this is notation, not encoding', () => { + expect(cwdToPath('~/a b/c (2)')).toBe('/a b/c (2)'); + }); + + test('a tilde inside a name is not a prefix and must not be rewritten', () => { + expect(cwdToPath('~/back~up')).toBe('/back~up'); + }); +}); diff --git a/src/workspaces/officerdev/src/components/Workspace/cwd-path.ts b/src/workspaces/officerdev/src/components/Workspace/cwd-path.ts new file mode 100644 index 00000000..244d840d --- /dev/null +++ b/src/workspaces/officerdev/src/components/Workspace/cwd-path.ts @@ -0,0 +1,20 @@ +// A workspace's `cwd` as a path the file APIs accept. +// +// A dashboard is created against a directory, and every file-shaped panel in it should open there. The +// framework stores that as a `cwd` in the shell's own notation — `~`, `~/Tests/platform` — while the file +// endpoints take a path rooted at the home directory: `/`, `/Tests/platform`. +// +// This existed twice, spelled differently, and the two disagreed: +// +// cwd.slice(1) '' -> '' '/abs' -> 'abs' ← both wrong +// cwd.replace(/^~\//, '/') '' -> '/' '/abs' -> '/abs' ← both right +// +// The second is the one kept. A third copy was about to be written for the code editor, which is what +// prompted collapsing them: two spellings of one conversion is a bug waiting for whichever caller got the +// short one. + +/** `~` and `~/x` become `/` and `/x`. An already-absolute path is returned unchanged. */ +export function cwdToPath(cwd: string | undefined | null): string { + if (!cwd || cwd === '~') return '/'; + return cwd.replace(/^~\//, '/'); +} diff --git a/src/workspaces/officerdev/src/components/Workspace/index.ts b/src/workspaces/officerdev/src/components/Workspace/index.ts index 1d55dd7e..00286aae 100644 --- a/src/workspaces/officerdev/src/components/Workspace/index.ts +++ b/src/workspaces/officerdev/src/components/Workspace/index.ts @@ -33,6 +33,7 @@ export { export type { WorkspaceIdentity } from './workspace-identity'; export { parseWorkspaceKey } from './workspace-identity'; export { WorkspaceProvider, useWorkspace } from './WorkspaceContext'; +export { cwdToPath } from './cwd-path'; export { usePanelFullscreen } from './panel-fullscreen'; export { usePanelConfig } from './usePanelConfig'; export { usePanelClose } from './usePanelClose';