Skip to content

Commit 3ed2faf

Browse files
authored
Merge pull request #250 from avalonmediasystem/select-heading-inside-timespan
Show heading inside timespan as a possible parent for a new child timespan
2 parents 7bbb06f + fdeea6e commit 3ed2faf

File tree

5 files changed

+147
-134
lines changed

5 files changed

+147
-134
lines changed

src/components/TimespanForm.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,9 @@ const TimespanForm = ({
6666
// Build wrapperSpans from sibling timespans
6767
let wrapperSpans = { before: prevSiblingRef.current, after: nextSiblingRef.current };
6868

69-
// Possible parent timespan that can fully contain the new span
70-
let parentTimespan = parentTimespanRef.current ? [parentTimespanRef.current] : [];
71-
7269
// Get all valid div headings and potential parent timespans
73-
let validHeadings = structuralMetadataUtils.getValidParents(newSpan, wrapperSpans, smData, parentTimespan);
70+
let validHeadings = structuralMetadataUtils
71+
.getValidParents(newSpan, wrapperSpans, smData, parentTimespanRef.current);
7472

7573
// Update state with valid headings
7674
setValidHeadings(validHeadings);

src/components/__tests__/ButtonSection.test.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,15 @@ describe('ButtonSection component', () => {
271271
current: {
272272
type: 'span', label: 'Segment 1.2', id: '123a-456b-789c-4d',
273273
begin: '00:00:11.231', end: '00:08:00.001', valid: true,
274-
timeRange: { start: 11.231, end: 480.001 }
274+
timeRange: { start: 11.231, end: 480.001 },
275+
items: [
276+
{ type: 'div', label: 'Sub-Segment 2.1.1', id: '123a-456b-789c-7d' },
277+
{
278+
type: 'span', label: 'Segment 2.1', id: '123a-456b-789c-8d',
279+
begin: '00:09:03.241', end: '00:15:00.001',
280+
valid: true, timeRange: { start: 543.241, end: 900.001 }
281+
}
282+
]
275283
},
276284
}
277285
}));
@@ -323,7 +331,15 @@ describe('ButtonSection component', () => {
323331
current: {
324332
type: 'span', label: 'Segment 1.2', id: '123a-456b-789c-4d',
325333
begin: '00:00:11.231', end: '00:08:00.001', valid: true,
326-
timeRange: { start: 11.231, end: 480.001 }
334+
timeRange: { start: 11.231, end: 480.001 },
335+
items: [
336+
{ type: 'div', label: 'Sub-Segment 2.1.1', id: '123a-456b-789c-7d' },
337+
{
338+
type: 'span', label: 'Segment 2.1', id: '123a-456b-789c-8d',
339+
begin: '00:09:03.241', end: '00:15:00.001',
340+
valid: true, timeRange: { start: 543.241, end: 900.001 }
341+
}
342+
]
327343
},
328344
}
329345
}));

src/components/__tests__/TimespanForm.test.js

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ describe('Timespan component', () => {
206206
current: {
207207
type: 'span', label: 'Segment 1.1', id: '123a-456b-789c-3d',
208208
begin: '00:00:03.321', end: '00:00:10.321', valid: true,
209-
timeRange: { start: 3.321, end: 10.321 }
209+
timeRange: { start: 3.321, end: 10.321 }, items: []
210210
}
211211
}
212212
}));
@@ -412,7 +412,7 @@ describe('Timespan component', () => {
412412
current: {
413413
type: 'span', label: 'Segment 1.1', id: '123a-456b-789c-3d',
414414
begin: '00:00:03.321', end: '00:00:10.321', valid: true,
415-
timeRange: { start: 3.321, end: 10.321 }
415+
timeRange: { start: 3.321, end: 10.321 }, items: []
416416
}
417417
}
418418
}));
@@ -429,25 +429,13 @@ describe('Timespan component', () => {
429429
test('when begin time is within an existing timespan', () => {
430430
// Update the neighbor timespan relationships
431431
jest.spyOn(hooks, 'useFindNeighborSegments').mockImplementation(() => ({
432-
prevSiblingRef: {
433-
current: {
434-
type: 'span', label: 'Segment 2.1.1', id: '123a-456b-789c-7d',
435-
begin: '00:09:10.241', end: '00:10:00.321', valid: true,
436-
timeRange: { start: 550.241, end: 660.321 }
437-
}
438-
},
439-
nextSiblingRef: {
440-
current: {
441-
type: 'span', label: 'Segment 2.1.2', id: '123a-456b-789c-8d',
442-
begin: '00:12:00.231', end: '00:13:00.001', valid: true,
443-
timeRange: { start: 720.231, end: 790.001 }
444-
}
445-
},
432+
prevSiblingRef: { current: { type: 'span', label: 'Segment 2.1.1', id: '123a-456b-789c-7d' } },
433+
nextSiblingRef: { current: { type: 'span', label: 'Segment 2.1.2', id: '123a-456b-789c-8d' } },
446434
parentTimespanRef: {
447435
current: {
448436
type: 'span', label: 'Segment 2.1', id: '123a-456b-789c-6d',
449-
begin: '00:09:00.241', end: '00:15:00.001', valid: true,
450-
timeRange: { start: 540.241, end: 900.001 },
437+
items: [{ type: 'span', label: 'Segment 2.1.1', id: '123a-456b-789c-7d' },
438+
{ type: 'span', label: 'Segment 2.1.2', id: '123a-456b-789c-8d' }]
451439
}
452440
}
453441
}));
@@ -521,7 +509,7 @@ describe('Timespan component', () => {
521509
});
522510
});
523511

524-
test('with parent element selection', () => {
512+
test('changes form state with parent element selection', () => {
525513
jest.spyOn(hooks, 'useFindNeighborSegments').mockImplementation(() => ({
526514
prevSiblingRef: { current: null },
527515
nextSiblingRef: {

src/services/StructuralMetadataUtils.js

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -439,24 +439,45 @@ export default class StructuralMetadataUtils {
439439
* find their parent 'divs'.
440440
* Timespans can have children, so to find possible parent timespans; combine the wrapper timespans with
441441
* the parent 'divs' of the wrapper timespans.
442-
* @param {Object} newSpan - New timespan created with values supplied by the user
443-
* @param {Object} wrapperSpans Object representing before and after spans of newSpan (if they exist)
444-
* @param {Array} allItems - All structural metadata items in tree
445-
* @param {Array} parentTimespan - Closest possible parent timespan that can contain the new timespan
442+
* @param {Object} newSpan - new timespan created with values supplied by the user
443+
* @param {Object} wrapperSpans object representing before and after spans of newSpan (if they exist)
444+
* @param {Array} allItems - all structural metadata items in tree
445+
* @param {Object} parentTimespan - closest possible parent timespan that can contain the new timespan
446446
* @return {Array} - of valid <div> and <span> objects in structural metadata tree
447447
*/
448-
getValidParents(newSpan, wrapperSpans, allItems, parentTimespan = []) {
448+
getValidParents(newSpan, wrapperSpans, allItems, parentTimespan = null) {
449449
let possibleValidParents = [];
450450
let sortedParents = [];
451451
let uniqueParents = [];
452452
// New timespan falls between timespans in the same parent
453453
let stuckInMiddle = false;
454454
const { toMs } = this;
455455

456-
// If there is a possible parent timespan, then it's the only choice for a parent for the
457-
// new timespan, since a timespan cannot span across multiple parent timespans.
458-
if (parentTimespan?.length > 0) {
459-
return parentTimespan;
456+
/**
457+
* If there is a parent timespan for the new timespan, then the choices for a possible
458+
* parent for the new timespan is limited to the parent timespan and any of its children
459+
* headings (if exists). Reasoning: a timespan cannot span across multiple parent timespans.
460+
*/
461+
if (parentTimespan) {
462+
const { before, after } = wrapperSpans;
463+
const prevSiblingIndex = parentTimespan.items.findIndex((c) => c.id === before?.id);
464+
const nextSiblingIndex = parentTimespan.items.findIndex((c) => c.id === after?.id);
465+
466+
let siblingHeadings = parentTimespan.items.filter((sib, index) => {
467+
if (sib.type !== 'div') return false;
468+
// No siblings
469+
if (prevSiblingIndex < 0 && nextSiblingIndex < 0) { return true; }
470+
// New timespan is at the start of the sibling list
471+
if (prevSiblingIndex < 0) { return index + 1 === nextSiblingIndex; }
472+
// New timespan is at the end of the sibling list
473+
if (nextSiblingIndex < 0) { return index === prevSiblingIndex + 1; }
474+
// New timespan is sandwiched between other siblings
475+
if (prevSiblingIndex >= 0 && nextSiblingIndex >= 0) {
476+
return index === prevSiblingIndex + 1 && index + 1 === nextSiblingIndex;
477+
}
478+
return false;
479+
});
480+
return [parentTimespan, ...siblingHeadings];
460481
}
461482

462483
const { before, after } = wrapperSpans;

src/services/__test__/StructuralMetadataUtils.test.js

Lines changed: 90 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -543,131 +543,121 @@ describe('StructuralMetadataUtils class', () => {
543543
id: '123a-456b-789c-1d',
544544
});
545545
});
546-
test('when wrapped by existing timespans', () => {
546+
547+
test('when new timespan is wrapped by existing timespans', () => {
547548
const newSpan = { begin: '00:08:00.001', end: '00:09:00.001' };
548549
const wrapperSpans = {
549-
before: {
550-
type: 'span',
551-
label: 'Segment 1.2',
552-
id: '123a-456b-789c-4d',
553-
begin: '00:00:11.231',
554-
end: '00:08:00.001',
555-
},
556-
after: {
557-
type: 'span',
558-
label: 'Segment 2.1',
559-
id: '123a-456b-789c-8d',
560-
begin: '00:09:03.241',
561-
end: '00:15:00.001',
562-
},
550+
before: { type: 'span', label: 'Segment 1.2', id: '123a-456b-789c-4d' },
551+
after: { type: 'span', label: 'Segment 2.1', id: '123a-456b-789c-8d' },
563552
};
564553
const expected = [
565-
{
566-
type: 'div',
567-
label: 'First segment',
568-
id: '123a-456b-789c-1d',
569-
},
570-
{
571-
type: 'div',
572-
label: 'Second segment',
573-
id: '123a-456b-789c-5d',
574-
},
575-
{
576-
type: 'div',
577-
label: 'Sub-Segment 2.1',
578-
id: '123a-456b-789c-6d',
579-
},
580-
{
581-
type: 'div',
582-
label: 'Sub-Segment 2.1.1',
583-
id: '123a-456b-789c-7d',
584-
},
554+
{ type: 'div', label: 'First segment', id: '123a-456b-789c-1d' },
555+
{ type: 'div', label: 'Second segment', id: '123a-456b-789c-5d' },
556+
{ type: 'div', label: 'Sub-Segment 2.1', id: '123a-456b-789c-6d' },
557+
{ type: 'div', label: 'Sub-Segment 2.1.1', id: '123a-456b-789c-7d' },
585558
];
586559
const value = smu.getValidParents(newSpan, wrapperSpans, testData);
587560
expect(value).toHaveLength(expected.length);
588-
expect(value).toContainEqual({
589-
type: 'div',
590-
label: 'Sub-Segment 2.1',
591-
id: '123a-456b-789c-6d',
592-
});
561+
expect(value).toContainEqual({ type: 'div', label: 'Sub-Segment 2.1', id: '123a-456b-789c-6d' });
593562
});
594-
test('when there are no timespans after', () => {
563+
564+
test('when there are no timespans after the new timespan', () => {
595565
const newSpan = { begin: '00:15:00.001', end: '00:16:00.001' };
596566
const wrapperSpans = {
597-
before: {
598-
type: 'span',
599-
label: 'Segment 2.1',
600-
id: '123a-456b-789c-8d',
601-
begin: '00:09:03.241',
602-
end: '00:15:00.001',
603-
},
567+
before: { type: 'span', label: 'Segment 2.1', id: '123a-456b-789c-8d' },
604568
after: null,
605569
};
606570
const expected = [
607-
{
608-
type: 'div',
609-
label: 'Title',
610-
id: '123a-456b-789c-0d',
611-
},
612-
{
613-
type: 'div',
614-
label: 'Second segment',
615-
id: '123a-456b-789c-5d',
616-
},
617-
{
618-
type: 'div',
619-
label: 'Sub-Segment 2.1',
620-
id: '123a-456b-789c-6d',
621-
},
622-
{
623-
type: 'div',
624-
label: 'A ',
625-
id: '123a-456b-789c-9d',
626-
},
571+
{ type: 'div', label: 'Title', id: '123a-456b-789c-0d' },
572+
{ type: 'div', label: 'Second segment', id: '123a-456b-789c-5d' },
573+
{ type: 'div', label: 'Sub-Segment 2.1', id: '123a-456b-789c-6d' },
574+
{ type: 'div', label: 'A ', id: '123a-456b-789c-9d' },
627575
];
628576
const value = smu.getValidParents(newSpan, wrapperSpans, testData);
629577
expect(value).toHaveLength(expected.length);
630-
expect(value).toContainEqual({
631-
type: 'div',
632-
label: 'Sub-Segment 2.1',
633-
id: '123a-456b-789c-6d',
634-
});
578+
expect(value).toContainEqual({ type: 'div', label: 'Sub-Segment 2.1', id: '123a-456b-789c-6d' });
635579
});
636-
test('when there are no timespans before', () => {
580+
581+
test('when there are no timespans before the new timespan', () => {
637582
const newSpan = { begin: '00:00:00.000', end: '00:00:03.321' };
638583
const wrapperSpans = {
639584
before: null,
640-
after: {
641-
type: 'span',
642-
label: 'Act 1',
643-
id: '123a-456b-789c-3d',
644-
begin: '00:00:03.321',
645-
end: '00:00:10.321',
646-
},
585+
after: { type: 'span', label: 'Act 1', id: '123a-456b-789c-3d' },
647586
};
648587
const expected = [
649-
{
650-
type: 'div',
651-
label: 'Title',
652-
id: '123a-456b-789c-0d',
653-
},
654-
{
655-
type: 'div',
656-
label: 'First segment',
657-
id: '123a-456b-789c-1d',
658-
},
659-
{
660-
type: 'div',
661-
label: 'Sub-Segment 1.1',
662-
id: '123a-456b-789c-2d',
663-
},
588+
{ type: 'div', label: 'Title', id: '123a-456b-789c-0d' },
589+
{ type: 'div', label: 'First segment', id: '123a-456b-789c-1d' },
590+
{ type: 'div', label: 'Sub-Segment 1.1', id: '123a-456b-789c-2d' },
664591
];
665592
const value = smu.getValidParents(newSpan, wrapperSpans, testData);
666593
expect(value).toHaveLength(expected.length);
667-
expect(value).toContainEqual({
668-
type: 'div',
669-
label: 'Sub-Segment 1.1',
670-
id: '123a-456b-789c-2d',
594+
expect(value).toContainEqual({ type: 'div', label: 'Sub-Segment 1.1', id: '123a-456b-789c-2d' });
595+
});
596+
597+
describe('when new timespan has a parent timespan', () => {
598+
test('without sibling headings', () => {
599+
const parentTimespan = nestedTestSmData[0].items[1].items[0].items[0];
600+
const newSpan = { begin: '00:10:00.321', end: '00:11:00.321' };
601+
const wrapperSpans = {
602+
before: { type: 'span', label: 'Segment 2.1.1', id: '123a-456b-789c-7d' },
603+
after: { type: 'span', label: 'Segment 2.1.2', id: '123a-456b-789c-8d' },
604+
};
605+
606+
const value = smu.getValidParents(newSpan, wrapperSpans, nestedTestSmData, parentTimespan);
607+
expect(value).toHaveLength(1);
608+
expect(value.map(h => h.id)).toContain('123a-456b-789c-6d');
609+
});
610+
611+
describe('with a sibling heading', () => {
612+
// Get a copy of the parent timeppan to modify as needed
613+
const parentTimespan = cloneDeep(nestedTestSmData[0].items[1].items[0].items[0]);
614+
beforeEach(() => {
615+
parentTimespan.items.splice(0, 0, {
616+
type: 'div', label: 'New Sub-segment Title Before', id: 'new-title-before',
617+
});
618+
});
619+
620+
test('next to the new timespan', () => {
621+
const newSpan = { begin: '00:09:00.241', end: '00:09:10.241' };
622+
const wrapperSpans = {
623+
before: null,
624+
after: { type: 'span', label: 'Segment 2.1.1', id: '123a-456b-789c-7d' }
625+
};
626+
627+
const value = smu.getValidParents(newSpan, wrapperSpans, nestedTestSmData, parentTimespan);
628+
expect(value).toHaveLength(2);
629+
// Have both parent and sibling heading as valid parents
630+
expect(value.map(h => h.id)).toContain('123a-456b-789c-6d');
631+
expect(value.map(h => h.id)).toContain('new-title-before');
632+
});
633+
634+
test('before previous sibling timespan (out of order)', () => {
635+
const newSpan = { begin: '00:10:00.321', end: '00:11:00.321' };
636+
const wrapperSpans = {
637+
before: { type: 'span', label: 'Segment 2.1.1', id: '123a-456b-789c-7d' },
638+
after: { type: 'span', label: 'Segment 2.1.2', id: '123a-456b-789c-8d' },
639+
};
640+
641+
const value = smu.getValidParents(newSpan, wrapperSpans, nestedTestSmData, parentTimespan);
642+
// Only have the parent timespan as a valid parent
643+
expect(value).toHaveLength(1);
644+
expect(value.map(h => h.id)).toContain('123a-456b-789c-6d');
645+
});
646+
647+
test('after next sibling timespan (out of order)', () => {
648+
const newSpan = { begin: '00:10:00.321', end: '00:11:00.321' };
649+
const wrapperSpans = {
650+
before: { type: 'span', label: 'Segment 2.1.1', id: '123a-456b-789c-7d' },
651+
after: { type: 'span', label: 'Segment 2.1.2', id: '123a-456b-789c-8d' },
652+
};
653+
// Add a title item to the end of the children list
654+
parentTimespan.items.push({ type: 'div', label: 'New Sub-segment Title After', id: 'new-title-after' });
655+
656+
const value = smu.getValidParents(newSpan, wrapperSpans, nestedTestSmData, parentTimespan);
657+
// Only have parent timespan as a valid parent
658+
expect(value).toHaveLength(1);
659+
expect(value.map(h => h.id)).toContain('123a-456b-789c-6d');
660+
});
671661
});
672662
});
673663
});

0 commit comments

Comments
 (0)