Skip to content

Commit 751377a

Browse files
authored
Add support for creating headings inside timespans with children (#249)
* Add support for creating headings inside timespans with children * Code review: getItemsOfType() accepts a list of types to get items in order
1 parent 9e800dd commit 751377a

File tree

7 files changed

+112
-106
lines changed

7 files changed

+112
-106
lines changed

src/components/HeadingForm.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,15 @@ const HeadingForm = ({ cancelClick, onSubmit }) => {
4444
};
4545

4646
const getOptions = () => {
47-
const rootHeader = structuralMetadataUtils.getItemsOfType('root', smData);
48-
const divHeaders = structuralMetadataUtils.getItemsOfType('div', smData);
49-
const allHeaders = rootHeader.concat(divHeaders);
47+
/**
48+
* Only get type='span' (timespans) items with children as possible headings.
49+
* This helps to keep the options list smaller, but allows to add heading
50+
* inside timespans. These headings then can be used as drop-zones for child
51+
* timespans inside them.
52+
*/
53+
const allHeaders = structuralMetadataUtils
54+
.getItemsOfType(['root', 'div', 'span'], smData)
55+
.filter(h => h.type !== 'span' || (h.items && h.items.length > 0));
5056
const options = allHeaders.map((header) => (
5157
<option value={header.id} key={header.id}>
5258
{header.label}

src/components/TimespanForm.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const TimespanForm = ({
4545

4646
const allSpans = useMemo(() => {
4747
if (smData?.length > 0) {
48-
return structuralMetadataUtils.getItemsOfType('span', smData);
48+
return structuralMetadataUtils.getItemsOfType(['span'], smData);
4949
}
5050
}, [smData]);
5151

src/components/TimespanInlineForm.js

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,7 @@ function TimespanInlineForm({ cancelFn, item, isInitializing, isTyping, saveFn,
8989
);
9090

9191
// Save a reference to all the spans for future calculations
92-
allSpansRef.current = structuralMetadataUtils.getItemsOfType(
93-
'span',
94-
tempSmDataRef.current
95-
);
92+
allSpansRef.current = structuralMetadataUtils.getItemsOfType(['span'], tempSmDataRef.current);
9693

9794
// Get segment from current peaks instance
9895
const currentSegment = peaksInstance.peaks.segments.getSegment(item.id);
@@ -136,13 +133,10 @@ function TimespanInlineForm({ cancelFn, item, isInitializing, isTyping, saveFn,
136133
*/
137134
const handleInvalidTimespan = () => {
138135
const itemIndex = structuralMetadataUtils
139-
.getItemsOfType('span', smData)
136+
.getItemsOfType(['span'], smData)
140137
.findIndex((i) => i.id === item.id);
141138

142-
const allSpans = structuralMetadataUtils.getItemsOfType(
143-
'span',
144-
tempSmDataRef.current
145-
);
139+
const allSpans = structuralMetadataUtils.getItemsOfType(['span'], tempSmDataRef.current);
146140

147141
const wrapperSpans = { prevSpan: null, nextSpan: null };
148142
wrapperSpans.prevSpan = allSpans[itemIndex - 1] || null;

src/components/__tests__/HeadingForm.test.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import { fireEvent } from '@testing-library/react';
33
import HeadingForm from '../HeadingForm';
4-
import { renderWithRedux, testSmData } from '../../services/testing-helpers';
4+
import { nestedTestSmData, renderWithRedux, testSmData } from '../../services/testing-helpers';
55

66
const initialState = {
77
structuralMetadata: {
@@ -54,6 +54,7 @@ describe('HeadingForm component', () => {
5454
expect(formControl.className.includes('is-valid')).toBeFalsy();
5555
expect(formControl.className.includes('is-invalid')).toBeTruthy();
5656
});
57+
5758
test('form with enabling save button', () => {
5859
const { getByLabelText, getByTestId, debug } = renderWithRedux(
5960
<HeadingForm />,
@@ -86,6 +87,7 @@ describe('HeadingForm component', () => {
8687
expect(submitButton).toBeDisabled();
8788
});
8889
});
90+
8991
describe('submits the form', () => {
9092
let utils;
9193
const onSubmitMock = jest.fn();
@@ -117,4 +119,19 @@ describe('HeadingForm component', () => {
117119
expect(utils.getByLabelText(/title/i).value).toBe('');
118120
});
119121
});
122+
123+
test('adds parent timespan to \'Child Of\' dropdown for nested structure', () => {
124+
const { container } = renderWithRedux(<HeadingForm />, {
125+
initialState: {
126+
structuralMetadata: {
127+
smData: nestedTestSmData,
128+
},
129+
},
130+
});
131+
const el = container.querySelector('#headingChildOf');
132+
expect(el.children.length).toBe(7);
133+
expect(el.children[1].value).toBe('123a-456b-789c-0d');
134+
// Adds parent timespan to the options list
135+
expect(el.children[5].value).toBe('123a-456b-789c-6d');
136+
});
120137
});

src/services/StructuralMetadataUtils.js

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export default class StructuralMetadataUtils {
148148

149149
// Set the scope of the drop-zones based on the dragSource
150150
let scopedItems = clonedItems;
151-
let allSpans = this.getItemsOfType('span', clonedItems);
151+
let allSpans = this.getItemsOfType(['span'], clonedItems);
152152
let parent = this.getParentItem(dragSource, clonedItems);
153153
let siblings = parent ? parent.items : [];
154154
let spanIndex = siblings.map((sibling) => sibling.id).indexOf(dragSource.id);
@@ -158,7 +158,7 @@ export default class StructuralMetadataUtils {
158158
scopedItems = [parent];
159159
// For nested timespans, scope drop target calculation within parent timespan only
160160
parent = this.getParentItem(dragSource, clonedItems);
161-
allSpans = this.getItemsOfType('span', [parent]);
161+
allSpans = this.getItemsOfType(['span'], [parent]);
162162
siblings = parent ? parent.items : [];
163163
const siblingHeadings = siblings.filter(sib => sib.type === 'div');
164164

@@ -181,7 +181,7 @@ export default class StructuralMetadataUtils {
181181
let grandParentDiv = this.getParentItem(parent, clonedItems);
182182
// A first/last child of siblings, or an only child
183183
if (grandParentDiv !== null) {
184-
let siblingTimespans = this.getItemsOfType('span', siblings);
184+
let siblingTimespans = this.getItemsOfType(['span'], siblings);
185185
let timespanIndex = siblingTimespans.map((sibling) => sibling.id).indexOf(dragSource.id);
186186

187187
let parentIndex = grandParentDiv.items.map((item) => item.id).indexOf(parent.id);
@@ -374,18 +374,23 @@ export default class StructuralMetadataUtils {
374374

375375
/**
376376
* Get all items in data structure of type 'div' or 'span'
377+
* @param {Array} itemTypes types of items to pick
377378
* @param {Array} json
378379
* @returns {Array} - all stripped down objects of type in the entire structured metadata collection
379380
*/
380-
getItemsOfType(type = 'div', items = []) {
381+
getItemsOfType(itemTypes = [], items = []) {
382+
if (itemTypes.length === 0) {
383+
return [];
384+
}
381385
let options = [];
382386

383387
// Recursive function to search the whole data structure
384388
let getItems = (items) => {
385389
for (let item of items) {
386-
if (item.type === type) {
390+
if (itemTypes.includes(item.type)) {
387391
let currentObj = { ...item };
388-
delete currentObj.items;
392+
// Keep items array to identify parent timespans in HeadingForm
393+
if (item.type != 'span') { delete currentObj.items; }
389394
options.push(currentObj);
390395
}
391396
if (item.items) {
@@ -455,11 +460,7 @@ export default class StructuralMetadataUtils {
455460
}
456461

457462
const { before, after } = wrapperSpans;
458-
const allPossibleParents = this.getItemsOfType('root', allItems).concat(
459-
this.getItemsOfType('div', allItems)
460-
).concat(
461-
this.getItemsOfType('span', allItems)
462-
);
463+
const allPossibleParents = this.getItemsOfType(['root', 'div', 'span'], allItems);
463464

464465
// Explore possible headings traversing outwards from a suggested heading
465466
let exploreOutwards = (heading) => {
@@ -505,9 +506,7 @@ export default class StructuralMetadataUtils {
505506
} else {
506507
divsBefore = wrapperParent.items.filter((item, i) => i < spanIndex);
507508
}
508-
const allParents = this.getItemsOfType('div', divsAfter.concat(divsBefore)).concat(
509-
this.getItemsOfType('span', divsAfter.concat(divsBefore))
510-
);
509+
const allParents = this.getItemsOfType(['div', 'span'], [...divsAfter, ...divsBefore]);
511510
return allParents;
512511
};
513512

@@ -680,7 +679,7 @@ export default class StructuralMetadataUtils {
680679
};
681680

682681
if (parentItem) {
683-
const allSpans = this.getItemsOfType('span', allItems);
682+
const allSpans = this.getItemsOfType(['span'], allItems);
684683
const { before, after } = this.findWrapperSpans(spanObj, allSpans);
685684
if (before) {
686685
let siblingBefore = getParentOfSpan(before);

src/services/__test__/StructuralMetadataUtils.test.js

Lines changed: 65 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ describe('StructuralMetadataUtils class', () => {
206206
describe('findWrapperSpans(), ', () => {
207207
let allSpans = [];
208208
beforeEach(() => {
209-
allSpans = smu.getItemsOfType('span', testData);
209+
allSpans = smu.getItemsOfType(['span'], testData);
210210
});
211211
test('for first timespan', () => {
212212
const obj = {
@@ -337,89 +337,79 @@ describe('StructuralMetadataUtils class', () => {
337337
});
338338

339339
describe('getItemsOfType()', () => {
340-
test('type === div', () => {
341-
const allDivs = [
342-
{
343-
type: 'div',
344-
label: 'Title',
345-
id: '123a-456b-789c-0d',
346-
},
347-
{
348-
type: 'div',
349-
label: 'First segment',
350-
id: '123a-456b-789c-1d',
351-
},
352-
{
353-
type: 'div',
354-
label: 'Sub-Segment 1.1',
355-
id: '123a-456b-789c-2d',
356-
},
357-
{
358-
type: 'div',
359-
label: 'Second segment',
360-
id: '123a-456b-789c-5d',
361-
},
362-
{
363-
type: 'div',
364-
label: 'Sub-Segment 2.1',
365-
id: '123a-456b-789c-6d',
366-
},
367-
{
368-
type: 'div',
369-
label: 'Sub-Segment 2.1.1',
370-
id: '123a-456b-789c-7d',
371-
},
372-
];
373-
const value = smu.getItemsOfType('div', testData);
340+
const allDivs = [
341+
{ type: 'div', label: 'Title', id: '123a-456b-789c-0d' },
342+
{ type: 'div', label: 'First segment', id: '123a-456b-789c-1d' },
343+
{ type: 'div', label: 'Sub-Segment 1.1', id: '123a-456b-789c-2d' },
344+
{ type: 'div', label: 'Second segment', id: '123a-456b-789c-5d' },
345+
{ type: 'div', label: 'Sub-Segment 2.1', id: '123a-456b-789c-6d' },
346+
{ type: 'div', label: 'Sub-Segment 2.1.1', id: '123a-456b-789c-7d' },
347+
];
348+
const allSpans = [
349+
{
350+
type: 'span', label: 'Segment 1.1', id: '123a-456b-789c-3d',
351+
begin: '00:00:03.321', end: '00:00:10.321',
352+
valid: true,
353+
timeRange: { start: 3.321, end: 10.321 }
354+
},
355+
{
356+
type: 'span', label: 'Segment 1.2', id: '123a-456b-789c-4d',
357+
begin: '00:00:11.231', end: '00:08:00.001',
358+
valid: true,
359+
timeRange: { start: 11.231, end: 480.001 }
360+
},
361+
{
362+
type: 'span', label: 'Segment 2.1', id: '123a-456b-789c-8d',
363+
begin: '00:09:03.241', end: '00:15:00.001',
364+
valid: true,
365+
timeRange: { start: 543.241, end: 900.001 }
366+
},
367+
];
368+
test('itemTypes = []', () => {
369+
const value = smu.getItemsOfType([], testData);
370+
expect(value).toEqual([]);
371+
});
372+
373+
test('itemTypes = [\'root\']', () => {
374+
const value = smu.getItemsOfType(['root'], testData);
375+
expect(value).toHaveLength(1);
376+
expect(value).toContainEqual({
377+
type: 'root', label: 'Ima Title', id: '123a-456b-789c-0d'
378+
});
379+
});
380+
381+
test('itemTypes = [\'div\']', () => {
382+
const value = smu.getItemsOfType(['div'], testData);
374383
expect(value).toHaveLength(allDivs.length);
375384
expect(value).toContainEqual({
376-
type: 'div',
377-
label: 'Sub-Segment 2.1',
378-
id: '123a-456b-789c-6d',
385+
type: 'div', label: 'Sub-Segment 2.1', id: '123a-456b-789c-6d'
379386
});
380387
});
381-
test('type === span', () => {
382-
const allSpans = [
383-
{
384-
type: 'span',
385-
label: 'Segment 1.1',
386-
id: '123a-456b-789c-3d',
387-
begin: '00:00:03.321',
388-
end: '00:00:10.321',
389-
valid: true,
390-
timeRange: { start: 3.321, end: 10.321 }
391-
},
392-
{
393-
type: 'span',
394-
label: 'Segment 1.2',
395-
id: '123a-456b-789c-4d',
396-
begin: '00:00:11.231',
397-
end: '00:08:00.001',
398-
valid: true,
399-
timeRange: { start: 11.231, end: 480.001 }
400-
},
401-
{
402-
type: 'span',
403-
label: 'Segment 2.1',
404-
id: '123a-456b-789c-8d',
405-
begin: '00:09:03.241',
406-
end: '00:15:00.001',
407-
valid: true,
408-
timeRange: { start: 543.241, end: 900.001 }
409-
},
410-
];
411-
const value = smu.getItemsOfType('span', testData);
388+
389+
test('itemTypes = [\'span\']', () => {
390+
const value = smu.getItemsOfType(['span'], testData);
412391
expect(value).toHaveLength(allSpans.length);
413392
expect(value).toContainEqual({
414-
type: 'span',
415-
label: 'Segment 2.1',
416-
id: '123a-456b-789c-8d',
417-
begin: '00:09:03.241',
418-
end: '00:15:00.001',
393+
type: 'span', label: 'Segment 2.1', id: '123a-456b-789c-8d',
394+
begin: '00:09:03.241', end: '00:15:00.001',
419395
valid: true,
420396
timeRange: { start: 543.241, end: 900.001 }
421397
});
422398
});
399+
400+
test('itemTypes = [\'div\', \'span\']', () => {
401+
const value = smu.getItemsOfType(['span', 'div'], testData);
402+
expect(value).toHaveLength(allSpans.length + allDivs.length);
403+
expect(value).toContainEqual({
404+
type: 'span', label: 'Segment 2.1', id: '123a-456b-789c-8d',
405+
begin: '00:09:03.241', end: '00:15:00.001',
406+
valid: true,
407+
timeRange: { start: 543.241, end: 900.001 }
408+
});
409+
expect(value).toContainEqual({
410+
type: 'div', label: 'Sub-Segment 2.1', id: '123a-456b-789c-6d'
411+
});
412+
});
423413
});
424414

425415
describe('getParentItem()', () => {
@@ -1103,7 +1093,7 @@ describe('StructuralMetadataUtils class', () => {
11031093
smu.determineDropTargets(dragSource, nestedTestSmData);
11041094

11051095
// getItemsOfType() was called with parent scope for the nested timespan
1106-
expect(getItemsOfTypeSpy).toHaveBeenCalledWith('span', [parentTimespan]);
1096+
expect(getItemsOfTypeSpy).toHaveBeenCalledWith(['span'], [parentTimespan]);
11071097
// Clear mock
11081098
getItemsOfTypeSpy.mockRestore();
11091099
});

src/services/sme-hooks.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const useFindNeighborSegments = ({ segment }) => {
2929
const nextSiblingRef = useRef(null);
3030

3131
const allSpans = useMemo(() => {
32-
return structuralMetadataUtils.getItemsOfType('span', smData);
32+
return structuralMetadataUtils.getItemsOfType(['span'], smData);
3333
}, [smData]);
3434

3535
useEffect(() => {
@@ -116,7 +116,7 @@ export const useFindNeighborTimespans = ({ item }) => {
116116
prevSiblingRef.current = currentIndex > 0 ? siblings[currentIndex - 1] : null;
117117
nextSiblingRef.current = currentIndex < siblings.length - 1 ? siblings[currentIndex + 1] : null;
118118
} else if (item) {
119-
const siblings = structuralMetadataUtils.getItemsOfType('span', smData);
119+
const siblings = structuralMetadataUtils.getItemsOfType(['span'], smData);
120120
const currentIndex = siblings.findIndex(sibling => sibling.id === item.id);
121121

122122
prevSiblingRef.current = currentIndex > 0 ? siblings[currentIndex - 1] : null;

0 commit comments

Comments
 (0)