mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-05 22:39:11 +00:00
refactor: inline quick add
This commit is contained in:
committed by
Carlos Valente
parent
a1ad1d47a4
commit
1fc6f9b3ea
@@ -0,0 +1,80 @@
|
|||||||
|
import { PropsWithChildren, ReactNode } from 'react';
|
||||||
|
import { Menu as BaseMenu } from '@base-ui-components/react/menu';
|
||||||
|
|
||||||
|
import style from './DropdownMenu.module.scss';
|
||||||
|
|
||||||
|
type DropdownMenuItemDivider = { type: 'divider' };
|
||||||
|
type DropdownMenuItem = {
|
||||||
|
type: 'item';
|
||||||
|
label: string;
|
||||||
|
icon?: ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DropdownMenuProps extends BaseMenu.Trigger.Props {
|
||||||
|
items: Array<DropdownMenuItemDivider | DropdownMenuItem>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DropdownMenu({ items, children, ...triggerProps }: PropsWithChildren<DropdownMenuProps>) {
|
||||||
|
return (
|
||||||
|
<BaseMenu.Root>
|
||||||
|
<BaseMenu.Trigger {...triggerProps}>{children}</BaseMenu.Trigger>
|
||||||
|
<BaseMenu.Portal>
|
||||||
|
<BaseMenu.Positioner className={style.positioner} align='start' sideOffset={8}>
|
||||||
|
<BaseMenu.Popup className={style.popup}>
|
||||||
|
{items.map((item, index) => {
|
||||||
|
if (item.type === 'divider') {
|
||||||
|
return <BaseMenu.Separator key={index} className={style.separator} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<BaseMenu.Item key={index} className={style.item} onClick={item.onClick} disabled={item.disabled}>
|
||||||
|
{item.icon} {item.label}
|
||||||
|
</BaseMenu.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</BaseMenu.Popup>
|
||||||
|
</BaseMenu.Positioner>
|
||||||
|
</BaseMenu.Portal>
|
||||||
|
</BaseMenu.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PositionedDropdownMenuProps {
|
||||||
|
items: Array<DropdownMenuItemDivider | DropdownMenuItem>;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
position: { x: number; y: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PositionedDropdownMenu({ items, isOpen, position, onClose }: PositionedDropdownMenuProps) {
|
||||||
|
return (
|
||||||
|
<BaseMenu.Root
|
||||||
|
open={isOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BaseMenu.Trigger
|
||||||
|
style={{ position: 'absolute', left: position.x, top: position.y, pointerEvents: 'none' }}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<BaseMenu.Portal>
|
||||||
|
<BaseMenu.Positioner className={style.positioner} align='start' sideOffset={8}>
|
||||||
|
<BaseMenu.Popup className={style.popup}>
|
||||||
|
{items.map((item, index) => {
|
||||||
|
if (item.type === 'divider') {
|
||||||
|
return <BaseMenu.Separator key={index} className={style.separator} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<BaseMenu.Item key={index} className={style.item} onClick={item.onClick} disabled={item.disabled}>
|
||||||
|
{item.icon} {item.label}
|
||||||
|
</BaseMenu.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</BaseMenu.Popup>
|
||||||
|
</BaseMenu.Positioner>
|
||||||
|
</BaseMenu.Portal>
|
||||||
|
</BaseMenu.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
import { Menu as BaseMenu } from '@base-ui-components/react/menu';
|
|
||||||
|
|
||||||
import style from './Menu.module.scss';
|
|
||||||
|
|
||||||
type MenuItemDivider = { type: 'divider' };
|
|
||||||
type MenuItem = {
|
|
||||||
type: 'item';
|
|
||||||
label: string;
|
|
||||||
icon?: ReactNode;
|
|
||||||
disabled?: boolean;
|
|
||||||
onClick: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface MenuProps {
|
|
||||||
items: Array<MenuItemDivider | MenuItem>;
|
|
||||||
isOpen: boolean;
|
|
||||||
position: { x: number; y: number };
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Menu({ items, isOpen, position, onClose }: MenuProps) {
|
|
||||||
return (
|
|
||||||
<BaseMenu.Root
|
|
||||||
open={isOpen}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<BaseMenu.Trigger
|
|
||||||
style={{ position: 'absolute', left: position.x, top: position.y, pointerEvents: 'none' }}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
<BaseMenu.Portal>
|
|
||||||
<BaseMenu.Positioner className={style.positioner} align='start' sideOffset={8}>
|
|
||||||
<BaseMenu.Popup className={style.popup}>
|
|
||||||
{items.map((item, index) => {
|
|
||||||
if (item.type === 'divider') {
|
|
||||||
return <BaseMenu.Separator key={index} className={style.separator} />;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<BaseMenu.Item key={index} className={style.item} onClick={item.onClick} disabled={item.disabled}>
|
|
||||||
{item.icon} {item.label}
|
|
||||||
</BaseMenu.Item>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</BaseMenu.Popup>
|
|
||||||
</BaseMenu.Positioner>
|
|
||||||
</BaseMenu.Portal>
|
|
||||||
</BaseMenu.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -5,8 +5,6 @@
|
|||||||
|
|
||||||
.rundownContainer {
|
.rundownContainer {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ import { useEntryCopy } from '../../common/stores/entryCopyStore';
|
|||||||
import { cloneEvent } from '../../common/utils/clone';
|
import { cloneEvent } from '../../common/utils/clone';
|
||||||
import { AppMode, sessionKeys } from '../../ontimeConfig';
|
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 BlockEnd from './rundown-block/BlockEnd';
|
||||||
import RundownBlock from './rundown-block/RundownBlock';
|
import RundownBlock from './rundown-block/RundownBlock';
|
||||||
import { makeRundownMetadata, makeSortableList } from './rundown.utils';
|
import { makeRundownMetadata, makeSortableList } from './rundown.utils';
|
||||||
@@ -398,6 +399,7 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
>
|
>
|
||||||
<SortableContext items={sortableData} strategy={verticalListSortingStrategy}>
|
<SortableContext items={sortableData} strategy={verticalListSortingStrategy}>
|
||||||
<div className={style.list}>
|
<div className={style.list}>
|
||||||
|
{isEditMode && <QuickAddButtons previousEventId={null} parentBlock={null} />}
|
||||||
{sortableData.map((entryId, index) => {
|
{sortableData.map((entryId, index) => {
|
||||||
const isFirst = index === 0;
|
const isFirst = index === 0;
|
||||||
const isLast = index === sortableData.length - 1;
|
const isLast = index === sortableData.length - 1;
|
||||||
@@ -407,27 +409,19 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
const parentId = entryId.split('end-')[1];
|
const parentId = entryId.split('end-')[1];
|
||||||
const isBlockCollapsed = getIsCollapsed(parentId);
|
const isBlockCollapsed = getIsCollapsed(parentId);
|
||||||
|
|
||||||
if (isBlockCollapsed && isEditMode && isLast) {
|
if (isBlockCollapsed) {
|
||||||
return <QuickAddBlock key={entryId} previousEventId={parentId} parentBlock={null} />;
|
|
||||||
} else if (isBlockCollapsed) {
|
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
const parentColour = (entries[parentId] as OntimeBlock | undefined)?.colour;
|
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
|
// 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
|
// and it does not cause the reassignment of the iteration id to the previous entry
|
||||||
const showPrependingQuickAdd = isEditMode && cursor !== rundownMetadata.thisId;
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={entryId}>
|
<Fragment key={entryId}>
|
||||||
{showPrependingQuickAdd && (
|
{isEditMode && rundownMetadata.groupEntries === 0 && (
|
||||||
<QuickAddBlock
|
<QuickAddButtons previousEventId={null} parentBlock={parentId} backgroundColor={parentColour} />
|
||||||
previousEventId={rundownMetadata.thisId}
|
|
||||||
parentBlock={parentId}
|
|
||||||
backgroundColor={parentColour}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<BlockEnd key={entryId} id={entryId} colour={parentColour} />
|
<BlockEnd key={entryId} id={entryId} colour={parentColour} />
|
||||||
{isEditMode && isLast && <QuickAddBlock previousEventId={parentId} parentBlock={null} />}
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -462,11 +456,10 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={entry.id}>
|
<Fragment key={entry.id}>
|
||||||
{isEditMode && (hasCursor || isFirst) && (
|
{isEditMode && hasCursor && !isFirst && (
|
||||||
<QuickAddBlock
|
<QuickAddInline
|
||||||
previousEventId={rundownMetadata.previousEntryId}
|
previousEventId={rundownMetadata.previousEntryId}
|
||||||
parentBlock={isFirst ? null : rundownMetadata.groupId}
|
parentBlock={rundownMetadata.thisId !== rundownMetadata.groupId ? rundownMetadata.groupId : null}
|
||||||
backgroundColor={isFirst ? undefined : blockColour}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isOntimeBlock(entry) ? (
|
{isOntimeBlock(entry) ? (
|
||||||
@@ -503,16 +496,15 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isEditMode && (hasCursor || isLast) && (
|
{isEditMode && hasCursor && rundownMetadata.groupEntries !== 0 && !isLast && (
|
||||||
<QuickAddBlock
|
<QuickAddInline previousEventId={entry.id} parentBlock={rundownMetadata.groupId} />
|
||||||
previousEventId={entry.id}
|
|
||||||
parentBlock={rundownMetadata.groupId}
|
|
||||||
backgroundColor={blockColour}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{isEditMode && (
|
||||||
|
<QuickAddButtons previousEventId={rundownMetadata.groupId ?? rundownMetadata.thisId} parentBlock={null} />
|
||||||
|
)}
|
||||||
<div className={style.spacer} />
|
<div className={style.spacer} />
|
||||||
</div>
|
</div>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ describe('makeRundownMetadata()', () => {
|
|||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
groupId: null,
|
groupId: null,
|
||||||
groupColour: undefined,
|
groupColour: undefined,
|
||||||
|
groupEntries: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(process(demoEvents['1'])).toStrictEqual({
|
expect(process(demoEvents['1'])).toStrictEqual({
|
||||||
@@ -110,6 +111,7 @@ describe('makeRundownMetadata()', () => {
|
|||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
groupId: null,
|
groupId: null,
|
||||||
groupColour: undefined,
|
groupColour: undefined,
|
||||||
|
groupEntries: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(process(demoEvents['block'])).toMatchObject({
|
expect(process(demoEvents['block'])).toMatchObject({
|
||||||
@@ -251,6 +253,7 @@ describe('makeRundownMetadata()', () => {
|
|||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
groupId: rundownStartsWithBlock.block.id,
|
groupId: rundownStartsWithBlock.block.id,
|
||||||
groupColour: 'red',
|
groupColour: 'red',
|
||||||
|
groupEntries: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(process(rundownStartsWithBlock['1'])).toStrictEqual({
|
expect(process(rundownStartsWithBlock['1'])).toStrictEqual({
|
||||||
@@ -266,6 +269,7 @@ describe('makeRundownMetadata()', () => {
|
|||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
groupId: rundownStartsWithBlock.block.id,
|
groupId: rundownStartsWithBlock.block.id,
|
||||||
groupColour: 'red',
|
groupColour: 'red',
|
||||||
|
groupEntries: 2,
|
||||||
});
|
});
|
||||||
expect(process(rundownStartsWithBlock['2'])).toStrictEqual({
|
expect(process(rundownStartsWithBlock['2'])).toStrictEqual({
|
||||||
previousEvent: rundownStartsWithBlock['1'],
|
previousEvent: rundownStartsWithBlock['1'],
|
||||||
@@ -280,6 +284,7 @@ describe('makeRundownMetadata()', () => {
|
|||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
groupId: rundownStartsWithBlock.block.id,
|
groupId: rundownStartsWithBlock.block.id,
|
||||||
groupColour: 'red',
|
groupColour: 'red',
|
||||||
|
groupEntries: 2,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+4
-1
@@ -4,6 +4,9 @@
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
padding-block: 0.5rem;
|
padding-block: 0.5rem;
|
||||||
padding-left: calc(2em + 0.5rem);
|
|
||||||
background: color-mix(in srgb, transparent 90%, var(--user-bg, transparent) 10%);
|
background: color-mix(in srgb, transparent 90%, var(--user-bg, transparent) 10%);
|
||||||
|
|
||||||
|
&.indent {
|
||||||
|
padding-left: calc(2em + 0.5rem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+18
-16
@@ -1,25 +1,24 @@
|
|||||||
import { memo, useRef } from 'react';
|
import { memo } from 'react';
|
||||||
import { IoAdd } from 'react-icons/io5';
|
import { IoAdd } from 'react-icons/io5';
|
||||||
import { Toolbar } from '@base-ui-components/react/toolbar';
|
import { Toolbar } from '@base-ui-components/react/toolbar';
|
||||||
import { MaybeString, SupportedEntry } from 'ontime-types';
|
import { MaybeString, SupportedEntry } from 'ontime-types';
|
||||||
|
|
||||||
import Button from '../../../common/components/buttons/Button';
|
import Button from '../../../../common/components/buttons/Button';
|
||||||
import { useEntryActions } from '../../../common/hooks/useEntryAction';
|
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;
|
previousEventId: MaybeString;
|
||||||
parentBlock: MaybeString;
|
parentBlock: MaybeString;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(QuickAddBlock);
|
export default memo(QuickAddButtons);
|
||||||
function QuickAddBlock({ previousEventId, parentBlock, backgroundColor }: QuickAddBlockProps) {
|
function QuickAddButtons({ previousEventId, parentBlock, backgroundColor }: QuickAddButtonsProps) {
|
||||||
const { addEntry } = useEntryActions();
|
const { addEntry } = useEntryActions();
|
||||||
|
|
||||||
const doLinkPrevious = useRef<HTMLInputElement | null>(null);
|
|
||||||
|
|
||||||
const addEvent = () => {
|
const addEvent = () => {
|
||||||
addEntry(
|
addEntry(
|
||||||
{
|
{
|
||||||
@@ -29,7 +28,6 @@ function QuickAddBlock({ previousEventId, parentBlock, backgroundColor }: QuickA
|
|||||||
{
|
{
|
||||||
after: previousEventId,
|
after: previousEventId,
|
||||||
lastEventId: previousEventId,
|
lastEventId: previousEventId,
|
||||||
linkPrevious: doLinkPrevious?.current?.checked,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -75,26 +73,30 @@ function QuickAddBlock({ previousEventId, parentBlock, backgroundColor }: QuickA
|
|||||||
const blockColour = backgroundColor === '' ? '#9d9d9d' : backgroundColor;
|
const blockColour = backgroundColor === '' ? '#9d9d9d' : backgroundColor;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Toolbar.Root className={style.quickAdd} style={blockColour ? { '--user-bg': blockColour } : {}}>
|
<Toolbar.Root
|
||||||
<Toolbar.Button render={<Button size='small' variant='subtle-white' />} onClick={addEvent}>
|
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 />
|
<IoAdd />
|
||||||
Event
|
Event
|
||||||
</Toolbar.Button>
|
</Toolbar.Button>
|
||||||
|
|
||||||
<Toolbar.Button render={<Button size='small' variant='subtle-white' />} onClick={addDelay}>
|
<Toolbar.Button render={<Button size='small' />} onClick={addDelay}>
|
||||||
<IoAdd />
|
<IoAdd />
|
||||||
Delay
|
Delay
|
||||||
</Toolbar.Button>
|
</Toolbar.Button>
|
||||||
|
|
||||||
<Toolbar.Button render={<Button size='small' variant='subtle-white' />} onClick={addMilestone}>
|
<Toolbar.Button render={<Button size='small' />} onClick={addMilestone}>
|
||||||
<IoAdd />
|
<IoAdd />
|
||||||
Milestone
|
Milestone
|
||||||
</Toolbar.Button>
|
</Toolbar.Button>
|
||||||
|
|
||||||
{parentBlock === null && (
|
{parentBlock === null && (
|
||||||
<Toolbar.Button render={<Button size='small' variant='subtle-white' />} onClick={addBlock}>
|
<Toolbar.Button render={<Button size='small' />} onClick={addBlock}>
|
||||||
<IoAdd />
|
<IoAdd />
|
||||||
Block
|
Group
|
||||||
</Toolbar.Button>
|
</Toolbar.Button>
|
||||||
)}
|
)}
|
||||||
</Toolbar.Root>
|
</Toolbar.Root>
|
||||||
+16
@@ -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 { useMemo } from 'react';
|
||||||
import { IoCheckmarkCircle } from 'react-icons/io5';
|
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 Tooltip from '../../../../common/components/tooltip/Tooltip';
|
||||||
import { usePlayback } from '../../../../common/hooks/useSocket';
|
import { usePlayback } from '../../../../common/hooks/useSocket';
|
||||||
import useReport from '../../../../common/hooks-query/useReport';
|
import useReport from '../../../../common/hooks-query/useReport';
|
||||||
import { cx } from '../../../../common/utils/styleUtils';
|
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';
|
import style from './RundownEventChip.module.scss';
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ function EventReport(props: EventReportProps) {
|
|||||||
|
|
||||||
const isOver = difference > 0;
|
const isOver = difference > 0;
|
||||||
|
|
||||||
const fullTimeValue = formatTime(absDifference);
|
const fullTimeValue = millisToString(absDifference);
|
||||||
|
|
||||||
const tooltip = `Event ran ${isOver ? 'over' : 'under'} time by ${fullTimeValue}`;
|
const tooltip = `Event ran ${isOver ? 'over' : 'under'} time by ${fullTimeValue}`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.header {
|
.header {
|
||||||
padding-inline: calc(2.5rem - 6px) 2rem;
|
padding-inline: 1rem 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
background-color: color-mix(in srgb, var(--user-bg, $block-bg) 15%, transparent 85%);
|
background-color: color-mix(in srgb, var(--user-bg, $block-bg) 15%, transparent 85%);
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2rem 8rem 1fr auto auto;
|
grid-template-columns: 2rem 8rem 1fr auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: $secondary-block-height;
|
height: $secondary-block-height;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MouseEvent, useCallback, useRef } from 'react';
|
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 { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { EntryId } from 'ontime-types';
|
import { EntryId } from 'ontime-types';
|
||||||
@@ -92,9 +92,6 @@ export default function RundownMilestone({ colour, cue, entryId, hasCursor, titl
|
|||||||
</div>
|
</div>
|
||||||
<MilestoneTextInput field='cue' initialValue={cue} placeholder='Cue' submitHandler={handleUpdate} />
|
<MilestoneTextInput field='cue' initialValue={cue} placeholder='Cue' submitHandler={handleUpdate} />
|
||||||
<MilestoneTextInput field='title' initialValue={title} placeholder='Title' submitHandler={handleUpdate} />
|
<MilestoneTextInput field='title' initialValue={title} placeholder='Title' submitHandler={handleUpdate} />
|
||||||
<Button variant='ghosted-white'>
|
|
||||||
<IoCheckmarkDone /> Done
|
|
||||||
</Button>
|
|
||||||
<Button variant='ghosted-destructive' onClick={handleDelete}>
|
<Button variant='ghosted-destructive' onClick={handleDelete}>
|
||||||
<IoClose /> Cancel
|
<IoClose /> Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type RundownMetadata = {
|
|||||||
isLoaded: boolean;
|
isLoaded: boolean;
|
||||||
groupId: MaybeString;
|
groupId: MaybeString;
|
||||||
groupColour: string | undefined;
|
groupColour: string | undefined;
|
||||||
|
groupEntries: number | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,6 +47,7 @@ export function makeRundownMetadata(selectedEventId: MaybeString) {
|
|||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
groupId: null,
|
groupId: null,
|
||||||
groupColour: undefined,
|
groupColour: undefined,
|
||||||
|
groupEntries: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
function process(entry: OntimeEntry): Readonly<RundownMetadata> {
|
function process(entry: OntimeEntry): Readonly<RundownMetadata> {
|
||||||
@@ -82,11 +84,13 @@ function processEntry(
|
|||||||
if (isOntimeBlock(entry)) {
|
if (isOntimeBlock(entry)) {
|
||||||
processedData.groupId = entry.id;
|
processedData.groupId = entry.id;
|
||||||
processedData.groupColour = entry.colour;
|
processedData.groupColour = entry.colour;
|
||||||
|
processedData.groupEntries = entry.entries.length;
|
||||||
} else {
|
} else {
|
||||||
// for delays and blocks, we insert the group metadata
|
// for delays and blocks, we insert the group metadata
|
||||||
if ((entry as OntimeEvent | OntimeDelay | OntimeMilestone).parent !== processedData.groupId) {
|
if ((entry as OntimeEvent | OntimeDelay | OntimeMilestone).parent !== processedData.groupId) {
|
||||||
// if the parent is not the current group, we need to update the groupId
|
// if the parent is not the current group, we need to update the groupId
|
||||||
processedData.groupId = (entry as OntimeEvent | OntimeDelay | OntimeMilestone).parent;
|
processedData.groupId = (entry as OntimeEvent | OntimeDelay | OntimeMilestone).parent;
|
||||||
|
processedData.groupEntries = undefined;
|
||||||
if ((entry as OntimeEvent | OntimeDelay | OntimeMilestone).parent === null) {
|
if ((entry as OntimeEvent | OntimeDelay | OntimeMilestone).parent === null) {
|
||||||
// if the entry has no parent, it cannot have a group colour
|
// if the entry has no parent, it cannot have a group colour
|
||||||
processedData.groupColour = undefined;
|
processedData.groupColour = undefined;
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@ import { memo } from 'react';
|
|||||||
import { IoAdd, IoArrowDown, IoArrowUp, IoDuplicateOutline, IoOptions, IoTrash } from 'react-icons/io5';
|
import { IoAdd, IoArrowDown, IoArrowUp, IoDuplicateOutline, IoOptions, IoTrash } from 'react-icons/io5';
|
||||||
import { SupportedEntry } from 'ontime-types';
|
import { SupportedEntry } from 'ontime-types';
|
||||||
|
|
||||||
import Menu from '../../../../common/components/dropdown-menu/DropdownMenu';
|
import { PositionedDropdownMenu } from '../../../../common/components/dropdown-menu/DropdownMenu';
|
||||||
import { useEntryActions } from '../../../../common/hooks/useEntryAction';
|
import { useEntryActions } from '../../../../common/hooks/useEntryAction';
|
||||||
import { useCuesheetEditModal } from '../../cuesheet-edit-modal/useCuesheetEditModal';
|
import { useCuesheetEditModal } from '../../cuesheet-edit-modal/useCuesheetEditModal';
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ function CuesheetTableMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<PositionedDropdownMenu
|
||||||
isOpen
|
isOpen
|
||||||
onClose={closeMenu}
|
onClose={closeMenu}
|
||||||
items={[
|
items={[
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ test('smoke test operator', async ({ page }) => {
|
|||||||
await page.getByTestId('entry-3').getByTestId('time-input-duration').fill('1m');
|
await page.getByTestId('entry-3').getByTestId('time-input-duration').fill('1m');
|
||||||
await page.getByTestId('entry-3').getByTestId('time-input-duration').press('Enter');
|
await page.getByTestId('entry-3').getByTestId('time-input-duration').press('Enter');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Block' }).nth(1).click();
|
await page.getByRole('button', { name: 'Group' }).nth(1).click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Edit' }).click();
|
await page.getByRole('button', { name: 'Edit' }).click();
|
||||||
await page.getByTestId('entry-1').click();
|
await page.getByTestId('entry-1').click();
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
//TODO:
|
||||||
|
|
||||||
test('show warning when event crosses midnight', async ({ page }) => {
|
test('show warning when event crosses midnight', async ({ page }) => {
|
||||||
await page.goto('http://localhost:4001/editor');
|
await page.goto('http://localhost:4001/editor');
|
||||||
|
|
||||||
@@ -32,7 +34,7 @@ test('show warning when event starts next day midnight', async ({ page }) => {
|
|||||||
await page.getByTestId('entry-2').getByTestId('time-input-timeEnd').click();
|
await page.getByTestId('entry-2').getByTestId('time-input-timeEnd').click();
|
||||||
await page.getByTestId('entry-2').getByTestId('time-input-timeEnd').fill('0');
|
await page.getByTestId('entry-2').getByTestId('time-input-timeEnd').fill('0');
|
||||||
await page.getByTestId('entry-2').getByTestId('time-input-timeEnd').press('Enter');
|
await page.getByTestId('entry-2').getByTestId('time-input-timeEnd').press('Enter');
|
||||||
await page.getByRole('button', { name: 'Event', exact: true }).nth(2).click();
|
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
||||||
|
|
||||||
await expect(page.getByText('(next day)')).toBeVisible();
|
await expect(page.getByText('(next day)')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user