Skip to content
Open
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
8 changes: 7 additions & 1 deletion __mocks__/peaks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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;
Expand Down
100 changes: 71 additions & 29 deletions src/App.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
manifestWoStructure,
manifestWithInvalidStruct,
manifestWEmptyCanvas,
manifestWEmptyRanges,
} from './services/testing-helpers';
import mockAxios from 'axios';
import Peaks from 'peaks.js';
Expand Down Expand Up @@ -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.'
);
});
Expand Down Expand Up @@ -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(<App {...props} />, { 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(<App {...props} />, { initialState });
};
const app = renderWithRedux(<App {...props} />, { 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();
});
});
});
});
Expand Down
17 changes: 4 additions & 13 deletions src/actions/forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
21 changes: 5 additions & 16 deletions src/actions/sm-data.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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,
};
}

Expand Down
3 changes: 1 addition & 2 deletions src/actions/types.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
10 changes: 7 additions & 3 deletions src/components/ButtonSection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
25 changes: 14 additions & 11 deletions src/components/ListItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -143,6 +137,15 @@ const ListItem = ({ item, children }) => {
className='structure-title heading'
data-testid='heading-label'
>
{(!valid && type !== 'root') && (
<>
<FontAwesomeIcon
icon={faExclamationTriangle}
className='icon-invalid'
title='Please add at least one timespan or remove this heading.' />
{' '}
</>
)}
{label}
</div>
)}
Expand Down
14 changes: 3 additions & 11 deletions src/components/ListItemControls.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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);
Expand All @@ -47,8 +41,6 @@ const ListItemControls = ({ handleDelete, handleEditClick, handleShowDropTargets
enableEditing();
setDeleteMessage('');
setShowDeleteConfirm(false);
// Change structureIsSaved to false
updateStructStatus(0);
};

const handleDeleteClick = (e) => {
Expand Down
13 changes: 5 additions & 8 deletions src/components/ListItemEditForm.js
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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();
Expand Down
Loading