diff --git a/client-v3/e2e/tests/06-show-config-characters.spec.ts b/client-v3/e2e/tests/06-show-config-characters.spec.ts
index e6e957bcf..c2207ffb9 100644
--- a/client-v3/e2e/tests/06-show-config-characters.spec.ts
+++ b/client-v3/e2e/tests/06-show-config-characters.spec.ts
@@ -83,6 +83,62 @@ test('deletes a character', async () => {
});
});
+// ── Rows per page selector ─────────────────────────────────────────────────
+
+test('rows per page selector is hidden when row count does not exceed minimum', async () => {
+ // Only Hamlet exists (1 row) — controls hidden when totalRows ≤ 10 (smallest option)
+ await expect(page.locator('select#character-table-per-page')).not.toBeVisible();
+});
+
+test('creates enough characters for pagination controls to appear', async () => {
+ for (const name of [
+ 'Alice',
+ 'Bob',
+ 'Carol',
+ 'Dave',
+ 'Eve',
+ 'Frank',
+ 'Grace',
+ 'Henry',
+ 'Iris',
+ 'Jack',
+ ]) {
+ await page.getByRole('button', { name: 'New Character', exact: true }).click();
+ await waitForModal(page, 'New Character');
+ await page.fill('.modal.show input[type="text"]', name);
+ await confirmModal(page);
+ await waitForModalClosed(page);
+ }
+ // 11 characters total (Hamlet + 10) — controls should now appear
+ await expect(page.locator('select#character-table-per-page')).toBeVisible();
+});
+
+test('rows per page selector has correct default', async () => {
+ await expect(page.locator('label[for="character-table-per-page"]')).toBeVisible();
+ await expect(page.locator('select#character-table-per-page')).toHaveValue('15');
+});
+
+test('rows per page selector has all expected options', async () => {
+ const options = await page
+ .locator('select#character-table-per-page')
+ .locator('option')
+ .allTextContents();
+ expect(options).toEqual(expect.arrayContaining(['10', '15', '25', '50', 'All']));
+});
+
+test('pagination nav shows when rows exceed per-page and hides on All', async () => {
+ // 11 rows, default perPage=15: nav hidden (11 ≤ 15)
+ await expect(page.locator('button[aria-controls="character-table"]').first()).not.toBeVisible();
+ // Change to 10 rows/page: nav appears (11 > 10)
+ await page.locator('select#character-table-per-page').selectOption('10');
+ await expect(page.locator('button[aria-controls="character-table"]').first()).toBeVisible();
+ // Select All: nav disappears
+ await page.locator('select#character-table-per-page').selectOption('0');
+ await expect(page.locator('button[aria-controls="character-table"]').first()).not.toBeVisible();
+ // Reset to default
+ await page.locator('select#character-table-per-page').selectOption('15');
+});
+
// ── Character Merge ────────────────────────────────────────────────────────
test('Merge button is visible for each character row', async () => {
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 b32cf7608..e2b92d04f 100644
--- a/client-v3/e2e/tests/10-show-config-script.spec.ts
+++ b/client-v3/e2e/tests/10-show-config-script.spec.ts
@@ -120,6 +120,73 @@ test('saves a dialogue line to the script', async () => {
).toBeVisible({ timeout: 10_000 });
});
+// ── Cut mode ──────────────────────────────────────────────────────────────
+
+test('enters cut mode', async () => {
+ await page.goto(`${UI_BASE}/show-config/script`);
+ await waitForAppReady(page);
+ await page.click('button:has-text("Cuts")');
+ await expect(page.locator('button:has-text("Stop Editing")')).toBeVisible({ timeout: 10_000 });
+});
+
+test('dialogue line becomes clickable in cut mode', async () => {
+ await expect(
+ page.locator('a.viewable-line-cut').filter({ hasText: 'To be or not to be' })
+ ).toBeVisible({ timeout: 5_000 });
+});
+
+test('clicking a line toggles cut styling', async () => {
+ await page.locator('a.viewable-line-cut').filter({ hasText: 'To be or not to be' }).click();
+ await expect(
+ page.locator('.cut-line-part').filter({ hasText: 'To be or not to be' })
+ ).toBeVisible({ timeout: 3_000 });
+});
+
+test('saves cuts', async () => {
+ await page.getByRole('button', { name: 'Save', exact: true }).click();
+ await expect(page.locator('.v-toast__text').filter({ hasText: /saved/i })).toBeVisible({
+ timeout: 5_000,
+ });
+});
+
+test('stops cut mode without blank-page flash', async () => {
+ // In cut mode the line is an anchor (.viewable-line-cut); verify it exists before stopping
+ await expect(
+ page.locator('.viewable-line-cut').filter({ hasText: 'To be or not to be' })
+ ).toBeVisible();
+
+ await page.click('button:has-text("Stop Editing")');
+
+ // Edit button reappears
+ await expect(page.getByRole('button', { name: 'Edit', exact: true })).toBeVisible({
+ timeout: 10_000,
+ });
+ // The saved line is still visible (no reload / blank flash)
+ await expect(
+ page.locator('.viewable-line').filter({ hasText: 'To be or not to be' })
+ ).toBeVisible();
+ // No edit controls are shown after stopping
+ await expect(page.locator('.script-edit-controls')).not.toBeVisible();
+});
+
+test('cut styling persists after re-entering cut mode', async () => {
+ await page.click('button:has-text("Cuts")');
+ await expect(page.locator('button:has-text("Stop Editing")')).toBeVisible({ timeout: 10_000 });
+ await expect(
+ page.locator('.cut-line-part').filter({ hasText: 'To be or not to be' })
+ ).toBeVisible({ timeout: 5_000 });
+ // Clean up: un-cut the line so later tests start from a known state
+ await page.locator('a.viewable-line-cut').filter({ hasText: 'To be or not to be' }).click();
+ await page.getByRole('button', { name: 'Save', exact: true }).click();
+ await expect(page.locator('.v-toast__text').filter({ hasText: /saved/i })).toBeVisible({
+ timeout: 5_000,
+ });
+ await page.click('button:has-text("Stop Editing")');
+ await expect(page.getByRole('button', { name: 'Edit', exact: true })).toBeVisible({
+ timeout: 10_000,
+ });
+});
+
// ── Stage Direction Styles ────────────────────────────────────────────────
test('switches to Stage Direction Styles sub-tab', async () => {
diff --git a/client-v3/package-lock.json b/client-v3/package-lock.json
index 08f2ce7b0..69ae23544 100644
--- a/client-v3/package-lock.json
+++ b/client-v3/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "client-v3",
- "version": "0.31.1",
+ "version": "0.31.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "client-v3",
- "version": "0.31.1",
+ "version": "0.31.2",
"dependencies": {
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
diff --git a/client-v3/package.json b/client-v3/package.json
index c231186b5..fcda5dfa1 100644
--- a/client-v3/package.json
+++ b/client-v3/package.json
@@ -1,6 +1,6 @@
{
"name": "client-v3",
- "version": "0.31.1",
+ "version": "0.31.2",
"description": "DigiScript front end (Vue 3)",
"author": "DreamTeamProd",
"private": true,
diff --git a/client-v3/src/components/config/ConfigShows.vue b/client-v3/src/components/config/ConfigShows.vue
index 071be64ec..62ca7a9d9 100644
--- a/client-v3/src/components/config/ConfigShows.vue
+++ b/client-v3/src/components/config/ConfigShows.vue
@@ -7,7 +7,7 @@
id="shows-table"
:items="availableShows"
:fields="showFields"
- :per-page="rowsPerPage"
+ :per-page="perPage"
:current-page="currentPage"
>
@@ -37,13 +37,11 @@
-
@@ -129,6 +127,7 @@ import { makeURL } from '@/js/utils';
import { useSystemStore } from '@/stores/system';
import { useShowStore } from '@/stores/show';
import { useConfirm } from '@/composables/useConfirm';
+import { usePagination } from '@/composables/usePagination';
import { toast } from '@/js/toast';
import type { Show } from '@/types/api/show';
@@ -144,8 +143,7 @@ const isSubmittingLoad = ref(false);
const isSubmittingShow = ref(false);
const isDeleting = ref(false);
const deletingId = ref(null);
-const currentPage = ref(1);
-const rowsPerPage = 15;
+const { perPage, currentPage } = usePagination();
const showFields = [
{ key: 'id', label: 'ID' },
diff --git a/client-v3/src/components/config/ConfigSystem.vue b/client-v3/src/components/config/ConfigSystem.vue
index ae5e74134..ecdb107a7 100644
--- a/client-v3/src/components/config/ConfigSystem.vue
+++ b/client-v3/src/components/config/ConfigSystem.vue
@@ -73,12 +73,11 @@
:current-page="currentPage"
small
/>
-
@@ -92,6 +91,7 @@ import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { storeToRefs } from 'pinia';
import { BModal } from 'bootstrap-vue-next';
import { useSystemStore } from '@/stores/system';
+import { usePagination } from '@/composables/usePagination';
import { toast } from '@/js/toast';
const systemStore = useSystemStore();
@@ -99,8 +99,7 @@ const { connectedSessions, versionStatus, serverInfo } = storeToRefs(systemStore
const loading = ref(true);
const isCheckingVersion = ref(false);
-const currentPage = ref(1);
-const perPage = 5;
+const { perPage, currentPage } = usePagination();
const clientsModal = ref>();
const clientFields = [
diff --git a/client-v3/src/components/shared/PaginationControls.vue b/client-v3/src/components/shared/PaginationControls.vue
new file mode 100644
index 000000000..1f37e7ca6
--- /dev/null
+++ b/client-v3/src/components/shared/PaginationControls.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client-v3/src/components/show/config/acts_and_scenes/ConfigActs.vue b/client-v3/src/components/show/config/acts_and_scenes/ConfigActs.vue
index 850df7cba..9bb104c4c 100644
--- a/client-v3/src/components/show/config/acts_and_scenes/ConfigActs.vue
+++ b/client-v3/src/components/show/config/acts_and_scenes/ConfigActs.vue
@@ -4,7 +4,7 @@
id="acts-table"
:items="actTableItems"
:fields="actFields"
- :per-page="rowsPerPage"
+ :per-page="perPage"
:current-page="currentPage"
show-empty
>
@@ -46,13 +46,11 @@
-
@@ -49,13 +49,11 @@
-
@@ -200,6 +198,7 @@ import log from 'loglevel';
import { useSystemStore } from '@/stores/system';
import { useShowStore } from '@/stores/show';
import { useConfirm } from '@/composables/useConfirm';
+import { usePagination } from '@/composables/usePagination';
import type { Scene } from '@/types/api/show';
const systemStore = useSystemStore();
@@ -207,8 +206,7 @@ const showStore = useShowStore();
const { confirm } = useConfirm();
const loading = ref(true);
-const rowsPerPage = 15;
-const currentPage = ref(1);
+const { perPage, currentPage } = usePagination();
const submittingNewScene = ref(false);
const submittingEditScene = ref(false);
const submittingFirstScene = ref(false);
diff --git a/client-v3/src/components/show/config/characters/CharacterGroups.vue b/client-v3/src/components/show/config/characters/CharacterGroups.vue
index 848d12611..68a1f1956 100644
--- a/client-v3/src/components/show/config/characters/CharacterGroups.vue
+++ b/client-v3/src/components/show/config/characters/CharacterGroups.vue
@@ -4,7 +4,7 @@
id="character-group-table"
:items="showStore.characterGroupList"
:fields="characterGroupFields"
- :per-page="rowsPerPage"
+ :per-page="perPage"
:current-page="currentPage"
show-empty
>
@@ -48,13 +48,11 @@
-
@@ -33,11 +33,10 @@
-
@@ -94,6 +93,7 @@ import { required, helpers } from '@vuelidate/validators';
import { useShowStore } from '@/stores/show';
import { useSystemStore } from '@/stores/system';
import { useConfirm } from '@/composables/useConfirm';
+import { usePagination } from '@/composables/usePagination';
import { useFormValidation } from '@/composables/useFormValidation';
import type { Microphone } from '@/types/api/microphones';
import log from 'loglevel';
@@ -103,8 +103,7 @@ const systemStore = useSystemStore();
const { confirm } = useConfirm();
const { validationState } = useFormValidation();
-const rowsPerPage = 15;
-const currentPage = ref(1);
+const { perPage, currentPage } = usePagination();
const isSubmitting = ref(false);
const newModal = ref>();
diff --git a/client-v3/src/components/show/config/script/ScriptEditor.vue b/client-v3/src/components/show/config/script/ScriptEditor.vue
index 44d98c963..ce862e05c 100644
--- a/client-v3/src/components/show/config/script/ScriptEditor.vue
+++ b/client-v3/src/components/show/config/script/ScriptEditor.vue
@@ -257,7 +257,7 @@ const isEditor = computed(
wsStore.internalUUID !== null &&
wsStore.internalUUID === scriptConfigStore.editStatus.currentEditor
);
-const canEdit = computed(() => isEditor.value && !scriptConfigStore.cutMode);
+const canEdit = computed(() => isEditor.value);
const saveProgressVariant = computed(() => {
if (savingInProgress.value) return 'primary';
@@ -336,13 +336,20 @@ async function stopEditing(): Promise {
);
if (!ok) return;
}
+ const wasCutMode = scriptConfigStore.cutMode;
editingLines.value.clear();
exitBulkEditMode();
- scriptConfigStore.emptyScript();
linePartCuts.value = [...scriptStore.cuts];
- sendObj({ OP: 'STOP_SCRIPT_EDIT', DATA: {} });
scriptConfigStore.setCutMode(false);
- await loadPage(currentPage.value);
+ scriptConfigStore.setEditStatus({
+ canRequestEdit: scriptConfigStore.editStatus.canRequestEdit,
+ currentEditor: null,
+ });
+ sendObj({ OP: 'STOP_SCRIPT_EDIT', DATA: {} });
+ if (!wasCutMode) {
+ scriptConfigStore.emptyScript();
+ await loadPage(currentPage.value);
+ }
}
async function loadPage(page: number): Promise {
diff --git a/client-v3/src/components/show/config/script/ScriptLineViewer.vue b/client-v3/src/components/show/config/script/ScriptLineViewer.vue
index 5c3c6c0e1..6bf13b507 100644
--- a/client-v3/src/components/show/config/script/ScriptLineViewer.vue
+++ b/client-v3/src/components/show/config/script/ScriptLineViewer.vue
@@ -1,5 +1,6 @@
+
Edit
@@ -305,4 +306,9 @@ function cutLinePart(partIndex: number): void {
.cut-line-part {
text-decoration: line-through;
}
+.script-line-row:has(.script-edit-controls:hover) {
+ background-color: rgba(255, 255, 255, 0.06);
+ border-radius: 4px;
+ transition: background-color 0.15s ease;
+}
diff --git a/client-v3/src/components/show/config/script/StageDirectionStyles.vue b/client-v3/src/components/show/config/script/StageDirectionStyles.vue
index 7f04c92e7..a23180d72 100644
--- a/client-v3/src/components/show/config/script/StageDirectionStyles.vue
+++ b/client-v3/src/components/show/config/script/StageDirectionStyles.vue
@@ -5,7 +5,7 @@
@@ -53,11 +53,10 @@
-
@@ -147,6 +146,7 @@ import log from 'loglevel';
import { useScriptStore } from '@/stores/script';
import { useSystemStore } from '@/stores/system';
import { useConfirm } from '@/composables/useConfirm';
+import { usePagination } from '@/composables/usePagination';
import { useFormValidation } from '@/composables/useFormValidation';
import type { StageDirectionStyle } from '@/types/api/script';
import { toast } from '@/js/toast';
@@ -156,8 +156,7 @@ const systemStore = useSystemStore();
const { confirm } = useConfirm();
const { validationState } = useFormValidation();
-const rowsPerPage = 15;
-const currentPage = ref(1);
+const { perPage, currentPage } = usePagination();
const isSubmittingNew = ref(false);
const isSubmittingEdit = ref(false);
const isDeleting = ref(false);
diff --git a/client-v3/src/components/show/config/sessions/SessionTagList.vue b/client-v3/src/components/show/config/sessions/SessionTagList.vue
index 40a43ad20..a0e6b34b6 100644
--- a/client-v3/src/components/show/config/sessions/SessionTagList.vue
+++ b/client-v3/src/components/show/config/sessions/SessionTagList.vue
@@ -4,7 +4,7 @@
id="session-tags-table"
:items="showStore.sessionTags"
:fields="tagFields"
- :per-page="rowsPerPage"
+ :per-page="perPage"
:current-page="currentPage"
show-empty
>
@@ -39,13 +39,11 @@
-
@@ -29,11 +29,10 @@
-
@@ -102,6 +101,7 @@ import { required } from '@vuelidate/validators';
import { useStageStore } from '@/stores/stage';
import { useSystemStore } from '@/stores/system';
import { useConfirm } from '@/composables/useConfirm';
+import { usePagination } from '@/composables/usePagination';
import { useFormValidation } from '@/composables/useFormValidation';
import type { Crew } from '@/types/api/stage';
import log from 'loglevel';
@@ -111,8 +111,7 @@ const systemStore = useSystemStore();
const { confirm } = useConfirm();
const { validationState } = useFormValidation();
-const rowsPerPage = 15;
-const currentPage = ref(1);
+const { perPage, currentPage } = usePagination();
const isSubmitting = ref(false);
const newModal = ref>();
diff --git a/client-v3/src/components/show/config/stage/PropsList.vue b/client-v3/src/components/show/config/stage/PropsList.vue
index 3eec6db21..7e65e62a3 100644
--- a/client-v3/src/components/show/config/stage/PropsList.vue
+++ b/client-v3/src/components/show/config/stage/PropsList.vue
@@ -6,7 +6,7 @@
@@ -38,11 +38,10 @@
-
@@ -50,7 +49,7 @@
@@ -81,11 +80,10 @@
-
@@ -212,6 +210,7 @@ import { required, helpers } from '@vuelidate/validators';
import { useStageStore } from '@/stores/stage';
import { useSystemStore } from '@/stores/system';
import { useConfirm } from '@/composables/useConfirm';
+import { usePagination } from '@/composables/usePagination';
import { useFormValidation } from '@/composables/useFormValidation';
import type { PropType, Props } from '@/types/api/stage';
import log from 'loglevel';
@@ -221,9 +220,8 @@ const systemStore = useSystemStore();
const { confirm } = useConfirm();
const { validationState } = useFormValidation();
-const rowsPerPage = 15;
-const currentPropTypesPage = ref(1);
-const currentPropsPage = ref(1);
+const { perPage: propTypesPerPage, currentPage: currentPropTypesPage } = usePagination();
+const { perPage: propsPerPage, currentPage: currentPropsPage } = usePagination();
const isSubmitting = ref(false);
const newPropTypeModal = ref>();
diff --git a/client-v3/src/components/show/config/stage/SceneryList.vue b/client-v3/src/components/show/config/stage/SceneryList.vue
index 272d6cf2f..bad527bb5 100644
--- a/client-v3/src/components/show/config/stage/SceneryList.vue
+++ b/client-v3/src/components/show/config/stage/SceneryList.vue
@@ -6,7 +6,7 @@
@@ -38,11 +38,10 @@
-
@@ -50,7 +49,7 @@
@@ -85,11 +84,10 @@
-
@@ -214,6 +212,7 @@ import { required, helpers } from '@vuelidate/validators';
import { useStageStore } from '@/stores/stage';
import { useSystemStore } from '@/stores/system';
import { useConfirm } from '@/composables/useConfirm';
+import { usePagination } from '@/composables/usePagination';
import { useFormValidation } from '@/composables/useFormValidation';
import type { SceneryType, Scenery } from '@/types/api/stage';
import log from 'loglevel';
@@ -223,9 +222,8 @@ const systemStore = useSystemStore();
const { confirm } = useConfirm();
const { validationState } = useFormValidation();
-const rowsPerPage = 15;
-const currentSceneryTypesPage = ref(1);
-const currentSceneryPage = ref(1);
+const { perPage: sceneryTypesPerPage, currentPage: currentSceneryTypesPage } = usePagination();
+const { perPage: sceneryPerPage, currentPage: currentSceneryPage } = usePagination();
const isSubmitting = ref(false);
const newSceneryTypeModal = ref>();
diff --git a/client-v3/src/components/show/config/stage/StageManager.vue b/client-v3/src/components/show/config/stage/StageManager.vue
index d5e6f61a7..cbd298eda 100644
--- a/client-v3/src/components/show/config/stage/StageManager.vue
+++ b/client-v3/src/components/show/config/stage/StageManager.vue
@@ -65,7 +65,7 @@
-
@@ -98,7 +97,7 @@
-
@@ -433,6 +431,7 @@ import { required, helpers } from '@vuelidate/validators';
import { useStageStore } from '@/stores/stage';
import { useShowStore } from '@/stores/show';
import { useConfirm } from '@/composables/useConfirm';
+import { usePagination } from '@/composables/usePagination';
import { useFormValidation } from '@/composables/useFormValidation';
import { findOrphanedAssignments } from '@/js/blockOrphanUtils';
import type { SceneryAllocation, PropsAllocation, CrewAssignment, Crew } from '@/types/api/stage';
@@ -451,9 +450,8 @@ const setExpanded = ref(false);
const strikeExpanded = ref(false);
const savingAssignment = ref(false);
const newCrewSelections = ref>({});
-const rowsPerPage = 10;
-const currentSceneryAllocPage = ref(1);
-const currentPropsAllocPage = ref(1);
+const { perPage: sceneryAllocPerPage, currentPage: currentSceneryAllocPage } = usePagination();
+const { perPage: propsAllocPerPage, currentPage: currentPropsAllocPage } = usePagination();
const goToSceneModal = ref>();
const addSceneryModal = ref>();
diff --git a/client-v3/src/components/user/settings/CueColourPreferences.vue b/client-v3/src/components/user/settings/CueColourPreferences.vue
index d53841c99..8b36b354c 100644
--- a/client-v3/src/components/user/settings/CueColourPreferences.vue
+++ b/client-v3/src/components/user/settings/CueColourPreferences.vue
@@ -5,7 +5,7 @@
id="cue-colour-table"
:items="tableData"
:fields="columns"
- :per-page="rowsPerPage"
+ :per-page="perPage"
:current-page="currentPage"
show-empty
>
@@ -54,6 +54,12 @@
+
No show loaded.
@@ -169,6 +175,7 @@ import log from 'loglevel';
import { useUserStore } from '@/stores/user';
import { useSystemStore } from '@/stores/system';
import { useFormValidation } from '@/composables/useFormValidation';
+import { usePagination } from '@/composables/usePagination';
import type { CueType } from '@/types/api/cues';
const userStore = useUserStore();
@@ -180,8 +187,7 @@ const columns = [
{ key: 'example', label: 'Example Cue Button' },
{ key: 'btn', label: '' },
];
-const rowsPerPage = 15;
-const currentPage = ref(1);
+const { perPage, currentPage } = usePagination();
const cueTypes = ref([]);
const isSubmittingNew = ref(false);
diff --git a/client-v3/src/components/user/settings/StageDirectionStyles.vue b/client-v3/src/components/user/settings/StageDirectionStyles.vue
index 31d6ffbfb..a0bcc42ba 100644
--- a/client-v3/src/components/user/settings/StageDirectionStyles.vue
+++ b/client-v3/src/components/user/settings/StageDirectionStyles.vue
@@ -5,7 +5,7 @@
id="stage-directions-table"
:items="tableData"
:fields="columns"
- :per-page="rowsPerPage"
+ :per-page="perPage"
:current-page="currentPage"
show-empty
>
@@ -54,6 +54,12 @@
+
No show loaded.
@@ -243,6 +249,7 @@ import { makeURL } from '@/js/utils';
import { useUserStore } from '@/stores/user';
import { useSystemStore } from '@/stores/system';
import { useFormValidation } from '@/composables/useFormValidation';
+import { usePagination } from '@/composables/usePagination';
import type { StageDirectionStyle } from '@/types/api/script';
interface StyleOption {
@@ -266,8 +273,7 @@ const columns = [
{ key: 'example', label: 'Example Stage Direction' },
{ key: 'btn', label: '' },
];
-const rowsPerPage = 15;
-const currentPage = ref(1);
+const { perPage, currentPage } = usePagination();
const stageDirectionStyles = ref([]);
const isSubmittingNew = ref(false);
diff --git a/client-v3/src/composables/usePagination.ts b/client-v3/src/composables/usePagination.ts
new file mode 100644
index 000000000..4a6e1ca30
--- /dev/null
+++ b/client-v3/src/composables/usePagination.ts
@@ -0,0 +1,20 @@
+import { ref, watch } from 'vue';
+
+export const PER_PAGE_OPTIONS = [
+ { value: 10, text: '10' },
+ { value: 15, text: '15' },
+ { value: 25, text: '25' },
+ { value: 50, text: '50' },
+ { value: 0, text: 'All' },
+] as const;
+
+export function usePagination(defaultPerPage = 15) {
+ const perPage = ref(defaultPerPage);
+ const currentPage = ref(1);
+
+ watch(perPage, () => {
+ currentPage.value = 1;
+ });
+
+ return { perPage, currentPage };
+}
diff --git a/client-v3/src/views/show/config/ConfigCast.vue b/client-v3/src/views/show/config/ConfigCast.vue
index accb669c4..d3bc61316 100644
--- a/client-v3/src/views/show/config/ConfigCast.vue
+++ b/client-v3/src/views/show/config/ConfigCast.vue
@@ -8,7 +8,7 @@
id="cast-table"
:items="showStore.castList"
:fields="castFields"
- :per-page="rowsPerPage"
+ :per-page="perPage"
:current-page="currentPage"
show-empty
>
@@ -40,13 +40,11 @@
-
@@ -126,6 +124,7 @@ import log from 'loglevel';
import { useSystemStore } from '@/stores/system';
import { useShowStore } from '@/stores/show';
import { useConfirm } from '@/composables/useConfirm';
+import { usePagination } from '@/composables/usePagination';
import CastLineStats from '@/components/show/config/cast/CastLineStats.vue';
import type { Cast } from '@/types/api/show';
@@ -133,8 +132,7 @@ const systemStore = useSystemStore();
const showStore = useShowStore();
const { confirm } = useConfirm();
-const rowsPerPage = 15;
-const currentPage = ref(1);
+const { perPage, currentPage } = usePagination();
const submittingNewCast = ref(false);
const submittingEditCast = ref(false);
const deletingCast = ref(false);
diff --git a/client-v3/src/views/show/config/ConfigCharacters.vue b/client-v3/src/views/show/config/ConfigCharacters.vue
index 41c9ddc00..b72a6b604 100644
--- a/client-v3/src/views/show/config/ConfigCharacters.vue
+++ b/client-v3/src/views/show/config/ConfigCharacters.vue
@@ -8,7 +8,7 @@
id="character-table"
:items="showStore.characterList"
:fields="characterFields"
- :per-page="rowsPerPage"
+ :per-page="perPage"
:current-page="currentPage"
show-empty
>
@@ -55,13 +55,11 @@
-
@@ -180,6 +178,7 @@ import 'vue-multiselect/dist/vue-multiselect.css';
import { useSystemStore } from '@/stores/system';
import { useShowStore } from '@/stores/show';
import { useConfirm } from '@/composables/useConfirm';
+import { usePagination } from '@/composables/usePagination';
import CharacterGroups from '@/components/show/config/characters/CharacterGroups.vue';
import CharacterLineStats from '@/components/show/config/characters/CharacterLineStats.vue';
import type { Character } from '@/types/api/show';
@@ -188,8 +187,7 @@ const systemStore = useSystemStore();
const showStore = useShowStore();
const { confirm } = useConfirm();
-const rowsPerPage = 15;
-const currentPage = ref(1);
+const { perPage, currentPage } = usePagination();
const submittingNewCharacter = ref(false);
const submittingEditCharacter = ref(false);
const deletingCharacter = ref(false);
diff --git a/client-v3/src/views/show/config/ConfigCues.vue b/client-v3/src/views/show/config/ConfigCues.vue
index 9326a1e2c..9958698f9 100644
--- a/client-v3/src/views/show/config/ConfigCues.vue
+++ b/client-v3/src/views/show/config/ConfigCues.vue
@@ -8,7 +8,7 @@
id="cue-types-table"
:items="showStore.cueTypes"
:fields="cueTypeFields"
- :per-page="rowsPerPage"
+ :per-page="perPage"
:current-page="currentPage"
show-empty
>
@@ -50,13 +50,11 @@
-
@@ -221,6 +219,7 @@ import log from 'loglevel';
import { useSystemStore } from '@/stores/system';
import { useShowStore } from '@/stores/show';
import { useConfirm } from '@/composables/useConfirm';
+import { usePagination } from '@/composables/usePagination';
import CueCountStats from '@/components/show/config/cues/CueCountStats.vue';
import CueEditor from '@/components/show/config/cues/CueEditor.vue';
import type { CueType } from '@/types/api/cues';
@@ -229,8 +228,7 @@ const systemStore = useSystemStore();
const showStore = useShowStore();
const { confirm } = useConfirm();
-const rowsPerPage = 15;
-const currentPage = ref(1);
+const { perPage, currentPage } = usePagination();
const submittingNewCueType = ref(false);
const submittingEditCueType = ref(false);
const deletingCueType = ref(false);
diff --git a/client/package-lock.json b/client/package-lock.json
index d5d4110f0..259c91dc2 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "client",
- "version": "0.31.1",
+ "version": "0.31.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "client",
- "version": "0.31.1",
+ "version": "0.31.2",
"dependencies": {
"bootstrap": "4.6.2",
"bootstrap-vue": "2.23.1",
diff --git a/client/package.json b/client/package.json
index 015d1bd52..2152dd630 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,6 +1,6 @@
{
"name": "client",
- "version": "0.31.1",
+ "version": "0.31.2",
"description": "DigiScript front end",
"author": "DreamTeamProd",
"private": true,
diff --git a/client/src/main.ts b/client/src/main.ts
index c322f39ad..a20913a7e 100644
--- a/client/src/main.ts
+++ b/client/src/main.ts
@@ -6,6 +6,7 @@ import Vuelidate from 'vuelidate';
import ToastPlugin from 'vue-toast-notification';
import Multiselect from 'vue-multiselect';
import { Splitpanes, Pane } from 'splitpanes';
+import PaginationControls from '@/vue_components/shared/PaginationControls.vue';
import store from '@/store/store';
import App from './App.vue';
@@ -30,6 +31,7 @@ Vue.use(IconsPlugin);
Vue.component('MultiSelect', Multiselect);
Vue.component('SplitPanes', Splitpanes);
Vue.component('SplitPane', Pane);
+Vue.component('PaginationControls', PaginationControls);
Vue.use(Vuex);
Vue.use(Vuelidate as any); // @types/vuelidate bundles its own Vue copy that conflicts with main package
diff --git a/client/src/mixins/paginationMixin.ts b/client/src/mixins/paginationMixin.ts
new file mode 100644
index 000000000..3ffd58a35
--- /dev/null
+++ b/client/src/mixins/paginationMixin.ts
@@ -0,0 +1,15 @@
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+ data() {
+ return {
+ rowsPerPage: 15 as number,
+ currentPage: 1 as number,
+ };
+ },
+ watch: {
+ rowsPerPage(this: { currentPage: number }) {
+ this.currentPage = 1;
+ },
+ },
+});
diff --git a/client/src/views/show/config/ConfigCast.vue b/client/src/views/show/config/ConfigCast.vue
index b607f3616..e03a2fae6 100644
--- a/client/src/views/show/config/ConfigCast.vue
+++ b/client/src/views/show/config/ConfigCast.vue
@@ -36,13 +36,11 @@
-
@@ -150,11 +148,12 @@ import { mapGetters, mapActions } from 'vuex';
import CastLineStats from '@/vue_components/show/config/cast/CastLineStats.vue';
import log from 'loglevel';
import formValidationMixin from '@/mixins/formValidationMixin';
+import paginationMixin from '@/mixins/paginationMixin';
export default defineComponent({
name: 'ConfigCast',
components: { CastLineStats },
- mixins: [formValidationMixin],
+ mixins: [formValidationMixin, paginationMixin],
data() {
return {
castFields: ['first_name', 'last_name', { key: 'btn', label: '' }],
@@ -162,8 +161,6 @@ export default defineComponent({
firstName: '',
lastName: '',
},
- rowsPerPage: 15,
- currentPage: 1,
editFormState: {
id: null as number | null,
showID: null as number | null,
diff --git a/client/src/views/show/config/ConfigCharacters.vue b/client/src/views/show/config/ConfigCharacters.vue
index 5c949575d..0e846fa1a 100644
--- a/client/src/views/show/config/ConfigCharacters.vue
+++ b/client/src/views/show/config/ConfigCharacters.vue
@@ -51,13 +51,11 @@
-
@@ -207,15 +205,14 @@ import CharacterTimeline from '@/vue_components/show/config/characters/Character
import log from 'loglevel';
import CharacterGroups from '@/vue_components/show/config/characters/CharacterGroups.vue';
import formValidationMixin from '@/mixins/formValidationMixin';
+import paginationMixin from '@/mixins/paginationMixin';
export default defineComponent({
name: 'ConfigCharacters',
components: { CharacterGroups, CharacterLineStats, CharacterTimeline },
- mixins: [formValidationMixin],
+ mixins: [formValidationMixin, paginationMixin],
data() {
return {
- rowsPerPage: 15,
- currentPage: 1,
characterFields: [
'name',
'description',
diff --git a/client/src/views/show/config/ConfigCues.vue b/client/src/views/show/config/ConfigCues.vue
index e7b3ccc09..1879e4ccc 100644
--- a/client/src/views/show/config/ConfigCues.vue
+++ b/client/src/views/show/config/ConfigCues.vue
@@ -49,13 +49,11 @@
-
@@ -228,18 +226,17 @@ import { defineComponent } from 'vue';
import { required, maxLength } from 'vuelidate/lib/validators';
import { mapGetters, mapActions } from 'vuex';
import log from 'loglevel';
-
+import paginationMixin from '@/mixins/paginationMixin';
import CueEditor from '@/vue_components/show/config/cues/CueEditor.vue';
import CueCountStats from '@/vue_components/show/config/cues/CueCountStats.vue';
export default defineComponent({
name: 'ConfigCues',
components: { CueCountStats, CueEditor },
+ mixins: [paginationMixin],
data() {
return {
cueTypeFields: ['prefix', 'description', 'colour', { key: 'btn', label: '' }],
- rowsPerPage: 15,
- currentPage: 1,
newCueTypeForm: {
prefix: '',
description: '',
diff --git a/client/src/vue_components/config/ConfigShows.vue b/client/src/vue_components/config/ConfigShows.vue
index fd787b518..775ee5135 100644
--- a/client/src/vue_components/config/ConfigShows.vue
+++ b/client/src/vue_components/config/ConfigShows.vue
@@ -42,13 +42,11 @@
-
@@ -152,9 +150,11 @@ import { required, maxLength } from 'vuelidate/lib/validators';
import { mapGetters, mapActions } from 'vuex';
import { makeURL } from '@/js/utils';
import log from 'loglevel';
+import paginationMixin from '@/mixins/paginationMixin';
export default defineComponent({
name: 'ConfigShows',
+ mixins: [paginationMixin],
data() {
return {
loaded: false,
@@ -166,8 +166,6 @@ export default defineComponent({
'created_at',
{ key: 'btn', label: '' },
],
- rowsPerPage: 15,
- currentPage: 1,
isSubmittingLoad: false,
isSubmittingShow: false,
isDeleting: false,
diff --git a/client/src/vue_components/config/ConfigSystem.vue b/client/src/vue_components/config/ConfigSystem.vue
index 9a139ef21..d99453293 100644
--- a/client/src/vue_components/config/ConfigSystem.vue
+++ b/client/src/vue_components/config/ConfigSystem.vue
@@ -81,16 +81,15 @@
id="connected-clients-table"
:items="connectedClients"
:fields="clientFields"
- :per-page="perPage"
- :current-page="currentPageClients"
+ :per-page="rowsPerPage"
+ :current-page="currentPage"
small
/>
-
@@ -103,6 +102,7 @@
import { defineComponent } from 'vue';
import log from 'loglevel';
import { makeURL } from '@/js/utils';
+import paginationMixin from '@/mixins/paginationMixin';
interface VersionStatus {
current_version: string | null;
@@ -115,11 +115,10 @@ interface VersionStatus {
export default defineComponent({
name: 'ConfigSystem',
+ mixins: [paginationMixin],
data() {
return {
- perPage: 5,
connectedClients: [] as unknown[],
- currentPageClients: 1,
clientFields: [
{ key: 'internal_id', label: 'UUID' },
{ key: 'remote_ip', label: 'IP' },
diff --git a/client/src/vue_components/shared/PaginationControls.vue b/client/src/vue_components/shared/PaginationControls.vue
new file mode 100644
index 000000000..76989e928
--- /dev/null
+++ b/client/src/vue_components/shared/PaginationControls.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/vue_components/show/config/acts_and_scenes/ConfigActs.vue b/client/src/vue_components/show/config/acts_and_scenes/ConfigActs.vue
index 01f5e0126..cbe583405 100644
--- a/client/src/vue_components/show/config/acts_and_scenes/ConfigActs.vue
+++ b/client/src/vue_components/show/config/acts_and_scenes/ConfigActs.vue
@@ -48,13 +48,11 @@
-
-
@@ -217,15 +215,14 @@ import { required, integer } from 'vuelidate/lib/validators';
import { mapGetters, mapActions } from 'vuex';
import log from 'loglevel';
import formValidationMixin from '@/mixins/formValidationMixin';
+import paginationMixin from '@/mixins/paginationMixin';
export default defineComponent({
name: 'ConfigScenes',
- mixins: [formValidationMixin],
+ mixins: [formValidationMixin, paginationMixin],
data() {
return {
loading: true,
- rowsPerPage: 15,
- currentPage: 1,
sceneFields: [
'name',
'act',
diff --git a/client/src/vue_components/show/config/characters/CharacterGroups.vue b/client/src/vue_components/show/config/characters/CharacterGroups.vue
index 1a3cc3c6b..e21670363 100644
--- a/client/src/vue_components/show/config/characters/CharacterGroups.vue
+++ b/client/src/vue_components/show/config/characters/CharacterGroups.vue
@@ -43,13 +43,11 @@
-
-
diff --git a/client/src/vue_components/show/config/sessions/SessionTagList.vue b/client/src/vue_components/show/config/sessions/SessionTagList.vue
index 75c3aafce..45c441a65 100644
--- a/client/src/vue_components/show/config/sessions/SessionTagList.vue
+++ b/client/src/vue_components/show/config/sessions/SessionTagList.vue
@@ -44,13 +44,11 @@
-
-
-
@@ -57,13 +55,11 @@
-
@@ -358,6 +354,12 @@ export default defineComponent({
},
...mapGetters(['PROPS_LIST', 'PROP_TYPES', 'IS_SHOW_EDITOR', 'PROP_TYPE_BY_ID']),
},
+ watch: {
+ rowsPerPage() {
+ this.currentPropPage = 1;
+ this.currentPropTypePage = 1;
+ },
+ },
async mounted(): Promise {
await Promise.all([(this as any).GET_PROP_TYPES(), (this as any).GET_PROPS_LIST()]);
},
diff --git a/client/src/vue_components/show/config/stage/SceneryList.vue b/client/src/vue_components/show/config/stage/SceneryList.vue
index e559479b4..e3025a1c1 100644
--- a/client/src/vue_components/show/config/stage/SceneryList.vue
+++ b/client/src/vue_components/show/config/stage/SceneryList.vue
@@ -23,13 +23,11 @@
-
@@ -57,13 +55,11 @@
-
@@ -366,6 +362,12 @@ export default defineComponent({
},
...mapGetters(['SCENERY_LIST', 'SCENERY_TYPES', 'IS_SHOW_EDITOR', 'SCENERY_TYPE_BY_ID']),
},
+ watch: {
+ rowsPerPage() {
+ this.currentSceneryPage = 1;
+ this.currentSceneryTypesPage = 1;
+ },
+ },
async mounted(): Promise {
await Promise.all([(this as any).GET_SCENERY_TYPES(), (this as any).GET_SCENERY_LIST()]);
},
diff --git a/client/src/vue_components/show/config/stage/StageManager.vue b/client/src/vue_components/show/config/stage/StageManager.vue
index 0e2b65012..bedfcd7c8 100644
--- a/client/src/vue_components/show/config/stage/StageManager.vue
+++ b/client/src/vue_components/show/config/stage/StageManager.vue
@@ -90,13 +90,11 @@
-
@@ -122,13 +120,11 @@
-
@@ -668,6 +664,12 @@ export default defineComponent({
'CREW_ASSIGNMENTS_BY_SCENERY',
]),
},
+ watch: {
+ rowsPerPage() {
+ this.currentSceneryAllocPage = 1;
+ this.currentPropsAllocPage = 1;
+ },
+ },
async mounted(): Promise {
await Promise.all([
(this as any).GET_ACT_LIST(),
diff --git a/electron/package-lock.json b/electron/package-lock.json
index fe6e86a62..4a93c7d1b 100644
--- a/electron/package-lock.json
+++ b/electron/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "digiscript-electron",
- "version": "0.31.1",
+ "version": "0.31.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "digiscript-electron",
- "version": "0.31.1",
+ "version": "0.31.2",
"license": "GPL-3.0",
"dependencies": {
"bonjour-service": "^1.4.0",
diff --git a/electron/package.json b/electron/package.json
index fd55c1cd1..a4f6cae3b 100644
--- a/electron/package.json
+++ b/electron/package.json
@@ -1,6 +1,6 @@
{
"name": "digiscript-electron",
- "version": "0.31.1",
+ "version": "0.31.2",
"description": "DigiScript Electron Desktop Application",
"author": "DreamTeamProd",
"license": "GPL-3.0",
diff --git a/server/pyproject.toml b/server/pyproject.toml
index 90327a909..3e077ffc2 100644
--- a/server/pyproject.toml
+++ b/server/pyproject.toml
@@ -11,7 +11,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "digiscript-server"
-version = "0.31.1"
+version = "0.31.2"
description = "DigiScript server - Digital script management for theatrical shows"
readme = "../README.md"
requires-python = ">=3.13"