Skip to content

Commit c5dd995

Browse files
authored
Merge pull request Sofie-Automation#1652 from nrkno/contrib/sofie-4219/actions-adlib-rundown-state-filter
2 parents f1e15aa + eebca15 commit c5dd995

13 files changed

Lines changed: 422 additions & 30 deletions

File tree

meteor/server/api/deviceTriggers/StudioObserver.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,16 @@ type PieceInstancesChangeHandler = (showStyleBaseId: ShowStyleBaseId, cache: Pie
2929

3030
const REACTIVITY_DEBOUNCE = 20
3131

32-
type RundownPlaylistFields = '_id' | 'nextPartInfo' | 'currentPartInfo' | 'activationId'
32+
type RundownPlaylistFields = '_id' | 'nextPartInfo' | 'currentPartInfo' | 'activationId' | 'rehearsal' | 'studioId'
3333
const rundownPlaylistFieldSpecifier = literal<
3434
MongoFieldSpecifierOnesStrict<Pick<DBRundownPlaylist, RundownPlaylistFields>>
3535
>({
3636
_id: 1,
3737
activationId: 1,
3838
currentPartInfo: 1,
3939
nextPartInfo: 1,
40+
rehearsal: 1,
41+
studioId: 1,
4042
})
4143

4244
type RundownFields = '_id' | 'showStyleBaseId'

meteor/server/api/deviceTriggers/reactiveContentCache.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,14 @@ import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mo
1414
import { literal } from '@sofie-automation/corelib/dist/lib'
1515
import { ReactiveCacheCollection } from '../../publications/lib/ReactiveCacheCollection'
1616

17-
export type RundownPlaylistFields = '_id' | 'name' | 'activationId' | 'currentPartInfo' | 'nextPartInfo'
17+
export type RundownPlaylistFields =
18+
| '_id'
19+
| 'name'
20+
| 'activationId'
21+
| 'currentPartInfo'
22+
| 'nextPartInfo'
23+
| 'studioId'
24+
| 'rehearsal'
1825
export const rundownPlaylistFieldSpecifier = literal<
1926
MongoFieldSpecifierOnesStrict<Pick<DBRundownPlaylist, RundownPlaylistFields>>
2027
>({
@@ -23,6 +30,8 @@ export const rundownPlaylistFieldSpecifier = literal<
2330
activationId: 1,
2431
currentPartInfo: 1,
2532
nextPartInfo: 1,
33+
studioId: 1,
34+
rehearsal: 1,
2635
})
2736

2837
export type SegmentFields = '_id' | '_rank' | 'isHidden' | 'name' | 'rundownId' | 'identifier'

meteor/server/api/deviceTriggers/triggersContext.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
import { SINGLE_USE_TOKEN_SALT } from '@sofie-automation/meteor-lib/dist/api/userActions'
77
import { assertNever, getHash } from '@sofie-automation/corelib/dist/lib'
88
import type { Time } from '@sofie-automation/shared-lib/dist/lib/lib'
9-
import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString'
9+
import { ProtectedString, protectString } from '@sofie-automation/corelib/dist/protectedString'
1010
import { getCurrentTime } from '../../lib/lib'
1111
import { MeteorCall } from '../methods'
1212
import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client'
@@ -209,10 +209,13 @@ async function rundownPlaylistFilter(
209209
case 'studioId':
210210
selector['$and']?.push({
211211
studioId: {
212-
$regex: link.value as any,
212+
$eq: protectString(link.value),
213213
},
214214
})
215215
break
216+
case 'rehearsal':
217+
selector['rehearsal'] = link.value
218+
break
216219
default:
217220
assertNever(link)
218221
break

packages/blueprints-integration/src/triggers.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ export type IRundownPlaylistFilterLink =
123123
field: 'name'
124124
value: string
125125
}
126+
| {
127+
object: 'rundownPlaylist'
128+
field: 'rehearsal'
129+
value: boolean
130+
}
126131

127132
export type IGUIContextFilterLink = {
128133
object: 'view'
@@ -167,6 +172,8 @@ export type IAdLibFilterLink =
167172
value: 'adLib' | 'adLibAction' | 'clear' | 'sticky'
168173
}
169174

175+
export type FilterType = (IRundownPlaylistFilterLink | IGUIContextFilterLink | IAdLibFilterLink)['object']
176+
170177
export interface IAdlibPlayoutActionArguments {
171178
triggerMode: string
172179
}

packages/meteor-lib/src/triggers/actionFactory.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ export interface ReactivePlaylistActionContext {
3838
studioId: TriggerReactiveVar<StudioId>
3939
rundownPlaylistId: TriggerReactiveVar<RundownPlaylistId>
4040
rundownPlaylist: TriggerReactiveVar<
41-
Pick<DBRundownPlaylist, '_id' | 'name' | 'activationId' | 'nextPartInfo' | 'currentPartInfo'>
41+
Pick<
42+
DBRundownPlaylist,
43+
'_id' | 'name' | 'activationId' | 'rehearsal' | 'nextPartInfo' | 'currentPartInfo' | 'studioId'
44+
>
4245
>
4346

4447
currentRundownId: TriggerReactiveVar<RundownId | null>

packages/meteor-lib/src/triggers/actionFilterChainCompilers.ts

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,12 @@ import { MountedAdLibTriggerType } from '../api/MountedTriggers.js'
2626
import { assertNever, generateTranslation } from '@sofie-automation/corelib/dist/lib'
2727
import { FindOptions } from '../collections/lib.js'
2828
import { TriggersContext, TriggerTrackerComputation } from './triggersContext.js'
29+
import { unprotectString } from '@sofie-automation/corelib/dist/protectedString'
2930

3031
export type AdLibFilterChainLink = IRundownPlaylistFilterLink | IGUIContextFilterLink | IAdLibFilterLink
3132

3233
/** This is a compiled Filter type, targetting a particular MongoCollection */
33-
type CompiledFilter<T> = {
34+
type CompiledAdLibFilter<T> = {
3435
selector: MongoQuery<T>
3536
options: FindOptions<T>
3637
pick: number | undefined
@@ -306,7 +307,7 @@ type AdLibActionType = RundownBaselineAdLibAction | AdLibAction
306307
function compileAdLibActionFilter(
307308
filterChain: IAdLibFilterLink[],
308309
sourceLayers: SourceLayers
309-
): CompiledFilter<AdLibActionType> {
310+
): CompiledAdLibFilter<AdLibActionType> {
310311
const selector: MongoQuery<AdLibActionType> = {}
311312
const options: FindOptions<AdLibActionType> = {}
312313
let pick: number | undefined = undefined
@@ -405,7 +406,7 @@ type AdLibPieceType =
405406
function compileAdLibPieceFilter(
406407
filterChain: IAdLibFilterLink[],
407408
sourceLayers: SourceLayers
408-
): CompiledFilter<AdLibPieceType> {
409+
): CompiledAdLibFilter<AdLibPieceType> {
409410
const selector: MongoQuery<AdLibPieceType> = {}
410411
const options: FindOptions<AdLibPieceType> = {}
411412
let pick: number | undefined = undefined
@@ -496,6 +497,61 @@ function compileAdLibPieceFilter(
496497
}
497498
}
498499

500+
type RundownSelector = {
501+
activationId: boolean | undefined
502+
name: RegExp | undefined
503+
studioId: string | undefined
504+
rehearsal: boolean | undefined
505+
}
506+
507+
function compileRundownPlaylistFilter(filterChain: IRundownPlaylistFilterLink[]): {
508+
selector: RundownSelector
509+
/**
510+
* The query compiler has determined that this filter will always match
511+
* it's safe to skip it entirely.
512+
*/
513+
matchAll?: true
514+
} {
515+
const selector: RundownSelector = {
516+
activationId: undefined,
517+
name: undefined,
518+
studioId: undefined,
519+
rehearsal: undefined,
520+
}
521+
522+
if (filterChain.length === 0) {
523+
// no filter, accept all
524+
return {
525+
selector,
526+
matchAll: true,
527+
}
528+
}
529+
530+
filterChain.forEach((link) => {
531+
switch (link.field) {
532+
case 'activationId':
533+
selector.activationId = link.value
534+
return
535+
case 'name':
536+
selector.name = new RegExp(link.value)
537+
return
538+
case 'studioId':
539+
selector.studioId = link.value
540+
return
541+
case 'rehearsal':
542+
selector.rehearsal = link.value
543+
return
544+
default:
545+
assertNever(link)
546+
return
547+
}
548+
})
549+
550+
return {
551+
selector,
552+
}
553+
}
554+
499555
/**
500556
* Compile the filter chain and return a reactive function that will return the result set for this adLib filter
501557
* @param filterChain
@@ -508,6 +564,13 @@ export function compileAdLibFilter(
508564
sourceLayers: SourceLayers
509565
): (context: ReactivePlaylistActionContext, computation: TriggerTrackerComputation | null) => Promise<IWrappedAdLib[]> {
510566
const onlyAdLibLinks = filterChain.filter((link) => link.object === 'adLib') as IAdLibFilterLink[]
567+
const onlyRundownPlaylistLinks = filterChain.filter(
568+
(link) => link.object === 'rundownPlaylist'
569+
) as IRundownPlaylistFilterLink[]
570+
571+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
572+
// @ts-ignore ignore unused
573+
const rundownPlaylistFilter = compileRundownPlaylistFilter(onlyRundownPlaylistLinks)
511574
const adLibPieceTypeFilter = compileAdLibPieceFilter(onlyAdLibLinks, sourceLayers)
512575
const adLibActionTypeFilter = compileAdLibActionFilter(onlyAdLibLinks, sourceLayers)
513576

@@ -556,6 +619,35 @@ export function compileAdLibFilter(
556619
}
557620
}
558621

622+
{
623+
const matchAll = rundownPlaylistFilter.matchAll
624+
const currentRundownPlaylist = context.rundownPlaylist.get(computation)
625+
626+
const activationStateMatches =
627+
rundownPlaylistFilter.selector.activationId !== undefined
628+
? (currentRundownPlaylist?.activationId !== undefined) ===
629+
rundownPlaylistFilter.selector.activationId
630+
: true
631+
const nameMatches =
632+
rundownPlaylistFilter.selector.name !== undefined
633+
? currentRundownPlaylist?.name.match(rundownPlaylistFilter.selector.name) !== null
634+
: true
635+
const studioMatches =
636+
rundownPlaylistFilter.selector.studioId !== undefined
637+
? unprotectString(currentRundownPlaylist?.studioId) === rundownPlaylistFilter.selector.studioId
638+
: true
639+
const rehearsalMatches =
640+
rundownPlaylistFilter.selector.rehearsal !== undefined
641+
? currentRundownPlaylist?.rehearsal === rundownPlaylistFilter.selector.rehearsal
642+
: true
643+
644+
if (!matchAll) {
645+
if (!activationStateMatches || !nameMatches || !studioMatches || !rehearsalMatches) {
646+
return []
647+
}
648+
}
649+
}
650+
559651
{
560652
let skip = adLibPieceTypeFilter.skip
561653
const currentNextOverride: MongoQuery<AdLibPieceType> = {}

packages/webui/src/client/lib/triggers/TriggersHandler.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,8 +323,15 @@ export const TriggersHandler: React.FC<IProps> = function TriggersHandler(
323323
activationId: 1,
324324
nextPartInfo: 1,
325325
currentPartInfo: 1,
326+
studioId: 1,
327+
rehearsal: 1,
326328
},
327-
}) as Pick<DBRundownPlaylist, '_id' | 'name' | 'activationId' | 'nextPartInfo' | 'currentPartInfo'> | undefined
329+
}) as
330+
| Pick<
331+
DBRundownPlaylist,
332+
'_id' | 'name' | 'activationId' | 'studioId' | 'rehearsal' | 'nextPartInfo' | 'currentPartInfo'
333+
>
334+
| undefined
328335
if (playlist) {
329336
let context = rundownPlaylistContext.get()
330337
if (context === null) {

packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/ActionEditor.tsx

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useCallback, useState } from 'react'
22
import _ from 'underscore'
33
import {
4+
FilterType,
45
IAdLibFilterLink,
56
IGUIContextFilterLink,
67
IRundownPlaylistFilterLink,
@@ -39,9 +40,8 @@ function isFinal(
3940
): boolean {
4041
if (action.action === PlayoutActions.adlib) {
4142
return chainLink?.object === 'adLib' && (chainLink?.field === 'pick' || chainLink?.field === 'pickEnd')
42-
} else {
43-
return chainLink?.object === 'view'
4443
}
44+
return false
4545
}
4646

4747
type ChainLink = IRundownPlaylistFilterLink | IGUIContextFilterLink | IAdLibFilterLink
@@ -75,6 +75,15 @@ export const ActionEditor: React.FC<IProps> = function ActionEditor({
7575
[action, overrideHelper]
7676
)
7777

78+
const onChangeType = useCallback(
79+
(filterIndex: number, newType: FilterType) => {
80+
action.filterChain[filterIndex].object = newType
81+
82+
overrideHelper().replaceItem(actionId, action).commit()
83+
},
84+
[action, overrideHelper]
85+
)
86+
7887
function onFilterInsertNext(filterIndex: number) {
7988
if (action.filterChain.length === filterIndex + 1) {
8089
const obj =
@@ -154,6 +163,7 @@ export const ActionEditor: React.FC<IProps> = function ActionEditor({
154163
index={chainIndex}
155164
opened={openFilterIndex === chainIndex}
156165
onChange={onFilterChange}
166+
onChangeType={onChangeType}
157167
sourceLayers={sourceLayers}
158168
outputLayers={outputLayers}
159169
onFocus={onFilterFocus}
@@ -173,9 +183,23 @@ export const ActionEditor: React.FC<IProps> = function ActionEditor({
173183
final={action.filterChain.length === 1 && isFinal(action, chainLink)}
174184
onInsertNext={onFilterInsertNext}
175185
onRemove={onFilterRemove}
186+
onChangeType={onChangeType}
176187
/>
177188
) : chainLink.object === 'rundownPlaylist' ? (
178-
<RundownPlaylistFilter link={chainLink} key={chainIndex} />
189+
<RundownPlaylistFilter
190+
link={chainLink}
191+
index={chainIndex}
192+
readonly={readonly}
193+
key={chainIndex}
194+
opened={openFilterIndex === chainIndex}
195+
onChange={onFilterChange}
196+
onFocus={onFilterFocus}
197+
onClose={onClose}
198+
onInsertNext={onFilterInsertNext}
199+
onRemove={onFilterRemove}
200+
final={action.filterChain.length === 1 && isFinal(action, chainLink)}
201+
onChangeType={onChangeType}
202+
/>
179203
) : (
180204
<dl className="triggered-action-entry__action__filter" key={chainIndex}>
181205
<dt>{(chainLink as any).object}</dt>

packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/filterPreviews/AdLibFilter.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import React from 'react'
22
import _ from 'underscore'
3-
import { IAdLibFilterLink, IOutputLayer, ISourceLayer, SourceLayerType } from '@sofie-automation/blueprints-integration'
3+
import {
4+
FilterType,
5+
IAdLibFilterLink,
6+
IOutputLayer,
7+
ISourceLayer,
8+
SourceLayerType,
9+
} from '@sofie-automation/blueprints-integration'
410
import { useTranslation } from 'react-i18next'
511
import { TFunction } from 'i18next'
612
import { assertNever } from '@sofie-automation/corelib/dist/lib'
@@ -22,6 +28,7 @@ interface IProps {
2228
outputLayers: OutputLayers | undefined
2329
readonly?: boolean
2430
opened: boolean
31+
onChangeType: (index: number, newType: FilterType) => void
2532
onChange: (index: number, newVal: IAdLibFilterLink, oldVal: IAdLibFilterLink) => void
2633
onFocus?: (index: number) => void
2734
onInsertNext?: (index: number) => void
@@ -92,7 +99,7 @@ function fieldToLabel(t: TFunction, field: IAdLibFilterLink['field']): string {
9299
return t('Type')
93100
default:
94101
assertNever(field)
95-
return field
102+
return t('AdLib filter')
96103
}
97104
}
98105

@@ -285,6 +292,7 @@ export const AdLibFilter: React.FC<IProps> = function AdLibFilter({
285292
onFocus,
286293
onInsertNext,
287294
onRemove,
295+
onChangeType,
288296
}: IProps) {
289297
const { t } = useTranslation()
290298

@@ -329,6 +337,7 @@ export const AdLibFilter: React.FC<IProps> = function AdLibFilter({
329337
return (
330338
<FilterEditor
331339
index={index}
340+
filterType="adLib"
332341
field={link.field}
333342
fields={getAvailableFields(t, fields)}
334343
fieldLabel={fieldToLabel(t, link.field)}
@@ -364,6 +373,7 @@ export const AdLibFilter: React.FC<IProps> = function AdLibFilter({
364373
onClose={onClose}
365374
onInsertNext={onInsertNext}
366375
onRemove={onRemove}
376+
onChangeType={(newType) => onChangeType(index, newType)}
367377
/>
368378
)
369379
}

0 commit comments

Comments
 (0)