diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget.jsx deleted file mode 100644 index 779266af6..000000000 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget.jsx +++ /dev/null @@ -1,87 +0,0 @@ -import React from 'react'; -import { useDispatch } from 'react-redux'; - -import { Col, Form } from '@edx/paragon'; - -import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n'; -import { keyStore } from '../../../../../utils'; -import CollapsibleFormWidget from './CollapsibleFormWidget'; -import hooks from './hooks'; -import { durationFromValue } from './duration'; -import messages from './messages'; - -/** - * Collapsible Form widget controlling video start and end times - * Also displays the total run time of the video. - */ -export const DurationWidget = ({ - // injected - intl, -}) => { - const dispatch = useDispatch(); - const { duration } = hooks.widgetValues({ - dispatch, - fields: { [hooks.selectorKeys.duration]: hooks.durationWidget }, - }); - const timeKeys = keyStore(duration.formValue); - - const getTotalLabel = (startTime, stopTime, subtitle) => { - if (!stopTime) { - if (!startTime) { - return intl.formatMessage(messages.fullVideoLength); - } - if (subtitle) { - return intl.formatMessage(messages.startsAt, { startTime: durationFromValue(startTime) }); - } - return null; - } - const total = stopTime - (startTime || 0); - return intl.formatMessage(messages.total, { total: durationFromValue(total) }); - }; - - return ( - - - - - - - - - - - - - - - - -
- {getTotalLabel(duration.formValue.startTime, duration.formValue.stopTime)} -
-
- ); -}; - -DurationWidget.propTypes = { - // injected - intl: intlShape.isRequired, -}; - -export default injectIntl(DurationWidget); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget.test.jsx deleted file mode 100644 index 68a9f8e57..000000000 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget.test.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; - -import { formatMessage } from '../../../../../../testUtils'; -import { DurationWidget } from './DurationWidget'; - -describe('DurationWidget', () => { - const props = { - isError: false, - subtitle: 'SuBTItle', - title: 'tiTLE', - // inject - intl: { formatMessage }, - }; - describe('render', () => { - test('snapshots: renders as expected with default props', () => { - expect( - shallow(), - ).toMatchSnapshot(); - }); - }); -}); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/__snapshots__/DurationWidget.test.jsx.snap b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/__snapshots__/index.test.jsx.snap similarity index 96% rename from src/editors/containers/VideoEditor/components/VideoSettingsModal/components/__snapshots__/DurationWidget.test.jsx.snap rename to src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/__snapshots__/index.test.jsx.snap index cc8d85d93..22bb051c5 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/__snapshots__/DurationWidget.test.jsx.snap +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/__snapshots__/index.test.jsx.snap @@ -3,7 +3,7 @@ exports[`DurationWidget render snapshots: renders as expected with default props 1`] = ` - Full video length + Total: 00:00:00 `; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks.js b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks.js new file mode 100644 index 000000000..e5b7437bd --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks.js @@ -0,0 +1,216 @@ +import { useEffect, useState } from 'react'; + +import messages from '../messages'; + +import * as module from './hooks'; + +const durationMatcher = /^(\d{0,2}):?(\d{0,2})?:?(\d{0,2})?$/i; + +export const durationWidget = ({ duration, updateField }) => { + const setDuration = (val) => updateField({ duration: val }); + const initialState = module.durationString(duration); + const [unsavedDuration, setUnsavedDuration] = useState(initialState); + + useEffect(() => { + setUnsavedDuration(module.durationString(duration)); + }, [duration]); + + return { + unsavedDuration, + onBlur: (index) => ( + (e) => module.updateDuration({ + duration, + setDuration, + unsavedDuration, + setUnsavedDuration, + index, + inputString: e.target.value, + }) + ), + onChange: (index) => ( + (e) => setUnsavedDuration(module.onDurationChange(unsavedDuration, index, e.target.value)) + ), + onKeyDown: (index) => ( + (e) => setUnsavedDuration(module.onDurationKeyDown(unsavedDuration, index, e)) + ), + getTotalLabel: ({ durationString, subtitle, intl }) => { + if (!durationString.stopTime) { + if (!durationString.startTime) { + return intl.formatMessage(messages.fullVideoLength); + } + if (subtitle) { + return intl.formatMessage( + messages.startsAt, + { startTime: module.durationStringFromValue(durationString.startTime) }, + ); + } + return null; + } + const total = durationString.stopTime - (durationString.startTime || 0); + return intl.formatMessage(messages.total, { total: module.durationStringFromValue(total) }); + }, + }; +}; + +/** + * durationString(duration) + * Returns the display value for embedded start and stop times + * @param {object} duration - object containing startTime and stopTime millisecond values + * @return {object} - start and stop time from incoming object mapped to duration strings. + */ +export const durationString = (duration) => ({ + startTime: module.durationStringFromValue(duration.startTime), + stopTime: module.durationStringFromValue(duration.stopTime), +}); + +/** + * durationStringFromValue(value) + * Returns a duration string in 'hh:mm:ss' format from the given ms value + * @param {number} value - duration (in milliseconds) + * @return {string} - duration in 'hh:mm:ss' format + */ +export const durationStringFromValue = (value) => { + // return 'why'; + if (!value || typeof value !== 'number' || value <= 0) { + return '00:00:00'; + } + const seconds = Math.floor((value / 1000) % 60); + const minutes = Math.floor((value / 60000) % 60); + const hours = Math.floor((value / 3600000) % 60); + const zeroPad = (num) => String(num).padStart(2, '0'); + return [hours, minutes, seconds].map(zeroPad).join(':'); +}; + +/** + * updateDuration({ duration, unsavedDuration, setUnsavedDuration, setDuration }) + * Returns a memoized callback based on inputs that updates unsavedDuration value and form value + * if the new string is valid (duration stores a number, unsavedDuration stores a string). + * If the duration string is invalid, resets the unsavedDuration value to the latest good value. + * @param {object} duration - redux-stored durations in milliseconds + * @param {object} unsavedDuration - hook-stored duration in 'hh:mm:ss' format + * @param {func} setDuration - set form value + * @param {func} setUnsavedDuration - set unsavedDuration object + * @param {string} index - startTime or stopTime + * @param {string} inputString - string value of user input for either the start or stop time field + * @return {func} - callback to update duration unsavedDurationly and in redux + * updateDuration(args)(index, durationString) + */ +export const updateDuration = ({ + duration, + unsavedDuration, + setDuration, + setUnsavedDuration, + index, + inputString, +}) => { + let newDurationString = inputString; + let newValue = module.valueFromDuration(newDurationString); + // maxTime is 23:59:59 or 86399 seconds + if (newValue > 86399000) { + newValue = 86399000; + } + // stopTime must be at least 1 second, if not zero + if (index === 'stopTime' && newValue > 0 && newValue < 1000) { + newValue = 1000; + } + // stopTime must be at least 1 second after startTime, except 0 means no custom stopTime + if (index === 'stopTime' && newValue > 0 && newValue < (duration.startTime + 1000)) { + newValue = duration.startTime + 1000; + } + // startTime must be at least 1 second before stopTime, except when stopTime is less than a second + // (stopTime should only be less than a second if it's zero, but we're being paranoid) + if (index === 'startTime' && duration.stopTime >= 1000 && newValue > (duration.stopTime - 1000)) { + newValue = duration.stopTime - 1000; + } + newDurationString = module.durationStringFromValue(newValue); + setUnsavedDuration({ ...unsavedDuration, [index]: newDurationString }); + setDuration({ ...duration, [index]: newValue }); +}; + +/** + * onDurationChange(duration) + * Returns a new duration value based on onChange event + * @param {object} duration - object containing startTime and stopTime millisecond values + * @param {string} index - 'startTime or 'stopTime' + * @param {string} val - duration in 'hh:mm:ss' format + * @return {object} duration - object containing startTime and stopTime millisecond values + */ +export const onDurationChange = (duration, index, val) => { + const match = val.trim().match(durationMatcher); + if (!match) { + return duration; + } + + const caretPos = document.activeElement.selectionStart; + let newDuration = val; + if (caretPos === newDuration.length && (newDuration.length === 2 || newDuration.length === 5)) { + newDuration += ':'; + } + + return { + ...duration, + [index]: newDuration, + }; +}; + +/** + * onDurationKeyDown(duration) + * Returns a new duration value based on onKeyDown event + * @param {object} duration - object containing startTime and stopTime millisecond values + * @param {string} index - 'startTime or 'stopTime' + * @param {Event} event - event from onKeyDown + * @return {object} duration - object containing startTime and stopTime millisecond values + */ +export const onDurationKeyDown = (duration, index, event) => { + const caretPos = document.activeElement.selectionStart; + let newDuration = duration[index]; + + switch (event.key) { + case 'Enter': + document.activeElement.blur(); + break; + case 'Backspace': + if (caretPos === newDuration.length && newDuration.slice(-1) === ':') { + newDuration = newDuration.slice(0, -1); + } + break; + default: + break; + } + + return { + ...duration, + [index]: newDuration, + }; +}; + +/** + * valueFromDuration(duration) + * Returns a millisecond duration value from the given 'hh:mm:ss' format string + * @param {string} duration - duration in 'hh:mm:ss' format + * @return {number} - duration in milliseconds. Returns null if duration is invalid. + */ +export const valueFromDuration = (duration) => { + let matches = duration.trim().match(durationMatcher); + if (!matches) { + return 0; + } + matches = matches.slice(1).filter(v => v !== undefined); + if (matches.length < 3) { + for (let i = 0; i <= 3 - matches.length; i++) { + matches.unshift(0); + } + } + const [hours, minutes, seconds] = matches.map(x => parseInt(x, 10) || 0); + return ((hours * 60 + minutes) * 60 + seconds) * 1000; +}; + +export default { + durationWidget, + durationString, + durationStringFromValue, + updateDuration, + onDurationChange, + onDurationKeyDown, + valueFromDuration, +}; diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks.test.js b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks.test.js new file mode 100644 index 000000000..782558829 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks.test.js @@ -0,0 +1,308 @@ +import React from 'react'; + +import * as hooks from './hooks'; +import messages from '../messages'; + +jest.mock('react', () => { + const updateState = jest.fn(); + return { + ...jest.requireActual('react'), + updateState, + useState: jest.fn(val => ([{ state: val }, (newVal) => updateState({ val, newVal })])), + useCallback: (cb, prereqs) => ({ useCallback: { cb, prereqs } }), + useEffect: jest.fn(), + }; +}); + +let testMethod; +const intl = { + formatMessage: jest.fn(val => val), +}; + +const [h, m, s] = [3600000, 60000, 1000]; +const durationPairs = [ + [0, '00:00:00'], + [5000, '00:00:05'], + [60000, '00:01:00'], + [3600000, '01:00:00'], + [3665000, '01:01:05'], +]; +const trickyDurations = [ + ['10:00', 600000], + ['23', 23000], + ['99:99:99', 99 * (m + s + h)], + ['23:42:81', 23 * h + 42 * m + 81 * s], +]; +let props; +const e = { + target: { + value: 'vAlUE', + }, +}; + +describe('Video Settings DurationWidget hooks', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + describe('durationWidget', () => { + const duration = { + startTime: '00:00:00', + stopTime: '00:00:10', + }; + const updateField = jest.fn(); + beforeEach(() => { + testMethod = hooks.durationWidget({ duration, updateField }); + }); + describe('behavior', () => { + describe('initialization', () => { + test('useEffect memoized on duration', () => { + hooks.durationWidget({ duration, updateField }); + expect(React.useEffect).toHaveBeenCalled(); + expect(React.useEffect.mock.calls[0][1]).toEqual([duration]); + }); + test('calls setUnsavedDuration with durationString(duration)', () => { + hooks.durationWidget({ duration, updateField }); + React.useEffect.mock.calls[0][0](); + expect(React.updateState).toHaveBeenCalled(); + }); + }); + }); + describe('returns', () => { + testMethod = hooks.durationWidget({ duration, updateField }); + afterEach(() => { + jest.restoreAllMocks(); + }); + describe('unsavedDuration, defaulted to duration', () => { + expect(testMethod.unsavedDuration).toEqual({ state: hooks.durationString(duration) }); + }); + describe('onBlur, calls updateDuration', () => { + jest.spyOn(hooks, 'updateDuration').mockImplementation(jest.fn()); + testMethod.onBlur('IndEX')(e); + expect(hooks.updateDuration).toHaveBeenCalled(); + }); + describe('onChange', () => { + testMethod.onChange('IndEX')(e); + expect(React.updateState).toHaveBeenCalled(); + }); + describe('onKeyDown', () => { + testMethod.onKeyDown('iNDex')(e); + expect(React.updateState).toHaveBeenCalled(); + }); + describe('getTotalLabel', () => { + describe('returns fullVideoLength message when no startTime and no stop Time are set', () => { + expect(testMethod.getTotalLabel({ + durationString: {}, + subtitle: true, + intl, + })).toEqual(messages.fullVideoLength); + }); + describe('returns startAt message for subtitle when only startTime is set', () => { + expect(testMethod.getTotalLabel({ + durationString: { + startTime: '00:00:00', + }, + subtitle: true, + intl, + })).toEqual(messages.startsAt); + }); + describe('returns null for widget (not subtitle) when there only startTime is set', () => { + expect(testMethod.getTotalLabel({ + durationString: { + startTime: '00:00:00', + }, + subtitle: false, + intl, + })).toEqual(null); + }); + describe('returns total message when at least stopTime is set', () => { + expect(testMethod.getTotalLabel({ + durationString: { + startTime: '00:00:00', + stopTime: '00:00:10', + }, + subtitle: true, + intl, + })).toEqual(messages.total); + }); + }); + }); + }); + describe('durationString', () => { + beforeEach(() => { + testMethod = hooks.durationString; + }); + it('returns an object that maps durationStringFromValue to the passed duration keys', () => { + const testDuration = { startTime: 1000, stopTime: 2000, other: 'values' }; + expect(testMethod(testDuration)).toEqual({ + startTime: '00:00:01', + stopTime: '00:00:02', + }); + }); + }); + describe('durationStringFromValue', () => { + beforeEach(() => { + testMethod = hooks.durationStringFromValue; + }); + it('returns 00:00:00 if given a bad value', () => { + const badChecks = ['a', '', null, -1]; + badChecks.forEach(val => expect(testMethod(val)).toEqual('00:00:00')); + }); + it('translates milliseconds into hh:mm:ss format', () => { + durationPairs.forEach( + ([val, dur]) => expect(testMethod(val)).toEqual(dur), + ); + }); + }); + describe('updateDuration', () => { + const testValidIndex = 'startTime'; + const testStopIndex = 'stopTime'; + const testValidDuration = '00:00:00'; + const testValidValue = 0; + const testInvalidDuration = 'abc'; + beforeEach(() => { + testMethod = hooks.updateDuration; + props = { + duration: { startTime: 23000, stopTime: 600000 }, + unsavedDuration: { startTime: '00:00:23', stopTime: '00:10:00' }, + setDuration: jest.fn(), + setUnsavedDuration: jest.fn(), + index: 'startTime', + inputString: '01:23:45', + }; + }); + describe('if the passed durationString is valid', () => { + it('sets the unsavedDuration to updated strings and duration to new timestamp value', () => { + testMethod({ + ...props, + index: testValidIndex, + inputString: testValidDuration, + }); + expect(props.setUnsavedDuration).toHaveBeenCalledWith({ + ...props.unsavedDuration, + [testValidIndex]: testValidDuration, + }); + expect(props.setDuration).toHaveBeenCalledWith({ + ...props.duration, + [testValidIndex]: testValidValue, + }); + }); + }); + describe('if the passed durationString is not valid', () => { + it('updates unsavedDuration values to 0 (the default)', () => { + testMethod({ + ...props, + index: testValidIndex, + inputString: testInvalidDuration, + }); + expect(props.setUnsavedDuration).toHaveBeenCalledWith({ + ...props.unsavedDuration, + [testValidIndex]: testValidDuration, + }); + expect(props.setDuration).toHaveBeenCalledWith({ + ...props.duration, + [testValidIndex]: testValidValue, + }); + }); + }); + describe('if the passed startTime is after (or equal to) the stored non-zero stopTime', () => { + it('updates unsavedDuration startTime values to 1 second before stopTime', () => { + testMethod({ + ...props, + index: testValidIndex, + inputString: '00:10:00', + }); + expect(props.setUnsavedDuration).toHaveBeenCalledWith({ + ...props.unsavedDuration, + [testValidIndex]: '00:09:59', + }); + expect(props.setDuration).toHaveBeenCalledWith({ + ...props.duration, + [testValidIndex]: 599000, + }); + }); + }); + describe('if the passed stopTime is before (or equal to) the stored startTime', () => { + it('updates unsavedDuration stopTime values to 1 second after startTime', () => { + testMethod({ + ...props, + index: testStopIndex, + inputString: '00:00:22', + }); + expect(props.setUnsavedDuration).toHaveBeenCalledWith({ + ...props.unsavedDuration, + [testStopIndex]: '00:00:24', + }); + expect(props.setDuration).toHaveBeenCalledWith({ + ...props.duration, + [testStopIndex]: 24000, + }); + }); + }); + }); + describe('onDurationChange', () => { + beforeEach(() => { + props = { + duration: { startTime: '00:00:00' }, + index: 'startTime', + val: 'vAl', + }; + testMethod = hooks.onDurationChange; + }); + it('returns duration with no change if duration[index] does not match HH:MM:SS format', () => { + const badChecks = [ + 'ab:cd:ef', // non-digit characters + '12:34:567', // characters past max length + ]; + badChecks.forEach(val => expect(testMethod(props.duration, props.index, val)).toEqual(props.duration)); + }); + it('returns duration with an added \':\' after 2 characters when caret is at end', () => { + props.duration = { startTime: '0' }; + props.val = '00'; + document.activeElement.selectionStart = props.duration[props.index].length + 1; + expect(testMethod(props.duration, props.index, props.val)).toEqual({ startTime: '00:' }); + }); + it('returns duration with an added \':\' after 5 characters when caret is at end', () => { + props.duration = { startTime: '00:0' }; + props.val = '00:00'; + document.activeElement.selectionStart = props.duration[props.index].length + 1; + expect(testMethod(props.duration, props.index, props.val)).toEqual({ startTime: '00:00:' }); + }); + }); + describe('onDurationKeyDown', () => { + beforeEach(() => { + props = { + duration: { startTime: '00:00:00' }, + index: 'startTime', + event: 'eVeNt', + }; + testMethod = hooks.onDurationKeyDown; + }); + it('enter event: calls blur()', () => { + props.event = { key: 'Enter' }; + const blurSpy = jest.spyOn(document.activeElement, 'blur'); + testMethod(props.duration, props.index, props.event); + expect(blurSpy).toHaveBeenCalled(); + }); + it('backspace event: returns duration with deleted end character when that character is \':\' and caret is at end', () => { + props.duration = { startTime: '00:' }; + props.event = { key: 'Backspace' }; + document.activeElement.selectionStart = props.duration[props.index].length; + expect(testMethod(props.duration, props.index, props.event)).toEqual({ startTime: '00' }); + }); + }); + describe('valueFromDuration', () => { + beforeEach(() => { + testMethod = hooks.valueFromDuration; + }); + it('returns 0 if given a bad duration string', () => { + const badChecks = ['a', '00:00:1f', '0adg:00:04']; + badChecks.forEach(dur => expect(testMethod(dur)).toEqual(0)); + }); + it('returns simple durations', () => { + durationPairs.forEach(([val, dur]) => expect(testMethod(dur)).toEqual(val)); + }); + it('returns tricky durations, prepending zeros and expanding out sections', () => { + trickyDurations.forEach(([dur, val]) => expect(testMethod(dur)).toEqual(val)); + }); + }); +}); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/index.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/index.jsx new file mode 100644 index 000000000..7b2b1144c --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/index.jsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; + +import { Col, Form } from '@edx/paragon'; +import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n'; + +import { actions, selectors } from '../../../../../../data/redux'; +import { keyStore } from '../../../../../../utils'; +import CollapsibleFormWidget from '../CollapsibleFormWidget'; +import hooks from './hooks'; +import messages from '../messages'; + +/** + * Collapsible Form widget controlling video start and end times + * Also displays the total run time of the video. + */ +export const DurationWidget = ({ + // redux + duration, + updateField, + // injected + intl, +}) => { + const { + unsavedDuration, + onBlur, + onChange, + onKeyDown, + getTotalLabel, + } = hooks.durationWidget({ duration, updateField }); + + const timeKeys = keyStore(duration); + + return ( + + + + + + + + + + + + + + + + +
+ {getTotalLabel({ + durationString: duration, + subtitle: false, + intl, + })} +
+
+ ); +}; + +DurationWidget.propTypes = { + // redux + duration: PropTypes.objectOf(PropTypes.number).isRequired, + updateField: PropTypes.func.isRequired, + // injected + intl: intlShape.isRequired, +}; + +export const mapStateToProps = (state) => ({ + duration: selectors.video.duration(state), +}); + +export const mapDispatchToProps = { + updateField: actions.video.updateField, +}; + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(DurationWidget)); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/index.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/index.test.jsx new file mode 100644 index 000000000..ddb9e8df9 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/index.test.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { actions, selectors } from '../../../../../../data/redux'; +import { formatMessage } from '../../../../../../../testUtils'; +import { DurationWidget, mapStateToProps, mapDispatchToProps } from '.'; + +jest.mock('../../../../../../data/redux', () => ({ + actions: { + video: { + updateField: jest.fn().mockName('actions.video.updateField'), + }, + }, + selectors: { + video: { + duration: jest.fn(state => ({ duration: state })), + }, + }, +})); + +describe('DurationWidget', () => { + const props = { + duration: { + startTime: '00:00:00', + stopTime: '00:00:10', + }, + updateField: jest.fn().mockName('updateField'), + // inject + intl: { formatMessage }, + }; + describe('render', () => { + test('snapshots: renders as expected with default props', () => { + expect( + shallow(), + ).toMatchSnapshot(); + }); + }); + describe('mapStateToProps', () => { + const testState = { A: 'pple', B: 'anana', C: 'ucumber' }; + test('duration from video.duration', () => { + expect( + mapStateToProps(testState).duration, + ).toEqual(selectors.video.duration(testState)); + }); + }); + describe('mapDispatchToProps', () => { + test('updateField from actions.video.updateField', () => { + expect(mapDispatchToProps.updateField).toEqual(actions.video.updateField); + }); + }); +}); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/duration.js b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/duration.js deleted file mode 100644 index 3f7c65176..000000000 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/duration.js +++ /dev/null @@ -1,155 +0,0 @@ -import { useCallback } from 'react'; -import * as module from './duration'; - -const durationMatcher = /^(\d{0,2}):?(\d{0,2})?:?(\d{0,2})?$/i; - -/** - * onDurationChange(duration) - * Returns a new duration value based on onChange event - * @param {object} duration - object containing startTime and stopTime millisecond values - * @param {string} index - 'startTime or 'stopTime' - * @param {string} val - duration in 'hh:mm:ss' format - * @return {object} duration - object containing startTime and stopTime millisecond values - */ -export const onDurationChange = (duration, index, val) => { - const match = val.trim().match(durationMatcher); - if (!match) { - return duration; - } - - const caretPos = document.activeElement.selectionStart; - let newDuration = val; - if (caretPos === newDuration.length && (newDuration.length === 2 || newDuration.length === 5)) { - newDuration += ':'; - } - - return { - ...duration, - [index]: newDuration, - }; -}; - -/** - * onDurationKeyDown(duration) - * Returns a new duration value based on onKeyDown event - * @param {object} duration - object containing startTime and stopTime millisecond values - * @param {string} index - 'startTime or 'stopTime' - * @param {Event} event - event from onKeyDown - * @return {object} duration - object containing startTime and stopTime millisecond values - */ -export const onDurationKeyDown = (duration, index, event) => { - const caretPos = document.activeElement.selectionStart; - let newDuration = duration[index]; - - switch (event.key) { - case 'Enter': - document.activeElement.blur(); - break; - case 'Backspace': - if (caretPos === newDuration.length && newDuration.slice(-1) === ':') { - newDuration = newDuration.slice(0, -1); - } - break; - default: - break; - } - - return { - ...duration, - [index]: newDuration, - }; -}; - -/** - * durationFromValue(value) - * Returns a duration string in 'hh:mm:ss' format from the given ms value - * @param {number} value - duration (in milliseconds) - * @return {string} - duration in 'hh:mm:ss' format - */ -export const durationFromValue = (value) => { - if (!value || typeof value !== 'number' || value <= 0) { - return '00:00:00'; - } - const seconds = Math.floor((value / 1000) % 60); - const minutes = Math.floor((value / 60000) % 60); - const hours = Math.floor((value / 3600000) % 60); - const zeroPad = (num) => String(num).padStart(2, '0'); - return [hours, minutes, seconds].map(zeroPad).join(':'); -}; - -/** - * valueFromDuration(duration) - * Returns a millisecond duration value from the given 'hh:mm:ss' format string - * @param {string} duration - duration in 'hh:mm:ss' format - * @return {number} - duration in milliseconds. Returns null if duration is invalid. - */ -export const valueFromDuration = (duration) => { - let matches = duration.trim().match(durationMatcher); - if (!matches) { - return 0; - } - matches = matches.slice(1).filter(v => v !== undefined); - if (matches.length < 3) { - for (let i = 0; i <= 3 - matches.length; i++) { - matches.unshift(0); - } - } - const [hours, minutes, seconds] = matches.map(x => parseInt(x, 10) || 0); - return ((hours * 60 + minutes) * 60 + seconds) * 1000; -}; - -/** - * durationValue(duration) - * Returns the display value for embedded start and stop times - * @param {object} duration - object containing startTime and stopTime millisecond values - * @return {object} - start and stop time from incoming object mapped to duration strings. - */ -export const durationValue = (duration) => ({ - startTime: module.durationFromValue(duration.startTime), - stopTime: module.durationFromValue(duration.stopTime), -}); - -/** - * updateDuration({ formValue, local, setLocal, setFormValue }) - * Returns a memoized callback based on inputs that updates local value and form value - * if the new string is valid (formValue stores a number, local stores a string). - * If the duration string is invalid, resets the local value to the latest good value. - * @param {object} formValue - redux-stored durations in milliseconds - * @param {object} local - hook-stored duration in 'hh:mm:ss' format - * @param {func} setFormValue - set form value - * @param {func} setLocal - set local object - * @return {func} - callback to update duration locally and in redux - * updateDuration(args)(index, durationString) - */ -export const updateDuration = ({ - formValue, - local, - setFormValue, - setLocal, -}) => useCallback( - (index, durationString) => { - let newDurationString = durationString; - let newValue = module.valueFromDuration(newDurationString); - // maxTime is 23:59:59 or 86399 seconds - if (newValue > 86399000) { - newValue = 86399000; - } - // stopTime must be at least 1 second, if not zero - if (index === 'stopTime' && newValue > 0 && newValue < 1000) { - newValue = 1000; - } - // stopTime must be at least 1 second after startTime, except 0 means no custom stopTime - if (index === 'stopTime' && newValue > 0 && newValue < (formValue.startTime + 1000)) { - newValue = formValue.startTime + 1000; - } - // startTime must be at least 1 second before stopTime, except when stopTime is less than a second - // (stopTime should only be less than a second if it's zero, but we're being paranoid) - if (index === 'startTime' && formValue.stopTime >= 1000 && newValue > (formValue.stopTime - 1000)) { - newValue = formValue.stopTime - 1000; - } - newDurationString = module.durationFromValue(newValue); - setLocal({ ...local, [index]: newDurationString }); - setFormValue({ ...formValue, [index]: newValue }); - }, - [formValue, local, setLocal, setFormValue], -); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/duration.test.js b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/duration.test.js deleted file mode 100644 index 533289ec3..000000000 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/duration.test.js +++ /dev/null @@ -1,216 +0,0 @@ -import { keyStore } from '../../../../../utils'; -import * as duration from './duration'; - -jest.mock('react', () => ({ - ...jest.requireActual('react'), - useCallback: (cb, prereqs) => ({ useCallback: { cb, prereqs } }), -})); - -let hook; -const durationKeys = keyStore(duration); -const [h, m, s] = [3600000, 60000, 1000]; -const durationPairs = [ - [0, '00:00:00'], - [5000, '00:00:05'], - [60000, '00:01:00'], - [3600000, '01:00:00'], - [3665000, '01:01:05'], -]; -const trickyDurations = [ - ['10:00', 600000], - ['23', 23000], - ['99:99:99', 99 * (m + s + h)], - ['23:42:81', 23 * h + 42 * m + 81 * s], -]; -let spies = {}; -let props; -let cb; -let prereqs; -let oldDuration; -describe('Video Settings Modal duration hooks', () => { - beforeEach(() => { - spies = {}; - oldDuration = { ...jest.requireActual('./duration') }; - }); - afterEach(() => { - Object.keys(oldDuration).forEach((key) => { - duration[key] = oldDuration[key]; - }); - Object.keys(spies).forEach((key) => { - spies[key].mockRestore(); - }); - }); - - describe('onDurationChange', () => { - beforeEach(() => { - props = { - duration: { startTime: '00:00:00' }, - index: 'startTime', - val: 'vAl', - }; - hook = duration.onDurationChange; - }); - it('returns duration with no change if duration[index] does not match HH:MM:SS format', () => { - const badChecks = [ - 'ab:cd:ef', // non-digit characters - '12:34:567', // characters past max length - ]; - badChecks.forEach(val => expect(hook(props.duration, props.index, val)).toEqual(props.duration)); - }); - it('returns duration with an added \':\' after 2 characters when caret is at end', () => { - props.duration = { startTime: '0' }; - props.val = '00'; - document.activeElement.selectionStart = props.duration[props.index].length + 1; - expect(hook(props.duration, props.index, props.val)).toEqual({ startTime: '00:' }); - }); - it('returns duration with an added \':\' after 5 characters when caret is at end', () => { - props.duration = { startTime: '00:0' }; - props.val = '00:00'; - document.activeElement.selectionStart = props.duration[props.index].length + 1; - expect(hook(props.duration, props.index, props.val)).toEqual({ startTime: '00:00:' }); - }); - }); - describe('onDurationKeyDown', () => { - beforeEach(() => { - props = { - duration: { startTime: '00:00:00' }, - index: 'startTime', - event: 'eVeNt', - }; - hook = duration.onDurationKeyDown; - }); - it('enter event: calls blur()', () => { - props.event = { key: 'Enter' }; - const blurSpy = jest.spyOn(document.activeElement, 'blur'); - hook(props.duration, props.index, props.event); - expect(blurSpy).toHaveBeenCalled(); - }); - it('backspace event: returns duration with deleted end character when that character is \':\' and caret is at end', () => { - props.duration = { startTime: '00:' }; - props.event = { key: 'Backspace' }; - document.activeElement.selectionStart = props.duration[props.index].length; - expect(hook(props.duration, props.index, props.event)).toEqual({ startTime: '00' }); - }); - }); - describe('durationFromValue', () => { - beforeEach(() => { - hook = duration.durationFromValue; - }); - it('returns 00:00:00 if given a bad value', () => { - const badChecks = ['a', '', null, -1]; - badChecks.forEach(val => expect(hook(val)).toEqual('00:00:00')); - }); - it('translates milliseconds into hh:mm:ss format', () => { - durationPairs.forEach( - ([val, dur]) => expect(hook(val)).toEqual(dur), - ); - }); - }); - describe('valueFromDuration', () => { - beforeEach(() => { - hook = duration.valueFromDuration; - }); - it('returns 0 if given a bad duration string', () => { - const badChecks = ['a', '00:00:1f', '0adg:00:04']; - badChecks.forEach(dur => expect(hook(dur)).toEqual(0)); - }); - it('returns simple durations', () => { - durationPairs.forEach(([val, dur]) => expect(hook(dur)).toEqual(val)); - }); - it('returns tricky durations, prepending zeros and expanding out sections', () => { - trickyDurations.forEach(([dur, val]) => expect(hook(dur)).toEqual(val)); - }); - }); - describe('durationValue', () => { - const mock = jest.fn(v => ({ duration: v })); - beforeEach(() => { - jest.spyOn(duration, durationKeys.durationFromValue).mockImplementation(mock); - }); - it('returns an object that maps durationFromValue to the passed duration keys', () => { - const testDuration = { startTime: 1, stopTime: 2, other: 'values' }; - expect(duration.durationValue(testDuration)).toEqual({ - startTime: mock(testDuration.startTime), - stopTime: mock(testDuration.stopTime), - }); - }); - }); - describe('updateDuration', () => { - const testValidIndex = 'startTime'; - const testStopIndex = 'stopTime'; - const testValidDuration = '00:00:00'; - const testValidValue = 0; - const testInvalidDuration = 'abc'; - beforeEach(() => { - props = { - formValue: { startTime: 23000, stopTime: 600000 }, - local: { startTime: '00:00:23', stopTime: '00:10:00' }, - setLocal: jest.fn(), - setFormValue: jest.fn(), - }; - hook = duration.updateDuration; - ({ cb, prereqs } = hook(props).useCallback); - }); - it('returns a useCallback field based on the passed args', () => { - expect(prereqs).toEqual([ - props.formValue, - props.local, - props.setLocal, - props.setFormValue, - ]); - }); - describe('callback', () => { - describe('if the passed durationString is valid', () => { - it('sets the local value to updated strings and form value to new timestamp value', () => { - cb(testValidIndex, testValidDuration); - expect(props.setLocal).toHaveBeenCalledWith({ - ...props.local, - [testValidIndex]: testValidDuration, - }); - expect(props.setFormValue).toHaveBeenCalledWith({ - ...props.formValue, - [testValidIndex]: testValidValue, - }); - }); - }); - describe('if the passed durationString is not valid', () => { - it('updates local values to 0 (the default)', () => { - hook(props).useCallback.cb(testValidIndex, testInvalidDuration); - expect(props.setLocal).toHaveBeenCalledWith({ - ...props.local, - [testValidIndex]: testValidDuration, - }); - expect(props.setFormValue).toHaveBeenCalledWith({ - ...props.formValue, - [testValidIndex]: testValidValue, - }); - }); - }); - describe('if the passed startTime is after (or equal to) the stored non-zero stopTime', () => { - it('updates local startTime values to 1 second before stopTime', () => { - hook(props).useCallback.cb(testValidIndex, '00:10:00'); - expect(props.setLocal).toHaveBeenCalledWith({ - ...props.local, - [testValidIndex]: '00:09:59', - }); - expect(props.setFormValue).toHaveBeenCalledWith({ - ...props.formValue, - [testValidIndex]: 599000, - }); - }); - }); - describe('if the passed stopTime is before (or equal to) the stored startTime', () => { - it('updates local stopTime values to 1 second after startTime', () => { - hook(props).useCallback.cb(testStopIndex, '00:00:22'); - expect(props.setLocal).toHaveBeenCalledWith({ - ...props.local, - [testStopIndex]: '00:00:24', - }); - expect(props.setFormValue).toHaveBeenCalledWith({ - ...props.formValue, - [testStopIndex]: 24000, - }); - }); - }); - }); - }); -}); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/hooks.js b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/hooks.js index 97400b049..621b079a2 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/hooks.js +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/hooks.js @@ -2,7 +2,6 @@ import { useCallback, useState, useEffect, - useMemo, } from 'react'; import { useSelector } from 'react-redux'; @@ -10,18 +9,9 @@ import { StrictDict, keyStore } from '../../../../../utils'; import { actions, selectors } from '../../../../../data/redux'; import { - updateDuration, - durationValue, - onDurationChange, - onDurationKeyDown, -} from './duration'; - -import { - handleIndexEvent, handleIndexTransformEvent, onValue, onChecked, - onEvent, } from './handlers'; import * as module from './hooks'; @@ -88,21 +78,6 @@ export const updateFormField = ({ dispatch, key }) => useCallback( [], ); -/** - * currentValue({ key, formValue }) - * Returns the current display value based on the form value. - * If duration, uses durationValue to transform the formValue - * @param {string} key - redux video state key - * @param {any} formValue - current value in the redux - * @return {any} - to-local translation of formValue - */ -export const currentValue = ({ key, formValue }) => { - if (key === selectorKeys.duration) { - return durationValue(formValue); - } - return formValue; -}; - /** * valueHooks({ dispatch, key }) * returns local and redux state associated with the given data key, as well as methods @@ -118,16 +93,11 @@ export const currentValue = ({ key, formValue }) => { */ export const valueHooks = ({ dispatch, key }) => { const formValue = useSelector(selectors.video[key]); - const initialValue = useMemo(() => module.currentValue({ key, formValue }), []); - const [local, setLocal] = module.state[key](initialValue); + const [local, setLocal] = module.state[key](formValue); const setFormValue = module.updateFormField({ dispatch, key }); useEffect(() => { - if (key === selectorKeys.duration) { - setLocal(durationValue(formValue)); - } else { - setLocal(formValue); - } + setLocal(formValue); }, [formValue]); const setAll = useCallback( @@ -264,62 +234,6 @@ export const objectWidget = ({ dispatch, key }) => { }; }; -/** - * durationWidget({ dispatch, key }) - * Returns the value-tied hooks for the video duration widget. - * Includes onChange, and onBlur. blur changes local and redux state, on-change affects - * only local state. - * The creators from this widget will require an index to provide the final event-handler. - * @param {func} dispatch - redux dispatch method - * @param {string} key - redux video shape key - * @return {object} - state hooks - * formValue - value state in redux - * setFormValue - sets form field in redux - * local - value state in hook - * setLocal - sets form field in hook - * setAll - sets form field in hook AND redux - * onChange(index) - handle input change by updating local state - * onBlur(index) - handle input blur by updating local and redux states - * onClear(index) - handle clear event by setting value to empty string - */ -export const durationWidget = ({ dispatch }) => { - const widget = module.valueHooks({ dispatch, key: selectorKeys.duration }); - const { - formValue, - local, - setFormField, - setLocal, - } = widget; - return { - ...widget, - onBlur: useCallback( - handleIndexEvent({ - handler: onValue, - transform: updateDuration(widget), - }), - [formValue, local, setFormField], - ), - onChange: useCallback( - handleIndexTransformEvent({ - handler: onValue, - setter: setLocal, - transform: onDurationChange, - local, - }), - [local], - ), - onKeyDown: useCallback( - handleIndexTransformEvent({ - handler: onEvent, - setter: setLocal, - transform: onDurationKeyDown, - local, - }), - [local], - ), - }; -}; - /** * widgetValues({ fields, dispatch }) * widget value populator, that takes a fields mapping (dataKey: widgetFn) and dispatch @@ -338,7 +252,6 @@ export const widgetValues = ({ fields, dispatch }) => Object.keys(fields).reduce export default { arrayWidget, - durationWidget, genericWidget, objectWidget, selectorKeys, diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/hooks.test.js b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/hooks.test.js index 571ff57cd..9f6d8a4e5 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/hooks.test.js +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/hooks.test.js @@ -5,7 +5,6 @@ import { keyStore } from '../../../../../utils'; import { actions, selectors } from '../../../../../data/redux'; import { MockUseState } from '../../../../../../testUtils'; -import * as duration from './duration'; import * as handlers from './handlers'; import * as hooks from './hooks'; @@ -17,13 +16,6 @@ jest.mock('react', () => ({ useMemo: jest.fn((cb, prereqs) => ({ useMemo: { cb, prereqs } })), })); -jest.mock('./duration', () => ({ - onDurationChange: jest.fn(value => ({ onDurationChange: value })), - onDurationKeyDown: jest.fn(value => ({ onDurationKeyDown: value })), - updateDuration: jest.fn(value => ({ updateDuration: value })), - durationValue: jest.fn(value => ({ durationValue: value })), -})); - jest.mock('./handlers', () => ({ handleIndexEvent: jest.fn(args => ({ handleIndexEvent: args })), handleIndexTransformEvent: jest.fn(args => ({ handleIndexTransformEvent: args })), @@ -56,8 +48,6 @@ jest.mock('../../../../../data/redux', () => ({ })); const keys = { - duration: keyStore(duration), - handlers: keyStore(handlers), hooks: keyStore(hooks), selectors: hooks.selectorKeys, }; @@ -126,18 +116,6 @@ describe('Video Settings modal hooks', () => { })); }); }); - describe('currentValue', () => { - it('returns duration display of form value if is duration key', () => { - expect( - hooks.currentValue({ key: keys.selectors.duration, formValue: testValue }), - ).toEqual(duration.durationValue(testValue)); - }); - it('returns the raw formValue by default', () => { - expect( - hooks.currentValue({ key: testKey, formValue: testValue }), - ).toEqual(testValue); - }); - }); describe('valueHooks', () => { let formValue; beforeEach(() => { @@ -150,13 +128,6 @@ describe('Video Settings modal hooks', () => { expect(useEffect).toHaveBeenCalled(); expect(useEffect.mock.calls[0][1]).toEqual([formValue]); }); - test('calls setLocal with durationValue(formValue) if is duration', () => { - hooks.valueHooks({ dispatch, key: keys.selectors.duration }); - useEffect.mock.calls[0][0](); - expect(state.setState[keys.selectors.duration]).toHaveBeenCalledWith( - duration.durationValue(useSelector(selectors.video.duration)), - ); - }); test('calls setLocal with formValue by default', () => { hooks.valueHooks({ dispatch, key: testKey }); useEffect.mock.calls[0][0](); @@ -165,13 +136,10 @@ describe('Video Settings modal hooks', () => { }); }); describe('returned object', () => { - const mockCurrentValue = (args) => ({ currentValue: args }); const mockUpdateFormField = (args) => jest.fn( (val) => ({ updateFormField: { args, val } }), ); beforeEach(() => { - jest.spyOn(hooks, keys.hooks.currentValue) - .mockImplementationOnce(mockCurrentValue); jest.spyOn(hooks, keys.hooks.updateFormField) .mockImplementationOnce(mockUpdateFormField); out = hooks.valueHooks({ dispatch, key: testKey }); @@ -180,10 +148,9 @@ describe('Video Settings modal hooks', () => { expect(out.formValue).toEqual(useSelector(selectors.video[testKey])); }); describe('local and setLocal', () => { - test('keyed to state, initialized with memo of currentValue that never updates', () => { + test('keyed to state, initialized with formValue', () => { const { local, setLocal } = out; - expect(local.useMemo.cb()).toEqual(mockCurrentValue({ key: testKey, formValue })); - expect(local.useMemo.prereqs).toEqual([]); + expect(local).toEqual(formValue); setLocal(testValue); expect(state.setState[testKey]).toHaveBeenCalledWith(testValue); }); @@ -315,60 +282,6 @@ describe('Video Settings modal hooks', () => { })); }); }); - describe('durationWidget', () => { - beforeEach(() => { - out = hooks.durationWidget({ dispatch }); - }); - it('forwards widget values', () => { - expect(out.formValue).toEqual(widgetValues.formValue); - expect(out.local).toEqual(widgetValues.local); - }); - describe('onBlur', () => { - test('memoized callback based on formValue, local, and setFormValue from widget', () => { - expect(out.onBlur.useCallback.prereqs).toEqual( - [widgetValues.formValue, widgetValues.local, widgetValues.setFormField], - ); - }); - test('calls handleIndexEvent with updateDuration', () => { - expect(out.onBlur.useCallback.cb).toEqual( - handlers.handleIndexEvent({ - handler: handlers.onValue, - transform: duration.updateDuration(widgetValues), - }), - ); - }); - }); - describe('onChange', () => { - test('memoized callback based on local from widget', () => { - expect(out.onChange.useCallback.prereqs).toEqual([widgetValues.local]); - }); - test('calls handleIndexTransformEvent with setLocal', () => { - expect(out.onChange.useCallback.cb).toEqual( - handlers.handleIndexTransformEvent({ - handler: handlers.onValue, - setter: widgetValues.setLocal, - transform: duration.onDurationChange, - local: widgetValues.local, - }), - ); - }); - }); - describe('onKeyDown', () => { - test('memoized callback based on local from widget', () => { - expect(out.onKeyDown.useCallback.prereqs).toEqual([widgetValues.local]); - }); - test('calls handleIndexTransformEvent with setLocal', () => { - expect(out.onKeyDown.useCallback.cb).toEqual( - handlers.handleIndexTransformEvent({ - handler: handlers.onEvent, - setter: widgetValues.setLocal, - transform: duration.onDurationKeyDown, - local: widgetValues.local, - }), - ); - }); - }); - }); }); describe('widgetValues', () => { describe('returned object', () => { diff --git a/src/editors/data/redux/thunkActions/video.js b/src/editors/data/redux/thunkActions/video.js index 2a3e037e8..500ba856a 100644 --- a/src/editors/data/redux/thunkActions/video.js +++ b/src/editors/data/redux/thunkActions/video.js @@ -2,7 +2,7 @@ import { actions, selectors } from '..'; import { removeItemOnce } from '../../../utils'; import * as requests from './requests'; import * as module from './video'; -import { valueFromDuration } from '../../../containers/VideoEditor/components/VideoSettingsModal/components/duration'; +import { valueFromDuration } from '../../../containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks'; import { parseYoutubeId } from '../../services/cms/api'; export const loadVideoData = () => (dispatch, getState) => { diff --git a/src/editors/data/services/cms/api.js b/src/editors/data/services/cms/api.js index 66183e99a..d666cb6aa 100644 --- a/src/editors/data/services/cms/api.js +++ b/src/editors/data/services/cms/api.js @@ -3,7 +3,7 @@ import * as urls from './urls'; import { get, post, deleteObject } from './utils'; import * as module from './api'; import * as mockApi from './mockApi'; -import { durationFromValue } from '../../../containers/VideoEditor/components/VideoSettingsModal/components/duration'; +import { durationStringFromValue } from '../../../containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks'; export const apiMethods = { fetchBlockById: ({ blockId, studioEndpointUrl }) => get( @@ -176,8 +176,8 @@ export const apiMethods = { track: '', // TODO Downloadable Transcript URL. Backend expects a file name, for example: "something.srt" show_captions: content.showTranscriptByDefault, handout: content.handout, - start_time: durationFromValue(content.duration.startTime), - end_time: durationFromValue(content.duration.stopTime), + start_time: durationStringFromValue(content.duration.startTime), + end_time: durationStringFromValue(content.duration.stopTime), license: module.processLicense(content.licenseType, content.licenseDetails), }, };