Skip to content

Commit b4dcf05

Browse files
authored
Merge pull request #442 from bcorfman/yamlfilename
Added YAML filename persist
2 parents 6fa97da + c5d8cfc commit b4dcf05

6 files changed

Lines changed: 165 additions & 6 deletions

File tree

src/app/layout.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,30 @@ textarea:focus-visible {
583583
justify-self: center;
584584
}
585585

586+
.viewbar-yaml-group {
587+
position: relative;
588+
}
589+
590+
.viewbar-yaml-file-label {
591+
position: absolute;
592+
top: 50%;
593+
right: calc(100% + 0.55rem);
594+
transform: translateY(-50%);
595+
width: 12rem;
596+
text-align: right;
597+
color: var(--muted);
598+
font-size: 0.8rem;
599+
font-weight: 600;
600+
white-space: nowrap;
601+
overflow: hidden;
602+
text-overflow: ellipsis;
603+
pointer-events: none;
604+
}
605+
606+
.viewbar-yaml-file-label-hidden {
607+
visibility: hidden;
608+
}
609+
586610
.section-title,
587611
.panel-title,
588612
.section-subtitle {

src/editor/ViewbarYamlControls.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type ViewbarYamlControlsViewProps = {
1313

1414
export function ViewbarYamlControlsView({ project, dispatch }: ViewbarYamlControlsViewProps) {
1515
const fileInputRef = useRef<HTMLInputElement | null>(null);
16+
const yamlFileLabel = getYamlFileSourceLabel();
1617

1718
const openFromPicker = async () => {
1819
dispatch({ type: 'set-error', error: undefined });
@@ -95,11 +96,14 @@ export function ViewbarYamlControlsView({ project, dispatch }: ViewbarYamlContro
9596
const saveAs = async () => {
9697
dispatch({ type: 'set-error', error: undefined });
9798
try {
98-
const result = await exportYamlToDisk(serializeProjectToYaml(project), { startIn: getYamlPickerStartIn() });
99+
const result = await exportYamlToDisk(serializeProjectToYaml(project), {
100+
suggestedName: yamlFileLabel ?? 'scene.yaml',
101+
startIn: getYamlPickerStartIn(),
102+
});
99103
if (result.kind === 'saved') {
100104
setYamlPickerStartIn(result.handle);
101105
setYamlFileHandle(result.handle);
102-
setYamlFileSourceLabel(getYamlFileSourceLabel() ?? 'scene.yaml');
106+
setYamlFileSourceLabel(result.label);
103107
dispatch({ type: 'mark-saved' });
104108
dispatch({ type: 'set-status', message: 'Saved YAML', expiresAt: Date.now() + 4000 });
105109
} else {
@@ -115,7 +119,16 @@ export function ViewbarYamlControlsView({ project, dispatch }: ViewbarYamlContro
115119

116120
return (
117121
<div className="viewbar-yaml" role="toolbar" aria-label="YAML file actions">
118-
<div className="viewbar-group">
122+
<div className="viewbar-group viewbar-yaml-group">
123+
{yamlFileLabel ? (
124+
<span className="viewbar-yaml-file-label" data-testid="yaml-file-label" title={yamlFileLabel}>
125+
{yamlFileLabel}
126+
</span>
127+
) : (
128+
<span className="viewbar-yaml-file-label viewbar-yaml-file-label-hidden" data-testid="yaml-file-label-hidden" aria-hidden="true">
129+
scene.yaml
130+
</span>
131+
)}
119132
<button className="button" type="button" data-testid="yaml-open-button" onClick={() => void openFromPicker()}>
120133
Open YAML…
121134
</button>

src/editor/yamlFileExport.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ function getSaveFilePicker(): FilePickerLike | null {
66
return typeof picker === 'function' ? (picker as FilePickerLike) : null;
77
}
88

9-
export type YamlExportResult = { kind: 'saved'; handle: any } | { kind: 'downloaded' };
9+
export type YamlExportResult = { kind: 'saved'; handle: any; label: string } | { kind: 'downloaded' };
1010

1111
export async function exportYamlToDisk(
1212
yaml: string,
@@ -31,7 +31,8 @@ export async function exportYamlToDisk(
3131
const writable = await handle.createWritable();
3232
await writable.write(yaml);
3333
await writable.close();
34-
return { kind: 'saved', handle };
34+
const label = typeof handle?.name === 'string' && handle.name.trim().length > 0 ? handle.name : suggestedName;
35+
return { kind: 'saved', handle, label };
3536
}
3637

3738
if (typeof document === 'undefined') throw new Error('Document unavailable for download fallback');

src/editor/yamlPickerState.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
11
let lastStartInHandle: any | undefined;
22
let lastYamlFileHandle: any | undefined;
3-
let lastYamlFileSourceLabel: string | undefined;
3+
const YAML_FILE_SOURCE_LABEL_STORAGE_KEY = 'phaserforge:last-yaml-file-label';
4+
5+
function readStoredYamlFileSourceLabel(): string | undefined {
6+
if (typeof window === 'undefined' || !('localStorage' in window)) return undefined;
7+
try {
8+
const stored = window.localStorage.getItem(YAML_FILE_SOURCE_LABEL_STORAGE_KEY);
9+
return stored && stored.trim().length > 0 ? stored : undefined;
10+
} catch {
11+
return undefined;
12+
}
13+
}
14+
15+
function writeStoredYamlFileSourceLabel(label: string | undefined): void {
16+
if (typeof window === 'undefined' || !('localStorage' in window)) return;
17+
try {
18+
if (label && label.trim().length > 0) {
19+
window.localStorage.setItem(YAML_FILE_SOURCE_LABEL_STORAGE_KEY, label);
20+
return;
21+
}
22+
window.localStorage.removeItem(YAML_FILE_SOURCE_LABEL_STORAGE_KEY);
23+
} catch {
24+
// Ignore storage failures so file actions keep working in restricted environments.
25+
}
26+
}
27+
28+
let lastYamlFileSourceLabel: string | undefined = readStoredYamlFileSourceLabel();
429

530
export function getYamlPickerStartIn(): any | undefined {
631
return lastStartInHandle;
@@ -24,4 +49,5 @@ export function getYamlFileSourceLabel(): string | undefined {
2449

2550
export function setYamlFileSourceLabel(label: string | undefined): void {
2651
lastYamlFileSourceLabel = label;
52+
writeStoredYamlFileSourceLabel(label);
2753
}

tests/editor/viewbar-yaml-controls.test.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,40 @@ describe('ViewbarYamlControls', () => {
132132
expiresAt: 1_700_000_004_000,
133133
});
134134
});
135+
136+
it('shows the persisted filename label without shifting the save buttons', () => {
137+
yamlControlsStore.state.project = sampleProject;
138+
yamlPickerState.setLabel('level-01.yaml');
139+
140+
render(<ViewbarYamlControls />);
141+
142+
expect(screen.getByTestId('yaml-file-label').textContent).toBe('level-01.yaml');
143+
expect(screen.queryByTestId('yaml-file-label-hidden')).toBeNull();
144+
});
145+
146+
it('hides the filename label until a filename exists', () => {
147+
yamlControlsStore.state.project = sampleProject;
148+
149+
render(<ViewbarYamlControls />);
150+
151+
expect(screen.queryByTestId('yaml-file-label')).toBeNull();
152+
expect(screen.getByTestId('yaml-file-label-hidden')).not.toBeNull();
153+
});
154+
155+
it('stores the chosen filename immediately after first save-as so it is visible for later saves', async () => {
156+
yamlControlsStore.state.project = sampleProject;
157+
const handle = { id: 'save-handle', name: 'first-save.yaml' };
158+
yamlIoMocks.exportYamlToDisk.mockResolvedValue({ kind: 'saved', handle, label: 'first-save.yaml' });
159+
160+
render(<ViewbarYamlControls />);
161+
const user = userEvent.setup();
162+
await user.click(screen.getByTestId('yaml-save-button'));
163+
164+
expect(yamlIoMocks.exportYamlToDisk).toHaveBeenCalledWith(
165+
serializeProjectToYaml(sampleProject),
166+
expect.objectContaining({ suggestedName: 'scene.yaml', startIn: undefined }),
167+
);
168+
expect(yamlPickerState.setHandle).toHaveBeenCalledWith(handle);
169+
expect(yamlPickerState.setLabel).toHaveBeenCalledWith('first-save.yaml');
170+
});
135171
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// @vitest-environment jsdom
2+
import { afterEach, describe, expect, it, vi } from 'vitest';
3+
4+
const storage = (() => {
5+
const values = new Map<string, string>();
6+
return {
7+
getItem: vi.fn((key: string) => values.get(key) ?? null),
8+
setItem: vi.fn((key: string, value: string) => {
9+
values.set(key, value);
10+
}),
11+
removeItem: vi.fn((key: string) => {
12+
values.delete(key);
13+
}),
14+
clear: vi.fn(() => {
15+
values.clear();
16+
}),
17+
};
18+
})();
19+
20+
const loadModule = async () => import('../../src/editor/yamlPickerState');
21+
22+
describe('yamlPickerState', () => {
23+
afterEach(() => {
24+
Object.defineProperty(window, 'localStorage', {
25+
configurable: true,
26+
value: storage,
27+
});
28+
storage.clear();
29+
storage.getItem.mockClear();
30+
storage.setItem.mockClear();
31+
storage.removeItem.mockClear();
32+
storage.clear.mockClear();
33+
vi.resetModules();
34+
});
35+
36+
it('persists the YAML filename label in localStorage', async () => {
37+
Object.defineProperty(window, 'localStorage', {
38+
configurable: true,
39+
value: storage,
40+
});
41+
const state = await loadModule();
42+
43+
state.setYamlFileSourceLabel('persisted-name.yaml');
44+
45+
expect(storage.setItem).toHaveBeenCalledWith('phaserforge:last-yaml-file-label', 'persisted-name.yaml');
46+
});
47+
48+
it('restores the YAML filename label from localStorage after reload', async () => {
49+
Object.defineProperty(window, 'localStorage', {
50+
configurable: true,
51+
value: storage,
52+
});
53+
storage.setItem('phaserforge:last-yaml-file-label', 'restored-name.yaml');
54+
55+
const state = await loadModule();
56+
57+
expect(state.getYamlFileSourceLabel()).toBe('restored-name.yaml');
58+
});
59+
});

0 commit comments

Comments
 (0)