mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-14 11:53:49 +00:00
refactor: gather group metadata
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { IoAdd } from 'react-icons/io5';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
|
||||
import style from './Empty.module.scss';
|
||||
|
||||
interface BlockEmptyProps {
|
||||
handleAddNew: () => void;
|
||||
}
|
||||
|
||||
export default function BlockEmpty(props: BlockEmptyProps) {
|
||||
const { handleAddNew } = props;
|
||||
|
||||
return (
|
||||
<div className={style.empty}>
|
||||
<Button size='sm' onClick={handleAddNew} variant='ontime-filled' leftIcon={<IoAdd />}>
|
||||
Create Event
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.empty {
|
||||
padding-block: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -23,15 +23,6 @@
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.alignCenter {
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
|
||||
.spaceTop {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
@@ -5,23 +5,19 @@ import { useHotkeys } from '@mantine/hooks';
|
||||
import {
|
||||
type EntryId,
|
||||
type MaybeString,
|
||||
type PlayableEvent,
|
||||
type Rundown,
|
||||
isOntimeBlock,
|
||||
isOntimeEvent,
|
||||
isPlayableEvent,
|
||||
Playback,
|
||||
SupportedEvent,
|
||||
} from 'ontime-types';
|
||||
import {
|
||||
checkIsNextDay,
|
||||
getFirstNormal,
|
||||
getLastNormal,
|
||||
getNextBlockNormal,
|
||||
getNextNormal,
|
||||
getPreviousBlockNormal,
|
||||
getPreviousNormal,
|
||||
isNewLatest,
|
||||
reorderArray,
|
||||
} from 'ontime-utils';
|
||||
|
||||
@@ -32,7 +28,10 @@ import { AppMode, useAppMode } from '../../common/stores/appModeStore';
|
||||
import { useEntryCopy } from '../../common/stores/entryCopyStore';
|
||||
import { cloneEvent } from '../../common/utils/eventsManager';
|
||||
|
||||
import BlockBlock from './block-block/BlockBlock';
|
||||
import QuickAddBlock from './quick-add-block/QuickAddBlock';
|
||||
import BlockEmpty from './BlockEmpty';
|
||||
import { makeRundownMetadata } from './rundown.utils';
|
||||
import RundownEmpty from './RundownEmpty';
|
||||
import { useEventSelection } from './useEventSelection';
|
||||
|
||||
@@ -264,20 +263,11 @@ export default function Rundown({ data }: RundownProps) {
|
||||
return <RundownEmpty handleAddNew={() => insertAtId(SupportedEvent.Event, cursor)} />;
|
||||
}
|
||||
|
||||
// last event is used to calculate relative timings
|
||||
let lastEvent: PlayableEvent | null = null; // used by indicators
|
||||
let thisEvent: PlayableEvent | null = null;
|
||||
// previous entry is used to infer position in the rundown for new events
|
||||
let previousEntryId: MaybeString = null;
|
||||
let thisId: MaybeString = null;
|
||||
|
||||
let eventIndex = 0;
|
||||
// all events before the current selected are in the past
|
||||
let isPast = Boolean(featureData?.selectedEventId);
|
||||
let isNextDay = false;
|
||||
let totalGap = 0;
|
||||
// 1. gather presentation options
|
||||
const isEditMode = appMode === AppMode.Edit;
|
||||
let isLinkedToLoaded = true; //check if the event can link all the way back to the currently playing event
|
||||
|
||||
// 2. initialise rundown metadata
|
||||
const process = makeRundownMetadata(featureData?.selectedEventId);
|
||||
|
||||
return (
|
||||
<div className={style.rundownContainer} ref={scrollRef} data-testid='rundown'>
|
||||
@@ -292,64 +282,96 @@ export default function Rundown({ data }: RundownProps) {
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
if (index === 0) {
|
||||
eventIndex = 0;
|
||||
}
|
||||
isNextDay = false;
|
||||
previousEntryId = thisId;
|
||||
thisId = entryId;
|
||||
if (isOntimeEvent(entry)) {
|
||||
// event indexes are 1 based in frontend
|
||||
eventIndex++;
|
||||
lastEvent = thisEvent;
|
||||
|
||||
if (isPlayableEvent(entry)) {
|
||||
isNextDay = checkIsNextDay(entry, lastEvent);
|
||||
if (!isPast) {
|
||||
totalGap += entry.gap;
|
||||
// We also include countToEnd in this test as the behavior of a linked event coming after a countToEnd is simelar to an unlinked event
|
||||
isLinkedToLoaded = isLinkedToLoaded && entry.linkStart !== null && !lastEvent?.countToEnd;
|
||||
}
|
||||
if (isNewLatest(entry, lastEvent)) {
|
||||
// populate previous entry
|
||||
thisEvent = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
const rundownMeta = process(entry);
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === order.length - 1;
|
||||
const isLoaded = featureData?.selectedEventId === entry.id;
|
||||
const isNext = featureData?.nextEventId === entry.id;
|
||||
const hasCursor = entry.id === cursor;
|
||||
if (isLoaded) {
|
||||
isPast = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment key={entry.id}>
|
||||
{isEditMode && (hasCursor || isFirst) && <QuickAddBlock previousEventId={previousEntryId} />}
|
||||
<div className={style.entryWrapper} data-testid={`entry-${eventIndex}`}>
|
||||
{isOntimeEvent(entry) && <div className={style.entryIndex}>{eventIndex}</div>}
|
||||
<div className={style.entry} key={entry.id} ref={hasCursor ? cursorRef : undefined}>
|
||||
<RundownEntry
|
||||
type={entry.type}
|
||||
isPast={isPast}
|
||||
eventIndex={eventIndex}
|
||||
data={entry}
|
||||
loaded={isLoaded}
|
||||
hasCursor={hasCursor}
|
||||
isNext={isNext}
|
||||
previousEntryId={previousEntryId}
|
||||
previousEventId={lastEvent?.id}
|
||||
playback={isLoaded ? featureData.playback : undefined}
|
||||
isRolling={featureData.playback === Playback.Roll}
|
||||
isNextDay={isNextDay}
|
||||
totalGap={totalGap}
|
||||
isLinkedToLoaded={isLinkedToLoaded}
|
||||
/>
|
||||
{isEditMode && (hasCursor || isFirst) && (
|
||||
<QuickAddBlock showBlocks previousEventId={rundownMeta.previousEntryId} />
|
||||
)}
|
||||
{isOntimeBlock(entry) ? (
|
||||
<BlockBlock data={entry} hasCursor={hasCursor}>
|
||||
{entry.events.length === 0 && (
|
||||
<BlockEmpty handleAddNew={() => insertAtId(SupportedEvent.Event, cursor)} />
|
||||
)}
|
||||
{entry.events.map((eventId, nestedIndex) => {
|
||||
const nestedEntry = entries[eventId];
|
||||
const nestedRundownMeta = process(nestedEntry);
|
||||
const isFirstInGroup = nestedIndex === 0;
|
||||
const isLastInGroup = nestedIndex === entry.events.length - 1;
|
||||
const hasNestedCursor = nestedEntry.id === cursor;
|
||||
|
||||
if (!isOntimeEvent(nestedEntry)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Fragment key={nestedEntry.id}>
|
||||
{isEditMode && (hasNestedCursor || isFirstInGroup) && (
|
||||
<QuickAddBlock previousEventId={rundownMeta.previousEntryId} />
|
||||
)}
|
||||
|
||||
<div
|
||||
key={nestedEntry.id}
|
||||
className={style.entryWrapper}
|
||||
data-testid={`entry-${nestedRundownMeta.eventIndex}`}
|
||||
>
|
||||
<div className={style.entryIndex}>{nestedRundownMeta.eventIndex}</div>
|
||||
<div className={style.entry} ref={hasNestedCursor ? cursorRef : undefined}>
|
||||
<RundownEntry
|
||||
key={nestedEntry.id}
|
||||
type={nestedEntry.type}
|
||||
isPast={nestedRundownMeta.isPast}
|
||||
eventIndex={nestedRundownMeta.eventIndex}
|
||||
data={nestedEntry}
|
||||
loaded={nestedRundownMeta.isLoaded}
|
||||
hasCursor={hasNestedCursor}
|
||||
isNext={isNext}
|
||||
previousEntryId={nestedRundownMeta.previousEntryId}
|
||||
previousEventId={nestedRundownMeta.previousEvent?.id}
|
||||
playback={nestedRundownMeta.isLoaded ? featureData.playback : undefined}
|
||||
isRolling={featureData.playback === Playback.Roll}
|
||||
isNextDay={nestedRundownMeta.isNextDay}
|
||||
totalGap={nestedRundownMeta.totalGap}
|
||||
isLinkedToLoaded={nestedRundownMeta.isLinkedToLoaded}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isEditMode && (hasNestedCursor || isLastInGroup) && (
|
||||
<QuickAddBlock previousEventId={entry.id} />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</BlockBlock>
|
||||
) : (
|
||||
<div className={style.entryWrapper} data-testid={`entry-${rundownMeta.eventIndex}`}>
|
||||
{isOntimeEvent(entry) && <div className={style.entryIndex}>{rundownMeta.eventIndex}</div>}
|
||||
<div className={style.entry} key={entry.id} ref={hasCursor ? cursorRef : undefined}>
|
||||
<RundownEntry
|
||||
type={entry.type}
|
||||
isPast={rundownMeta.isPast}
|
||||
eventIndex={rundownMeta.eventIndex}
|
||||
data={entry}
|
||||
loaded={rundownMeta.isLoaded}
|
||||
hasCursor={hasCursor}
|
||||
isNext={isNext}
|
||||
previousEntryId={rundownMeta.previousEntryId}
|
||||
previousEventId={rundownMeta.previousEvent?.id}
|
||||
playback={rundownMeta.isLoaded ? featureData.playback : undefined}
|
||||
isRolling={featureData.playback === Playback.Roll}
|
||||
isNextDay={rundownMeta.isNextDay}
|
||||
totalGap={rundownMeta.totalGap}
|
||||
isLinkedToLoaded={rundownMeta.isLinkedToLoaded}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isEditMode && (hasCursor || isLast) && <QuickAddBlock previousEventId={entry.id} />}
|
||||
)}
|
||||
{isEditMode && (hasCursor || isLast) && <QuickAddBlock showBlocks previousEventId={entry.id} />}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Button } from '@chakra-ui/react';
|
||||
|
||||
import Empty from '../../common/components/state/Empty';
|
||||
|
||||
import style from './Rundown.module.scss';
|
||||
import style from './Empty.module.scss';
|
||||
|
||||
interface RundownEmptyProps {
|
||||
handleAddNew: () => void;
|
||||
@@ -13,9 +13,9 @@ export default function RundownEmpty(props: RundownEmptyProps) {
|
||||
const { handleAddNew } = props;
|
||||
|
||||
return (
|
||||
<div className={style.alignCenter}>
|
||||
<Empty style={{ marginTop: '7vh' }} />
|
||||
<Button onClick={handleAddNew} variant='ontime-filled' className={style.spaceTop} leftIcon={<IoAdd />}>
|
||||
<div className={style.empty}>
|
||||
<Empty style={{ marginTop: '7vh', marginBottom: '1.5rem' }} />
|
||||
<Button onClick={handleAddNew} variant='ontime-filled' leftIcon={<IoAdd />}>
|
||||
Create Event
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
isOntimeBlock,
|
||||
isOntimeDelay,
|
||||
isOntimeEvent,
|
||||
MaybeString,
|
||||
@@ -15,7 +14,6 @@ import useMemoisedFn from '../../common/hooks/useMemoisedFn';
|
||||
import { useEmitLog } from '../../common/stores/logger';
|
||||
import { cloneEvent } from '../../common/utils/eventsManager';
|
||||
|
||||
import BlockBlock from './block-block/BlockBlock';
|
||||
import DelayBlock from './delay-block/DelayBlock';
|
||||
import EventBlock from './event-block/EventBlock';
|
||||
import { useEventSelection } from './useEventSelection';
|
||||
@@ -193,14 +191,6 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
actionHandler={actionHandler}
|
||||
/>
|
||||
);
|
||||
} else if (isOntimeBlock(data)) {
|
||||
return (
|
||||
<BlockBlock data={data} hasCursor={hasCursor}>
|
||||
{data.events.map((eventId) => {
|
||||
return <div key={eventId}>{eventId}</div>;
|
||||
})}
|
||||
</BlockBlock>
|
||||
);
|
||||
} else if (isOntimeDelay(data)) {
|
||||
return <DelayBlock data={data} hasCursor={hasCursor} />;
|
||||
}
|
||||
|
||||
@@ -2,19 +2,75 @@
|
||||
|
||||
.block {
|
||||
@include block-styling;
|
||||
|
||||
background-color: $block-bg2;
|
||||
overflow: hidden;
|
||||
|
||||
min-width: 34rem;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 2rem 1fr auto;
|
||||
grid-template-areas:
|
||||
'binder header'
|
||||
'content content'
|
||||
'footer footer';
|
||||
align-items: center;
|
||||
height: $secondary-block-height;
|
||||
gap: 0.5rem;
|
||||
|
||||
&.hasCursor {
|
||||
outline: 1px solid $block-cursor-color;
|
||||
}
|
||||
|
||||
.binder {
|
||||
grid-area: binder;
|
||||
height: 100%;
|
||||
background-color: $gray-1050; // to override inline
|
||||
color: $section-white;
|
||||
font-size: 1rem;
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
grid-area: header;
|
||||
padding-inline: 0.5rem;
|
||||
background-color: $block-bg2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.titleRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.metaRow {
|
||||
display: flex;
|
||||
gap: 3rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.metaEntry {
|
||||
font-size: calc(1rem - 3px);
|
||||
width: 4.5em;
|
||||
|
||||
:first-child {
|
||||
color: $label-gray;
|
||||
}
|
||||
}
|
||||
|
||||
.group {
|
||||
background-color: color-mix(in srgb, var(--user-bg, $gray-1050) 10%, transparent 90%);
|
||||
grid-area: content;
|
||||
padding-right: 2px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.footer {
|
||||
grid-area: footer;
|
||||
background-color: var(--user-bg, $gray-1050) ;
|
||||
height: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.drag {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { PropsWithChildren, useRef } from 'react';
|
||||
import { IoReorderTwo } from 'react-icons/io5';
|
||||
import { IoChevronDown, IoChevronUp, IoReorderTwo } from 'react-icons/io5';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { useSessionStorage } from '@mantine/hooks';
|
||||
import { OntimeBlock } from 'ontime-types';
|
||||
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||
import { formatDuration, formatTime } from '../../../common/utils/time';
|
||||
import EditableBlockTitle from '../common/EditableBlockTitle';
|
||||
|
||||
import style from './BlockBlock.module.scss';
|
||||
@@ -16,7 +18,7 @@ interface BlockBlockProps {
|
||||
|
||||
export default function BlockBlock(props: PropsWithChildren<BlockBlockProps>) {
|
||||
const { data, hasCursor, children } = props;
|
||||
|
||||
const [collapsed, setCollapsed] = useSessionStorage<boolean>({ key: `block-${data.id}`, defaultValue: false });
|
||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||
|
||||
const {
|
||||
@@ -35,16 +37,54 @@ export default function BlockBlock(props: PropsWithChildren<BlockBlockProps>) {
|
||||
transition,
|
||||
};
|
||||
|
||||
const blockClasses = cx([style.block, hasCursor ? style.hasCursor : null]);
|
||||
const binderColours = data.colour && getAccessibleColour(data.colour);
|
||||
|
||||
return (
|
||||
<div className={blockClasses} ref={setNodeRef} style={dragStyle}>
|
||||
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
|
||||
<IoReorderTwo />
|
||||
</span>
|
||||
<EditableBlockTitle title={data.title} eventId={data.id} placeholder='Block title' />
|
||||
<button>+++</button>
|
||||
<div>{children}</div>
|
||||
<div
|
||||
className={cx([style.block, hasCursor && style.hasCursor])}
|
||||
ref={setNodeRef}
|
||||
style={{
|
||||
...(binderColours ? { '--user-bg': binderColours.backgroundColor } : {}),
|
||||
...dragStyle,
|
||||
}}
|
||||
>
|
||||
<div className={style.binder} style={{ ...binderColours }} tabIndex={-1}>
|
||||
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
|
||||
<IoReorderTwo />
|
||||
</span>
|
||||
</div>
|
||||
<div className={style.header}>
|
||||
<div className={style.titleRow}>
|
||||
<EditableBlockTitle title={data.title} eventId={data.id} placeholder='Block title' />
|
||||
<button onClick={() => setCollapsed((prev) => !prev)}>
|
||||
{collapsed ? <IoChevronUp /> : <IoChevronDown />}
|
||||
</button>
|
||||
</div>
|
||||
<div className={style.metaRow}>
|
||||
<div className={style.metaEntry}>
|
||||
<div>Start</div>
|
||||
<div>{formatTime(data.startTime)}</div>
|
||||
</div>
|
||||
<div className={style.metaEntry}>
|
||||
<div>End</div>
|
||||
<div>{formatTime(data.endTime)}</div>
|
||||
</div>
|
||||
<div className={style.metaEntry}>
|
||||
<div>Duration</div>
|
||||
<div>{formatDuration(data.duration)}</div>
|
||||
</div>
|
||||
<div className={style.metaEntry}>
|
||||
<div>Events</div>
|
||||
<div>{data.numEvents}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div className={style.group} style={binderColours ? { '--user-bg': binderColours.backgroundColor } : {}}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
<div className={style.footer} style={binderColours ? { '--user-bg': binderColours.backgroundColor } : {}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from 'react-icons/io5';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { EndAction, MaybeString, OntimeEvent, Playback, TimerType, TimeStrategy } from 'ontime-types';
|
||||
import { EndAction, OntimeEvent, Playback, TimerType, TimeStrategy } from 'ontime-types';
|
||||
|
||||
import { useContextMenu } from '../../../common/hooks/useContextMenu';
|
||||
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||
@@ -32,7 +32,7 @@ interface EventBlockProps {
|
||||
timeEnd: number;
|
||||
duration: number;
|
||||
timeStrategy: TimeStrategy;
|
||||
linkStart: MaybeString;
|
||||
linkStart: boolean;
|
||||
countToEnd: boolean;
|
||||
eventIndex: number;
|
||||
isPublic: boolean;
|
||||
@@ -151,7 +151,7 @@ export default function EventBlock(props: EventBlockProps) {
|
||||
onClick: () =>
|
||||
actionHandler('update', {
|
||||
field: 'linkStart',
|
||||
value: linkStart ? null : 'true',
|
||||
value: linkStart,
|
||||
}),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
IoTime,
|
||||
} from 'react-icons/io5';
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { EndAction, MaybeString, Playback, TimerType, TimeStrategy } from 'ontime-types';
|
||||
import { EndAction, Playback, TimerType, TimeStrategy } from 'ontime-types';
|
||||
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
@@ -30,7 +30,7 @@ interface EventBlockInnerProps {
|
||||
timeEnd: number;
|
||||
duration: number;
|
||||
timeStrategy: TimeStrategy;
|
||||
linkStart: MaybeString;
|
||||
linkStart: boolean;
|
||||
countToEnd: boolean;
|
||||
eventIndex: number;
|
||||
isPublic: boolean;
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function RundownEventEditor() {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedEventId = data.order.find((entryId) => selectedEvents.has(entryId));
|
||||
const selectedEventId = Array.from(selectedEvents).at(0);
|
||||
if (!selectedEventId) {
|
||||
setEvent(null);
|
||||
return;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { memo } from 'react';
|
||||
import { IoInformationCircle } from 'react-icons/io5';
|
||||
import { Select, Switch, Tooltip } from '@chakra-ui/react';
|
||||
import { EndAction, MaybeString, TimerType, TimeStrategy } from 'ontime-types';
|
||||
import { EndAction, TimerType, TimeStrategy } from 'ontime-types';
|
||||
import { millisToString, parseUserTime } from 'ontime-utils';
|
||||
|
||||
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
|
||||
@@ -18,7 +18,7 @@ interface EventEditorTimesProps {
|
||||
timeEnd: number;
|
||||
duration: number;
|
||||
timeStrategy: TimeStrategy;
|
||||
linkStart: MaybeString;
|
||||
linkStart: boolean;
|
||||
countToEnd: boolean;
|
||||
delay: number;
|
||||
isPublic: boolean;
|
||||
|
||||
@@ -10,12 +10,13 @@ import style from './QuickAddBlock.module.scss';
|
||||
|
||||
interface QuickAddBlockProps {
|
||||
previousEventId: MaybeString;
|
||||
showBlocks?: boolean;
|
||||
}
|
||||
|
||||
export default memo(QuickAddBlock);
|
||||
|
||||
function QuickAddBlock(props: QuickAddBlockProps) {
|
||||
const { previousEventId } = props;
|
||||
const { previousEventId, showBlocks } = props;
|
||||
const { addEvent } = useEventAction();
|
||||
const { emitError } = useEmitLog();
|
||||
|
||||
@@ -86,16 +87,18 @@ function QuickAddBlock(props: QuickAddBlockProps) {
|
||||
>
|
||||
Delay
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleCreateEvent(SupportedEvent.Block)}
|
||||
size='xs'
|
||||
variant='ontime-subtle-white'
|
||||
className={style.quickBtn}
|
||||
leftIcon={<IoAdd />}
|
||||
color='#b1b1b1' // $gray-400
|
||||
>
|
||||
Block
|
||||
</Button>
|
||||
{showBlocks && (
|
||||
<Button
|
||||
onClick={() => handleCreateEvent(SupportedEvent.Block)}
|
||||
size='xs'
|
||||
variant='ontime-subtle-white'
|
||||
className={style.quickBtn}
|
||||
leftIcon={<IoAdd />}
|
||||
color='#b1b1b1' // $gray-400
|
||||
>
|
||||
Block
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { isOntimeEvent, isPlayableEvent, MaybeString, OntimeEntry, PlayableEvent } from 'ontime-types';
|
||||
import { checkIsNextDay, isNewLatest } from 'ontime-utils';
|
||||
|
||||
type RundownMetadata = {
|
||||
previousEvent: PlayableEvent | null; // The playableEvent from the previous iteration, used by indicators
|
||||
latestEvent: PlayableEvent | null; // The playableEvent most forwards in time processed so far
|
||||
previousEntryId: MaybeString; // previous entry is used to infer position in the rundown for new events
|
||||
thisId: MaybeString;
|
||||
eventIndex: number;
|
||||
isPast: boolean;
|
||||
isNext: boolean;
|
||||
isNextDay: boolean;
|
||||
totalGap: number;
|
||||
isLinkedToLoaded: boolean; // check if the event can link all the way back to the currently playing event
|
||||
isLoaded: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a process function which aggregates the rundown metadata and event metadata
|
||||
*/
|
||||
export function makeRundownMetadata(selectedEventId: MaybeString) {
|
||||
let rundownMeta: RundownMetadata = {
|
||||
previousEvent: null,
|
||||
latestEvent: null,
|
||||
previousEntryId: null,
|
||||
thisId: null,
|
||||
eventIndex: 0,
|
||||
isPast: Boolean(selectedEventId), // all events before the current selected are in the past
|
||||
isNext: false,
|
||||
isNextDay: false,
|
||||
totalGap: 0,
|
||||
isLinkedToLoaded: true,
|
||||
isLoaded: false,
|
||||
};
|
||||
|
||||
function process(entry: OntimeEntry): Readonly<RundownMetadata> {
|
||||
const processedRundownMetadata = processEntry(rundownMeta, selectedEventId, entry);
|
||||
rundownMeta = processedRundownMetadata;
|
||||
return rundownMeta;
|
||||
}
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a rundown entry and processes its place in the rundown
|
||||
*
|
||||
*/
|
||||
function processEntry(
|
||||
rundownMetadata: RundownMetadata,
|
||||
selectedEventId: MaybeString,
|
||||
entry: Readonly<OntimeEntry>,
|
||||
): Readonly<RundownMetadata> {
|
||||
const processedData = { ...rundownMetadata };
|
||||
processedData.isNextDay = false;
|
||||
processedData.isLoaded = false;
|
||||
processedData.previousEntryId = processedData.thisId;
|
||||
processedData.thisId = entry.id;
|
||||
|
||||
if (entry.id === selectedEventId) {
|
||||
processedData.isLoaded = true;
|
||||
processedData.isPast = false;
|
||||
}
|
||||
|
||||
if (isOntimeEvent(entry)) {
|
||||
// event indexes are 1 based in UI
|
||||
processedData.eventIndex += 1;
|
||||
processedData.previousEvent = processedData.latestEvent;
|
||||
|
||||
if (isPlayableEvent(entry)) {
|
||||
processedData.isNextDay = checkIsNextDay(entry, processedData.previousEvent);
|
||||
|
||||
if (!processedData.isPast) {
|
||||
processedData.totalGap += entry.gap;
|
||||
/**
|
||||
* isLinkToLoaded is a chain value that we maintain until we find an unlinked event
|
||||
* or we find a countToEnd event
|
||||
*/
|
||||
processedData.isLinkedToLoaded =
|
||||
processedData.isLinkedToLoaded && entry.linkStart && !processedData.previousEvent?.countToEnd;
|
||||
}
|
||||
|
||||
if (isNewLatest(entry, processedData.previousEvent)) {
|
||||
// this event is the forward most event in rundown, for next iteration
|
||||
processedData.latestEvent = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return processedData;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { memo } from 'react';
|
||||
import { IoAlertCircleOutline, IoLink, IoLockClosed, IoLockOpenOutline, IoUnlink } from 'react-icons/io5';
|
||||
import { InputRightElement, Tooltip } from '@chakra-ui/react';
|
||||
import { MaybeString, TimeField, TimeStrategy } from 'ontime-types';
|
||||
import { TimeField, TimeStrategy } from 'ontime-types';
|
||||
import { dayInMs } from 'ontime-utils';
|
||||
|
||||
import TimeInputWithButton from '../../../common/components/input/time-input/TimeInputWithButton';
|
||||
@@ -19,7 +19,7 @@ interface EventBlockTimerProps {
|
||||
timeEnd: number;
|
||||
duration: number;
|
||||
timeStrategy: TimeStrategy;
|
||||
linkStart: MaybeString;
|
||||
linkStart: boolean;
|
||||
delay: number;
|
||||
showLabels?: boolean;
|
||||
}
|
||||
@@ -38,7 +38,7 @@ function TimeInputFlow(props: EventBlockTimerProps) {
|
||||
};
|
||||
|
||||
const handleLink = (doLink: boolean) => {
|
||||
updateEvent({ id: eventId, linkStart: doLink ? 'true' : null });
|
||||
updateEvent({ id: eventId, linkStart: doLink });
|
||||
};
|
||||
|
||||
const warnings = [];
|
||||
@@ -55,9 +55,9 @@ function TimeInputFlow(props: EventBlockTimerProps) {
|
||||
const isLockedEnd = timeStrategy === TimeStrategy.LockEnd;
|
||||
const isLockedDuration = timeStrategy === TimeStrategy.LockDuration;
|
||||
|
||||
const activeStart = cx([style.timeAction, linkStart ? style.active : null]);
|
||||
const activeEnd = cx([style.timeAction, isLockedEnd ? style.active : null]);
|
||||
const activeDuration = cx([style.timeAction, isLockedDuration ? style.active : null]);
|
||||
const activeStart = cx([style.timeAction, linkStart && style.active]);
|
||||
const activeEnd = cx([style.timeAction, isLockedEnd && style.active]);
|
||||
const activeDuration = cx([style.timeAction, isLockedDuration && style.active]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -69,7 +69,7 @@ function TimeInputFlow(props: EventBlockTimerProps) {
|
||||
time={timeStart}
|
||||
hasDelay={hasDelay}
|
||||
placeholder='Start'
|
||||
disabled={Boolean(linkStart)}
|
||||
disabled={linkStart}
|
||||
>
|
||||
<Tooltip label='Link start to previous end' openDelay={tooltipDelayMid}>
|
||||
<InputRightElement className={activeStart} onClick={() => handleLink(!linkStart)}>
|
||||
|
||||
Reference in New Issue
Block a user