Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions client-v3/e2e/tests/14-user-settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,97 @@ test('switches to Stage Direction Styles tab', async () => {
await expect(page.locator('#stage-directions-table')).toBeVisible({ timeout: 5_000 });
});

// ── Default Stage Direction Style ─────────────────────────────────────────

test('"Default Stage Direction Style" section is visible', async () => {
await expect(page.locator('.card:has-text("Default Stage Direction Style")')).toBeVisible();
});

test('default preview uses darkslateblue before any override', async () => {
// The card's example element should reflect the built-in fallback colour
const preview = page
.locator('.card:has-text("Default Stage Direction Style")')
.locator('.example-stage-direction');
await expect(preview).toHaveCSS('background-color', 'rgb(72, 61, 139)'); // darkslateblue
});

test('"Customise" button opens the default style modal', async () => {
await page
.locator('.card:has-text("Default Stage Direction Style") button:has-text("Customise")')
.click();
await waitForModal(page, 'Customise Default Stage Direction Style');
await expect(page.locator('#default-bg-colour-input')).toBeVisible();
});

test('setting a custom background colour saves and updates the card preview', async () => {
await page.locator('#default-bg-colour-input').fill('#FF0000');
await confirmModal(page);
await waitForModalClosed(page);
// Reset button becomes enabled (verifies PATCH /api/v1/user/settings was persisted)
await expect(
page.locator(
'.card:has-text("Default Stage Direction Style") button:has-text("Reset to Default")'
)
).toBeEnabled({ timeout: 5_000 });
// Card preview reflects saved colour (verifies WS GET_USER_SETTINGS sync updated the store)
const preview = page
.locator('.card:has-text("Default Stage Direction Style")')
.locator('.example-stage-direction');
await expect(preview).toHaveCSS('background-color', 'rgb(255, 0, 0)');
});

test('custom colour persists after page reload', async () => {
await page.reload();
await waitForAppReady(page);
await page.click('.nav-link:has-text("Stage Direction")');
await expect(page.locator('#stage-directions-table')).toBeVisible({ timeout: 5_000 });
// Reset button enabled (colour was persisted to DB via GET /api/v1/user/settings)
await expect(
page.locator(
'.card:has-text("Default Stage Direction Style") button:has-text("Reset to Default")'
)
).toBeEnabled({ timeout: 5_000 });
const preview = page
.locator('.card:has-text("Default Stage Direction Style")')
.locator('.example-stage-direction');
await expect(preview).toHaveCSS('background-color', 'rgb(255, 0, 0)');
});

test('text colour override can be enabled and saved', async () => {
await page
.locator('.card:has-text("Default Stage Direction Style") button:has-text("Customise")')
.click();
await waitForModal(page, 'Customise Default Stage Direction Style');
// Enable text colour override
await page.locator('.modal.show').getByText('Override text colour').click();
await expect(page.locator('#default-text-colour-input')).toBeVisible();
await page.locator('#default-text-colour-input').fill('#FFFFFF');
await confirmModal(page);
await waitForModalClosed(page);
// Preview text colour should update (white text on red background)
const preview = page
.locator('.card:has-text("Default Stage Direction Style")')
.locator('.example-stage-direction');
await expect(preview).toHaveCSS('color', 'rgb(255, 255, 255)', { timeout: 5_000 });
});

test('"Reset to Default" disables the Reset button and reverts the preview', async () => {
await page
.locator('.card:has-text("Default Stage Direction Style") button:has-text("Reset to Default")')
.click();
// Reset button becomes disabled once both overrides are cleared
await expect(
page.locator(
'.card:has-text("Default Stage Direction Style") button:has-text("Reset to Default")'
)
).toBeDisabled({ timeout: 5_000 });
// Preview reverts to darkslateblue
const preview = page
.locator('.card:has-text("Default Stage Direction Style")')
.locator('.example-stage-direction');
await expect(preview).toHaveCSS('background-color', 'rgb(72, 61, 139)', { timeout: 5_000 });
});

test('"New Override" button is enabled when stage direction styles exist', async () => {
// Scope to thead to avoid matching the "New Override" button in the Cue Colour Preferences tab
// (BVN BTabs without lazy mounts all tab panels, so inactive tab content stays in the DOM)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ import { useScriptDisplay } from '@/composables/useScriptDisplay';
import { toast } from '@/js/toast';
import type { ScriptLine, StageDirectionStyle } from '@/types/api/script';
import type { Act, Scene, Character, CharacterGroup } from '@/types/api/show';
import type { UserSettings } from '@/types/api/user';

const props = defineProps<{
line: ScriptLine;
Expand Down Expand Up @@ -261,7 +262,10 @@ const currentStyle = computed(() =>
);

const stageDirectionStylingWithCuts = computed<Record<string, string>>(() => {
const base = stageDirectionStyling(currentStyle.value);
const base = stageDirectionStyling(currentStyle.value, {
background_colour: (userStore.userSettings as UserSettings).default_sd_background_colour,
text_colour: (userStore.userSettings as UserSettings).default_sd_text_colour,
});
const firstPartId = props.line.line_parts[0]?.id;
if (firstPartId != null && props.linePartCuts.includes(firstPartId)) {
const existing = base['text-decoration-line'];
Expand Down
8 changes: 7 additions & 1 deletion client-v3/src/components/show/live/ScriptLineViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ import { LINE_TYPES } from '@/constants/lineTypes';
import type { ScriptLine, ScriptCut, StageDirectionStyle } from '@/types/api/script';
import type { Act, Scene, Character, CharacterGroup } from '@/types/api/show';
import type { CueType } from '@/types/api/cues';
import type { UserSettings } from '@/types/api/user';

const props = defineProps<{
line: ScriptLine;
Expand Down Expand Up @@ -359,7 +360,12 @@ const sceneLabel = computed(
const stageDirectionStyle = computed(() =>
getStageDirectionStyle(props.line, props.stageDirectionStyles, props.stageDirectionStyleOverrides)
);
const stageDirectionStylingObj = computed(() => computeStyling(stageDirectionStyle.value));
const stageDirectionStylingObj = computed(() =>
computeStyling(stageDirectionStyle.value, {
background_colour: (userStore.userSettings as UserSettings).default_sd_background_colour,
text_colour: (userStore.userSettings as UserSettings).default_sd_text_colour,
})
);

const scriptTextAlign = computed(() => computeTextAlign(userStore.userSettings));
const headingStyle = computed(() => ({ textAlign: scriptTextAlign.value }));
Expand Down
132 changes: 132 additions & 0 deletions client-v3/src/components/user/settings/StageDirectionStyles.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,63 @@
<template>
<div>
<!-- Default stage direction style section -->
<BCard class="mb-3">
<template #header>
<strong>Default Stage Direction Style</strong>
</template>
<p class="text-muted small mb-2">
Applied to stage direction lines that have no specific style assigned.
</p>
<div class="d-flex align-items-center gap-3 flex-wrap">
<i class="example-stage-direction" :style="defaultExampleCss">
{{ exampleText }}
</i>
<BButton variant="outline-primary" @click="defaultModal?.show()">Customise</BButton>
<BButton
variant="outline-secondary"
:disabled="!hasDefaultOverride || isSubmittingDefault"
@click="resetDefaultStyle"
>
Reset to Default
</BButton>
</div>
</BCard>

<!-- Default style edit modal -->
<BModal
ref="defaultModal"
title="Customise Default Stage Direction Style"
size="lg"
:ok-disabled="isSubmittingDefault"
@show="openDefaultModal"
@ok.prevent="onSubmitDefaultStyle"
>
<div class="mb-3">
<h4>Preview</h4>
<i class="example-stage-direction" :style="defaultFormExampleCss">
{{ exampleText }}
</i>
</div>
<BFormGroup label="Background Colour" label-for="default-bg-colour-input">
<BFormInput
id="default-bg-colour-input"
v-model="defaultFormState.backgroundColour"
type="color"
/>
</BFormGroup>
<BFormGroup label="Text Colour" label-for="default-text-colour-input">
<BFormCheckbox v-model="defaultFormState.enableTextColour" switch class="mb-1">
Override text colour
</BFormCheckbox>
<BFormInput
v-if="defaultFormState.enableTextColour"
id="default-text-colour-input"
v-model="defaultFormState.textColour"
type="color"
/>
</BFormGroup>
</BModal>

<template v-if="systemStore.currentShow != null">
<BTable
id="stage-directions-table"
Expand Down Expand Up @@ -251,6 +309,7 @@ import { useSystemStore } from '@/stores/system';
import { useFormValidation } from '@/composables/useFormValidation';
import { usePagination } from '@/composables/usePagination';
import type { StageDirectionStyle } from '@/types/api/script';
import type { UserSettings } from '@/types/api/user';

interface StyleOption {
caption: string;
Expand Down Expand Up @@ -278,14 +337,52 @@ const { perPage, currentPage } = usePagination(15, 'user_stage_direction_styles'
const stageDirectionStyles = ref<StageDirectionStyle[]>([]);
const isSubmittingNew = ref(false);
const isSubmittingEdit = ref(false);
const isSubmittingDefault = ref(false);
const isDeleting = ref(false);
const pendingDeleteId = ref<number | null>(null);

const defaultModal = ref<InstanceType<typeof BModal>>();
const selectModal = ref<InstanceType<typeof BModal>>();
const newModal = ref<InstanceType<typeof BModal>>();
const editModal = ref<InstanceType<typeof BModal>>();
const deleteModal = ref<InstanceType<typeof BModal>>();

const defaultFormState = ref({
backgroundColour: '#483d8b',
enableTextColour: false,
textColour: '#FFFFFF',
});

const settings = computed(() => userStore.userSettings as UserSettings);

const hasDefaultOverride = computed(
() =>
settings.value.default_sd_background_colour != null ||
settings.value.default_sd_text_colour != null
);

const defaultExampleCss = computed((): Record<string, string> => {
const result: Record<string, string> = {
'background-color': settings.value.default_sd_background_colour ?? 'darkslateblue',
'font-style': 'italic',
};
if (settings.value.default_sd_text_colour) {
result['color'] = settings.value.default_sd_text_colour;
}
return result;
});

const defaultFormExampleCss = computed((): Record<string, string> => {
const result: Record<string, string> = {
'background-color': defaultFormState.value.backgroundColour,
'font-style': 'italic',
};
if (defaultFormState.value.enableTextColour) {
result['color'] = defaultFormState.value.textColour;
}
return result;
});

const newStyleFormState = ref({
styleId: null as number | null,
styleOptions: defaultStyleOptions(),
Expand Down Expand Up @@ -366,6 +463,41 @@ const editFormExampleCss = computed((): Record<string, string> => {
return style;
});

function openDefaultModal(): void {
const s = settings.value;
defaultFormState.value.backgroundColour = s.default_sd_background_colour ?? '#483d8b';
defaultFormState.value.enableTextColour = s.default_sd_text_colour != null;
defaultFormState.value.textColour = s.default_sd_text_colour ?? '#FFFFFF';
}

async function onSubmitDefaultStyle(): Promise<void> {
if (isSubmittingDefault.value) return;
isSubmittingDefault.value = true;
try {
await userStore.updateDefaultStageDirectionStyle(
defaultFormState.value.backgroundColour,
defaultFormState.value.enableTextColour ? defaultFormState.value.textColour : null
);
defaultModal.value?.hide();
} catch (e) {
log.error('Error updating default stage direction style:', e);
} finally {
isSubmittingDefault.value = false;
}
}

async function resetDefaultStyle(): Promise<void> {
if (isSubmittingDefault.value) return;
isSubmittingDefault.value = true;
try {
await userStore.updateDefaultStageDirectionStyle(null, null);
} catch (e) {
log.error('Error resetting default stage direction style:', e);
} finally {
isSubmittingDefault.value = false;
}
}

onMounted(async () => {
if (systemStore.currentShow) {
try {
Expand Down
14 changes: 12 additions & 2 deletions client-v3/src/composables/useScriptDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,18 @@ function getStageDirectionStyle(
return override ? override.settings : style;
}

function stageDirectionStyling(style: StageDirectionStyle | null): Record<string, string> {
if (!style) return { 'background-color': 'darkslateblue', 'font-style': 'italic' };
function stageDirectionStyling(
style: StageDirectionStyle | null,
defaultColors?: { background_colour?: string | null; text_colour?: string | null }
): Record<string, string> {
if (!style) {
const result: Record<string, string> = {
'background-color': defaultColors?.background_colour ?? 'darkslateblue',
'font-style': 'italic',
};
if (defaultColors?.text_colour) result['color'] = defaultColors.text_colour;
return result;
}
const result: Record<string, string> = {
'font-weight': style.bold ? 'bold' : 'normal',
'font-style': style.italic ? 'italic' : 'normal',
Expand Down
14 changes: 14 additions & 0 deletions client-v3/src/stores/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,20 @@ export const useUserStore = defineStore('user', {
}
},

async updateDefaultStageDirectionStyle(
backgroundColour: string | null,
textColour: string | null
): Promise<void> {
await fetch(makeURL('/api/v1/user/settings'), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
default_sd_background_colour: backgroundColour,
default_sd_text_colour: textColour,
}),
});
},

async updateTablePageSize(tableKey: string, value: number): Promise<void> {
const current = (this.userSettings as UserSettings).table_page_sizes ?? {};
await fetch(makeURL('/api/v1/user/settings'), {
Expand Down
2 changes: 2 additions & 0 deletions client-v3/src/types/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ export interface UserSettings {
character_combined_dropdown: boolean;
preferred_ui: string | null;
table_page_sizes: Record<string, number> | null;
default_sd_text_colour: string | null;
default_sd_background_colour: string | null;
}
10 changes: 9 additions & 1 deletion client/src/mixins/scriptDisplayMixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,15 @@ export default defineComponent({
stageDirectionStyling(): Record<string, string> {
const line: ScriptLine = (this as any).line;
if (line.stage_direction_style_id == null || (this as any).stageDirectionStyle == null) {
return { 'background-color': 'darkslateblue', 'font-style': 'italic' };
const userSettings = (this as any).USER_SETTINGS ?? {};
const result: Record<string, string> = {
'background-color': userSettings.default_sd_background_colour ?? 'darkslateblue',
'font-style': 'italic',
};
if (userSettings.default_sd_text_colour) {
result['color'] = userSettings.default_sd_text_colour;
}
return result;
}
const style: StageDirectionStyle = (this as any).stageDirectionStyle;
const result: Record<string, string> = {
Expand Down
2 changes: 2 additions & 0 deletions client/src/types/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ export interface UserSettings {
character_mru_sort: boolean;
character_combined_dropdown: boolean;
table_page_sizes: Record<string, number> | null;
default_sd_text_colour: string | null;
default_sd_background_colour: string | null;
}
Loading
Loading