From f0c2479acdc23e784c0853babb29c8f623ccd46c Mon Sep 17 00:00:00 2001 From: James S Perrin Date: Wed, 11 Dec 2024 11:29:53 +0000 Subject: [PATCH 1/5] Fixes #1512, don't save edits with no clips --- src/i18n/locales/en-US.json | 4 +++- src/main/Save.tsx | 12 +++++++++--- src/main/WorkflowSelection.tsx | 20 +++++++++++++++++--- src/redux/videoSlice.ts | 14 ++++++++++++++ 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index accb088a3..63b6e890c 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -76,7 +76,9 @@ "success-tooltip-aria": "Saved successfully", "saveArea-tooltip": "Save Area", "confirm-success": "Okay", - "cancel-save": "Don't save" + "cancel-save": "Don't save", + "invalid-headline-text": "Invalid Edits", + "invalid-text": "The segments do not create a valid video. Either change or discard your edits if you wish to proceed. If you wanted to delete this video use the Opencast Admin UI. Contact an adminstrator for further help." }, "discard": { diff --git a/src/main/Save.tsx b/src/main/Save.tsx index 75ecc09e9..102da0edb 100644 --- a/src/main/Save.tsx +++ b/src/main/Save.tsx @@ -14,6 +14,8 @@ import { selectSegments, selectTracks, setHasChanges as videoSetHasChanges, + selectValidSegments, + validateSegments, } from "../redux/videoSlice"; import { postVideoInformation, selectStatus, selectError } from "../redux/workflowPostSlice"; @@ -50,6 +52,10 @@ const Save: React.FC = () => { const hasChanges = useAppSelector(selectHasChanges); const subtitleHasChanges = useAppSelector(selectSubtitleHasChanges); + const dispatch = useAppDispatch(); + dispatch(validateSegments()); + const validSegments = useAppSelector(selectValidSegments); + const saveStyle = css({ display: "flex", flexDirection: "column", @@ -74,11 +80,11 @@ const Save: React.FC = () => { return ( <> - {t("save.info-text")} + {validSegments ? t("save.info-text") : t("save.invalid-text")}
- + {validSegments && }
); @@ -87,7 +93,7 @@ const Save: React.FC = () => { return (
-

{t("save.headline-text")}

+

{validSegments ? t("save.headline-text") : t("save.invalid-headline-text")}

{render()}
{t("various.error-text")}
diff --git a/src/main/WorkflowSelection.tsx b/src/main/WorkflowSelection.tsx index 48fb01a92..11488bfb0 100644 --- a/src/main/WorkflowSelection.tsx +++ b/src/main/WorkflowSelection.tsx @@ -8,13 +8,13 @@ import { selectWorkflows, setSelectedWorkflowIndex } from "../redux/videoSlice"; import { PageButton } from "./Finish"; import { LuChevronLeft, LuDatabase } from "react-icons/lu"; +import { selectValidSegments, validateSegments } from "../redux/videoSlice"; import { selectStatus as saveSelectStatus, selectError as saveSelectError } from "../redux/workflowPostSlice"; import { httpRequestState, Workflow } from "../types"; import { SaveButton } from "./Save"; import { EmotionJSX } from "@emotion/react/types/jsx-namespace"; -import { useTranslation } from "react-i18next"; -import { Trans } from "react-i18next"; +import { useTranslation, Trans } from "react-i18next"; import { FormControlLabel, Radio, RadioGroup } from "@mui/material"; import { useTheme } from "../themes"; @@ -39,6 +39,9 @@ const WorkflowSelection: React.FC = () => { const saveStatus = useAppSelector(saveSelectStatus); const saveError = useAppSelector(saveSelectError); + dispatch(validateSegments()); + const validSegments = useAppSelector(selectValidSegments); + const workflowSelectionStyle = css({ padding: "20px", display: "flex", @@ -109,7 +112,18 @@ const WorkflowSelection: React.FC = () => { // Fills the layout template with values based on how many workflows are available const renderSelection = () => { - if (workflows.length <= 0) { + if (!validSegments) { + return ( + render( + t("save.invalid-headline-text"), + {t("save.invalid-text")}, + false, +
, + saveStatus, + saveError + ) + ); + } else if (workflows.length <= 0) { return ( render( t("workflowSelection.saveAndProcess-text"), diff --git a/src/redux/videoSlice.ts b/src/redux/videoSlice.ts index b7ff287be..eb580085d 100644 --- a/src/redux/videoSlice.ts +++ b/src/redux/videoSlice.ts @@ -19,6 +19,7 @@ export interface video { tracks: Track[], subtitlesFromOpencast: SubtitlesFromOpencast[], activeSegmentIndex: number, // Index of the segment that is currenlty hovered + validSegments: boolean, // Whether the segment will result in a valid video edit selectedWorkflowId: string, // Id of the currently selected workflow aspectRatios: { width: number, height: number; }[], // Aspect ratios of every video hasChanges: boolean, // Did user make changes in cutting view since last save @@ -54,6 +55,7 @@ export const initialState: video & httpRequestState = { tracks: [], subtitlesFromOpencast: [], activeSegmentIndex: 0, + validSegments: true, selectedWorkflowId: "", previewTriggered: false, clickTriggered: false, @@ -184,6 +186,15 @@ const videoSlice = createSlice({ updateCurrentlyAt(state, jumpTarget); state.jumpTriggered = true; }, + validateSegments: state => { + // Test if whole video has been deleted + if (state.segments.length === 1 && state.segments[0].deleted && state.segments[0].start === 0 && + state.segments[0].end === state.duration) { + state.validSegments = false; + } else { + state.validSegments = true; + } + }, addSegment: (state, action: PayloadAction) => { state.segments.push(action.payload); }, @@ -363,6 +374,7 @@ const videoSlice = createSlice({ selectCurrentlyAtInSeconds: state => state.currentlyAt / 1000, selectSegments: state => state.segments, selectActiveSegmentIndex: state => state.activeSegmentIndex, + selectValidSegments: state => state.validSegments, selectIsCurrentSegmentAlive: state => !state.segments[state.activeSegmentIndex].deleted, selectSelectedWorkflowId: state => state.selectedWorkflowId, selectHasChanges: state => state.hasChanges, @@ -545,6 +557,7 @@ export const { setJumpTriggered, jumpToPreviousSegment, jumpToNextSegment, + validateSegments, } = videoSlice.actions; export const selectVideos = createSelector( @@ -565,6 +578,7 @@ export const { selectCurrentlyAtInSeconds, selectSegments, selectActiveSegmentIndex, + selectValidSegments, selectIsCurrentSegmentAlive, selectSelectedWorkflowId, selectHasChanges, From 382e5c9e7037f7621a6134ce775378625894783b Mon Sep 17 00:00:00 2001 From: James Perrin Date: Wed, 18 Dec 2024 11:23:07 +0000 Subject: [PATCH 2/5] #1512, remove confusing reference --- src/i18n/locales/en-US.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 63b6e890c..d2c2edb91 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -78,7 +78,7 @@ "confirm-success": "Okay", "cancel-save": "Don't save", "invalid-headline-text": "Invalid Edits", - "invalid-text": "The segments do not create a valid video. Either change or discard your edits if you wish to proceed. If you wanted to delete this video use the Opencast Admin UI. Contact an adminstrator for further help." + "invalid-text": "The segments do not create a valid video. Either change or discard your edits if you wish to proceed. Contact an adminstrator for further help." }, "discard": { From 95cdcb2d021db1e1e000dd26ac5c55bac2935f70 Mon Sep 17 00:00:00 2001 From: James Perrin Date: Wed, 18 Dec 2024 11:23:49 +0000 Subject: [PATCH 3/5] #1512, improve deletion detection --- src/redux/videoSlice.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/redux/videoSlice.ts b/src/redux/videoSlice.ts index 19947f81d..54dc6a7ab 100644 --- a/src/redux/videoSlice.ts +++ b/src/redux/videoSlice.ts @@ -1,5 +1,5 @@ -import { clamp } from "lodash"; -import { createSlice, nanoid, PayloadAction, createSelector } from "@reduxjs/toolkit"; +import { clamp, forEach } from "lodash"; +import { createSlice, nanoid, createAsyncThunk, PayloadAction, createSelector } from "@reduxjs/toolkit"; import { client } from "../util/client"; import { Segment, httpRequestState, Track, Workflow, SubtitlesFromOpencast } from "../types"; @@ -188,9 +188,15 @@ const videoSlice = createSlice({ state.jumpTriggered = true; }, validateSegments: state => { + let allDeleted = true; + // Test if whole video has been deleted - if (state.segments.length === 1 && state.segments[0].deleted && state.segments[0].start === 0 && - state.segments[0].end === state.duration) { + state.segments.forEach(segment => { + if(!segment.deleted) { + allDeleted = false; + } + }) + if(allDeleted) { state.validSegments = false; } else { state.validSegments = true; From cda6ffc612f50aeddf2809a9bab474d64055fa25 Mon Sep 17 00:00:00 2001 From: James Perrin Date: Wed, 18 Dec 2024 11:37:56 +0000 Subject: [PATCH 4/5] #1512, use errorbox --- src/i18n/locales/en-US.json | 1 - src/main/Save.tsx | 13 +++++++++---- src/main/WorkflowSelection.tsx | 8 ++++++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index d2c2edb91..1c37eb513 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -77,7 +77,6 @@ "saveArea-tooltip": "Save Area", "confirm-success": "Okay", "cancel-save": "Don't save", - "invalid-headline-text": "Invalid Edits", "invalid-text": "The segments do not create a valid video. Either change or discard your edits if you wish to proceed. Contact an adminstrator for further help." }, diff --git a/src/main/Save.tsx b/src/main/Save.tsx index 927f02068..2c087352c 100644 --- a/src/main/Save.tsx +++ b/src/main/Save.tsx @@ -80,9 +80,14 @@ const Save: React.FC = () => { } else { return ( <> - - {validSegments ? t("save.info-text") : t("save.invalid-text")} - + {validSegments ? + {t("save.info-text")} + : + + {t("save.invalid-text")} + + + }
{validSegments && } @@ -94,7 +99,7 @@ const Save: React.FC = () => { return (
-

{validSegments ? t("save.headline-text") : t("save.invalid-headline-text")}

+

{t("save.headline-text")}

{render()} {postWorkflowStatus === "failed" && diff --git a/src/main/WorkflowSelection.tsx b/src/main/WorkflowSelection.tsx index 331885952..cd3ce77e0 100644 --- a/src/main/WorkflowSelection.tsx +++ b/src/main/WorkflowSelection.tsx @@ -116,8 +116,12 @@ const WorkflowSelection: React.FC = () => { if (!validSegments) { return ( render( - t("save.invalid-headline-text"), - {t("save.invalid-text")}, + t("workflowSelection.saveAndProcess-text"), + + + {t("save.invalid-text")} + + , false,
, saveStatus, From 51cf5c7ee6d342b6ae6c0f7fb985178b19873fc4 Mon Sep 17 00:00:00 2001 From: James Perrin Date: Wed, 18 Dec 2024 14:09:25 +0000 Subject: [PATCH 5/5] #1512, refactor cutting validation --- src/main/Save.tsx | 12 ++++-------- src/main/WorkflowSelection.tsx | 8 +++----- src/redux/videoSlice.ts | 31 ++++++++++--------------------- 3 files changed, 17 insertions(+), 34 deletions(-) diff --git a/src/main/Save.tsx b/src/main/Save.tsx index 2c087352c..0c82edf1a 100644 --- a/src/main/Save.tsx +++ b/src/main/Save.tsx @@ -14,8 +14,7 @@ import { selectSegments, selectTracks, setHasChanges as videoSetHasChanges, - selectValidSegments, - validateSegments, + selectValidCutting, } from "../redux/videoSlice"; import { postVideoInformation, selectStatus, selectError } from "../redux/workflowPostSlice"; @@ -52,10 +51,7 @@ const Save: React.FC = () => { const metadataHasChanges = useAppSelector(metadataSelectHasChanges); const hasChanges = useAppSelector(selectHasChanges); const subtitleHasChanges = useAppSelector(selectSubtitleHasChanges); - - const dispatch = useAppDispatch(); - dispatch(validateSegments()); - const validSegments = useAppSelector(selectValidSegments); + const validCutting = useAppSelector(selectValidCutting); const saveStyle = css({ display: "flex", @@ -80,7 +76,7 @@ const Save: React.FC = () => { } else { return ( <> - {validSegments ? + {validCutting ? {t("save.info-text")} : @@ -90,7 +86,7 @@ const Save: React.FC = () => { }
- {validSegments && } + {validCutting && }
); diff --git a/src/main/WorkflowSelection.tsx b/src/main/WorkflowSelection.tsx index cd3ce77e0..b11225354 100644 --- a/src/main/WorkflowSelection.tsx +++ b/src/main/WorkflowSelection.tsx @@ -8,7 +8,7 @@ import { selectWorkflows, setSelectedWorkflowIndex } from "../redux/videoSlice"; import { PageButton } from "./Finish"; import { LuChevronLeft, LuDatabase } from "react-icons/lu"; -import { selectValidSegments, validateSegments } from "../redux/videoSlice"; +import { selectValidCutting } from "../redux/videoSlice"; import { selectStatus as saveSelectStatus, selectError as saveSelectError } from "../redux/workflowPostSlice"; import { httpRequestState, Workflow } from "../types"; import { SaveButton } from "./Save"; @@ -37,9 +37,7 @@ const WorkflowSelection: React.FC = () => { const saveStatus = useAppSelector(saveSelectStatus); const saveError = useAppSelector(saveSelectError); - - dispatch(validateSegments()); - const validSegments = useAppSelector(selectValidSegments); + const validCutting = useAppSelector(selectValidCutting); const workflowSelectionStyle = css({ padding: "20px", @@ -113,7 +111,7 @@ const WorkflowSelection: React.FC = () => { // Fills the layout template with values based on how many workflows are available const renderSelection = () => { - if (!validSegments) { + if (!validCutting) { return ( render( t("workflowSelection.saveAndProcess-text"), diff --git a/src/redux/videoSlice.ts b/src/redux/videoSlice.ts index 54dc6a7ab..8890d03d6 100644 --- a/src/redux/videoSlice.ts +++ b/src/redux/videoSlice.ts @@ -1,4 +1,4 @@ -import { clamp, forEach } from "lodash"; +import { clamp } from "lodash"; import { createSlice, nanoid, createAsyncThunk, PayloadAction, createSelector } from "@reduxjs/toolkit"; import { client } from "../util/client"; @@ -20,7 +20,6 @@ export interface video { tracks: Track[], subtitlesFromOpencast: SubtitlesFromOpencast[], activeSegmentIndex: number, // Index of the segment that is currenlty hovered - validSegments: boolean, // Whether the segment will result in a valid video edit selectedWorkflowId: string, // Id of the currently selected workflow aspectRatios: { width: number, height: number; }[], // Aspect ratios of every video hasChanges: boolean, // Did user make changes in cutting view since last save @@ -56,7 +55,6 @@ export const initialState: video & httpRequestState = { tracks: [], subtitlesFromOpencast: [], activeSegmentIndex: 0, - validSegments: true, selectedWorkflowId: "", previewTriggered: false, clickTriggered: false, @@ -187,21 +185,6 @@ const videoSlice = createSlice({ updateCurrentlyAt(state, jumpTarget); state.jumpTriggered = true; }, - validateSegments: state => { - let allDeleted = true; - - // Test if whole video has been deleted - state.segments.forEach(segment => { - if(!segment.deleted) { - allDeleted = false; - } - }) - if(allDeleted) { - state.validSegments = false; - } else { - state.validSegments = true; - } - }, addSegment: (state, action: PayloadAction) => { state.segments.push(action.payload); }, @@ -381,7 +364,14 @@ const videoSlice = createSlice({ selectCurrentlyAtInSeconds: state => state.currentlyAt / 1000, selectSegments: state => state.segments, selectActiveSegmentIndex: state => state.activeSegmentIndex, - selectValidSegments: state => state.validSegments, + selectValidCutting: state => { + let validSegment = false; + // Test if whole video hasn't been deleted + state.segments.forEach(segment => { + validSegment ||= !segment.deleted; + }) + return validSegment; + }, selectIsCurrentSegmentAlive: state => !state.segments[state.activeSegmentIndex].deleted, selectSelectedWorkflowId: state => state.selectedWorkflowId, selectHasChanges: state => state.hasChanges, @@ -564,7 +554,6 @@ export const { setJumpTriggered, jumpToPreviousSegment, jumpToNextSegment, - validateSegments, } = videoSlice.actions; export const selectVideos = createSelector( @@ -585,7 +574,7 @@ export const { selectCurrentlyAtInSeconds, selectSegments, selectActiveSegmentIndex, - selectValidSegments, + selectValidCutting, selectIsCurrentSegmentAlive, selectSelectedWorkflowId, selectHasChanges,