Skip to content

Commit 3504b75

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

25 files changed

Lines changed: 282 additions & 48 deletions

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: NoteSeverity.ERROR,
171+
message: partInstance.invalidReason,
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: 6 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,6 +44,9 @@ export const partInstanceFieldSpecifier = literal<
4444
rundownId: 1,
4545
orphaned: 1,
4646
reset: 1,
47+
invalidReason: 1,
48+
// @ts-expect-error Deep not supported
49+
'part._id': 1,
4750
// @ts-expect-error Deep not supported
4851
'part.title': 1,
4952
})
@@ -52,17 +55,15 @@ export interface ContentCache {
5255
Rundowns: ReactiveCacheCollection<Pick<Rundown, RundownFields>>
5356
Segments: ReactiveCacheCollection<Pick<DBSegment, SegmentFields>>
5457
Parts: ReactiveCacheCollection<Pick<DBPart, PartFields>>
55-
DeletedPartInstances: ReactiveCacheCollection<Pick<PartInstance, PartInstanceFields>>
58+
PartInstances: ReactiveCacheCollection<Pick<PartInstance, PartInstanceFields>>
5659
}
5760

5861
export function createReactiveContentCache(): ContentCache {
5962
const cache: ContentCache = {
6063
Rundowns: new ReactiveCacheCollection<Pick<Rundown, RundownFields>>('rundowns'),
6164
Segments: new ReactiveCacheCollection<Pick<DBSegment, SegmentFields>>('segments'),
6265
Parts: new ReactiveCacheCollection<Pick<DBPart, PartFields>>('parts'),
63-
DeletedPartInstances: new ReactiveCacheCollection<Pick<PartInstance, PartInstanceFields>>(
64-
'deletedPartInstances'
65-
),
66+
PartInstances: new ReactiveCacheCollection<Pick<PartInstance, PartInstanceFields>>('partInstances'),
6667
}
6768

6869
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

packages/corelib/src/dataModel/PartInstance.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { PartEndState, Time } from '@sofie-automation/blueprints-integration'
22
import { PartCalculatedTimings } from '../playout/timings.js'
33
import { PartInstanceId, RundownId, RundownPlaylistActivationId, SegmentId, SegmentPlayoutId } from './Ids.js'
44
import { DBPart } from './Part.js'
5+
import type { ITranslatableMessage } from '../TranslatableMessage.js'
56

67
export interface DBPartInstance {
78
_id: PartInstanceId
@@ -40,6 +41,13 @@ export interface DBPartInstance {
4041

4142
/** If taking out of the current part is blocked, this is the time it is blocked until */
4243
blockTakeUntil?: number
44+
45+
/**
46+
* If set, this PartInstance exists and is valid as being next, but it cannot be taken in its current state.
47+
* This can be used to block taking a PartInstance that requires user action to resolve.
48+
* This is a runtime validation issue, distinct from the planned `invalidReason` on the Part itself.
49+
*/
50+
invalidReason?: ITranslatableMessage
4351
}
4452

4553
export interface PartInstanceTimings {

packages/corelib/src/error.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export enum UserErrorMessage {
6464
IdempotencyKeyAlreadyUsed = 48,
6565
RateLimitExceeded = 49,
6666
SystemSingleStudio = 50,
67+
TakePartInstanceInvalid = 51,
6768
}
6869

6970
const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = {
@@ -126,6 +127,7 @@ const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = {
126127
[UserErrorMessage.IdempotencyKeyAlreadyUsed]: t(`Idempotency-Key is already used`),
127128
[UserErrorMessage.RateLimitExceeded]: t(`Rate limit exceeded`),
128129
[UserErrorMessage.SystemSingleStudio]: t(`System must have exactly one studio`),
130+
[UserErrorMessage.TakePartInstanceInvalid]: t(`Part has issues and cannot be taken`),
129131
}
130132

131133
export interface SerializedUserError {

0 commit comments

Comments
 (0)