mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-07 15:29:10 +00:00
refactor: style tweaks and bug fixes
- italic and bold for rundown metadata entries - allow deleting groups - make groups in order - correct style for milestones - open playing groups - style tweaks
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 400;
|
font-weight: inherit;
|
||||||
color: $gray-200;
|
color: $gray-200;
|
||||||
border-radius: $component-border-radius-md;
|
border-radius: $component-border-radius-md;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 400;
|
font-weight: inherit;
|
||||||
color: $gray-200;
|
color: $gray-200;
|
||||||
border-radius: $component-border-radius-md;
|
border-radius: $component-border-radius-md;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
@@ -34,11 +34,13 @@
|
|||||||
|
|
||||||
.subtle {
|
.subtle {
|
||||||
background-color: $gray-1200;
|
background-color: $gray-1200;
|
||||||
|
padding-top: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ghosted {
|
.ghosted {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
padding-top: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fluid {
|
.fluid {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
transition-property: color;
|
transition-property: color;
|
||||||
transition-duration: $transition-time-action;
|
transition-duration: $transition-time-action;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
&.inline {
|
&.inline {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -20,4 +21,4 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 1px 0 0 currentColor;
|
box-shadow: 0 1px 0 0 currentColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
padding-inline: 1rem;
|
padding-inline: 1rem;
|
||||||
min-width: min(680px, 90vw);
|
min-width: min(680px, 90vw);
|
||||||
min-height: min(200px, 10vh);
|
min-height: min(200px, 10vh);
|
||||||
max-width: min(680px, 90vw);
|
max-width: min(800px, 90vw);
|
||||||
|
|
||||||
background-color: $gray-1250;
|
background-color: $gray-1250;
|
||||||
color: $ui-white;
|
color: $ui-white;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { dayInMs, generateId, MILLIS_PER_SECOND, parseUserTime, swapEventData } from 'ontime-utils';
|
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 { RUNDOWN } from '../api/constants';
|
||||||
import {
|
import {
|
||||||
deleteEntries,
|
deleteEntries,
|
||||||
@@ -622,13 +622,22 @@ export const useEntryActions = () => {
|
|||||||
*/
|
*/
|
||||||
const groupEntries = useCallback(
|
const groupEntries = useCallback(
|
||||||
async (entryIds: EntryId[]) => {
|
async (entryIds: EntryId[]) => {
|
||||||
|
if (entryIds.length === 0) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await groupEntriesMutation(entryIds);
|
if (entryIds.length === 1) {
|
||||||
|
await groupEntriesMutation(entryIds);
|
||||||
|
} else {
|
||||||
|
const rundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||||
|
if (!rundown) return;
|
||||||
|
const orderedIds = orderEntries(entryIds, rundown.flatOrder);
|
||||||
|
await groupEntriesMutation(orderedIds);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logAxiosError('Error grouping entries', error);
|
logAxiosError('Error grouping entries', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[groupEntriesMutation],
|
[groupEntriesMutation, queryClient],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+6
-2
@@ -107,7 +107,9 @@ export default function AutomationSettingsForm(props: AutomationSettingsProps) {
|
|||||||
<Switch
|
<Switch
|
||||||
size='large'
|
size='large'
|
||||||
checked={watch('enabledAutomations')}
|
checked={watch('enabledAutomations')}
|
||||||
onCheckedChange={(value: boolean) => setValue('enabledAutomations', value, { shouldDirty: true })}
|
onCheckedChange={(value: boolean) =>
|
||||||
|
setValue('enabledAutomations', value, { shouldDirty: true, shouldValidate: true })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
</Panel.ListGroup>
|
</Panel.ListGroup>
|
||||||
@@ -125,7 +127,9 @@ export default function AutomationSettingsForm(props: AutomationSettingsProps) {
|
|||||||
<Switch
|
<Switch
|
||||||
size='large'
|
size='large'
|
||||||
checked={watch('enabledOscIn')}
|
checked={watch('enabledOscIn')}
|
||||||
onCheckedChange={(value: boolean) => setValue('enabledOscIn', value, { shouldDirty: true })}
|
onCheckedChange={(value: boolean) =>
|
||||||
|
setValue('enabledOscIn', value, { shouldDirty: true, shouldValidate: true })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
|
|||||||
@@ -22,8 +22,15 @@ interface TriggerFormProps {
|
|||||||
postSubmit: () => void;
|
postSubmit: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TriggerForm(props: TriggerFormProps) {
|
export default function TriggerForm({
|
||||||
const { automations, initialId, initialTitle, initialAutomationId, initialTrigger, onCancel, postSubmit } = props;
|
automations,
|
||||||
|
initialId,
|
||||||
|
initialTitle,
|
||||||
|
initialAutomationId,
|
||||||
|
initialTrigger,
|
||||||
|
onCancel,
|
||||||
|
postSubmit,
|
||||||
|
}: TriggerFormProps) {
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
register,
|
||||||
@@ -35,8 +42,8 @@ export default function TriggerForm(props: TriggerFormProps) {
|
|||||||
} = useForm<TriggerDTO>({
|
} = useForm<TriggerDTO>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
title: initialTitle,
|
title: initialTitle,
|
||||||
trigger: initialTrigger,
|
trigger: initialTrigger ?? (cycles[0].value as TimerLifeCycle | undefined),
|
||||||
automationId: initialAutomationId,
|
automationId: initialAutomationId ?? automations?.[Object.keys(automations)[0]]?.id,
|
||||||
},
|
},
|
||||||
resetOptions: {
|
resetOptions: {
|
||||||
keepDirtyValues: true,
|
keepDirtyValues: true,
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ const staticSelectProperties = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const staticNextSelectProperties = [
|
const staticNextSelectProperties = [
|
||||||
{ value: 'eventNow.id', label: 'Next ID' },
|
{ value: 'eventNext.id', label: 'Next ID' },
|
||||||
{ value: 'eventNow.title', label: 'Next Title' },
|
{ value: 'eventNext.title', label: 'Next Title' },
|
||||||
{ value: 'eventNow.cue', label: 'Next Cue' },
|
{ value: 'eventNext.cue', label: 'Next Cue' },
|
||||||
];
|
];
|
||||||
|
|
||||||
type SelectableField = {
|
type SelectableField = {
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ export default function PreviewRundown(props: PreviewRundownProps) {
|
|||||||
const fieldLabels = fieldKeys.map((key) => customFields[key].label);
|
const fieldLabels = fieldKeys.map((key) => customFields[key].label);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel.Table>
|
<Panel.Table className={style.nowrap}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from 'react';
|
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 { Playback, TimerPhase } from 'ontime-types';
|
||||||
import { validatePlayback } from 'ontime-utils';
|
import { validatePlayback } from 'ontime-utils';
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className={style.extra}>
|
<div className={style.extra}>
|
||||||
<TapButton onClick={setPlayback.roll} disabled={disableRoll} theme={Playback.Roll} active={isRolling}>
|
<TapButton onClick={setPlayback.roll} disabled={disableRoll} theme={Playback.Roll} active={isRolling}>
|
||||||
<IoTime />
|
Roll
|
||||||
</TapButton>
|
</TapButton>
|
||||||
<TapButton onClick={setPlayback.reload} disabled={disableReload}>
|
<TapButton onClick={setPlayback.reload} disabled={disableReload}>
|
||||||
<IoReload className={style.invertX} />
|
<IoReload className={style.invertX} />
|
||||||
|
|||||||
@@ -298,8 +298,15 @@ export default function Rundown({ data }: RundownProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const index = order.findIndex((id) => id === featureData.selectedEventId);
|
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 });
|
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
|
* On drag end, we reorder the events
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { EntryId, OntimeBlock, OntimeDelay, OntimeEvent, RundownEntries, SupportedEntry } from 'ontime-types';
|
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()', () => {
|
describe('makeRundownMetadata()', () => {
|
||||||
it('processes nested rundown data', () => {
|
it('processes nested rundown data', () => {
|
||||||
@@ -339,7 +339,6 @@ describe('makeSortableList()', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('moveUp()', () => {
|
describe('moveUp()', () => {
|
||||||
const rundown = {
|
const rundown = {
|
||||||
entries: {
|
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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ export default function RundownBlock({ data, hasCursor, collapsed, onCollapse }:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
|
|
||||||
label: 'Ungroup',
|
label: 'Ungroup',
|
||||||
icon: IoFolderOpenOutline,
|
icon: IoFolderOpenOutline,
|
||||||
onClick: () => ungroup(data.id),
|
onClick: () => ungroup(data.id),
|
||||||
@@ -55,7 +54,6 @@ export default function RundownBlock({ data, hasCursor, collapsed, onCollapse }:
|
|||||||
label: 'Delete Group',
|
label: 'Delete Group',
|
||||||
icon: IoTrash,
|
icon: IoTrash,
|
||||||
onClick: () => deleteEntry([data.id]),
|
onClick: () => deleteEntry([data.id]),
|
||||||
disabled: true,
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ $skip-opacity: 0.2;
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
background-color: $gray-1050; // to override inline
|
background-color: $gray-1050; // to override inline
|
||||||
color: $section-white;
|
color: $section-white; // to override inline
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
border-radius: 3px 0 0 3px;
|
border-radius: 3px 0 0 3px;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
margin-left: calc(2rem + 1px); // binder + border
|
margin-left: calc(2rem + 1px); // binder + border
|
||||||
margin-block: 0.125rem;
|
margin-block: 0.125rem;
|
||||||
padding-right: 0.25rem;
|
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;
|
display: grid;
|
||||||
grid-template-columns: 2rem 8rem 1fr auto;
|
grid-template-columns: 2rem 8rem 1fr auto;
|
||||||
@@ -23,7 +24,8 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
background-color: var(--user-bg, $block-bg);
|
background-color: $gray-1050; // to override inline
|
||||||
|
color: $section-white; // to override inline
|
||||||
}
|
}
|
||||||
|
|
||||||
.drag {
|
.drag {
|
||||||
|
|||||||
@@ -76,17 +76,11 @@ export default function RundownMilestone({ colour, cue, entryId, hasCursor, titl
|
|||||||
className={cx([style.milestone, hasCursor ? style.hasCursor : null])}
|
className={cx([style.milestone, hasCursor ? style.hasCursor : null])}
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
onClick={handleFocusClick}
|
onClick={handleFocusClick}
|
||||||
style={{ ...dragStyle, '--user-bg': colour }}
|
style={dragStyle}
|
||||||
data-testid='rundown-milestone'
|
data-testid='rundown-milestone'
|
||||||
>
|
>
|
||||||
<div className={style.binder}>
|
<div className={style.binder} style={{ ...binderColours }}>
|
||||||
<span
|
<span className={style.drag} ref={handleRef} {...dragAttributes} {...dragListeners}>
|
||||||
className={style.drag}
|
|
||||||
style={{ ...binderColours }}
|
|
||||||
ref={handleRef}
|
|
||||||
{...dragAttributes}
|
|
||||||
{...dragListeners}
|
|
||||||
>
|
|
||||||
<IoReorderTwo />
|
<IoReorderTwo />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -306,3 +306,17 @@ export function moveDown(
|
|||||||
// default - swap positions with next entry
|
// default - swap positions with next entry
|
||||||
return { destinationId: nextEntryId, order: 'after' };
|
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;
|
||||||
|
}
|
||||||
|
|||||||
+2
@@ -14,6 +14,8 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
|
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
td {
|
td {
|
||||||
min-height: 3.5rem;
|
min-height: 3.5rem;
|
||||||
padding-top: 0.75rem !important; // fighting styles from cuesheet-table
|
padding-top: 0.75rem !important; // fighting styles from cuesheet-table
|
||||||
|
|||||||
+2
@@ -4,6 +4,8 @@
|
|||||||
background: color-mix(in srgb, transparent 92%, var(--user-bg, $gray-500) 8%);
|
background: color-mix(in srgb, transparent 92%, var(--user-bg, $gray-500) 8%);
|
||||||
border-left: 4px solid var(--user-bg, $gray-500);
|
border-left: 4px solid var(--user-bg, $gray-500);
|
||||||
|
|
||||||
|
font-style: italic;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
outline: 1px solid $blue-500;
|
outline: 1px solid $blue-500;
|
||||||
outline-offset: -1px;
|
outline-offset: -1px;
|
||||||
|
|||||||
+1
@@ -3,6 +3,7 @@
|
|||||||
height: 2rem;
|
height: 2rem;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-radius: $component-border-radius-md;
|
border-radius: $component-border-radius-md;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1620,7 +1620,7 @@ describe('rundownMutation.clone()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('rundownMutation.group()', () => {
|
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({
|
const rundown = makeRundown({
|
||||||
order: ['1', '2', '3'],
|
order: ['1', '2', '3'],
|
||||||
entries: {
|
entries: {
|
||||||
|
|||||||
@@ -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
|
* 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
|
* Mutates the given rundown
|
||||||
*/
|
*/
|
||||||
function group(rundown: Rundown, entryIds: EntryId[]): OntimeBlock {
|
function group(rundown: Rundown, entryIds: EntryId[]): OntimeBlock {
|
||||||
|
|||||||
Reference in New Issue
Block a user