Skip to content

Commit e8e13b8

Browse files
committed
feat: partInstances invalid state
1 parent 0734349 commit e8e13b8

31 files changed

+531
-63
lines changed

meteor/server/publications/segmentPartNotesUI/__tests__/generateNotesForSegment.test.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,4 +486,121 @@ describe('generateNotesForSegment', () => {
486486
])
487487
)
488488
})
489+
490+
test('partInstance with runtime invalidReason', async () => {
491+
const playlistId = protectString<RundownPlaylistId>('playlist0')
492+
const nrcsName = 'some nrcs'
493+
494+
const segment: Pick<DBSegment, SegmentFields> = {
495+
_id: protectString('segment0'),
496+
_rank: 1,
497+
rundownId: protectString('rundown0'),
498+
name: 'A segment',
499+
notes: [],
500+
orphaned: undefined,
501+
}
502+
503+
const partInstance0: Pick<DBPartInstance, PartInstanceFields> = {
504+
_id: protectString('instance0'),
505+
segmentId: segment._id,
506+
rundownId: segment.rundownId,
507+
orphaned: undefined,
508+
reset: false,
509+
invalidReason: {
510+
message: generateTranslation('Runtime error occurred'),
511+
severity: NoteSeverity.ERROR,
512+
},
513+
part: {
514+
_id: protectString('part0'),
515+
title: 'Test Part',
516+
} as any,
517+
}
518+
519+
const notes = generateNotesForSegment(playlistId, segment, nrcsName, [], [partInstance0])
520+
expect(notes).toEqual(
521+
literal<UISegmentPartNote[]>([
522+
{
523+
_id: protectString('segment0_partinstance_instance0_invalid_runtime'),
524+
note: {
525+
type: NoteSeverity.ERROR,
526+
message: partInstance0.invalidReason!.message,
527+
rank: segment._rank,
528+
origin: {
529+
segmentId: segment._id,
530+
rundownId: segment.rundownId,
531+
name: partInstance0.part.title,
532+
partId: partInstance0.part._id,
533+
segmentName: segment.name,
534+
},
535+
},
536+
playlistId: playlistId,
537+
rundownId: segment.rundownId,
538+
segmentId: segment._id,
539+
},
540+
])
541+
)
542+
})
543+
544+
test('partInstance with runtime invalidReason but reset - no note', async () => {
545+
const playlistId = protectString<RundownPlaylistId>('playlist0')
546+
const nrcsName = 'some nrcs'
547+
548+
const segment: Pick<DBSegment, SegmentFields> = {
549+
_id: protectString('segment0'),
550+
_rank: 1,
551+
rundownId: protectString('rundown0'),
552+
name: 'A segment',
553+
notes: [],
554+
orphaned: undefined,
555+
}
556+
557+
const partInstance0: Pick<DBPartInstance, PartInstanceFields> = {
558+
_id: protectString('instance0'),
559+
segmentId: segment._id,
560+
rundownId: segment.rundownId,
561+
orphaned: undefined,
562+
reset: true,
563+
invalidReason: {
564+
message: generateTranslation('Runtime error occurred'),
565+
severity: NoteSeverity.ERROR,
566+
},
567+
part: {
568+
_id: protectString('part0'),
569+
title: 'Test Part',
570+
} as any,
571+
}
572+
573+
const notes = generateNotesForSegment(playlistId, segment, nrcsName, [], [partInstance0])
574+
expect(notes).toHaveLength(0)
575+
})
576+
577+
test('partInstance without invalidReason - no note', async () => {
578+
const playlistId = protectString<RundownPlaylistId>('playlist0')
579+
const nrcsName = 'some nrcs'
580+
581+
const segment: Pick<DBSegment, SegmentFields> = {
582+
_id: protectString('segment0'),
583+
_rank: 1,
584+
rundownId: protectString('rundown0'),
585+
name: 'A segment',
586+
notes: [],
587+
orphaned: undefined,
588+
}
589+
590+
const partInstance0: Pick<DBPartInstance, PartInstanceFields> = {
591+
_id: protectString('instance0'),
592+
segmentId: segment._id,
593+
rundownId: segment.rundownId,
594+
orphaned: undefined,
595+
reset: false,
596+
invalidReason: undefined,
597+
part: {
598+
_id: protectString('part0'),
599+
title: 'Test Part',
600+
} as any,
601+
}
602+
603+
const notes = generateNotesForSegment(playlistId, segment, nrcsName, [], [partInstance0])
604+
expect(notes).toHaveLength(0)
605+
})
489606
})

meteor/server/publications/segmentPartNotesUI/__tests__/publication.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ describe('manipulateUISegmentPartNotesPublicationData', () => {
6969
Rundowns: new ReactiveCacheCollection('Rundowns'),
7070
Segments: new ReactiveCacheCollection('Segments'),
7171
Parts: new ReactiveCacheCollection('Parts'),
72-
DeletedPartInstances: new ReactiveCacheCollection('DeletedPartInstances'),
72+
PartInstances: new ReactiveCacheCollection('PartInstances'),
7373
}
7474

7575
newCache.Rundowns.insert({
@@ -356,11 +356,11 @@ describe('manipulateUISegmentPartNotesPublicationData', () => {
356356
invalid: false,
357357
invalidReason: undefined,
358358
})
359-
newCache.DeletedPartInstances.insert({
359+
newCache.PartInstances.insert({
360360
_id: 'instance0',
361361
segmentId: segmentId0,
362362
rundownId: rundownId,
363-
orphaned: undefined,
363+
orphaned: 'deleted',
364364
reset: false,
365365
part: 'part' as any,
366366
})
@@ -421,6 +421,7 @@ describe('manipulateUISegmentPartNotesPublicationData', () => {
421421
[
422422
{
423423
_id: 'instance0',
424+
orphaned: 'deleted',
424425
part: 'part',
425426
reset: false,
426427
rundownId: 'rundown0',

meteor/server/publications/segmentPartNotesUI/generateNotesForSegment.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,5 +155,31 @@ export function generateNotesForSegment(
155155
}
156156
}
157157

158+
// Generate notes for runtime invalidReason on PartInstances
159+
// This is distinct from planned invalidReason on Parts - these are runtime validation issues
160+
for (const partInstance of partInstances) {
161+
// Skip if the PartInstance has been reset (no longer relevant) or has no runtime invalidReason
162+
if (partInstance.reset || !partInstance.invalidReason) continue
163+
164+
notes.push({
165+
_id: protectString(`${segment._id}_partinstance_${partInstance._id}_invalid_runtime`),
166+
playlistId,
167+
rundownId: partInstance.rundownId,
168+
segmentId: segment._id,
169+
note: {
170+
type: partInstance.invalidReason.severity ?? NoteSeverity.ERROR,
171+
message: partInstance.invalidReason.message,
172+
rank: segment._rank,
173+
origin: {
174+
segmentId: partInstance.segmentId,
175+
partId: partInstance.part._id,
176+
rundownId: partInstance.rundownId,
177+
segmentName: segment.name,
178+
name: partInstance.part.title,
179+
},
180+
},
181+
})
182+
}
183+
158184
return notes
159185
}

meteor/server/publications/segmentPartNotesUI/publication.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ async function setupUISegmentPartNotesPublicationObservers(
9191
triggerUpdate({ invalidateSegmentIds: [doc.segmentId, oldDoc.segmentId] }),
9292
removed: (doc) => triggerUpdate({ invalidateSegmentIds: [doc.segmentId] }),
9393
}),
94-
cache.DeletedPartInstances.find({}).observe({
94+
cache.PartInstances.find({}).observe({
9595
added: (doc) => triggerUpdate({ invalidateSegmentIds: [doc.segmentId] }),
9696
changed: (doc, oldDoc) =>
9797
triggerUpdate({ invalidateSegmentIds: [doc.segmentId, oldDoc.segmentId] }),
@@ -184,13 +184,13 @@ export async function manipulateUISegmentPartNotesPublicationData(
184184
interface UpdateNotesData {
185185
rundownsCache: Map<RundownId, Pick<Rundown, RundownFields>>
186186
parts: Map<SegmentId, Pick<DBPart, PartFields>[]>
187-
deletedPartInstances: Map<SegmentId, Pick<DBPartInstance, PartInstanceFields>[]>
187+
partInstances: Map<SegmentId, Pick<DBPartInstance, PartInstanceFields>[]>
188188
}
189189
function compileUpdateNotesData(cache: ReadonlyDeep<ContentCache>): UpdateNotesData {
190190
return {
191191
rundownsCache: normalizeArrayToMap(cache.Rundowns.find({}).fetch(), '_id'),
192192
parts: groupByToMap(cache.Parts.find({}).fetch(), 'segmentId'),
193-
deletedPartInstances: groupByToMap(cache.DeletedPartInstances.find({}).fetch(), 'segmentId'),
193+
partInstances: groupByToMap(cache.PartInstances.find({}).fetch(), 'segmentId'),
194194
}
195195
}
196196

@@ -205,7 +205,7 @@ function updateNotesForSegment(
205205
segment,
206206
getRundownNrcsName(state.rundownsCache.get(segment.rundownId)),
207207
state.parts.get(segment._id) ?? [],
208-
state.deletedPartInstances.get(segment._id) ?? []
208+
state.partInstances.get(segment._id) ?? []
209209
)
210210

211211
// Insert generated notes

meteor/server/publications/segmentPartNotesUI/reactiveContentCache.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const partFieldSpecifier = literal<MongoFieldSpecifierOnesStrict<Pick<DBP
3535
invalidReason: 1,
3636
})
3737

38-
export type PartInstanceFields = '_id' | 'segmentId' | 'rundownId' | 'orphaned' | 'reset' | 'part'
38+
export type PartInstanceFields = '_id' | 'segmentId' | 'rundownId' | 'orphaned' | 'reset' | 'part' | 'invalidReason'
3939
export const partInstanceFieldSpecifier = literal<
4040
MongoFieldSpecifierOnesStrict<Pick<PartInstance, PartInstanceFields>>
4141
>({
@@ -44,25 +44,25 @@ export const partInstanceFieldSpecifier = literal<
4444
rundownId: 1,
4545
orphaned: 1,
4646
reset: 1,
47+
invalidReason: 1,
4748
// @ts-expect-error Deep not supported
49+
'part._id': 1,
4850
'part.title': 1,
4951
})
5052

5153
export interface ContentCache {
5254
Rundowns: ReactiveCacheCollection<Pick<Rundown, RundownFields>>
5355
Segments: ReactiveCacheCollection<Pick<DBSegment, SegmentFields>>
5456
Parts: ReactiveCacheCollection<Pick<DBPart, PartFields>>
55-
DeletedPartInstances: ReactiveCacheCollection<Pick<PartInstance, PartInstanceFields>>
57+
PartInstances: ReactiveCacheCollection<Pick<PartInstance, PartInstanceFields>>
5658
}
5759

5860
export function createReactiveContentCache(): ContentCache {
5961
const cache: ContentCache = {
6062
Rundowns: new ReactiveCacheCollection<Pick<Rundown, RundownFields>>('rundowns'),
6163
Segments: new ReactiveCacheCollection<Pick<DBSegment, SegmentFields>>('segments'),
6264
Parts: new ReactiveCacheCollection<Pick<DBPart, PartFields>>('parts'),
63-
DeletedPartInstances: new ReactiveCacheCollection<Pick<PartInstance, PartInstanceFields>>(
64-
'deletedPartInstances'
65-
),
65+
PartInstances: new ReactiveCacheCollection<Pick<PartInstance, PartInstanceFields>>('partInstances'),
6666
}
6767

6868
return cache

meteor/server/publications/segmentPartNotesUI/rundownContentObserver.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,12 @@ export class RundownContentObserver {
5858
}
5959
),
6060
PartInstances.observeChanges(
61-
{ rundownId: { $in: rundownIds }, reset: { $ne: true }, orphaned: 'deleted' },
62-
cache.DeletedPartInstances.link(),
61+
{
62+
rundownId: { $in: rundownIds },
63+
reset: { $ne: true },
64+
$or: [{ invalidReason: { $exists: true } }, { orphaned: 'deleted' }],
65+
},
66+
cache.PartInstances.link(),
6367
{ projection: partInstanceFieldSpecifier }
6468
),
6569
])

packages/blueprints-integration/src/context/onSetAsNextContext.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
IBlueprintMutatablePart,
3+
IBlueprintMutatablePartInstance,
34
IBlueprintPart,
45
IBlueprintPartInstance,
56
IBlueprintPiece,
@@ -72,10 +73,16 @@ export interface IOnSetAsNextContext extends IShowStyleUserContext, IEventContex
7273
/** Update a piecesInstance from the partInstance being set as Next */
7374
updatePieceInstance(pieceInstanceId: string, piece: Partial<IBlueprintPiece>): Promise<IBlueprintPieceInstance>
7475

75-
/** Update a partInstance */
76+
/**
77+
* Update a partInstance
78+
* @param part Which part to update
79+
* @param props Properties of the Part itself
80+
* @param instanceProps Properties of the PartInstance (runtime state)
81+
*/
7682
updatePartInstance(
7783
part: 'current' | 'next',
78-
props: Partial<IBlueprintMutatablePart>
84+
props: Partial<IBlueprintMutatablePart>,
85+
instanceProps?: Partial<IBlueprintMutatablePartInstance>
7986
): Promise<IBlueprintPartInstance>
8087

8188
/**

packages/blueprints-integration/src/context/partsAndPieceActionContext.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ReadonlyDeep } from 'type-fest'
22
import {
33
IBlueprintMutatablePart,
4+
IBlueprintMutatablePartInstance,
45
IBlueprintPart,
56
IBlueprintPartInstance,
67
IBlueprintPiece,
@@ -64,10 +65,16 @@ export interface IPartAndPieceActionContext {
6465
/** Update a piecesInstance */
6566
updatePieceInstance(pieceInstanceId: string, piece: Partial<IBlueprintPiece>): Promise<IBlueprintPieceInstance>
6667

67-
/** Update a partInstance */
68+
/**
69+
* Update a partInstance
70+
* @param part Which part to update
71+
* @param props Properties of the Part itself
72+
* @param instanceProps Properties of the PartInstance (runtime state)
73+
*/
6874
updatePartInstance(
6975
part: 'current' | 'next',
70-
props: Partial<IBlueprintMutatablePart>
76+
props: Partial<IBlueprintMutatablePart>,
77+
instanceProps?: Partial<IBlueprintMutatablePartInstance>
7178
): Promise<IBlueprintPartInstance>
7279
/** Inform core that a take out of the partinstance should be blocked until the specified time */
7380
blockTakeUntil(time: Time | null): Promise<void>

packages/blueprints-integration/src/context/syncIngestChangesContext.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { IRundownUserContext } from './rundownContext.js'
22
import type {
33
IBlueprintMutatablePart,
4+
IBlueprintMutatablePartInstance,
45
IBlueprintPartInstance,
56
IBlueprintPiece,
67
IBlueprintPieceInstance,
@@ -37,8 +38,15 @@ export interface ISyncIngestUpdateToPartInstanceContext extends IRundownUserCont
3738
// /** Remove a ActionInstance */
3839
// removeActionInstances(...actionInstanceIds: string[]): string[]
3940

40-
/** Update a partInstance */
41-
updatePartInstance(props: Partial<IBlueprintMutatablePart>): IBlueprintPartInstance
41+
/**
42+
* Update a partInstance
43+
* @param props Properties of the Part itself
44+
* @param instanceProps Properties of the PartInstance (runtime state)
45+
*/
46+
updatePartInstance(
47+
props: Partial<IBlueprintMutatablePart>,
48+
instanceProps?: Partial<IBlueprintMutatablePartInstance>
49+
): IBlueprintPartInstance
4250

4351
/** Remove the partInstance. This is only valid when `playstatus: 'next'` */
4452
removePartInstance(): void

packages/blueprints-integration/src/documents/partInstance.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
11
import type { Time } from '../common.js'
22
import type { IBlueprintPartDB } from './part.js'
3+
import type { ITranslatableMessage } from '../translations.js'
34

45
export type PartEndState = unknown
56

7+
/**
8+
* Properties of a PartInstance that can be modified at runtime by blueprints.
9+
* These are runtime state properties, distinct from the planned Part properties.
10+
*/
11+
export interface IBlueprintMutatablePartInstance {
12+
/**
13+
* If set, this PartInstance exists and is valid as being next, but it cannot be taken in its current state.
14+
* This can be used to block taking a PartInstance that requires user action to resolve.
15+
* This is a runtime validation issue, distinct from the planned `invalidReason` on the Part itself.
16+
*/
17+
invalidReason?: ITranslatableMessage
18+
}
19+
620
/** The Part instance sent from Core */
7-
export interface IBlueprintPartInstance<TPrivateData = unknown, TPublicData = unknown> {
21+
export interface IBlueprintPartInstance<TPrivateData = unknown, TPublicData = unknown>
22+
extends IBlueprintMutatablePartInstance {
823
_id: string
924
/** The segment ("Title") this line belongs to */
1025
segmentId: string

0 commit comments

Comments
 (0)