diff --git a/apps/client/src/common/components/menu/Menu.module.scss b/apps/client/src/common/components/dropdown-menu/DropdownMenu.module.scss similarity index 100% rename from apps/client/src/common/components/menu/Menu.module.scss rename to apps/client/src/common/components/dropdown-menu/DropdownMenu.module.scss diff --git a/apps/client/src/common/components/dropdown-menu/DropdownMenu.tsx b/apps/client/src/common/components/dropdown-menu/DropdownMenu.tsx new file mode 100644 index 000000000..b7f2a4e12 --- /dev/null +++ b/apps/client/src/common/components/dropdown-menu/DropdownMenu.tsx @@ -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; +} + +export function DropdownMenu({ items, children, ...triggerProps }: PropsWithChildren) { + return ( + + {children} + + + + {items.map((item, index) => { + if (item.type === 'divider') { + return ; + } + return ( + + {item.icon} {item.label} + + ); + })} + + + + + ); +} + +interface PositionedDropdownMenuProps { + items: Array; + isOpen: boolean; + onClose: () => void; + position: { x: number; y: number }; +} + +export function PositionedDropdownMenu({ items, isOpen, position, onClose }: PositionedDropdownMenuProps) { + return ( + { + if (!open) onClose(); + }} + > + + + + + {items.map((item, index) => { + if (item.type === 'divider') { + return ; + } + return ( + + {item.icon} {item.label} + + ); + })} + + + + + ); +} diff --git a/apps/client/src/common/components/menu/Menu.tsx b/apps/client/src/common/components/menu/Menu.tsx deleted file mode 100644 index b28b0ab69..000000000 --- a/apps/client/src/common/components/menu/Menu.tsx +++ /dev/null @@ -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; - isOpen: boolean; - position: { x: number; y: number }; - onClose: () => void; -} - -export default function Menu({ items, isOpen, position, onClose }: MenuProps) { - return ( - { - if (!open) onClose(); - }} - > - - - - - {items.map((item, index) => { - if (item.type === 'divider') { - return ; - } - return ( - - {item.icon} {item.label} - - ); - })} - - - - - ); -} diff --git a/apps/client/src/features/rundown/Rundown.module.scss b/apps/client/src/features/rundown/Rundown.module.scss index b010324ea..caa533455 100644 --- a/apps/client/src/features/rundown/Rundown.module.scss +++ b/apps/client/src/features/rundown/Rundown.module.scss @@ -5,8 +5,6 @@ .rundownContainer { margin-top: 1rem; - display: flex; - flex-direction: column; overflow-y: scroll; height: 100%; } diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index 25be3a568..333875144 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -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) { >
+ {isEditMode && } {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 ; - } 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 ( - {showPrependingQuickAdd && ( - + {isEditMode && rundownMetadata.groupEntries === 0 && ( + )} - {isEditMode && isLast && } ); } @@ -462,11 +456,10 @@ export default function Rundown({ data }: RundownProps) { return ( - {isEditMode && (hasCursor || isFirst) && ( - )} {isOntimeBlock(entry) ? ( @@ -503,16 +496,15 @@ export default function Rundown({ data }: RundownProps) {
)} - {isEditMode && (hasCursor || isLast) && ( - + {isEditMode && hasCursor && rundownMetadata.groupEntries !== 0 && !isLast && ( + )} ); })} + {isEditMode && ( + + )}
diff --git a/apps/client/src/features/rundown/__tests__/rundown.utils.test.ts b/apps/client/src/features/rundown/__tests__/rundown.utils.test.ts index 945686585..7a61b9e25 100644 --- a/apps/client/src/features/rundown/__tests__/rundown.utils.test.ts +++ b/apps/client/src/features/rundown/__tests__/rundown.utils.test.ts @@ -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, }); }); }); diff --git a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.module.scss b/apps/client/src/features/rundown/entry-editor/quick-add-buttons/QuickAddButtons.module.scss similarity index 75% rename from apps/client/src/features/rundown/quick-add-block/QuickAddBlock.module.scss rename to apps/client/src/features/rundown/entry-editor/quick-add-buttons/QuickAddButtons.module.scss index 7a8fb4b87..f1175abe5 100644 --- a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.module.scss +++ b/apps/client/src/features/rundown/entry-editor/quick-add-buttons/QuickAddButtons.module.scss @@ -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); + } } diff --git a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx b/apps/client/src/features/rundown/entry-editor/quick-add-buttons/QuickAddButtons.tsx similarity index 62% rename from apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx rename to apps/client/src/features/rundown/entry-editor/quick-add-buttons/QuickAddButtons.tsx index 0bcec3098..add3c2293 100644 --- a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx +++ b/apps/client/src/features/rundown/entry-editor/quick-add-buttons/QuickAddButtons.tsx @@ -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(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 ( - - } onClick={addEvent}> + + } onClick={addEvent}> Event - } onClick={addDelay}> + } onClick={addDelay}> Delay - } onClick={addMilestone}> + } onClick={addMilestone}> Milestone {parentBlock === null && ( - } onClick={addBlock}> + } onClick={addBlock}> - Block + Group )} diff --git a/apps/client/src/features/rundown/entry-editor/quick-add-cursor/QuickAddInline.module.scss b/apps/client/src/features/rundown/entry-editor/quick-add-cursor/QuickAddInline.module.scss new file mode 100644 index 000000000..dbc1c4d9c --- /dev/null +++ b/apps/client/src/features/rundown/entry-editor/quick-add-cursor/QuickAddInline.module.scss @@ -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%); +} diff --git a/apps/client/src/features/rundown/entry-editor/quick-add-cursor/QuickAddInline.tsx b/apps/client/src/features/rundown/entry-editor/quick-add-cursor/QuickAddInline.tsx new file mode 100644 index 000000000..a424929f3 --- /dev/null +++ b/apps/client/src/features/rundown/entry-editor/quick-add-cursor/QuickAddInline.tsx @@ -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 ( +
+ , label: 'Add Event', onClick: addEvent }, + { type: 'item', icon: , label: 'Add Delay', onClick: addDelay }, + { type: 'item', icon: , label: 'Add Milestone', onClick: addMilestone }, + { type: 'item', icon: , label: 'Add Group', onClick: addBlock, disabled: parentBlock !== null }, + ]} + render={} + > + + +
+ ); +} diff --git a/apps/client/src/features/rundown/rundown-event/composite/RundownEventChip.tsx b/apps/client/src/features/rundown/rundown-event/composite/RundownEventChip.tsx index 415370f96..0f8f06d13 100644 --- a/apps/client/src/features/rundown/rundown-event/composite/RundownEventChip.tsx +++ b/apps/client/src/features/rundown/rundown-event/composite/RundownEventChip.tsx @@ -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}`; diff --git a/apps/client/src/features/rundown/rundown-header/RundownHeader.module.scss b/apps/client/src/features/rundown/rundown-header/RundownHeader.module.scss index a2dcd61fe..b16f67f2d 100644 --- a/apps/client/src/features/rundown/rundown-header/RundownHeader.module.scss +++ b/apps/client/src/features/rundown/rundown-header/RundownHeader.module.scss @@ -1,5 +1,5 @@ .header { - padding-inline: calc(2.5rem - 6px) 2rem; + padding-inline: 1rem 2rem; display: flex; align-items: center; diff --git a/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.module.scss b/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.module.scss index 9831033c6..c923da758 100644 --- a/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.module.scss +++ b/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.module.scss @@ -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; diff --git a/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.tsx b/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.tsx index 7795594f4..3c02d6e27 100644 --- a/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.tsx +++ b/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.tsx @@ -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 - diff --git a/apps/client/src/features/rundown/rundown.utils.ts b/apps/client/src/features/rundown/rundown.utils.ts index 428c7c5d0..4066b85ed 100644 --- a/apps/client/src/features/rundown/rundown.utils.ts +++ b/apps/client/src/features/rundown/rundown.utils.ts @@ -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 { @@ -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; diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/CuesheetTableMenu.tsx b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/CuesheetTableMenu.tsx index c9d51c666..957abcfb4 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/CuesheetTableMenu.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-menu/CuesheetTableMenu.tsx @@ -2,7 +2,7 @@ import { memo } from 'react'; import { IoAdd, IoArrowDown, IoArrowUp, IoDuplicateOutline, IoOptions, IoTrash } from 'react-icons/io5'; 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 { useCuesheetEditModal } from '../../cuesheet-edit-modal/useCuesheetEditModal'; @@ -20,7 +20,7 @@ function CuesheetTableMenu() { } return ( - { await page.getByTestId('entry-3').getByTestId('time-input-duration').fill('1m'); 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.getByTestId('entry-1').click(); diff --git a/e2e/tests/features/211-report-data.spec.ts b/e2e/tests/features/211-report-data.spec.ts index 5fbddae64..75c97c4bb 100644 --- a/e2e/tests/features/211-report-data.spec.ts +++ b/e2e/tests/features/211-report-data.spec.ts @@ -1,5 +1,7 @@ import { expect, test } from '@playwright/test'; +//TODO: + test('show warning when event crosses midnight', async ({ page }) => { 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').fill('0'); 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(); });