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..b5c99d42 100644 --- a/client-v3/src/components/show/config/script/ScriptLinePart.vue +++ b/client-v3/src/components/show/config/script/ScriptLinePart.vue @@ -47,6 +47,16 @@ /> + + + + + @@ -94,6 +104,7 @@ const props = defineProps<{ characterGroups: CharacterGroup[]; showAddButton: boolean; enableAddButton: boolean; + showRemoveButton: boolean; lineType: number; lineParts: ScriptLinePart[]; stageDirectionStyles?: StageDirectionStyle[]; @@ -104,6 +115,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..b7901721 100644 --- a/client/src/vue_components/show/config/script/ScriptLinePart.vue +++ b/client/src/vue_components/show/config/script/ScriptLinePart.vue @@ -69,6 +69,16 @@ /> + + + + + @@ -129,6 +139,10 @@ export default defineComponent({ required: true, type: Boolean, }, + showRemoveButton: { + required: true, + type: Boolean, + }, lineType: { required: true, type: Number, @@ -271,6 +285,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);