feat: add ability to exclude seasons in slot schedulers

This commit is contained in:
Christian Benincasa
2026-04-11 08:12:30 -04:00
parent 96180dc22c
commit 7fe1f2592e
12 changed files with 86 additions and 17 deletions

File diff suppressed because one or more lines are too long

View File

@@ -155,6 +155,7 @@ describe('TimeSlotService', () => {
order: 'next',
direction: 'asc',
seasonFilter: [],
seasonExcludeFilter: [],
},
{
startTime: 12 * 60 * 60 * 1000, // Noon
@@ -212,6 +213,7 @@ describe('TimeSlotService', () => {
order: 'next',
direction: 'asc',
seasonFilter: [],
seasonExcludeFilter: [],
},
],
period: 'day',
@@ -269,6 +271,7 @@ describe('TimeSlotService', () => {
order: 'next',
direction: 'asc',
seasonFilter: [],
seasonExcludeFilter: [],
},
],
period: 'day',
@@ -318,6 +321,7 @@ describe('TimeSlotService', () => {
order: 'next',
direction: 'asc',
seasonFilter: [],
seasonExcludeFilter: [],
},
],
period: 'day',
@@ -395,6 +399,7 @@ describe('TimeSlotService', () => {
order: 'next',
direction: 'desc',
seasonFilter: [],
seasonExcludeFilter: [],
},
],
period: 'day',
@@ -507,6 +512,7 @@ describe('TimeSlotService', () => {
order: 'next',
direction: 'asc',
seasonFilter: [],
seasonExcludeFilter: [],
},
],
period: 'day',
@@ -564,6 +570,7 @@ describe('TimeSlotService', () => {
order: 'next',
direction: 'asc',
seasonFilter: [],
seasonExcludeFilter: [],
},
],
period: 'day',
@@ -713,6 +720,7 @@ describe('TimeSlotService', () => {
order: 'next',
direction: 'asc',
seasonFilter: [],
seasonExcludeFilter: [],
},
],
period: 'week',
@@ -755,6 +763,7 @@ describe('TimeSlotService', () => {
order: 'next',
direction: 'asc',
seasonFilter: [],
seasonExcludeFilter: [],
},
],
period: 'day',
@@ -1051,6 +1060,7 @@ describe('TimeSlotService', () => {
order: 'next',
direction: 'asc',
seasonFilter: [],
seasonExcludeFilter: [],
},
],
period: 'day',

View File

@@ -448,11 +448,19 @@ function getContentProgramIterator(
(p) => p.uuid,
);
if (slot.type === 'show' && slot.seasonFilter.length > 0) {
programs = programs.filter((program) => {
const season = program.season?.index ?? program.seasonNumber;
return season && slot.seasonFilter.includes(season);
});
if (slot.type === 'show') {
if (slot.seasonFilter.length > 0) {
programs = programs.filter((program) => {
const season = program.season?.index ?? program.seasonNumber;
return season && slot.seasonFilter.includes(season);
});
}
if (slot.seasonExcludeFilter?.length > 0) {
programs = programs.filter((program) => {
const season = program.season?.index ?? program.seasonNumber;
return !season || !slot.seasonExcludeFilter.includes(season);
});
}
}
switch (slot.order) {

View File

@@ -61,6 +61,7 @@ export const ShowProgrammingSlotSchema = z.object({
type: z.literal('show'),
showId: z.string(),
seasonFilter: z.number().array().default([]).catch([]),
seasonExcludeFilter: z.number().array().default([]).catch([]),
...BaseSlotOrdering.shape,
...Slot.shape,
});

View File

@@ -103,6 +103,7 @@ export const AddRandomSlotButton = ({ onAdd }: AddRandomSlotButtonProps) => {
order: 'next',
show: null,
seasonFilter: [],
seasonExcludeFilter: [],
}))
.with({ type: 'flex' }, () => ({
...baseSlot,

View File

@@ -72,6 +72,7 @@ export const AddTimeSlotButton = ({
showId: sortBy(opts, (opt) => opt.value)?.[0].showId,
show: null,
seasonFilter: [],
seasonExcludeFilter: [],
} satisfies ShowTimeSlotViewModel;
} else if (
optionsByType['custom-show'] &&

View File

@@ -192,6 +192,7 @@ export const EditRandomSlotDialogContent = ({
order: 'next',
direction: 'asc',
seasonFilter: [],
seasonExcludeFilter: [],
}))
.with('smart-collection', () => {
const opt = programOptions.find(

View File

@@ -166,6 +166,7 @@ export const EditTimeSlotDialogContent = ({
title: opt?.description ?? '',
show: null,
seasonFilter: [],
seasonExcludeFilter: [],
};
})
.with('smart-collection', () => {

View File

@@ -14,7 +14,8 @@ export const ShowSearchSlotProgrammingForm = () => {
const [searchQuery, setSearchQuery] = useState('');
const enabled = useMemo(() => searchQuery.length >= 1, [searchQuery]);
const show = watch('show');
console.log(watch());
const seasonFilter = watch('seasonFilter');
const seasonExcludeFilter = watch('seasonExcludeFilter');
const search = useMemo(
() => ({
@@ -32,7 +33,7 @@ export const ShowSearchSlotProgrammingForm = () => {
enabled: !!show,
});
const seasonAutocompleteOpts = useMemo(
const allSeasons = useMemo(
() =>
showChildrenQuery.data?.result.programs.filter(
(x) => x.type === 'season',
@@ -40,6 +41,16 @@ export const ShowSearchSlotProgrammingForm = () => {
[showChildrenQuery.data],
);
const includeOptions = useMemo(
() => allSeasons.filter((s) => !seasonExcludeFilter.includes(s.index)),
[allSeasons, seasonExcludeFilter],
);
const excludeOptions = useMemo(
() => allSeasons.filter((s) => !seasonFilter.includes(s.index)),
[allSeasons, seasonFilter],
);
return (
<>
<Controller
@@ -56,6 +67,7 @@ export const ShowSearchSlotProgrammingForm = () => {
field.onChange(show.uuid);
setValue('show', show);
setValue('seasonFilter', []);
setValue('seasonExcludeFilter', []);
}}
onQueryChange={setSearchQuery}
label="Show"
@@ -67,20 +79,18 @@ export const ShowSearchSlotProgrammingForm = () => {
name="seasonFilter"
render={({ field }) => (
<Autocomplete
options={seasonAutocompleteOpts}
options={includeOptions}
value={
field.value.length === 0
? []
: seasonAutocompleteOpts.filter((opt) =>
field.value.includes(opt.index),
)
: allSeasons.filter((opt) => field.value.includes(opt.index))
}
disabled={!show || showChildrenQuery.isLoading}
multiple
getOptionKey={(season) => season.index}
getOptionLabel={(season) => season.title}
renderInput={(params) => (
<TextField {...params} label={'Seasons'} />
<TextField {...params} label={'Include Seasons'} />
)}
onChange={(_, seasons) =>
setValue(
@@ -92,6 +102,34 @@ export const ShowSearchSlotProgrammingForm = () => {
/>
)}
/>
<Controller
control={control}
name="seasonExcludeFilter"
render={({ field }) => (
<Autocomplete
options={excludeOptions}
value={
field.value.length === 0
? []
: allSeasons.filter((opt) => field.value.includes(opt.index))
}
disabled={!show || showChildrenQuery.isLoading}
multiple
getOptionKey={(season) => season.index}
getOptionLabel={(season) => season.title}
renderInput={(params) => (
<TextField {...params} label={'Exclude Seasons'} />
)}
onChange={(_, seasons) =>
setValue(
'seasonExcludeFilter',
seasons.map((s) => s.index),
)
}
filterSelectedOptions
/>
)}
/>
<SlotOrderFormControl />
</>
);

View File

@@ -109,6 +109,7 @@ const SlotSchedulerCyclicShuffleDialogContent = ({ onClose }: Props) => {
show,
weight: 100,
seasonFilter: [],
seasonExcludeFilter: [],
});
}

View File

@@ -3704,6 +3704,7 @@ export type GetApiChannelsByIdProgrammingResponses = {
type: 'show';
showId: string;
seasonFilter: Array<number>;
seasonExcludeFilter: Array<number>;
order: 'next' | 'shuffle' | 'ordered_shuffle' | 'alphanumeric' | 'chronological';
direction: 'asc' | 'desc';
filler?: Array<{
@@ -3805,6 +3806,7 @@ export type GetApiChannelsByIdProgrammingResponses = {
type: 'show';
showId: string;
seasonFilter: Array<number>;
seasonExcludeFilter: Array<number>;
} | {
cooldownMs: number;
periodMs?: number;
@@ -4026,6 +4028,7 @@ export type PostApiChannelsByIdProgrammingData = {
type: 'show';
showId: string;
seasonFilter?: Array<number>;
seasonExcludeFilter?: Array<number>;
order: 'next' | 'shuffle' | 'ordered_shuffle' | 'alphanumeric' | 'chronological';
direction?: 'asc' | 'desc';
filler?: Array<{
@@ -4133,6 +4136,7 @@ export type PostApiChannelsByIdProgrammingData = {
type: 'show';
showId: string;
seasonFilter?: Array<number>;
seasonExcludeFilter?: Array<number>;
} | {
cooldownMs: number;
periodMs?: number;
@@ -4359,6 +4363,7 @@ export type PostApiChannelsByIdProgrammingResponses = {
type: 'show';
showId: string;
seasonFilter: Array<number>;
seasonExcludeFilter: Array<number>;
order: 'next' | 'shuffle' | 'ordered_shuffle' | 'alphanumeric' | 'chronological';
direction: 'asc' | 'desc';
filler?: Array<{
@@ -4460,6 +4465,7 @@ export type PostApiChannelsByIdProgrammingResponses = {
type: 'show';
showId: string;
seasonFilter: Array<number>;
seasonExcludeFilter: Array<number>;
} | {
cooldownMs: number;
periodMs?: number;
@@ -5013,6 +5019,7 @@ export type PostApiChannelsByChannelIdScheduleTimeSlotsData = {
type: 'show';
showId: string;
seasonFilter?: Array<number>;
seasonExcludeFilter?: Array<number>;
order: 'next' | 'shuffle' | 'ordered_shuffle' | 'alphanumeric' | 'chronological';
direction?: 'asc' | 'desc';
filler?: Array<{
@@ -5215,6 +5222,7 @@ export type PostApiChannelsByChannelIdScheduleSlotsData = {
type: 'show';
showId: string;
seasonFilter?: Array<number>;
seasonExcludeFilter?: Array<number>;
} | {
cooldownMs: number;
periodMs?: number;
@@ -5445,6 +5453,7 @@ export type GetApiChannelsByIdScheduleResponses = {
type: 'show';
showId: string;
seasonFilter: Array<number>;
seasonExcludeFilter: Array<number>;
order: 'next' | 'shuffle' | 'ordered_shuffle' | 'alphanumeric' | 'chronological';
direction: 'asc' | 'desc';
filler?: Array<{
@@ -5686,6 +5695,7 @@ export type GetApiChannelsByIdScheduleResponses = {
type: 'show';
showId: string;
seasonFilter: Array<number>;
seasonExcludeFilter: Array<number>;
show: Show | null;
/**
* A show that existed in the DB at schedule time, but no longer exists.

View File

@@ -63,6 +63,7 @@ export const CommonShowSlotViewModel = z.object({
})
.optional(),
seasonFilter: z.number().array().default([]),
seasonExcludeFilter: z.number().array().default([]),
});
export type CommonShowSlotViewModel = z.infer<typeof CommonShowSlotViewModel>;