From d72f7a805a467a3df8d30633e829d28e1ace22d1 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sat, 20 Jun 2026 00:52:12 +0100 Subject: [PATCH 1/3] Add ability to delete individual line parts from the script editor Users previously had to delete and re-add an entire line to remove a single part. A remove button now appears in the character row of each ScriptLinePart when the line has more than one part, and disappears when only one part remains. Implemented in both client-v3 and client with E2E coverage added to the script spec. Co-Authored-By: Claude Sonnet 4.6 --- .../e2e/tests/10-show-config-script.spec.ts | 76 +++++++++++++++++++ .../show/config/script/ScriptLineEditor.vue | 9 +++ .../show/config/script/ScriptLinePart.vue | 14 ++++ .../show/config/script/ScriptLineEditor.vue | 9 +++ .../show/config/script/ScriptLinePart.vue | 19 +++++ 5 files changed, 127 insertions(+) diff --git a/client-v3/e2e/tests/10-show-config-script.spec.ts b/client-v3/e2e/tests/10-show-config-script.spec.ts index 068f8b80..d1a4c1b4 100644 --- a/client-v3/e2e/tests/10-show-config-script.spec.ts +++ b/client-v3/e2e/tests/10-show-config-script.spec.ts @@ -393,3 +393,79 @@ test('deletes the cue', async () => { // After deletion only the add-cue-btn remains; no actual cue buttons should exist await expect(page.locator('.cue-button:not(.add-cue-btn)')).not.toBeVisible({ timeout: 5_000 }); }); + +// ── Line part removal ────────────────────────────────────────────────────── + +test('remove button is absent on a single-part dialogue line', async () => { + await page.goto(`${UI_BASE}/show-config/script`); + await waitForAppReady(page); + await page.getByRole('button', { name: 'Edit', exact: true }).click(); + await expect(page.locator('button:has-text("Stop Editing")')).toBeVisible({ timeout: 10_000 }); + + await page.click('button:has-text("Add Dialogue")'); + const lineEditor = page + .locator('div.row') + .filter({ has: page.locator('button:has-text("Done")') }) + .first(); + await expect(lineEditor.locator('button:has-text("Done")')).toBeVisible({ timeout: 5_000 }); + + // With only 1 part no remove button should be present + await expect(lineEditor.locator('button.btn-outline-danger')).not.toBeVisible(); +}); + +test('remove button appears when a second part is added', async () => { + const lineEditor = page + .locator('div.row') + .filter({ has: page.locator('button:has-text("Done")') }) + .first(); + + // btn-secondary is the add-line-part (+) button — only btn-secondary in the editor row + await lineEditor.locator('button.btn-secondary').click(); + + // One remove button per part + await expect(lineEditor.locator('button.btn-outline-danger')).toHaveCount(2, { timeout: 3_000 }); +}); + +test('clicking remove reduces to single part and hides the remove button', async () => { + const lineEditor = page + .locator('div.row') + .filter({ has: page.locator('button:has-text("Done")') }) + .first(); + + await lineEditor.locator('button.btn-outline-danger').first().click(); + + // Back to 1 part — remove button gone + await expect(lineEditor.locator('button.btn-outline-danger')).not.toBeVisible({ timeout: 3_000 }); +}); + +test('dialogue line with removed part saves correctly', async () => { + const lineEditor = page + .locator('div.row') + .filter({ has: page.locator('button:has-text("Done")') }) + .first(); + + await lineEditor.locator('select').nth(0).selectOption({ label: 'Act 1' }); + await lineEditor.locator('select').nth(1).selectOption({ label: 'Scene 1' }); + await lineEditor + .locator('select') + .filter({ hasText: 'Hamlet' }) + .selectOption({ label: 'Hamlet' }); + await lineEditor.locator('input[type="text"]').last().fill('Surviving part text'); + + await lineEditor.locator('button:has-text("Done")').click(); + // Delete the auto-added blank editor that appears after Done + await page.locator('button.btn-danger:has-text("Delete")').first().click(); + await expect(page.locator('button:has-text("Done")')).not.toBeVisible({ timeout: 5_000 }); + + await page.getByRole('button', { name: 'Save', exact: true }).click(); + await waitForModal(page, 'Saving Script'); + await expect(page.locator('.modal.show').getByText('Finished saving script.')).toBeVisible({ + timeout: 15_000, + }); + await confirmModal(page); + await waitForModalClosed(page); + + await expect( + page.locator('.viewable-line').filter({ hasText: 'Surviving part text' }) + ).toBeVisible({ timeout: 10_000 }); +}); diff --git a/client-v3/src/components/show/config/script/ScriptLineEditor.vue b/client-v3/src/components/show/config/script/ScriptLineEditor.vue index af36a7ec..f6eea110 100644 --- a/client-v3/src/components/show/config/script/ScriptLineEditor.vue +++ b/client-v3/src/components/show/config/script/ScriptLineEditor.vue @@ -50,6 +50,7 @@ scriptMode === 1 " :enable-add-button="state.line_parts.length < 4 && lineType === LINE_TYPES.DIALOGUE" + :show-remove-button="state.line_parts.length > 1 && lineType === LINE_TYPES.DIALOGUE" :line-type="lineType" :line-parts="state.line_parts" :stage-direction-styles=" @@ -58,6 +59,7 @@ :stage-direction-style-id="state.stage_direction_style_id" @update:model-value="onPartUpdate(index, $event)" @add-line-part="addLinePart" + @remove-line-part="removeLinePart(index)" @try-finish-line="tryFinishLine" @stage-direction-style-change="onStageDirectionStyleChange" /> @@ -210,6 +212,13 @@ function onPartUpdate(index: number, part: ScriptLinePartType): void { onStateChange(); } +function removeLinePart(index: number): void { + state.value.line_parts = state.value.line_parts + .filter((_, i) => i !== index) + .map((part, i) => ({ ...part, part_index: i })); + onStateChange(); +} + function addLinePart(): void { const blank = blankLinePart(); blank.part_index = state.value.line_parts.length; diff --git a/client-v3/src/components/show/config/script/ScriptLinePart.vue b/client-v3/src/components/show/config/script/ScriptLinePart.vue index aeb88a22..31f9c5a6 100644 --- a/client-v3/src/components/show/config/script/ScriptLinePart.vue +++ b/client-v3/src/components/show/config/script/ScriptLinePart.vue @@ -47,6 +47,18 @@ /> + + + + + + + @@ -94,6 +106,7 @@ const props = defineProps<{ characterGroups: CharacterGroup[]; showAddButton: boolean; enableAddButton: boolean; + showRemoveButton: boolean; lineType: number; lineParts: ScriptLinePart[]; stageDirectionStyles?: StageDirectionStyle[]; @@ -104,6 +117,7 @@ const props = defineProps<{ const emit = defineEmits<{ 'update:modelValue': [part: ScriptLinePart]; 'add-line-part': []; + 'remove-line-part': []; 'try-finish-line': []; 'stage-direction-style-change': [id: number | null]; }>(); diff --git a/client/src/vue_components/show/config/script/ScriptLineEditor.vue b/client/src/vue_components/show/config/script/ScriptLineEditor.vue index d879a627..fb66f979 100644 --- a/client/src/vue_components/show/config/script/ScriptLineEditor.vue +++ b/client/src/vue_components/show/config/script/ScriptLineEditor.vue @@ -53,6 +53,7 @@ CURRENT_SHOW.script_mode === 1 " :enable-add-button="state.line_parts.length < 4 && lineType === LINE_TYPES.DIALOGUE" + :show-remove-button="state.line_parts.length > 1 && lineType === LINE_TYPES.DIALOGUE" :line-type="lineType" :line-parts="state.line_parts" :stage-direction-styles=" @@ -61,6 +62,7 @@ :stage-direction-style-id="state.stage_direction_style_id" @input="stateChange" @addLinePart="addLinePart" + @removeLinePart="removeLinePart(index)" @tryFinishLine="tryFinishLine" @stage-direction-style-change="onStageDirectionStyleChange" /> @@ -374,6 +376,13 @@ export default defineComponent({ (this as any).$v.state.$touch(); this.$emit('input', (this as any).state); }, + removeLinePart(index: number): void { + const state = (this as any).state; + state.line_parts = state.line_parts + .filter((_: any, i: number) => i !== index) + .map((part: any, i: number) => ({ ...part, part_index: i })); + this.stateChange(); + }, addLinePart(): void { const state = (this as any).state; const blankLine = JSON.parse(JSON.stringify(this.blankLinePartObj)); diff --git a/client/src/vue_components/show/config/script/ScriptLinePart.vue b/client/src/vue_components/show/config/script/ScriptLinePart.vue index 71ed7315..b41049c5 100644 --- a/client/src/vue_components/show/config/script/ScriptLinePart.vue +++ b/client/src/vue_components/show/config/script/ScriptLinePart.vue @@ -69,6 +69,18 @@ /> + + + + + + + @@ -129,6 +141,10 @@ export default defineComponent({ required: true, type: Boolean, }, + showRemoveButton: { + required: true, + type: Boolean, + }, lineType: { required: true, type: Number, @@ -271,6 +287,9 @@ export default defineComponent({ addLinePart(): void { this.$emit('addLinePart'); }, + removeLinePart(): void { + this.$emit('removeLinePart'); + }, stateChange(): void { (this as any).$v.state.$touch(); this.$emit('input', (this as any).state); From 19c3dff2fcd610ab9d007dc06fe6636393c6a2a5 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sat, 20 Jun 2026 00:59:16 +0100 Subject: [PATCH 2/3] Fix vertical alignment of remove line part button Replace BFormGroup label=" " wrapper with align-self-end mb-3 on the column directly, which is the standard Bootstrap way to align a button with adjacent form controls without adding a dummy label. Co-Authored-By: Claude Sonnet 4.6 --- .../show/config/script/ScriptLinePart.vue | 20 +++++++++---------- .../show/config/script/ScriptLinePart.vue | 20 +++++++++---------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/client-v3/src/components/show/config/script/ScriptLinePart.vue b/client-v3/src/components/show/config/script/ScriptLinePart.vue index 31f9c5a6..b5c99d42 100644 --- a/client-v3/src/components/show/config/script/ScriptLinePart.vue +++ b/client-v3/src/components/show/config/script/ScriptLinePart.vue @@ -47,17 +47,15 @@ /> - - - - - - + + + + diff --git a/client/src/vue_components/show/config/script/ScriptLinePart.vue b/client/src/vue_components/show/config/script/ScriptLinePart.vue index b41049c5..b7901721 100644 --- a/client/src/vue_components/show/config/script/ScriptLinePart.vue +++ b/client/src/vue_components/show/config/script/ScriptLinePart.vue @@ -69,17 +69,15 @@ /> - - - - - - + + + + From 1aeeefb366db2f2f5d563e0a6767c08a1145ce85 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sat, 20 Jun 2026 21:25:18 +0100 Subject: [PATCH 3/3] Fix cut stage directions not showing strikethrough in cue editor Stage directions in the cue editor were missing strikethrough styling when cut, because the render path used the base sdStyling computed (no cut check). Replace it with sdStylingWithCuts, which appends line-through to the existing text-decoration-line value when the stage direction's first part ID is in cuts. Co-Authored-By: Claude Sonnet 4.6 --- .../show/config/cues/ScriptLineCueEditor.vue | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/client-v3/src/components/show/config/cues/ScriptLineCueEditor.vue b/client-v3/src/components/show/config/cues/ScriptLineCueEditor.vue index 9c2853d8..7bf5c4e6 100644 --- a/client-v3/src/components/show/config/cues/ScriptLineCueEditor.vue +++ b/client-v3/src/components/show/config/cues/ScriptLineCueEditor.vue @@ -83,7 +83,7 @@