mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-14 21:29:34 +00:00
fix(studio): key FX rows by node id so a mid-edit stays with its effect
The rack's rows were keyed `${type}-${index}`. Two effects of the SAME type
keep those keys through a reorder — position 0 is `peaking-0` before and after
— so React reused each row where it stood rather than moving it with its node.
The controls hold real state (a number field mid-edit is held as text, a drag
holds a local value), and that state stayed at the position: moving an effect
handed its half-typed value to whichever effect took its slot.
Different types happened to be safe, because the type is in the key. Same types
were not, and a rack with two EQ bands is the common case.
Now keyed by `node.id`, which the carve module's own list above already does,
falling back to the old form for a node minted without one.
Falsified by restoring the index key: the slot keeps showing 123 after the
effect carrying it moved away.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7ecc996057
commit
fddd596e8f
@@ -163,6 +163,55 @@ describe("FxSection chain", () => {
|
||||
expect(next.nodes.map((n) => n.type)).toEqual(["reverb", "peaking"]);
|
||||
});
|
||||
|
||||
it("keeps a half-typed value with its own effect across a reorder", () => {
|
||||
// Rows used to be keyed `${type}-${index}`, so two effects of the same type
|
||||
// kept their keys through a reorder and React reused each row where it
|
||||
// stood. The controls hold real state — a number field mid-edit is held as
|
||||
// text — so the buffer stayed at the position and landed on whichever
|
||||
// effect moved into it.
|
||||
const peaking = (id: string, frequency: number) => ({
|
||||
type: "peaking",
|
||||
id,
|
||||
enabled: true,
|
||||
params: { ...defaultAudioFxParams("peaking"), frequency },
|
||||
});
|
||||
const a = peaking("pa", 400);
|
||||
const b = peaking("pb", 1600);
|
||||
const chainOfNodes = (...nodes: unknown[]): HfAudioFxChain =>
|
||||
({ version: 1, nodes }) as HfAudioFxChain;
|
||||
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const root = createRoot(host);
|
||||
const render = (chain: HfAudioFxChain) =>
|
||||
act(() => {
|
||||
root.render(
|
||||
<FxSection
|
||||
chain={chain}
|
||||
onChainChange={vi.fn()}
|
||||
onChainPreview={vi.fn()}
|
||||
carve={null}
|
||||
onCarveChange={vi.fn()}
|
||||
sourceOptions={[]}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
render(chainOfNodes(a, b));
|
||||
// Only the first card is open, which is the one being edited.
|
||||
const openFrequency = (): HTMLInputElement =>
|
||||
host.querySelector<HTMLInputElement>(".hf-fx-node .hf-fx-number")!;
|
||||
|
||||
expect(openFrequency().value).toBe("400");
|
||||
typeInto(openFrequency(), "123");
|
||||
expect(openFrequency().value).toBe("123");
|
||||
|
||||
// The author moves that effect down; the other one takes the open slot.
|
||||
render(chainOfNodes(b, a));
|
||||
|
||||
expect(openFrequency().value).toBe("1600");
|
||||
});
|
||||
|
||||
it("cannot move the ends past themselves", () => {
|
||||
const { host } = mount({ chain: chainOf("peaking", "reverb") });
|
||||
const ups = host.querySelectorAll('.hf-fx-move[title="Move up"]');
|
||||
|
||||
@@ -795,7 +795,13 @@ export function FxSection({
|
||||
handBuilt.map(({ node, i }) => {
|
||||
return (
|
||||
<FxNodeRow
|
||||
key={`${node.type}-${i}`}
|
||||
// Keyed by id, as the carve module's list above already is.
|
||||
// On `${type}-${index}` two effects of the same type keep their
|
||||
// keys through a reorder, so React reuses each row where it
|
||||
// stands — and the controls hold real state (a half-typed
|
||||
// number, an in-flight drag), which then lands on whichever
|
||||
// effect moved into that slot.
|
||||
key={node.id ?? `${node.type}-${i}`}
|
||||
node={node}
|
||||
index={i}
|
||||
automatedTargets={automatedTargets}
|
||||
|
||||
Reference in New Issue
Block a user