fix: preserve Scene shot labels for double-click rename

This commit is contained in:
Sameh Khamis committed 2026-09-13 03:56:23 -07:00
1 parent 9b6d34cb8a
commit 1f76f81484
6 files changed
+65 -3

No files matched your search

+1
View File
@@ -14,6 +14,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
## [Unreleased]
- Separate Scene controls and text presentation from project/playback operations; revoke replaced row listeners and suppress stale completion feedback.
- Preserve shot-label identity on selection so double-click rename can complete.
- Split Cockpit camera/controller, instruments, briefing, signals and layout into focused modules with explicit application operations.
- Give Display portal moves cancellable focus/scroll restoration and stop Cockpit work before asynchronous UI teardown.
+2 -1
View File
@@ -5,7 +5,8 @@
Scene controls own creation/deletion prompts, panel listeners, shot rows, playback/recording presentation
and keyboard cancellation. The director supplies project reads and explicit
editing/playback actions while retaining persistence, camera and layer sequencing.
Replacing rows revokes their old listeners. Disposal stops controls immediately;
Shot selection updates the highlight without replacing the row, preserving
native double-click rename. Replacing rows revokes their old listeners. Disposal stops controls immediately;
late file and failed-action completions cannot update removed presentation.
## Cockpit component ownership
+1 -1
View File
@@ -409,7 +409,7 @@ export class SceneDirector {
this._selectedShotId = this._getSelectedScene()?.shots[0]?.id || null;
this._renderShotList();
},
selectShot: (id) => { this._selectedShotId = id; this._renderShotList(); },
selectShot: (id) => { this._selectedShotId = id; },
renameShot: (sceneId, shotId, title) => {
const { shot } = this._getShot(sceneId, shotId);
if (!shot) return;
+9 -1
View File
@@ -3,6 +3,7 @@ import {
sceneElements,
renderSceneOptions,
renderSceneShots,
presentSceneSelection,
presentSceneButtons,
presentSceneProgress,
presentSceneRuntime,
@@ -112,7 +113,14 @@ export class SceneControls {
renderSceneShots(this.elements.shots, this.read(), {
listen: (element, type, callback) =>
this.listen(element, type, callback, this.rowRemovers),
select: (id) => this.run('selectShot', id),
select: (id) => {
this.run('selectShot', id);
if (!this.destroyed)
presentSceneSelection(
this.elements.shots,
this.read().selectedShotId,
);
},
rename: (sceneId, shotId, title) =>
this.run('renameShot', sceneId, shotId, title),
load: (sceneId, shotId) => this.run('load', sceneId, shotId),
+45
View File
@@ -1,6 +1,7 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { SceneControls } from './sceneControls.js';
import { SceneDirector } from '../scenes/director.js';
function fixture() {
class Element extends EventTarget {
@@ -8,6 +9,7 @@ function fixture() {
super();
this.children = [];
this.style = {};
this.dataset = {};
this.value = '';
this._text = '';
const classes = new Set();
@@ -304,3 +306,46 @@ test('creation and deletion prompts precede their model actions and remain inert
f.restore();
}
});
test('the real director preserves a selected shot label for the following double-click', async () => {
const f = fixture();
const previousStorage = globalThis.localStorage;
let director;
try {
f.owner.destroy();
const ids = {
'scene-panel': 'panel',
'scene-select': 'select',
'scene-new-btn': 'new',
'scene-delete-btn': 'delete',
'scene-capture-btn': 'capture',
'scene-update-shot-btn': 'update',
'scene-shot-list': 'shots',
'scene-start-btn': 'start',
'scene-stop-btn': 'stop',
'scene-next-btn': 'next',
'scene-export-btn': 'export',
'scene-import-btn': 'import',
'scene-import-file': 'file',
'scene-download-btn': 'download',
'scene-status': 'status',
'scene-progress-fill': 'progress',
'scene-runtime': 'runtime',
};
f.document.getElementById = (id) => f.elements[ids[id]] || null;
globalThis.localStorage = {
getItem: () => JSON.stringify({ version: 3, scenes: f.state.scenes }),
setItem() {},
};
director = new SceneDirector({ camera: { cancelFlight() {} } }, {}, {});
const label = f.elements.shots.children[0].children[0].children[0];
label.click();
assert.equal(f.elements.shots.children[0].children[0].children[0], label);
label.dispatchEvent(new Event('dblclick'));
assert.equal(director._getSelectedScene().shots[0].title, 'Renamed');
} finally {
await director?.destroy();
globalThis.localStorage = previousStorage;
f.restore();
}
});
+7
View File
@@ -54,6 +54,7 @@ export function renderSceneShots(
for (const shot of scene.shots) {
const row = document.createElement('div');
row.className = 'scene-shot-row';
row.dataset.sceneShotId = shot.id;
row.classList.toggle('active', shot.id === state.selectedShotId);
const top = document.createElement('div');
top.className = 'scene-shot-top';
@@ -90,6 +91,12 @@ export function renderSceneShots(
}
}
export function presentSceneSelection(element, selectedShotId) {
for (const row of element?.children || []) {
row.classList.toggle('active', row.dataset.sceneShotId === selectedShotId);
}
}
export function presentSceneButtons(elements, running, hasRun) {
for (const name of [
'start',