diff --git a/apps/editor/src/domain/commands/elements/element-animation-commands.ts b/apps/editor/src/domain/commands/elements/element-animation-commands.ts index 7e502cce..432eea8d 100644 --- a/apps/editor/src/domain/commands/elements/element-animation-commands.ts +++ b/apps/editor/src/domain/commands/elements/element-animation-commands.ts @@ -37,15 +37,13 @@ class SetElementAnimationBuildsCommand implements EditorCommand { selectedIds.has(elementId), ); if (orderedSelectedIds.length === 0) return page; + const existingBuilds = getPageAnimationBuilds(page); const existingByElementId = new Map( - getPageAnimationBuilds(page).map((build) => [build.elementId, build]), + existingBuilds.map((build) => [build.elementId, build]), ); - const retainedBuilds = getPageAnimationBuilds(page).filter( - (build) => !selectedIds.has(build.elementId) && pageElementIds.has(build.elementId), - ); - const nextBuilds = orderedSelectedIds.map((elementId) => { + const updatedByElementId = new Map(orderedSelectedIds.map((elementId) => { const existing = existingByElementId.get(elementId); - return { + return [elementId, { id: existing?.id ?? this.createBuildId(elementId), elementId, effect: this.patch.effect, @@ -60,9 +58,18 @@ class SetElementAnimationBuildsCommand implements EditorCommand { ? { lineDrawDirection: this.patch.lineDrawDirection } : {}), ...(this.patch.mediaAction ? { mediaAction: this.patch.mediaAction } : {}), - }; + }] as const; + })); + const nextBuilds = existingBuilds.flatMap((build) => { + if (!pageElementIds.has(build.elementId)) return []; + if (!selectedIds.has(build.elementId)) return [build]; + const updated = updatedByElementId.get(build.elementId); + if (!updated) return []; + updatedByElementId.delete(build.elementId); + return [updated]; }); - return { ...page, animationBuilds: [...retainedBuilds, ...nextBuilds] }; + nextBuilds.push(...updatedByElementId.values()); + return { ...page, animationBuilds: nextBuilds }; }), updatedAt: projectMutationUtils.getProjectUpdatedAt(), }; diff --git a/apps/editor/tests/unit/domain/commands/basicCommands.test.ts b/apps/editor/tests/unit/domain/commands/basicCommands.test.ts index 04e8b5a5..33c27434 100644 --- a/apps/editor/tests/unit/domain/commands/basicCommands.test.ts +++ b/apps/editor/tests/unit/domain/commands/basicCommands.test.ts @@ -542,6 +542,38 @@ describe('editor commands', () => { expect(project.pages[0]?.animationBuilds).toBeUndefined(); }); + it('updates an existing animation build without changing build order or neighboring builds', () => { + const project = new basicCommands.SetElementAnimationBuildsCommand( + 'page-1', + ['image-hero', 'text-subtitle', 'text-title'], + (elementId) => `build-${elementId}`, + { effect: 'reveal', trigger: 'on-click', delayMs: 0 }, + ).execute(sampleProject.createSampleProject()); + const previousBuilds = project.pages[0]?.animationBuilds; + + const next = new basicCommands.SetElementAnimationBuildsCommand( + 'page-1', + ['text-subtitle'], + (elementId) => `replacement-${elementId}`, + { effect: 'dissolve', trigger: 'after-previous', delayMs: 500 }, + ).execute(project); + + expect(next.pages[0]?.animationBuilds?.map((build) => build.elementId)).toEqual([ + 'image-hero', + 'text-subtitle', + 'text-title', + ]); + expect(next.pages[0]?.animationBuilds?.[1]).toEqual({ + id: 'build-text-subtitle', + elementId: 'text-subtitle', + effect: 'dissolve', + trigger: 'after-previous', + delayMs: 500, + }); + expect(next.pages[0]?.animationBuilds?.[0]).toBe(previousBuilds?.[0]); + expect(next.pages[0]?.animationBuilds?.[2]).toBe(previousBuilds?.[2]); + }); + it('removes an element animation build without affecting other builds', () => { const project = new basicCommands.SetElementAnimationBuildsCommand( 'page-1', diff --git a/tests/e2e/editor/animation-mixed-sequence.ts b/tests/e2e/editor/animation-mixed-sequence.ts index 3e49375a..021855b6 100644 --- a/tests/e2e/editor/animation-mixed-sequence.ts +++ b/tests/e2e/editor/animation-mixed-sequence.ts @@ -30,6 +30,11 @@ export const animationMixedSequence = { await page.getByRole('button', { name: 'Move Typing headline animation up' }).click(); await expect(page.getByRole('listitem', { name: /Build 1: Typing headline/ })).toBeVisible(); await expect(page.getByRole('listitem', { name: /Build 2: Arrow/ })).toBeVisible(); + await page.getByLabel('Effect for Typing headline').selectOption('dissolve'); + await expect(page.getByRole('listitem', { name: /Build 1: Typing headline/ })).toBeVisible(); + await expect(page.getByRole('listitem', { name: /Build 2: Arrow/ })).toBeVisible(); + await expect(page.getByLabel('Effect for Typing headline')).toHaveValue('dissolve'); + await expect(page.getByLabel('Effect for Arrow')).toHaveValue('line-draw'); await page.getByLabel('Start for Typing headline').selectOption('after-transition'); await page.getByLabel('Start for Arrow').selectOption('after-previous'); const canvasFrame = page.getByTestId('slide-canvas-frame');