diff --git a/__mocks__/peaks.js b/__mocks__/peaks.js
index 2bfc8851..dcd3e18c 100644
--- a/__mocks__/peaks.js
+++ b/__mocks__/peaks.js
@@ -62,7 +62,7 @@ export const Peaks = jest.fn((opts) => {
return peaks;
});
-// segements are built in match with timespans from 'testSmData'
+// segments are built in match with timespans from 'testSmData'
// in ./testing-helpers.js file
const peaksSegments = (opts, peaks) => {
let segments = [
@@ -104,6 +104,12 @@ const peaksSegments = (opts, peaks) => {
export const Segment = jest.fn((opts) => {
let segment = { ...opts };
+ // Ensure both id and _id are set
+ if (opts._id && !opts.id) {
+ segment.id = opts._id;
+ } else if (opts.id && !opts._id) {
+ segment._id = opts.id;
+ }
let checkProp = (newOpts, prop) => {
if (newOpts.hasOwnProperty(prop)) {
return true;
diff --git a/src/App.test.js b/src/App.test.js
index 3fcf6522..ae57e0a7 100644
--- a/src/App.test.js
+++ b/src/App.test.js
@@ -10,6 +10,7 @@ import {
manifestWoStructure,
manifestWithInvalidStruct,
manifestWEmptyCanvas,
+ manifestWEmptyRanges,
} from './services/testing-helpers';
import mockAxios from 'axios';
import Peaks from 'peaks.js';
@@ -289,8 +290,9 @@ describe('App component', () => {
await act(() => Promise.resolve());
expect(app.queryByTestId('waveform-container')).toBeInTheDocument();
- expect(app.queryByTestId('alert-container')).toBeInTheDocument();
- expect(app.getByTestId('alert-message').innerHTML).toBe(
+ // Display 2 alerts for empty media and invalid structure
+ expect(app.queryAllByTestId('alert-container').length).toEqual(2);
+ expect(app.getAllByTestId('alert-message')[1].innerHTML).toBe(
'No available media. Editing structure is disabled.'
);
});
@@ -432,39 +434,79 @@ describe('App component', () => {
});
});
- test('when structure has invalid timespans', async () => {
- mockAxios.get.mockImplementationOnce(() => {
- return Promise.resolve({
- status: 200,
- data: manifestWithInvalidStruct
+ describe('when structure has', () => {
+ test('invalid timespans', async () => {
+ mockAxios.get.mockImplementationOnce(() => {
+ return Promise.resolve({
+ status: 200,
+ data: manifestWithInvalidStruct
+ });
});
- });
- mockAxios.head.mockImplementationOnce(() => {
- return Promise.resolve({
- status: 200,
- request: {
- responseURL: 'https://example.com/lunchroom_manners/waveform.json',
+ mockAxios.head.mockImplementationOnce(() => {
+ return Promise.resolve({
+ status: 200,
+ request: {
+ responseURL: 'https://example.com/lunchroom_manners/waveform.json',
+ },
+ });
+ });
+ const initialState = {
+ manifest: {
+ manifestFetched: true,
+ manifest: manifestWithInvalidStruct,
+ mediaInfo: {
+ src: 'http://example.com/volleyball-for-boys/high/volleyball-for-boys.mp4',
+ duration: 662.037,
+ },
},
+ };
+ const app = renderWithRedux(, { initialState });
+
+ await waitFor(() => {
+ expect(app.queryAllByTestId('list-item').length).toBeGreaterThan(0);
+ expect(app.getAllByTestId('heading-label')[0].innerHTML).toEqual('Lunchroom Manners');
+ expect(app.getByTestId('alert-container')).toBeInTheDocument();
+ expect(app.getByTestId('alert-message').innerHTML)
+ .toEqual('Please check the marked invalid timespan(s)/heading(s).');
+ expect(app.getByTestId('structure-save-button')).not.toBeEnabled();
});
});
- const initialState = {
- manifest: {
- manifestFetched: true,
- manifest: manifestWithInvalidStruct,
- mediaInfo: {
- src: 'http://example.com/volleyball-for-boys/high/volleyball-for-boys.mp4',
- duration: 662.037,
+
+ test('invalid headings (empty)', async () => {
+ mockAxios.get.mockImplementationOnce(() => {
+ return Promise.resolve({
+ status: 200,
+ data: manifestWEmptyRanges
+ });
+ });
+ mockAxios.head.mockImplementationOnce(() => {
+ return Promise.resolve({
+ status: 200,
+ request: {
+ responseURL: 'https://example.com/lunchroom_manners/waveform.json',
+ },
+ });
+ });
+ const initialState = {
+ manifest: {
+ manifestFetched: true,
+ manifest: manifestWEmptyRanges,
+ mediaInfo: {
+ src: 'http://example.com/volleyball-for-boys/high/volleyball-for-boys.mp4',
+ duration: 662.037,
+ },
},
- },
- };
- const app = renderWithRedux(, { initialState });
+ };
+ const app = renderWithRedux(, { initialState });
- await waitFor(() => {
- expect(app.queryAllByTestId('list-item').length).toBeGreaterThan(0);
- expect(app.getAllByTestId('heading-label')[0].innerHTML).toEqual('Lunchroom Manners');
- expect(app.getByTestId('alert-container')).toBeInTheDocument();
- expect(app.getByTestId('alert-message').innerHTML)
- .toEqual('Please check start/end times of the marked invalid timespan(s).');
+ await waitFor(() => {
+ expect(app.queryAllByTestId('list-item').length).toBeGreaterThan(0);
+ expect(app.getAllByTestId('heading-label')[0].innerHTML).toEqual('Root');
+ expect(app.getByTestId('alert-container')).toBeInTheDocument();
+ expect(app.getByTestId('alert-message').innerHTML)
+ .toEqual('Please check the marked invalid timespan(s)/heading(s).');
+ expect(app.getByTestId('structure-save-button')).not.toBeEnabled();
+ });
});
});
});
diff --git a/src/actions/forms.js b/src/actions/forms.js
index 7beed4ea..14839e5c 100644
--- a/src/actions/forms.js
+++ b/src/actions/forms.js
@@ -6,19 +6,10 @@ import { v4 as uuidv4 } from 'uuid';
* Enable/disable other editing actions when editing a list item
* @param {Integer} code - choose from; 1(true) | 0(false)
*/
-export const handleEditingTimespans =
- (
- code,
- valid = true // assumes structure data is valid by default
- ) =>
- (dispatch) => {
- dispatch({ type: types.IS_EDITING_TIMESPAN, code });
- // Remove dismissible alerts when a CRUD action has been initiated
- // given editing is starting (code = 1) and structure is validated.
- if (code == 1 && valid) {
- dispatch(clearExistingAlerts());
- }
- };
+export const handleEditingTimespans = (code) => ({
+ type: types.IS_EDITING_TIMESPAN,
+ code
+});
export const setAlert = (alert) => (dispatch) => {
const id = uuidv4();
diff --git a/src/actions/sm-data.js b/src/actions/sm-data.js
index 1ece0da8..708e6805 100644
--- a/src/actions/sm-data.js
+++ b/src/actions/sm-data.js
@@ -1,17 +1,5 @@
import * as types from './types';
-import { updateStructureStatus, clearExistingAlerts } from './forms';
-
-export function reBuildSMUI(items, duration) {
- return (dispatch, getState) => {
- dispatch(buildSMUI(items, duration));
- const { structuralMetadata } = getState();
- // Remove invalid structure alert when data is corrected
- if (structuralMetadata.smDataIsValid) {
- dispatch(clearExistingAlerts());
- }
- dispatch(updateStructureStatus(0));
- };
-}
+import { updateStructureStatus } from './forms';
export function buildSMUI(json, duration) {
return {
@@ -21,10 +9,11 @@ export function buildSMUI(json, duration) {
};
}
-export function deleteItem(id) {
+export function updateSMUI(json, isValid) {
return {
- type: types.DELETE_ITEM,
- id,
+ type: types.UPDATE_SM_UI,
+ json,
+ isValid,
};
}
diff --git a/src/actions/types.js b/src/actions/types.js
index cafef0c4..e223def1 100644
--- a/src/actions/types.js
+++ b/src/actions/types.js
@@ -1,6 +1,5 @@
export const BUILD_SM_UI = 'BUILD_SM_UI';
-export const REBUILD_SM_UI = 'REBUILD_SM_UI';
-export const DELETE_ITEM = 'DELETE_ITEM';
+export const UPDATE_SM_UI = 'UPDATE_SM_UI';
export const ADD_DROP_TARGETS = 'ADD_DROP_TARGETS';
export const REMOVE_DROP_TARGETS = 'REMOVE_DROP_TARGETS';
diff --git a/src/components/ButtonSection.js b/src/components/ButtonSection.js
index d87e94d8..f852bce0 100644
--- a/src/components/ButtonSection.js
+++ b/src/components/ButtonSection.js
@@ -7,7 +7,8 @@ import HeadingFormContainer from '../containers/HeadingFormContainer';
import TimespanFormContainer from '../containers/TimespanFormContainer';
import * as peaksActions from '../actions/peaks-instance';
import { configureAlert } from '../services/alert-status';
-import { handleEditingTimespans, setAlert } from '../actions/forms';
+import { setAlert } from '../actions/forms';
+import { useStructureUpdate } from '../services/sme-hooks';
const styles = {
well: {
@@ -27,8 +28,11 @@ const ButtonSection = () => {
const dispatch = useDispatch();
const createTempSegment = () => dispatch(peaksActions.insertTempSegment());
const removeTempSegment = (id) => dispatch(peaksActions.deleteTempSegment(id));
- const updateEditingTimespans = (value) => dispatch(handleEditingTimespans(value));
const settingAlert = (alert) => dispatch(setAlert(alert));
+ const dragSegment = (id, startTimeChanged, flag) =>
+ dispatch(peaksActions.dragSegment(id, startTimeChanged, flag));
+
+ const { updateEditingTimespans } = useStructureUpdate();
// Get state variables from Redux store
const { editingDisabled, structureInfo, streamInfo } = useSelector((state) => state.forms);
@@ -90,7 +94,7 @@ const ButtonSection = () => {
settingAlert(noSpaceAlert);
} else {
// Initialize Redux store with temporary segment
- dispatch(peaksActions.dragSegment(tempSegment.id, null, 0));
+ dragSegment(tempSegment.id, null, 0);
setInitSegment(tempSegment);
setTimespanOpen(true);
setIsInitializing(true);
diff --git a/src/components/ListItem.js b/src/components/ListItem.js
index b9c61269..defa05e3 100644
--- a/src/components/ListItem.js
+++ b/src/components/ListItem.js
@@ -10,23 +10,18 @@ import ListItemEditForm from './ListItemEditForm';
import ListItemControls from './ListItemControls';
import { ItemTypes } from '../services/Constants';
import * as actions from '../actions/sm-data';
-import { deleteSegment } from '../actions/peaks-instance';
-import { handleEditingTimespans } from '../actions/forms';
+import { useStructureUpdate } from '../services/sme-hooks';
const ListItem = ({ item, children }) => {
// Dispatch actions to Redux store
const dispatch = useDispatch();
- const deleteItem = (id) => dispatch(actions.deleteItem(id));
const addDropTargets = (item) => dispatch(actions.addDropTargets(item));
const removeDropTargets = () => dispatch(actions.removeDropTargets());
const removeActiveDragSources = () => dispatch(actions.removeActiveDragSources());
const setActiveDragSource = (id) => dispatch(actions.setActiveDragSource(id));
const handleListItemDrop = (item, dropItem) => dispatch(actions.handleListItemDrop(item, dropItem));
- const removeSegment = (item) => dispatch(deleteSegment(item));
- const updateEditingTimespans = (value, smDataIsValid) => dispatch(handleEditingTimespans(value, smDataIsValid));
- // Get state variables from Redux store
- const { smDataIsValid } = useSelector((state) => state.structuralMetadata);
+ const { deleteStructItem, updateEditingTimespans } = useStructureUpdate();
const [editing, setEditing] = useState(false);
@@ -54,21 +49,20 @@ const ListItem = ({ item, children }) => {
const handleDelete = () => {
try {
- deleteItem(item.id);
- removeSegment(item);
+ deleteStructItem(item);
} catch (error) {
showBoundary(error);
}
};
const handleEditClick = () => {
- updateEditingTimespans(1, smDataIsValid);
+ updateEditingTimespans(1);
setEditing(true);
};
const handleEditFormCancel = () => {
setEditing(false);
- updateEditingTimespans(0, smDataIsValid);
+ updateEditingTimespans(0);
};
const handleShowDropTargetsClick = () => {
@@ -143,6 +137,15 @@ const ListItem = ({ item, children }) => {
className='structure-title heading'
data-testid='heading-label'
>
+ {(!valid && type !== 'root') && (
+ <>
+
+ {' '}
+ >
+ )}
{label}
)}
diff --git a/src/components/ListItemControls.js b/src/components/ListItemControls.js
index cba59382..075da484 100644
--- a/src/components/ListItemControls.js
+++ b/src/components/ListItemControls.js
@@ -7,12 +7,9 @@ import PopoverBody from 'react-bootstrap/PopoverBody';
import PopoverHeader from 'react-bootstrap/PopoverHeader';
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { useDispatch, useSelector } from 'react-redux';
-import {
- handleEditingTimespans,
- updateStructureStatus,
-} from '../actions/forms';
+import { useSelector } from 'react-redux';
import { faPen, faTrash, faDotCircle } from '@fortawesome/free-solid-svg-icons';
+import { useStructureUpdate } from '../services/sme-hooks';
const styles = {
buttonToolbar: {
@@ -26,10 +23,7 @@ const styles = {
};
const ListItemControls = ({ handleDelete, handleEditClick, handleShowDropTargetsClick, item }) => {
- // Dispatch actions to Redux store
- const dispatch = useDispatch();
- const updateEditingTimespans = (value) => dispatch(handleEditingTimespans(value));
- const updateStructStatus = (value) => dispatch(updateStructureStatus(value));
+ const { updateEditingTimespans } = useStructureUpdate();
// Get state variables from Redux store
const { editingDisabled } = useSelector((state) => state.forms);
@@ -47,8 +41,6 @@ const ListItemControls = ({ handleDelete, handleEditClick, handleShowDropTargets
enableEditing();
setDeleteMessage('');
setShowDeleteConfirm(false);
- // Change structureIsSaved to false
- updateStructStatus(0);
};
const handleDeleteClick = (e) => {
diff --git a/src/components/ListItemEditForm.js b/src/components/ListItemEditForm.js
index f11c9f13..69c45be2 100644
--- a/src/components/ListItemEditForm.js
+++ b/src/components/ListItemEditForm.js
@@ -1,23 +1,20 @@
import React, { useEffect, useState } from 'react';
import { useErrorBoundary } from 'react-error-boundary';
import PropTypes from 'prop-types';
-import { useDispatch, useSelector } from 'react-redux';
+import { useSelector } from 'react-redux';
import TimespanInlineForm from './TimespanInlineForm';
import HeadingInlineForm from './HeadingInlineForm';
-import { reBuildSMUI } from '../actions/sm-data';
import { cloneDeep } from 'lodash';
import StructuralMetadataUtils from '../services/StructuralMetadataUtils';
+import { useStructureUpdate } from '../services/sme-hooks';
const structuralMetadataUtils = new StructuralMetadataUtils();
const ListItemEditForm = ({ item, handleEditFormCancel }) => {
- // Dispatch actions to Redux store
- const dispatch = useDispatch();
- const updateSMUI = (cloned, duration) => dispatch(reBuildSMUI(cloned, duration));
+ const { updateStructure } = useStructureUpdate();
// Get state variables from Redux store
const { smData } = useSelector((state) => state.structuralMetadata);
- const { duration } = useSelector((state) => state.peaksInstance);
const [isTyping, _setIsTyping] = useState(false);
const [isInitializing, _setIsInitializing] = useState(true);
@@ -78,8 +75,8 @@ const ListItemEditForm = ({ item, handleEditFormCancel }) => {
// Update item values
item = addUpdatedValues(item, payload);
- // Send updated smData back to redux
- updateSMUI(clonedItems, duration);
+ // Send updated smData back to redux via custom hook
+ updateStructure(clonedItems);
// Turn off editing state
handleEditFormCancel();
diff --git a/src/components/TimespanForm.js b/src/components/TimespanForm.js
index 398eb994..744747df 100644
--- a/src/components/TimespanForm.js
+++ b/src/components/TimespanForm.js
@@ -43,12 +43,6 @@ const TimespanForm = ({
}
}, [initSegment]);
- const allSpans = useMemo(() => {
- if (smData?.length > 0) {
- return structuralMetadataUtils.getItemsOfType(['span'], smData);
- }
- }, [smData]);
-
// Find neighboring timespans of the currently editing timespan
const { prevSiblingRef, nextSiblingRef, parentTimespanRef } = useFindNeighborSegments({ segment });
@@ -75,45 +69,42 @@ const TimespanForm = ({
};
const isValidTimespan = useMemo(() => {
- const { valid } = validTimespans(beginTime, endTime, duration, allSpans);
+ const { valid } = validTimespans(beginTime, endTime, duration);
if (valid) {
buildHeadingsOptions();
} else {
setValidHeadings([]);
}
return valid;
- }, [beginTime, endTime, duration, allSpans]);
-
- useEffect(() => {
- if (!isInitializing) {
- setIsInitializing(false);
- }
- }, [smData, isInitializing]);
+ }, [beginTime, endTime, duration]);
useEffect(() => {
if (!isTyping) {
if (initSegment && isInitializing) {
// Set isInitializing flag to false
setIsInitializing(false);
- }
- if (!isInitializing) {
- const { startTime, endTime } = waveformDataUtils.validateSegment(
- segment, startTimeChanged, duration,
- {
- previousSibling: prevSiblingRef.current,
- nextSibling: nextSiblingRef.current,
- parentTimespan: parentTimespanRef.current
- },
- );
- setBeginTime(structuralMetadataUtils.toHHmmss(startTime));
- setEndTime(structuralMetadataUtils.toHHmmss(endTime));
+ validateSegmentInPeaks();
}
}
if (isDragging) {
setIsTyping(0);
+ validateSegmentInPeaks();
}
}, [initSegment, isDragging, isInitializing, peaksInstance]);
+ const validateSegmentInPeaks = () => {
+ const { startTime, endTime } = waveformDataUtils.validateSegment(
+ segment, startTimeChanged, duration,
+ {
+ previousSibling: prevSiblingRef.current,
+ nextSibling: nextSiblingRef.current,
+ parentTimespan: parentTimespanRef.current
+ },
+ );
+ setBeginTime(structuralMetadataUtils.toHHmmss(startTime));
+ setEndTime(structuralMetadataUtils.toHHmmss(endTime));
+ };
+
const clearFormValues = () => {
setBeginTime('');
setEndTime('');
diff --git a/src/components/TimespanInlineForm.js b/src/components/TimespanInlineForm.js
index 591a20fd..cbe55431 100644
--- a/src/components/TimespanInlineForm.js
+++ b/src/components/TimespanInlineForm.js
@@ -6,7 +6,7 @@ import Col from 'react-bootstrap/Col';
import { getExistingFormValues, isTitleValid } from '../services/form-helper';
import { useDispatch, useSelector } from 'react-redux';
import StructuralMetadataUtils from '../services/StructuralMetadataUtils';
-import { cloneDeep, isEmpty } from 'lodash';
+import { cloneDeep } from 'lodash';
import ListItemInlineEditControls from './ListItemInlineEditControls';
import * as peaksActions from '../actions/peaks-instance';
import WaveformDataUtils from '../services/WaveformDataUtils';
@@ -70,7 +70,6 @@ function TimespanInlineForm({ cancelFn, item, isInitializing, isTyping, saveFn,
setEndTime(formValues.endTime);
setTimespanTitle(formValues.timespanTitle);
setClonedSegment(formValues.clonedSegment);
-
activateSegment(
item.id,
{
@@ -99,12 +98,6 @@ function TimespanInlineForm({ cancelFn, item, isInitializing, isTyping, saveFn,
}, []);
useEffect(() => {
- if (!isDragging && isInitializing && !isTyping && !isEmpty(segment)) {
- const { startTime, endTime } = segment;
- setBeginTime(structuralMetadataUtils.toHHmmss(startTime));
- setEndTime(structuralMetadataUtils.toHHmmss(endTime));
- }
-
if (isDragging) {
// When handles in waveform are dragged clear out isInitializing and isTyping flags
if (isInitializing) setIsInitializing(0);
@@ -124,7 +117,7 @@ function TimespanInlineForm({ cancelFn, item, isInitializing, isTyping, saveFn,
setBeginTime(structuralMetadataUtils.toHHmmss(startTime));
setEndTime(structuralMetadataUtils.toHHmmss(endTime));
}
- }, [isDragging, isInitializing, isTyping, segment, peaksInstance]);
+ }, [isDragging, isInitializing, isTyping, peaksInstance]);
/**
* When there are invalid timespans in the structure, to edit them
diff --git a/src/components/__tests__/ListItemEditForm.test.js b/src/components/__tests__/ListItemEditForm.test.js
index b4c1c465..6b6b7e1c 100644
--- a/src/components/__tests__/ListItemEditForm.test.js
+++ b/src/components/__tests__/ListItemEditForm.test.js
@@ -60,11 +60,9 @@ describe('ListItemEditForm component', () => {
test("TimespanInlineForm for item type 'span'", () => {
const itemProp = {
- type: 'span',
- label: 'Segment 1.2',
- id: '123a-456b-789c-4d',
- begin: '00:00:11.231',
- end: '00:08:00.001',
+ type: 'span', label: 'Segment 1.2', id: '123a-456b-789c-4d',
+ begin: '00:00:11.231', end: '00:08:00.001',
+ timeRange: { start: 11.231, end: 480.001 }
};
const { getByTestId } = renderWithRedux(
@@ -80,11 +78,9 @@ describe('ListItemEditForm component', () => {
test('clicking on cancel button closes the form', async () => {
const itemProp = {
- type: 'span',
- label: 'Segment 1.2',
- id: '123a-456b-789c-4d',
- begin: '00:00:11.231',
- end: '00:08:00.001',
+ type: 'span', label: 'Segment 1.2', id: '123a-456b-789c-4d',
+ begin: '00:00:11.231', end: '00:08:00.001',
+ timeRange: { start: 11.231, end: 480.001 }
};
const { getByTestId } = renderWithRedux(
diff --git a/src/components/__tests__/TimespanForm.test.js b/src/components/__tests__/TimespanForm.test.js
index b3c0cad8..22647ca0 100644
--- a/src/components/__tests__/TimespanForm.test.js
+++ b/src/components/__tests__/TimespanForm.test.js
@@ -429,13 +429,13 @@ describe('Timespan component', () => {
test('when begin time is within an existing timespan', () => {
// Update the neighbor timespan relationships
jest.spyOn(hooks, 'useFindNeighborSegments').mockImplementation(() => ({
- prevSiblingRef: { current: { type: 'span', label: 'Segment 2.1.1', id: '123a-456b-789c-7d' } },
- nextSiblingRef: { current: { type: 'span', label: 'Segment 2.1.2', id: '123a-456b-789c-8d' } },
+ prevSiblingRef: { current: { begin: '00:09:10.241', end: '00:10:00.321' } },
+ nextSiblingRef: { current: { begin: '00:12:00.231', end: '00:13:00.001' } },
parentTimespanRef: {
current: {
- type: 'span', label: 'Segment 2.1', id: '123a-456b-789c-6d',
+ id: '123a-456b-789c-6d', begin: '00:09:00.241', end: '00:15:00.001',
items: [{ type: 'span', label: 'Segment 2.1.1', id: '123a-456b-789c-7d' },
- { type: 'span', label: 'Segment 2.1.2', id: '123a-456b-789c-8d' }]
+ { type: 'span', label: 'Segment 2.1.2', id: '123a-456b-789c-8d' }],
}
}
}));
diff --git a/src/components/__tests__/TimespanInlineForm.test.js b/src/components/__tests__/TimespanInlineForm.test.js
index 071efebb..186ea595 100644
--- a/src/components/__tests__/TimespanInlineForm.test.js
+++ b/src/components/__tests__/TimespanInlineForm.test.js
@@ -57,12 +57,10 @@ describe('TimespanInlineForm component', () => {
props = {
...props,
item: {
- type: 'span',
- label: 'Segment 2.1',
- id: '123a-456b-789c-8d',
- begin: '00:09:03.241',
- end: '00:15:00.001',
+ type: 'span', label: 'Segment 2.1', id: '123a-456b-789c-8d',
+ begin: '00:09:03.241', end: '00:15:00.001',
valid: true,
+ timeRange: { start: 543.241, end: 900.001 }
},
};
const timespanInlineForm = renderWithRedux(, {
@@ -88,12 +86,10 @@ describe('TimespanInlineForm component', () => {
props = {
...props,
item: {
- type: 'span',
- label: 'Segment 2.1',
- id: '123a-456b-789c-8d',
- begin: '00:09:03.241',
- end: '00:15:00.001',
+ type: 'span', label: 'Segment 2.1', id: '123a-456b-789c-8d',
+ begin: '00:09:03.241', end: '00:15:00.001',
valid: true,
+ timeRange: { start: 543.241, end: 900.001 }
},
};
timespanInlineForm = renderWithRedux(, {
@@ -147,12 +143,10 @@ describe('TimespanInlineForm component', () => {
props = {
...props,
item: {
- type: 'span',
- label: 'Invalid timespan',
- id: '123a-456b-789c-5d',
- begin: '00:20:21.000',
- end: '00:15:00.001',
+ type: 'span', label: 'Invalid timespan', id: '123a-456b-789c-5d',
+ begin: '00:20:21.000', end: '00:15:00.001',
valid: false,
+ timeRange: { start: 261.00, end: 900.001 }
},
};
const timespanInlineForm = renderWithRedux(, { initialState });
@@ -274,12 +268,10 @@ describe('TimespanInlineForm component', () => {
props = {
...props,
item: {
- type: 'span',
- label: 'Segment 1.2',
- id: '123a-456b-789c-4d',
- begin: '00:00:11.231',
- end: '00:08:00.001',
+ type: 'span', label: 'Segment 1.2', id: '123a-456b-789c-4d',
+ begin: '00:00:11.231', end: '00:08:00.001',
valid: true,
+ timeRange: { start: 11.231, end: 480.001 }
},
};
timespanInlineForm = renderWithRedux(
diff --git a/src/containers/HeadingFormContainer.js b/src/containers/HeadingFormContainer.js
index d373ec89..e6b7f258 100644
--- a/src/containers/HeadingFormContainer.js
+++ b/src/containers/HeadingFormContainer.js
@@ -2,18 +2,16 @@ import React from 'react';
import { useErrorBoundary } from 'react-error-boundary';
import PropTypes from 'prop-types';
import HeadingForm from '../components/HeadingForm';
-import { useDispatch, useSelector } from 'react-redux';
-import * as smActions from '../actions/sm-data';
+import { useSelector } from 'react-redux';
import StructuralMetadataUtils from '../services/StructuralMetadataUtils';
+import { useStructureUpdate } from '../services/sme-hooks';
const structuralMetadataUtils = new StructuralMetadataUtils();
const HeadingFormContainer = ({ cancelClick }) => {
const { smData } = useSelector((state) => state.structuralMetadata);
- const { duration } = useSelector((state) => state.peaksInstance);
- const dispatch = useDispatch();
- const updateSMData = (updatedData, duration) => dispatch(smActions.reBuildSMUI(updatedData, duration));
+ const { updateStructure } = useStructureUpdate();
const { showBoundary } = useErrorBoundary();
@@ -28,8 +26,8 @@ const HeadingFormContainer = ({ cancelClick }) => {
// Update the data structure with new heading
updatedSmData = structuralMetadataUtils.insertNewHeader(submittedItem, smData);
- // Update redux store
- updateSMData(updatedSmData, duration);
+ // Update redux store via custom hook
+ updateStructure(updatedSmData);
// Close the form
cancelClick();
diff --git a/src/containers/StructureOutputContainer.js b/src/containers/StructureOutputContainer.js
index 3ee5fb12..afa7d05e 100644
--- a/src/containers/StructureOutputContainer.js
+++ b/src/containers/StructureOutputContainer.js
@@ -90,7 +90,7 @@ const StructureOutputContainer = ({ disableSave, structureIsSaved, structureURL
variant="primary"
onClick={handleSaveItClick}
data-testid="structure-save-button"
- disabled={editingDisabled}
+ disabled={editingDisabled || !smDataIsValid}
className="float-end"
>
Save Structure
diff --git a/src/containers/TimespanFormContainer.js b/src/containers/TimespanFormContainer.js
index 06a82a39..ef001147 100644
--- a/src/containers/TimespanFormContainer.js
+++ b/src/containers/TimespanFormContainer.js
@@ -4,20 +4,20 @@ import PropTypes from 'prop-types';
import TimespanForm from '../components/TimespanForm';
import { useDispatch, useSelector } from 'react-redux';
import StructuralMetadataUtils from '../services/StructuralMetadataUtils';
-import * as smActions from '../actions/sm-data';
-import * as peaksActions from '../actions/peaks-instance';
+import { insertNewSegment } from '../actions/peaks-instance';
+import { useStructureUpdate } from '../services/sme-hooks';
const structuralMetadataUtils = new StructuralMetadataUtils();
const TimespanFormContainer = ({ cancelClick, ...restProps }) => {
// Dispatch actions from Redux store
const dispatch = useDispatch();
- const updateSMUI = (data, duration) => dispatch(smActions.reBuildSMUI(data, duration));
- const addNewSegment = (newSpan) => dispatch(peaksActions.insertNewSegment(newSpan));
+ const addNewSegment = (newSpan) => dispatch(insertNewSegment(newSpan));
+
+ const { updateStructure } = useStructureUpdate();
// State variables from Redux store
const smData = useSelector((state) => state.structuralMetadata.smData);
- const duration = useSelector((state) => state.peaksInstance.duration);
const [isTyping, _setIsTyping] = useState(false);
@@ -31,8 +31,8 @@ const TimespanFormContainer = ({ cancelClick, ...restProps }) => {
// Update the waveform segments with new timespan
addNewSegment(newSpan);
- // Update redux store
- updateSMUI(updatedData, duration);
+ // Update redux store via custom hook
+ updateStructure(updatedData);
// Close the form
cancelClick();
diff --git a/src/containers/__tests__/StructureOutputContainer.test.js b/src/containers/__tests__/StructureOutputContainer.test.js
index 0b6e51b2..a10148e5 100644
--- a/src/containers/__tests__/StructureOutputContainer.test.js
+++ b/src/containers/__tests__/StructureOutputContainer.test.js
@@ -1,5 +1,5 @@
import React from 'react';
-import { fireEvent } from '@testing-library/react';
+import { fireEvent, waitFor } from '@testing-library/react';
import StructureOutputContainer from '../StructureOutputContainer';
import { renderWithRedux, testSmData } from '../../services/testing-helpers';
import Peaks from 'peaks';
@@ -73,8 +73,8 @@ describe('StructureOutputContainer component', () => {
});
describe('displays updated structure tree after', () => {
- test('deleting a timespan', () => {
- const { getByTestId, queryAllByTestId } = renderWithRedux(
+ test('deleting a timespan', async () => {
+ const { getByTestId, queryAllByTestId, queryByText } = renderWithRedux(
,
{ initialState }
);
@@ -96,10 +96,14 @@ describe('StructureOutputContainer component', () => {
);
// Confirm delete action
fireEvent.click(getByTestId('delete-confirmation-confirm-btn'));
- expect(timespanToDelete).not.toBeInTheDocument();
+
+ // Wait for the Redux state updates and verify the timespan was deleted
+ await waitFor(() => {
+ expect(timespanToDelete).not.toBeInTheDocument();
+ });
});
- test('deleting a heading without children', () => {
+ test('deleting a heading without children', async () => {
const { getByTestId, queryAllByTestId } = renderWithRedux(
,
{ initialState }
@@ -107,10 +111,8 @@ describe('StructureOutputContainer component', () => {
expect(getByTestId('structure-output-list')).toBeInTheDocument();
let headingToDelete = queryAllByTestId('list-item')[2];
- expect(
- headingToDelete.children[0].innerHTML).toEqual(
- 'Sub-Segment 1.1'
- );
+ expect(headingToDelete.children[0].textContent).toEqual(' Sub-Segment 1.1');
+
// Get the delete button from the list item controls
let deleteButton = headingToDelete.children[1].children[1];
fireEvent.click(deleteButton);
@@ -122,21 +124,23 @@ describe('StructureOutputContainer component', () => {
);
// Confirm delete action
fireEvent.click(getByTestId('delete-confirmation-confirm-btn'));
- expect(headingToDelete).not.toBeInTheDocument();
+
+ // Wait for the Redux state updates and verify the heading was deleted
+ await waitFor(() => {
+ expect(headingToDelete).not.toBeInTheDocument();
+ });
});
- test('deleting a heading with children', () => {
- const { getByTestId, queryAllByTestId } = renderWithRedux(
+ test('deleting a heading with children', async () => {
+ const { getByTestId, getByTitle, queryAllByTestId } = renderWithRedux(
,
{ initialState }
);
expect(getByTestId('structure-output-list')).toBeInTheDocument();
let headingToDelete = queryAllByTestId('list-item')[5];
- expect(
- headingToDelete.children[0].innerHTML).toEqual(
- 'Second segment'
- );
+ expect(headingToDelete.children[0].textContent).toEqual(' Second segment');
+
// Get the delete button from the list item controls
let deleteButton = headingToDelete.children[1].children[1];
fireEvent.click(deleteButton);
@@ -148,7 +152,11 @@ describe('StructureOutputContainer component', () => {
);
// Confirm delete action
fireEvent.click(getByTestId('delete-confirmation-confirm-btn'));
- expect(headingToDelete).not.toBeInTheDocument();
+
+ // Wait for the Redux state updates and verify the heading was deleted
+ await waitFor(() => {
+ expect(headingToDelete).not.toBeInTheDocument();
+ });
});
});
});
diff --git a/src/integration.test.js b/src/integration.test.js
index 3f162209..d9f8afc5 100644
--- a/src/integration.test.js
+++ b/src/integration.test.js
@@ -157,8 +157,8 @@ describe('ButtonSection/StructureOutputContainer renders', () => {
// Check the new heading was added to the end of the 'First Segment' div
expect(
- app.queryAllByTestId('heading-label')[3].innerHTML).toEqual(
- 'New Heading'
+ app.queryAllByTestId('heading-label')[3].textContent).toEqual(
+ ' New Heading'
);
});
});
diff --git a/src/reducers/sm-data.js b/src/reducers/sm-data.js
index 8ae380b9..fef98729 100644
--- a/src/reducers/sm-data.js
+++ b/src/reducers/sm-data.js
@@ -14,14 +14,14 @@ let newState = null;
const structuralMetadata = (state = initialState, action) => {
switch (action.type) {
case types.BUILD_SM_UI:
- newState = structuralMetadataUtils.buildSMUI(
+ const { newSmData, newSmDataStatus } = structuralMetadataUtils.buildSMUI(
action.json,
action.duration
);
- return { ...state, smData: newState[0], smDataIsValid: newState[1] };
+ return { ...state, smData: newSmData, smDataIsValid: newSmDataStatus };
- case types.REBUILD_SM_UI:
- return { ...state, smData: action.items };
+ case types.UPDATE_SM_UI:
+ return { ...state, smData: action.json, smDataIsValid: action.isValid };
case types.SAVE_INIT_SMDATA:
return {
@@ -29,13 +29,6 @@ const structuralMetadata = (state = initialState, action) => {
initSmData: action.payload,
};
- case types.DELETE_ITEM:
- newState = structuralMetadataUtils.deleteListItem(
- action.id,
- state.smData
- );
- return { ...state, smData: newState };
-
case types.ADD_DROP_TARGETS:
newState = structuralMetadataUtils.determineDropTargets(
action.payload,
diff --git a/src/services/StructuralMetadataUtils.js b/src/services/StructuralMetadataUtils.js
index ffd10363..de5af298 100644
--- a/src/services/StructuralMetadataUtils.js
+++ b/src/services/StructuralMetadataUtils.js
@@ -86,6 +86,7 @@ export default class StructuralMetadataUtils {
* so that they can be used in the validation logic and Peaks instance
* @param {Array} allItems - array of all the items in structured metadata
* @param {Float} duration - end time of the media file in seconds
+ * @return {Object} { newSmData: Array