diff --git a/apps/client/src/common/components/input/input/Input.module.scss b/apps/client/src/common/components/input/input/Input.module.scss index c72eee9a9..bd1fa2563 100644 --- a/apps/client/src/common/components/input/input/Input.module.scss +++ b/apps/client/src/common/components/input/input/Input.module.scss @@ -2,7 +2,7 @@ box-sizing: border-box; font-size: 1rem; - font-weight: 400; + font-weight: inherit; color: $gray-200; border-radius: $component-border-radius-md; border: 1px solid transparent; diff --git a/apps/client/src/common/components/input/textarea/Textarea.module.scss b/apps/client/src/common/components/input/textarea/Textarea.module.scss index d5b30ca09..cb4cb0454 100644 --- a/apps/client/src/common/components/input/textarea/Textarea.module.scss +++ b/apps/client/src/common/components/input/textarea/Textarea.module.scss @@ -4,7 +4,7 @@ display: block; font-size: 1rem; - font-weight: 400; + font-weight: inherit; color: $gray-200; border-radius: $component-border-radius-md; border: 1px solid transparent; @@ -34,11 +34,13 @@ .subtle { background-color: $gray-1200; + padding-top: 0.5em; } .ghosted { background-color: transparent; padding: 0; + padding-top: 0.5em; } .fluid { diff --git a/apps/client/src/common/components/link/external-link/ExternalLink.module.scss b/apps/client/src/common/components/link/external-link/ExternalLink.module.scss index 9fb22c59a..8b71d6b90 100644 --- a/apps/client/src/common/components/link/external-link/ExternalLink.module.scss +++ b/apps/client/src/common/components/link/external-link/ExternalLink.module.scss @@ -7,6 +7,7 @@ transition-property: color; transition-duration: $transition-time-action; width: fit-content; + text-decoration: none; &.inline { display: inline-flex; @@ -20,4 +21,4 @@ outline: none; box-shadow: 0 1px 0 0 currentColor; } -} +} diff --git a/apps/client/src/common/components/modal/Modal.module.scss b/apps/client/src/common/components/modal/Modal.module.scss index 3138d139e..8c1c43760 100644 --- a/apps/client/src/common/components/modal/Modal.module.scss +++ b/apps/client/src/common/components/modal/Modal.module.scss @@ -7,7 +7,7 @@ padding-inline: 1rem; min-width: min(680px, 90vw); min-height: min(200px, 10vh); - max-width: min(680px, 90vw); + max-width: min(800px, 90vw); background-color: $gray-1250; color: $ui-white; diff --git a/apps/client/src/common/hooks/useEntryAction.ts b/apps/client/src/common/hooks/useEntryAction.ts index f155c3b20..8bd9c9350 100644 --- a/apps/client/src/common/hooks/useEntryAction.ts +++ b/apps/client/src/common/hooks/useEntryAction.ts @@ -15,7 +15,7 @@ import { } from 'ontime-types'; import { dayInMs, generateId, MILLIS_PER_SECOND, parseUserTime, swapEventData } from 'ontime-utils'; -import { moveDown, moveUp } from '../../features/rundown/rundown.utils'; +import { moveDown, moveUp, orderEntries } from '../../features/rundown/rundown.utils'; import { RUNDOWN } from '../api/constants'; import { deleteEntries, @@ -622,13 +622,22 @@ export const useEntryActions = () => { */ const groupEntries = useCallback( async (entryIds: EntryId[]) => { + if (entryIds.length === 0) return; + try { - await groupEntriesMutation(entryIds); + if (entryIds.length === 1) { + await groupEntriesMutation(entryIds); + } else { + const rundown = queryClient.getQueryData(RUNDOWN); + if (!rundown) return; + const orderedIds = orderEntries(entryIds, rundown.flatOrder); + await groupEntriesMutation(orderedIds); + } } catch (error) { logAxiosError('Error grouping entries', error); } }, - [groupEntriesMutation], + [groupEntriesMutation, queryClient], ); /** diff --git a/apps/client/src/features/app-settings/panel/automations-panel/AutomationSettingsForm.tsx b/apps/client/src/features/app-settings/panel/automations-panel/AutomationSettingsForm.tsx index 77fdffade..f381e8fe1 100644 --- a/apps/client/src/features/app-settings/panel/automations-panel/AutomationSettingsForm.tsx +++ b/apps/client/src/features/app-settings/panel/automations-panel/AutomationSettingsForm.tsx @@ -107,7 +107,9 @@ export default function AutomationSettingsForm(props: AutomationSettingsProps) { setValue('enabledAutomations', value, { shouldDirty: true })} + onCheckedChange={(value: boolean) => + setValue('enabledAutomations', value, { shouldDirty: true, shouldValidate: true }) + } /> @@ -125,7 +127,9 @@ export default function AutomationSettingsForm(props: AutomationSettingsProps) { setValue('enabledOscIn', value, { shouldDirty: true })} + onCheckedChange={(value: boolean) => + setValue('enabledOscIn', value, { shouldDirty: true, shouldValidate: true }) + } /> diff --git a/apps/client/src/features/app-settings/panel/automations-panel/TriggerForm.tsx b/apps/client/src/features/app-settings/panel/automations-panel/TriggerForm.tsx index 02868160a..2a19d94c8 100644 --- a/apps/client/src/features/app-settings/panel/automations-panel/TriggerForm.tsx +++ b/apps/client/src/features/app-settings/panel/automations-panel/TriggerForm.tsx @@ -22,8 +22,15 @@ interface TriggerFormProps { postSubmit: () => void; } -export default function TriggerForm(props: TriggerFormProps) { - const { automations, initialId, initialTitle, initialAutomationId, initialTrigger, onCancel, postSubmit } = props; +export default function TriggerForm({ + automations, + initialId, + initialTitle, + initialAutomationId, + initialTrigger, + onCancel, + postSubmit, +}: TriggerFormProps) { const { handleSubmit, register, @@ -35,8 +42,8 @@ export default function TriggerForm(props: TriggerFormProps) { } = useForm({ defaultValues: { title: initialTitle, - trigger: initialTrigger, - automationId: initialAutomationId, + trigger: initialTrigger ?? (cycles[0].value as TimerLifeCycle | undefined), + automationId: initialAutomationId ?? automations?.[Object.keys(automations)[0]]?.id, }, resetOptions: { keepDirtyValues: true, diff --git a/apps/client/src/features/app-settings/panel/automations-panel/automationUtils.ts b/apps/client/src/features/app-settings/panel/automations-panel/automationUtils.ts index 892946389..7184a3d83 100644 --- a/apps/client/src/features/app-settings/panel/automations-panel/automationUtils.ts +++ b/apps/client/src/features/app-settings/panel/automations-panel/automationUtils.ts @@ -36,9 +36,9 @@ const staticSelectProperties = [ ]; const staticNextSelectProperties = [ - { value: 'eventNow.id', label: 'Next ID' }, - { value: 'eventNow.title', label: 'Next Title' }, - { value: 'eventNow.cue', label: 'Next Cue' }, + { value: 'eventNext.id', label: 'Next ID' }, + { value: 'eventNext.title', label: 'Next Title' }, + { value: 'eventNext.cue', label: 'Next Cue' }, ]; type SelectableField = { diff --git a/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/preview/PreviewRundown.tsx b/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/preview/PreviewRundown.tsx index c8456aafc..6efdd4d8d 100644 --- a/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/preview/PreviewRundown.tsx +++ b/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/preview/PreviewRundown.tsx @@ -28,7 +28,7 @@ export default function PreviewRundown(props: PreviewRundownProps) { const fieldLabels = fieldKeys.map((key) => customFields[key].label); return ( - + # diff --git a/apps/client/src/features/control/playback/playback-buttons/PlaybackButtons.tsx b/apps/client/src/features/control/playback/playback-buttons/PlaybackButtons.tsx index 23efcdc65..059902467 100644 --- a/apps/client/src/features/control/playback/playback-buttons/PlaybackButtons.tsx +++ b/apps/client/src/features/control/playback/playback-buttons/PlaybackButtons.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { IoPause, IoPlay, IoPlaySkipBack, IoPlaySkipForward, IoReload, IoStop, IoTime } from 'react-icons/io5'; +import { IoPause, IoPlay, IoPlaySkipBack, IoPlaySkipForward, IoReload, IoStop } from 'react-icons/io5'; import { Playback, TimerPhase } from 'ontime-types'; import { validatePlayback } from 'ontime-utils'; @@ -73,7 +73,7 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
- + Roll diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index 333875144..3ccada4bc 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -298,8 +298,15 @@ export default function Rundown({ data }: RundownProps) { return; } const index = order.findIndex((id) => id === featureData.selectedEventId); + // @ts-expect-error -- but we safely check if the parent property exists + const maybeParent = entries[featureData.selectedEventId]?.parent; + if (maybeParent) { + // open the group + setCollapsedGroups((prev) => [...prev].filter((id) => id !== maybeParent)); + } + setSelectedEvents({ id: featureData.selectedEventId, selectMode: 'click', index }); - }, [editorMode, featureData.selectedEventId, order, setSelectedEvents]); + }, [editorMode, entries, featureData.selectedEventId, order, setCollapsedGroups, setSelectedEvents]); /** * On drag end, we reorder the events 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 7a61b9e25..047a05d51 100644 --- a/apps/client/src/features/rundown/__tests__/rundown.utils.test.ts +++ b/apps/client/src/features/rundown/__tests__/rundown.utils.test.ts @@ -1,6 +1,6 @@ import { EntryId, OntimeBlock, OntimeDelay, OntimeEvent, RundownEntries, SupportedEntry } from 'ontime-types'; -import { makeRundownMetadata, makeSortableList, moveDown, moveUp } from '../rundown.utils'; +import { makeRundownMetadata, makeSortableList, moveDown, moveUp, orderEntries } from '../rundown.utils'; describe('makeRundownMetadata()', () => { it('processes nested rundown data', () => { @@ -339,7 +339,6 @@ describe('makeSortableList()', () => { }); }); - describe('moveUp()', () => { const rundown = { entries: { @@ -535,3 +534,40 @@ describe('moveDown()', () => { }); }); }); + +describe('orderEntries()', () => { + it('should return an empty array when both inputs are empty', () => { + const unorderedArray: string[] = []; + const flatOrder: string[] = []; + const result = orderEntries(unorderedArray, flatOrder); + expect(result).toEqual([]); + }); + + it('should return an ordered array based on flatOrder', () => { + const unorderedArray = ['b', 'a', 'c']; + const flatOrder = ['a', 'b', 'c']; + const result = orderEntries(unorderedArray, flatOrder); + expect(result).toEqual(['a', 'b', 'c']); + }); + + it('should ignore elements in unorderedArray not present in flatOrder', () => { + const unorderedArray = ['b', 'a', 'c', 'd']; + const flatOrder = ['a', 'b', 'c']; + const result = orderEntries(unorderedArray, flatOrder); + expect(result).toEqual(['a', 'b', 'c']); + }); + + it('should handle cases where flatOrder has elements not in unorderedArray', () => { + const unorderedArray = ['b', 'a']; + const flatOrder = ['a', 'b', 'c']; + const result = orderEntries(unorderedArray, flatOrder); + expect(result).toEqual(['a', 'b']); + }); + + it('should return an empty array if unorderedArray has no matching elements in flatOrder', () => { + const unorderedArray = ['x', 'y', 'z']; + const flatOrder = ['a', 'b', 'c']; + const result = orderEntries(unorderedArray, flatOrder); + expect(result).toEqual([]); + }); +}); diff --git a/apps/client/src/features/rundown/rundown-block/RundownBlock.tsx b/apps/client/src/features/rundown/rundown-block/RundownBlock.tsx index 08a064f2d..21d6bdb41 100644 --- a/apps/client/src/features/rundown/rundown-block/RundownBlock.tsx +++ b/apps/client/src/features/rundown/rundown-block/RundownBlock.tsx @@ -43,7 +43,6 @@ export default function RundownBlock({ data, hasCursor, collapsed, onCollapse }: }, { type: 'item', - label: 'Ungroup', icon: IoFolderOpenOutline, onClick: () => ungroup(data.id), @@ -55,7 +54,6 @@ export default function RundownBlock({ data, hasCursor, collapsed, onCollapse }: label: 'Delete Group', icon: IoTrash, onClick: () => deleteEntry([data.id]), - disabled: true, }, ]); diff --git a/apps/client/src/features/rundown/rundown-event/RundownEvent.module.scss b/apps/client/src/features/rundown/rundown-event/RundownEvent.module.scss index dac3c67d8..0b96b901f 100644 --- a/apps/client/src/features/rundown/rundown-event/RundownEvent.module.scss +++ b/apps/client/src/features/rundown/rundown-event/RundownEvent.module.scss @@ -94,7 +94,7 @@ $skip-opacity: 0.2; cursor: pointer; background-color: $gray-1050; // to override inline - color: $section-white; + color: $section-white; // to override inline font-size: 1rem; border-radius: 3px 0 0 3px; 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 c923da758..048c921fd 100644 --- a/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.module.scss +++ b/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.module.scss @@ -6,7 +6,8 @@ margin-left: calc(2rem + 1px); // binder + border margin-block: 0.125rem; padding-right: 0.25rem; - background-color: color-mix(in srgb, var(--user-bg, $block-bg) 15%, transparent 85%); + background-color: $gray-1050; // to override inline + color: $section-white; // to override inline display: grid; grid-template-columns: 2rem 8rem 1fr auto; @@ -23,7 +24,8 @@ height: 100%; display: grid; place-content: center; - background-color: var(--user-bg, $block-bg); + background-color: $gray-1050; // to override inline + color: $section-white; // to override inline } .drag { diff --git a/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.tsx b/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.tsx index 3c02d6e27..6caeae62c 100644 --- a/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.tsx +++ b/apps/client/src/features/rundown/rundown-milestone/RundownMilestone.tsx @@ -76,17 +76,11 @@ export default function RundownMilestone({ colour, cue, entryId, hasCursor, titl className={cx([style.milestone, hasCursor ? style.hasCursor : null])} ref={setNodeRef} onClick={handleFocusClick} - style={{ ...dragStyle, '--user-bg': colour }} + style={dragStyle} data-testid='rundown-milestone' > -
- +
+
diff --git a/apps/client/src/features/rundown/rundown.utils.ts b/apps/client/src/features/rundown/rundown.utils.ts index 4066b85ed..368f400fb 100644 --- a/apps/client/src/features/rundown/rundown.utils.ts +++ b/apps/client/src/features/rundown/rundown.utils.ts @@ -306,3 +306,17 @@ export function moveDown( // default - swap positions with next entry return { destinationId: nextEntryId, order: 'after' }; } + +/** + * Reorders unorderedArray to match the flatOrder entries + * Useful for operations that convert selections (out of order) to rundown + */ +export function orderEntries(unorderedArray: EntryId[], flatOrder: EntryId[]): EntryId[] { + const orderedArray: EntryId[] = []; + for (const id of flatOrder) { + if (unorderedArray.includes(id)) { + orderedArray.push(id); + } + } + return orderedArray; +} diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/BlockRow.module.scss b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/BlockRow.module.scss index b707b8142..a75b8937c 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/BlockRow.module.scss +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/BlockRow.module.scss @@ -14,6 +14,8 @@ position: relative; line-height: 1em; + font-weight: bold; + td { min-height: 3.5rem; padding-top: 0.75rem !important; // fighting styles from cuesheet-table diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/MilestoneRow.module.scss b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/MilestoneRow.module.scss index 6298c5edf..c9867f8e3 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/MilestoneRow.module.scss +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/MilestoneRow.module.scss @@ -4,6 +4,8 @@ background: color-mix(in srgb, transparent 92%, var(--user-bg, $gray-500) 8%); border-left: 4px solid var(--user-bg, $gray-500); + font-style: italic; + &:hover { outline: 1px solid $blue-500; outline-offset: -1px; diff --git a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/TextLikeInput.module.scss b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/TextLikeInput.module.scss index ae189f55a..cdba636fc 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/TextLikeInput.module.scss +++ b/apps/client/src/views/cuesheet/cuesheet-table/cuesheet-table-elements/TextLikeInput.module.scss @@ -3,6 +3,7 @@ height: 2rem; background-color: transparent; border-radius: $component-border-radius-md; + text-wrap: nowrap; display: flex; align-items: center; diff --git a/apps/server/src/api-data/rundown/__tests__/rundown.dao.test.ts b/apps/server/src/api-data/rundown/__tests__/rundown.dao.test.ts index 3c0b17e92..99a9c737b 100644 --- a/apps/server/src/api-data/rundown/__tests__/rundown.dao.test.ts +++ b/apps/server/src/api-data/rundown/__tests__/rundown.dao.test.ts @@ -1620,7 +1620,7 @@ describe('rundownMutation.clone()', () => { }); describe('rundownMutation.group()', () => { - it('groups a list of existing events into a new block', () => { + it('groups a list of existing events into a new group entry', () => { const rundown = makeRundown({ order: ['1', '2', '3'], entries: { diff --git a/apps/server/src/api-data/rundown/rundown.dao.ts b/apps/server/src/api-data/rundown/rundown.dao.ts index 96e10aa02..1f691fc96 100644 --- a/apps/server/src/api-data/rundown/rundown.dao.ts +++ b/apps/server/src/api-data/rundown/rundown.dao.ts @@ -490,9 +490,9 @@ function clone(rundown: Rundown, entry: OntimeEntry): OntimeEntry { } /** - * Groups a list of entries into a block + * Groups a list of entries * It ensures that the entries get reassigned parent and the block gets a list of events - * The block will be created at the index of the first event in the order, not at the lowest index + * The group will be created at the index of the first event in the order, not at the lowest index * Mutates the given rundown */ function group(rundown: Rundown, entryIds: EntryId[]): OntimeBlock {