refactor: inline quick add

This commit is contained in:
Carlos Valente
2025-07-08 07:24:41 +02:00
committed by Carlos Valente
parent a1ad1d47a4
commit 1fc6f9b3ea
18 changed files with 235 additions and 107 deletions
@@ -5,8 +5,6 @@
.rundownContainer {
margin-top: 1rem;
display: flex;
flex-direction: column;
overflow-y: scroll;
height: 100%;
}
+15 -23
View File
@@ -39,7 +39,8 @@ import { useEntryCopy } from '../../common/stores/entryCopyStore';
import { cloneEvent } from '../../common/utils/clone';
import { AppMode, sessionKeys } from '../../ontimeConfig';
import QuickAddBlock from './quick-add-block/QuickAddBlock';
import QuickAddButtons from './entry-editor/quick-add-buttons/QuickAddButtons';
import QuickAddInline from './entry-editor/quick-add-cursor/QuickAddInline';
import BlockEnd from './rundown-block/BlockEnd';
import RundownBlock from './rundown-block/RundownBlock';
import { makeRundownMetadata, makeSortableList } from './rundown.utils';
@@ -398,6 +399,7 @@ export default function Rundown({ data }: RundownProps) {
>
<SortableContext items={sortableData} strategy={verticalListSortingStrategy}>
<div className={style.list}>
{isEditMode && <QuickAddButtons previousEventId={null} parentBlock={null} />}
{sortableData.map((entryId, index) => {
const isFirst = index === 0;
const isLast = index === sortableData.length - 1;
@@ -407,27 +409,19 @@ export default function Rundown({ data }: RundownProps) {
const parentId = entryId.split('end-')[1];
const isBlockCollapsed = getIsCollapsed(parentId);
if (isBlockCollapsed && isEditMode && isLast) {
return <QuickAddBlock key={entryId} previousEventId={parentId} parentBlock={null} />;
} else if (isBlockCollapsed) {
if (isBlockCollapsed) {
return null;
} else {
const parentColour = (entries[parentId] as OntimeBlock | undefined)?.colour;
// if the previous element is selected, it will have its own QuickAddBlock
// if the previous element is selected, it will have its own QuickAddInline
// we use thisId instead of previousEntryId because the block end does not process
// and it does not cause the reassignment of the iteration id to the previous entry
const showPrependingQuickAdd = isEditMode && cursor !== rundownMetadata.thisId;
return (
<Fragment key={entryId}>
{showPrependingQuickAdd && (
<QuickAddBlock
previousEventId={rundownMetadata.thisId}
parentBlock={parentId}
backgroundColor={parentColour}
/>
{isEditMode && rundownMetadata.groupEntries === 0 && (
<QuickAddButtons previousEventId={null} parentBlock={parentId} backgroundColor={parentColour} />
)}
<BlockEnd key={entryId} id={entryId} colour={parentColour} />
{isEditMode && isLast && <QuickAddBlock previousEventId={parentId} parentBlock={null} />}
</Fragment>
);
}
@@ -462,11 +456,10 @@ export default function Rundown({ data }: RundownProps) {
return (
<Fragment key={entry.id}>
{isEditMode && (hasCursor || isFirst) && (
<QuickAddBlock
{isEditMode && hasCursor && !isFirst && (
<QuickAddInline
previousEventId={rundownMetadata.previousEntryId}
parentBlock={isFirst ? null : rundownMetadata.groupId}
backgroundColor={isFirst ? undefined : blockColour}
parentBlock={rundownMetadata.thisId !== rundownMetadata.groupId ? rundownMetadata.groupId : null}
/>
)}
{isOntimeBlock(entry) ? (
@@ -503,16 +496,15 @@ export default function Rundown({ data }: RundownProps) {
</div>
</div>
)}
{isEditMode && (hasCursor || isLast) && (
<QuickAddBlock
previousEventId={entry.id}
parentBlock={rundownMetadata.groupId}
backgroundColor={blockColour}
/>
{isEditMode && hasCursor && rundownMetadata.groupEntries !== 0 && !isLast && (
<QuickAddInline previousEventId={entry.id} parentBlock={rundownMetadata.groupId} />
)}
</Fragment>
);
})}
{isEditMode && (
<QuickAddButtons previousEventId={rundownMetadata.groupId ?? rundownMetadata.thisId} parentBlock={null} />
)}
<div className={style.spacer} />
</div>
</SortableContext>
@@ -95,6 +95,7 @@ describe('makeRundownMetadata()', () => {
isLoaded: false,
groupId: null,
groupColour: undefined,
groupEntries: undefined,
});
expect(process(demoEvents['1'])).toStrictEqual({
@@ -110,6 +111,7 @@ describe('makeRundownMetadata()', () => {
isLoaded: false,
groupId: null,
groupColour: undefined,
groupEntries: undefined,
});
expect(process(demoEvents['block'])).toMatchObject({
@@ -251,6 +253,7 @@ describe('makeRundownMetadata()', () => {
isLoaded: false,
groupId: rundownStartsWithBlock.block.id,
groupColour: 'red',
groupEntries: 2,
});
expect(process(rundownStartsWithBlock['1'])).toStrictEqual({
@@ -266,6 +269,7 @@ describe('makeRundownMetadata()', () => {
isLoaded: false,
groupId: rundownStartsWithBlock.block.id,
groupColour: 'red',
groupEntries: 2,
});
expect(process(rundownStartsWithBlock['2'])).toStrictEqual({
previousEvent: rundownStartsWithBlock['1'],
@@ -280,6 +284,7 @@ describe('makeRundownMetadata()', () => {
isLoaded: false,
groupId: rundownStartsWithBlock.block.id,
groupColour: 'red',
groupEntries: 2,
});
});
});
@@ -4,6 +4,9 @@
gap: 1rem;
padding-block: 0.5rem;
padding-left: calc(2em + 0.5rem);
background: color-mix(in srgb, transparent 90%, var(--user-bg, transparent) 10%);
&.indent {
padding-left: calc(2em + 0.5rem);
}
}
@@ -1,25 +1,24 @@
import { memo, useRef } from 'react';
import { memo } from 'react';
import { IoAdd } from 'react-icons/io5';
import { Toolbar } from '@base-ui-components/react/toolbar';
import { MaybeString, SupportedEntry } from 'ontime-types';
import Button from '../../../common/components/buttons/Button';
import { useEntryActions } from '../../../common/hooks/useEntryAction';
import Button from '../../../../common/components/buttons/Button';
import { useEntryActions } from '../../../../common/hooks/useEntryAction';
import { cx } from '../../../../common/utils/styleUtils';
import style from './QuickAddBlock.module.scss';
import style from './QuickAddButtons.module.scss';
interface QuickAddBlockProps {
interface QuickAddButtonsProps {
previousEventId: MaybeString;
parentBlock: MaybeString;
backgroundColor?: string;
}
export default memo(QuickAddBlock);
function QuickAddBlock({ previousEventId, parentBlock, backgroundColor }: QuickAddBlockProps) {
export default memo(QuickAddButtons);
function QuickAddButtons({ previousEventId, parentBlock, backgroundColor }: QuickAddButtonsProps) {
const { addEntry } = useEntryActions();
const doLinkPrevious = useRef<HTMLInputElement | null>(null);
const addEvent = () => {
addEntry(
{
@@ -29,7 +28,6 @@ function QuickAddBlock({ previousEventId, parentBlock, backgroundColor }: QuickA
{
after: previousEventId,
lastEventId: previousEventId,
linkPrevious: doLinkPrevious?.current?.checked,
},
);
};
@@ -75,26 +73,30 @@ function QuickAddBlock({ previousEventId, parentBlock, backgroundColor }: QuickA
const blockColour = backgroundColor === '' ? '#9d9d9d' : backgroundColor;
return (
<Toolbar.Root className={style.quickAdd} style={blockColour ? { '--user-bg': blockColour } : {}}>
<Toolbar.Button render={<Button size='small' variant='subtle-white' />} onClick={addEvent}>
<Toolbar.Root
className={cx([style.quickAdd, Boolean(parentBlock) && style.indent])}
style={blockColour ? { '--user-bg': blockColour } : {}}
data-testid='quick-add-buttons'
>
<Toolbar.Button render={<Button size='small' />} onClick={addEvent}>
<IoAdd />
Event
</Toolbar.Button>
<Toolbar.Button render={<Button size='small' variant='subtle-white' />} onClick={addDelay}>
<Toolbar.Button render={<Button size='small' />} onClick={addDelay}>
<IoAdd />
Delay
</Toolbar.Button>
<Toolbar.Button render={<Button size='small' variant='subtle-white' />} onClick={addMilestone}>
<Toolbar.Button render={<Button size='small' />} onClick={addMilestone}>
<IoAdd />
Milestone
</Toolbar.Button>
{parentBlock === null && (
<Toolbar.Button render={<Button size='small' variant='subtle-white' />} onClick={addBlock}>
<Toolbar.Button render={<Button size='small' />} onClick={addBlock}>
<IoAdd />
Block
Group
</Toolbar.Button>
)}
</Toolbar.Root>
@@ -0,0 +1,16 @@
.quickAdd {
display: flex;
align-items: center;
gap: 1rem;
position: relative;
height: 1px;
background: $blue-500;
}
.addButton {
position: absolute;
top: 0;
left: 0;
transform: translateY(-50%);
}
@@ -0,0 +1,81 @@
import { memo } from 'react';
import { IoAdd } from 'react-icons/io5';
import { MaybeString, SupportedEntry } from 'ontime-types';
import IconButton from '../../../../common/components/buttons/IconButton';
import { DropdownMenu } from '../../../../common/components/dropdown-menu/DropdownMenu';
import { useEntryActions } from '../../../../common/hooks/useEntryAction';
import style from './QuickAddInline.module.scss';
interface QuickAddInlineProps {
previousEventId: MaybeString;
parentBlock: MaybeString;
}
export default memo(QuickAddInline);
function QuickAddInline({ previousEventId, parentBlock }: QuickAddInlineProps) {
const { addEntry } = useEntryActions();
const addEvent = () => {
addEntry(
{
type: SupportedEntry.Event,
parent: parentBlock,
},
{
after: previousEventId,
lastEventId: previousEventId,
},
);
};
const addDelay = () => {
addEntry(
{ type: SupportedEntry.Delay, parent: parentBlock },
{
lastEventId: previousEventId,
after: previousEventId,
},
);
};
const addMilestone = () => {
addEntry(
{ type: SupportedEntry.Milestone, parent: parentBlock },
{
lastEventId: previousEventId,
after: previousEventId,
},
);
};
const addBlock = () => {
if (parentBlock !== null) {
return;
}
addEntry(
{ type: SupportedEntry.Block },
{
lastEventId: previousEventId,
after: previousEventId,
},
);
};
return (
<div className={style.quickAdd} data-testid='quick-add-inline'>
<DropdownMenu
items={[
{ type: 'item', icon: <IoAdd />, label: 'Add Event', onClick: addEvent },
{ type: 'item', icon: <IoAdd />, label: 'Add Delay', onClick: addDelay },
{ type: 'item', icon: <IoAdd />, label: 'Add Milestone', onClick: addMilestone },
{ type: 'item', icon: <IoAdd />, label: 'Add Group', onClick: addBlock, disabled: parentBlock !== null },
]}
render={<IconButton size='small' variant='primary' className={style.addButton} />}
>
<IoAdd />
</DropdownMenu>
</div>
);
}
@@ -1,12 +1,12 @@
import { useMemo } from 'react';
import { IoCheckmarkCircle } from 'react-icons/io5';
import { isPlaybackActive, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils';
import { isPlaybackActive, MILLIS_PER_MINUTE, MILLIS_PER_SECOND, millisToString } from 'ontime-utils';
import Tooltip from '../../../../common/components/tooltip/Tooltip';
import { usePlayback } from '../../../../common/hooks/useSocket';
import useReport from '../../../../common/hooks-query/useReport';
import { cx } from '../../../../common/utils/styleUtils';
import { formatDuration, formatTime, useTimeUntilStart } from '../../../../common/utils/time';
import { formatDuration, useTimeUntilStart } from '../../../../common/utils/time';
import style from './RundownEventChip.module.scss';
@@ -115,7 +115,7 @@ function EventReport(props: EventReportProps) {
const isOver = difference > 0;
const fullTimeValue = formatTime(absDifference);
const fullTimeValue = millisToString(absDifference);
const tooltip = `Event ran ${isOver ? 'over' : 'under'} time by ${fullTimeValue}`;
@@ -1,5 +1,5 @@
.header {
padding-inline: calc(2.5rem - 6px) 2rem;
padding-inline: 1rem 2rem;
display: flex;
align-items: center;
@@ -9,7 +9,7 @@
background-color: color-mix(in srgb, var(--user-bg, $block-bg) 15%, transparent 85%);
display: grid;
grid-template-columns: 2rem 8rem 1fr auto auto;
grid-template-columns: 2rem 8rem 1fr auto;
align-items: center;
height: $secondary-block-height;
gap: 0.5rem;
@@ -1,5 +1,5 @@
import { MouseEvent, useCallback, useRef } from 'react';
import { IoCheckmarkDone, IoClose, IoReorderTwo } from 'react-icons/io5';
import { IoClose, IoReorderTwo } from 'react-icons/io5';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { EntryId } from 'ontime-types';
@@ -92,9 +92,6 @@ export default function RundownMilestone({ colour, cue, entryId, hasCursor, titl
</div>
<MilestoneTextInput field='cue' initialValue={cue} placeholder='Cue' submitHandler={handleUpdate} />
<MilestoneTextInput field='title' initialValue={title} placeholder='Title' submitHandler={handleUpdate} />
<Button variant='ghosted-white'>
<IoCheckmarkDone /> Done
</Button>
<Button variant='ghosted-destructive' onClick={handleDelete}>
<IoClose /> Cancel
</Button>
@@ -27,6 +27,7 @@ type RundownMetadata = {
isLoaded: boolean;
groupId: MaybeString;
groupColour: string | undefined;
groupEntries: number | undefined;
};
/**
@@ -46,6 +47,7 @@ export function makeRundownMetadata(selectedEventId: MaybeString) {
isLoaded: false,
groupId: null,
groupColour: undefined,
groupEntries: undefined,
};
function process(entry: OntimeEntry): Readonly<RundownMetadata> {
@@ -82,11 +84,13 @@ function processEntry(
if (isOntimeBlock(entry)) {
processedData.groupId = entry.id;
processedData.groupColour = entry.colour;
processedData.groupEntries = entry.entries.length;
} else {
// for delays and blocks, we insert the group metadata
if ((entry as OntimeEvent | OntimeDelay | OntimeMilestone).parent !== processedData.groupId) {
// if the parent is not the current group, we need to update the groupId
processedData.groupId = (entry as OntimeEvent | OntimeDelay | OntimeMilestone).parent;
processedData.groupEntries = undefined;
if ((entry as OntimeEvent | OntimeDelay | OntimeMilestone).parent === null) {
// if the entry has no parent, it cannot have a group colour
processedData.groupColour = undefined;