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
4 changes: 2 additions & 2 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "client",
"version": "0.26.0",
"version": "0.27.0",
"description": "DigiScript front end",
"author": "DreamTeamProd",
"private": true,
Expand Down
11 changes: 11 additions & 0 deletions client/src/store/modules/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,17 @@ export default {
Vue.$toast.error('Unable to edit stage direction style');
}
},
async GET_IMPORTABLE_STAGE_DIRECTION_STYLES() {
const response = await fetch(
`${makeURL('/api/v1/show/script/stage_direction_styles/import')}`,
{ method: 'GET' }
);
if (!response.ok) {
log.error('Unable to fetch importable stage direction styles');
throw new Error('Failed to fetch importable styles');
}
return response.json();
},
async GET_COMPILED_SCRIPTS(context) {
const response = await fetch(`${makeURL('/api/v1/show/script/compiled_scripts')}`, {
method: 'GET',
Expand Down
123 changes: 123 additions & 0 deletions client/src/vue_components/show/config/script/StageDirectionStyles.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,71 @@
<b-button v-if="IS_SCRIPT_EDITOR" v-b-modal.new-config-modal variant="outline-success">
New Style
</b-button>
<b-button
v-if="IS_SCRIPT_EDITOR"
variant="outline-info"
class="ml-2"
@click="openImportModal"
>
Import Style
</b-button>
<b-modal
id="import-style-modal"
ref="import-style-modal"
title="Import Stage Direction Style"
size="xl"
ok-only
ok-title="Close"
@hidden="resetImportState"
>
<div v-if="isLoadingImport" class="text-center py-3">
<b-spinner />
</div>
<div v-else-if="importStyleGroups.length === 0" class="text-muted text-center py-3">
No styles available to import from other shows.
</div>
<div v-else>
<b-card v-for="show in importStyleGroups" :key="show.id" no-body class="mb-2">
<b-card-header class="section-card-header" @click="toggleImportShow(show.id)">
<div class="d-flex justify-content-between align-items-center">
<span>{{ show.name }}</span>
<b-icon-chevron-down v-if="styleGroupExpanded[show.id]" font-scale="0.8" />
<b-icon-chevron-up v-else font-scale="0.8" />
</div>
</b-card-header>
<b-collapse :visible="styleGroupExpanded[show.id]">
<b-card-body class="p-0">
<b-table :items="show.styles" :fields="importColumns" small show-empty class="mb-0">
<template #cell(example)="row">
<i class="example-stage-direction" :style="exampleCss(row.item)">
<template v-if="row.item.text_format === 'upper'">
{{ exampleText | uppercase }}
</template>
<template v-else-if="row.item.text_format === 'lower'">
{{ exampleText | lowercase }}
</template>
<template v-else>
{{ exampleText }}
</template>
</i>
</template>
<template #cell(btn)="row">
<b-button
variant="outline-success"
size="sm"
:disabled="!!isImporting[row.item.id]"
@click="importStyle(row.item)"
>
<b-spinner v-if="isImporting[row.item.id]" small />
<span v-else>Import</span>
</b-button>
</template>
</b-table>
</b-card-body>
</b-collapse>
</b-card>
</div>
</b-modal>
<b-modal
id="new-config-modal"
ref="new-config-modal"
Expand Down Expand Up @@ -292,6 +357,11 @@ export default {
{ key: 'example', label: 'Example Stage Direction' },
{ key: 'btn', label: '' },
],
importColumns: [
'description',
{ key: 'example', label: 'Example Stage Direction' },
{ key: 'btn', label: '' },
],
rowsPerPage: 15,
currentPage: 1,
newStyleFormState: {
Expand Down Expand Up @@ -322,6 +392,10 @@ export default {
isSubmittingNew: false,
isSubmittingEdit: false,
isDeleting: false,
importStyleGroups: [],
styleGroupExpanded: {},
isLoadingImport: false,
isImporting: {},
};
},
computed: {
Expand Down Expand Up @@ -587,11 +661,60 @@ export default {
}
return style;
},
async openImportModal() {
this.$bvModal.show('import-style-modal');
this.isLoadingImport = true;
try {
const data = await this.GET_IMPORTABLE_STAGE_DIRECTION_STYLES();
this.importStyleGroups = data.style_groups;
const expanded = {};
data.style_groups.forEach((group) => {
expanded[group.id] = true;
});
this.styleGroupExpanded = expanded;
} catch (error) {
log.error('Error fetching importable stage direction styles:', error);
this.$toast.error('Failed to load styles for import');
} finally {
this.isLoadingImport = false;
}
},
resetImportState() {
this.importStyleGroups = [];
this.styleGroupExpanded = {};
this.isLoadingImport = false;
this.isImporting = {};
},
toggleImportShow(showId) {
this.$set(this.styleGroupExpanded, showId, !this.styleGroupExpanded[showId]);
},
async importStyle(style) {
this.$set(this.isImporting, style.id, true);
try {
await this.ADD_STAGE_DIRECTION_STYLE({
description: style.description,
bold: style.bold,
italic: style.italic,
underline: style.underline,
textFormat: style.text_format,
textColour: style.text_colour,
enableBackgroundColour: style.enable_background_colour,
backgroundColour: style.background_colour,
});
this.$toast.success(`Imported "${style.description}"`);
} catch (error) {
log.error('Error importing stage direction style:', error);
this.$toast.error(`Failed to import "${style.description}"`);
} finally {
this.$set(this.isImporting, style.id, false);
}
},
...mapActions([
'GET_STAGE_DIRECTION_STYLES',
'ADD_STAGE_DIRECTION_STYLE',
'DELETE_STAGE_DIRECTION_STYLE',
'UPDATE_STAGE_DIRECTION_STYLE',
'GET_IMPORTABLE_STAGE_DIRECTION_STYLES',
]),
},
};
Expand Down
24 changes: 24 additions & 0 deletions docs/pages/script_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,30 @@ Click **New Revision** to create a new revision. You'll need to provide a descri

When you create a new revision, its base state is copied from the currently loaded revision. Any changes you make to the script after that point will only affect the active revision - other revisions remain unchanged, preserving the complete history of your script.

### Stage Direction Styles

The **Stage Direction Styles** tab lets you define the visual appearance of stage direction lines throughout your script. Each style controls the text formatting applied when that style is assigned to a stage direction line.

#### Creating a New Style

Click **New Style** to open the style editor. You can configure:

- **Description** — a name for the style (e.g. "Narrator", "Whisper")
- **Default Styles** — toggle Bold, Italic, and Underline
- **Default Text Format** — Default, Uppercase, or Lowercase
- **Text Colour** — a colour picker for the text colour
- **Background Colour** — optionally enable and choose a background highlight colour

A live preview of the style is shown at the top of the editor so you can see how it will appear in the script before saving.

#### Importing Styles from Another Show

Click **Import Style** to open the import modal. This shows all stage direction styles from every other show in your DigiScript instance, grouped by show name. Each style is displayed with its live preview so you can see exactly what it will look like.

Click **Import** next to any style to copy it into the current show. The import creates an independent copy — changes to the imported style will not affect the original show, and vice versa. The modal stays open so you can import multiple styles in a single session.

If no other shows have stage direction styles defined, a message is shown indicating there are no styles available to import.

### Script Content

The **Script** tab is where you edit the actual script content. When you first navigate to this tab, you'll see an empty script interface:
Expand Down
4 changes: 2 additions & 2 deletions electron/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion electron/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "digiscript-electron",
"version": "0.26.0",
"version": "0.27.0",
"description": "DigiScript Electron Desktop Application",
"author": "DreamTeamProd",
"license": "GPL-3.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""fix_duplicate_sessions_user_id_fk

Migration 29471f7cf7d2 added the named FK fk_sessions_user_id_user to the
sessions table but did not drop the pre-existing unnamed FK on the same column.
Databases upgraded through that migration therefore ended up with two FK
constraints on sessions.user_id, which causes SQLAlchemy to emit a SAWarning
during every subsequent migration run.

This migration drops the unnamed duplicate, leaving only the named constraint
with ON DELETE SET NULL. The try/except mirrors the pattern in a4d42ccfb71a:
databases that were created from scratch (never had the unnamed FK) would raise
IndexError from drop_constraint(None), which we safely ignore.

Revision ID: d2e0b8414d17
Revises: d3e9f0c1a2b4
Create Date: 2026-05-06 19:13:55.162159

"""

from typing import Sequence, Union

from alembic import op


# revision identifiers, used by Alembic.
revision: str = "d2e0b8414d17"
down_revision: Union[str, None] = "d3e9f0c1a2b4"

Check warning on line 27 in server/alembic_config/versions/d2e0b8414d17_fix_duplicate_sessions_user_id_fk.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a union type expression for this type hint.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ3-4Ta3foiGubrVGEV3&open=AZ3-4Ta3foiGubrVGEV3&pullRequest=990
branch_labels: Union[str, Sequence[str], None] = None

Check warning on line 28 in server/alembic_config/versions/d2e0b8414d17_fix_duplicate_sessions_user_id_fk.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a union type expression for this type hint.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ3-4Ta3foiGubrVGEV4&open=AZ3-4Ta3foiGubrVGEV4&pullRequest=990
depends_on: Union[str, Sequence[str], None] = None

Check warning on line 29 in server/alembic_config/versions/d2e0b8414d17_fix_duplicate_sessions_user_id_fk.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a union type expression for this type hint.

See more on https://sonarcloud.io/project/issues?id=dreamteamprod_DigiScript&issues=AZ3-4Ta3foiGubrVGEV5&open=AZ3-4Ta3foiGubrVGEV5&pullRequest=990


def upgrade() -> None:
# The unnamed duplicate FK is invisible to SQLAlchemy reflection, so
# drop_constraint(None) finds nothing. Instead, drop and recreate the
# named constraint: this forces SQLite batch mode to rebuild the table
# from the reflected schema (which only sees the named FK), so the
# unnamed duplicate is silently excluded from the new table.
with op.batch_alter_table("sessions", schema=None) as batch_op:
batch_op.drop_constraint(
batch_op.f("fk_sessions_user_id_user"), type_="foreignkey"
)
batch_op.create_foreign_key(
batch_op.f("fk_sessions_user_id_user"),
"user",
["user_id"],
["id"],
ondelete="SET NULL",
)


def downgrade() -> None:
# Restore the unnamed FK (pre-29471f7cf7d2 state) alongside the named one.
with op.batch_alter_table("sessions", schema=None) as batch_op:
batch_op.create_foreign_key(None, "user", ["user_id"], ["id"])
39 changes: 38 additions & 1 deletion server/controllers/api/show/script/stage_direction_styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from schemas.schemas import StageDirectionStyleSchema
from utils.web.base_controller import BaseAPIController
from utils.web.route import ApiRoute, ApiVersion
from utils.web.web_decorators import no_live_session, requires_show
from utils.web.web_decorators import api_authenticated, no_live_session, requires_show


VALID_TEXT_FORMATS = ("default", "upper", "lower")
Expand Down Expand Up @@ -229,3 +229,40 @@ async def delete(self):
else:
self.set_status(404)
await self.finish({"message": ERROR_SHOW_NOT_FOUND})


@ApiRoute("/show/script/stage_direction_styles/import", ApiVersion.V1)
class StageDirectionStylesImportController(BaseAPIController):
@api_authenticated
@requires_show
def get(self):
"""
Return all stage direction styles from all other shows, grouped by show.

:returns: JSON with a ``shows`` key containing a list of show objects,
each with ``id``, ``name``, and ``styles`` fields.
"""
current_show_id = self.get_current_show()["id"]
schema = StageDirectionStyleSchema()

with self.make_session() as session:
rows = session.execute(
select(Show, StageDirectionStyle)
.join(Script, Script.show_id == Show.id)
.join(StageDirectionStyle, StageDirectionStyle.script_id == Script.id)
.where(Show.id != current_show_id)
.order_by(Show.id)
).all()

shows_map: dict = {}
for show, style in rows:
if show.id not in shows_map:
shows_map[show.id] = {
"id": show.id,
"name": show.name,
"styles": [],
}
shows_map[show.id]["styles"].append(schema.dump(style))

self.set_status(200)
self.finish({"style_groups": list(shows_map.values())})
2 changes: 1 addition & 1 deletion server/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "digiscript-server"
version = "0.26.0"
version = "0.27.0"
description = "DigiScript server - Digital script management for theatrical shows"
readme = "../README.md"
requires-python = ">=3.13"
Expand Down
Loading
Loading