mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-13 11:23:50 +00:00
feat: milestones
This commit is contained in:
committed by
Carlos Valente
parent
a7ae8598db
commit
f2913971db
@@ -232,9 +232,9 @@ export const useEntryActions = () => {
|
||||
* Updates existing entry
|
||||
*/
|
||||
const updateEntry = useCallback(
|
||||
async (event: Partial<OntimeEntry>) => {
|
||||
async (entry: Partial<OntimeEntry>) => {
|
||||
try {
|
||||
await updateEntryMutation(event);
|
||||
await updateEntryMutation(entry);
|
||||
} catch (error) {
|
||||
logAxiosError('Error updating event', error);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
|
||||
import {
|
||||
isOntimeDelay,
|
||||
isOntimeEvent,
|
||||
isOntimeMilestone,
|
||||
MaybeString,
|
||||
OntimeEntry,
|
||||
OntimeEvent,
|
||||
@@ -16,6 +17,7 @@ import { cloneEvent } from '../../common/utils/clone';
|
||||
|
||||
import RundownDelay from './rundown-delay/RundownDelay';
|
||||
import RundownEvent from './rundown-event/RundownEvent';
|
||||
import RundownMilestone from './rundown-milestone/RundownMilestone';
|
||||
import { useEventSelection } from './useEventSelection';
|
||||
|
||||
export type EventItemActions =
|
||||
@@ -201,6 +203,16 @@ export default function RundownEntry({
|
||||
);
|
||||
} else if (isOntimeDelay(data)) {
|
||||
return <RundownDelay data={data} hasCursor={hasCursor} />;
|
||||
} else if (isOntimeMilestone(data)) {
|
||||
return (
|
||||
<RundownMilestone
|
||||
colour={data.colour}
|
||||
cue={data.cue}
|
||||
entryId={data.id}
|
||||
hasCursor={hasCursor}
|
||||
title={data.title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||
import TextLikeInput from '../../../views/cuesheet/cuesheet-table/cuesheet-table-elements/TextLikeInput';
|
||||
|
||||
import EntryEditorCustomFields from './composite/EventEditorCustomFields';
|
||||
import EventTextArea from './composite/EventTextArea';
|
||||
import EntryEditorTextInput from './composite/EventTextInput';
|
||||
|
||||
import style from './EntryEditor.module.scss';
|
||||
@@ -132,6 +133,7 @@ export default function BlockEditor({ block }: BlockEditorProps) {
|
||||
<SwatchSelect name='colour' value={block.colour} handleChange={handleSubmit} />
|
||||
</div>
|
||||
<EntryEditorTextInput field='title' label='Title' initialValue={block.title} submitHandler={handleSubmit} />
|
||||
<EventTextArea field='note' label='Note' initialValue={block.note} submitHandler={handleSubmit} />
|
||||
</div>
|
||||
|
||||
<div className={style.column}>
|
||||
@@ -139,7 +141,7 @@ export default function BlockEditor({ block }: BlockEditorProps) {
|
||||
Custom Fields
|
||||
{isEditor && <AppLink search='settings=feature_settings__custom'>Manage Custom Fields</AppLink>}
|
||||
</Editor.Title>
|
||||
<EntryEditorCustomFields fields={customFields} handleSubmit={handleSubmit} event={block} />
|
||||
<EntryEditorCustomFields fields={customFields} handleSubmit={handleSubmit} entry={block} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -69,7 +69,7 @@ export default function EventEditor({ event }: EventEditorProps) {
|
||||
Custom Fields
|
||||
{isEditor && <AppLink search='settings=feature_settings__custom'>Manage Custom Fields</AppLink>}
|
||||
</Editor.Title>
|
||||
<EntryEditorCustomFields fields={customFields} handleSubmit={handleSubmit} event={event} />
|
||||
<EntryEditorCustomFields fields={customFields} handleSubmit={handleSubmit} entry={event} />
|
||||
</div>
|
||||
<div className={style.column}>
|
||||
<Editor.Title>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { useCallback } from 'react';
|
||||
import { OntimeMilestone } from 'ontime-types';
|
||||
|
||||
import * as Editor from '../../../common/components/editor-utils/EditorUtils';
|
||||
import SwatchSelect from '../../../common/components/input/colour-input/SwatchSelect';
|
||||
import Input from '../../../common/components/input/input/Input';
|
||||
import AppLink from '../../../common/components/link/app-link/AppLink';
|
||||
import { useEntryActions } from '../../../common/hooks/useEntryAction';
|
||||
import useCustomFields from '../../../common/hooks-query/useCustomFields';
|
||||
|
||||
import EntryEditorCustomFields from './composite/EventEditorCustomFields';
|
||||
import EventTextArea from './composite/EventTextArea';
|
||||
import EntryEditorTextInput from './composite/EventTextInput';
|
||||
|
||||
import style from './EntryEditor.module.scss';
|
||||
|
||||
// cue + title + colour + custom field labels
|
||||
export type MilestoneEditorUpdateTextFields = 'cue' | 'title' | 'colour' | string;
|
||||
|
||||
interface MilestoneEditorProps {
|
||||
milestone: OntimeMilestone;
|
||||
}
|
||||
export default function MilestoneEditor({ milestone }: MilestoneEditorProps) {
|
||||
const { data: customFields } = useCustomFields();
|
||||
const { updateEntry } = useEntryActions();
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(field: MilestoneEditorUpdateTextFields, value: string) => {
|
||||
// Handle custom fields
|
||||
if (typeof field === 'string' && field.startsWith('custom-')) {
|
||||
const fieldLabel = field.split('custom-')[1];
|
||||
updateEntry({ id: milestone.id, custom: { [fieldLabel]: value } });
|
||||
return;
|
||||
}
|
||||
// all other strings are text fields
|
||||
return updateEntry({ id: milestone.id, [field]: value });
|
||||
},
|
||||
[milestone.id, updateEntry],
|
||||
);
|
||||
|
||||
const isEditor = window.location.pathname.includes('editor');
|
||||
|
||||
return (
|
||||
<div className={style.content}>
|
||||
<div className={style.column}>
|
||||
<Editor.Title>Milestone data</Editor.Title>
|
||||
<div className={style.splitTwo}>
|
||||
<div>
|
||||
<Editor.Label htmlFor='entryId'>Milestone ID (read only)</Editor.Label>
|
||||
<Input id='entryId' data-testid='input-textfield' value={milestone.id} readOnly fluid />
|
||||
</div>
|
||||
<EntryEditorTextInput
|
||||
field='cue'
|
||||
label='Cue'
|
||||
initialValue={milestone.cue}
|
||||
submitHandler={handleSubmit}
|
||||
maxLength={10}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Editor.Label>Colour</Editor.Label>
|
||||
<SwatchSelect name='colour' value={milestone.colour} handleChange={handleSubmit} />
|
||||
</div>
|
||||
<EntryEditorTextInput field='title' label='Title' initialValue={milestone.title} submitHandler={handleSubmit} />
|
||||
<EventTextArea field='note' label='Note' initialValue={milestone.note} submitHandler={handleSubmit} />
|
||||
</div>
|
||||
|
||||
<div className={style.column}>
|
||||
<Editor.Title>
|
||||
Custom Fields
|
||||
{isEditor && <AppLink search='settings=feature_settings__custom'>Manage Custom Fields</AppLink>}
|
||||
</Editor.Title>
|
||||
<EntryEditorCustomFields fields={customFields} handleSubmit={handleSubmit} entry={milestone} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isOntimeBlock, isOntimeDelay, OntimeBlock, OntimeEvent } from 'ontime-types';
|
||||
import {
|
||||
isOntimeBlock,
|
||||
isOntimeDelay,
|
||||
isOntimeEvent,
|
||||
isOntimeMilestone,
|
||||
OntimeBlock,
|
||||
OntimeEvent,
|
||||
OntimeMilestone,
|
||||
} from 'ontime-types';
|
||||
|
||||
import useRundown from '../../../common/hooks-query/useRundown';
|
||||
import { useEventSelection } from '../useEventSelection';
|
||||
@@ -8,6 +16,7 @@ import EventEditorFooter from './composite/EventEditorFooter';
|
||||
import BlockEditor from './BlockEditor';
|
||||
import EventEditor from './EventEditor';
|
||||
import EventEditorEmpty from './EventEditorEmpty';
|
||||
import MilestoneEditor from './MilestoneEditor';
|
||||
|
||||
import style from './EntryEditor.module.scss';
|
||||
|
||||
@@ -15,44 +24,56 @@ export default function RundownEntryEditor() {
|
||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||
const { data } = useRundown();
|
||||
|
||||
const [event, setEvent] = useState<OntimeEvent | OntimeBlock | null>(null);
|
||||
const [entry, setEntry] = useState<OntimeEvent | OntimeBlock | OntimeMilestone | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (data.order.length === 0) {
|
||||
setEvent(null);
|
||||
setEntry(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedEventId = Array.from(selectedEvents).at(0);
|
||||
if (!selectedEventId) {
|
||||
setEvent(null);
|
||||
setEntry(null);
|
||||
return;
|
||||
}
|
||||
const event = data.entries[selectedEventId];
|
||||
|
||||
if (event && !isOntimeDelay(event)) {
|
||||
setEvent(event);
|
||||
setEntry(event);
|
||||
} else {
|
||||
setEvent(null);
|
||||
setEntry(null);
|
||||
}
|
||||
}, [data.order, data.entries, selectedEvents]);
|
||||
|
||||
if (!event) {
|
||||
if (!entry) {
|
||||
return <EventEditorEmpty />;
|
||||
}
|
||||
|
||||
if (isOntimeBlock(event)) {
|
||||
if (isOntimeEvent(entry)) {
|
||||
return (
|
||||
<div className={style.entryEditor} data-testid='editor-container'>
|
||||
<BlockEditor block={event} />
|
||||
<EventEditor event={entry} />
|
||||
<EventEditorFooter id={entry.id} cue={entry.cue} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={style.entryEditor} data-testid='editor-container'>
|
||||
<EventEditor event={event} />
|
||||
<EventEditorFooter id={event.id} cue={event.cue} />
|
||||
</div>
|
||||
);
|
||||
if (isOntimeMilestone(entry)) {
|
||||
return (
|
||||
<div className={style.entryEditor} data-testid='editor-container'>
|
||||
<MilestoneEditor milestone={entry} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isOntimeBlock(entry)) {
|
||||
return (
|
||||
<div className={style.entryEditor} data-testid='editor-container'>
|
||||
<BlockEditor block={entry} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CSSProperties, Fragment } from 'react';
|
||||
import { CustomFields, OntimeBlock, OntimeEvent } from 'ontime-types';
|
||||
import { CustomFields, OntimeBlock, OntimeEvent, OntimeMilestone } from 'ontime-types';
|
||||
|
||||
import { getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||
import { EventEditorUpdateFields } from '../EventEditor';
|
||||
@@ -12,21 +12,21 @@ import style from '../EntryEditor.module.scss';
|
||||
|
||||
interface EntryEditorCustomFieldsProps {
|
||||
fields: CustomFields;
|
||||
event: OntimeEvent | OntimeBlock;
|
||||
entry: OntimeEvent | OntimeBlock | OntimeMilestone;
|
||||
handleSubmit: (field: EventEditorUpdateFields, value: string) => void;
|
||||
}
|
||||
|
||||
export default function EntryEditorCustomFields({
|
||||
fields: customFields,
|
||||
handleSubmit,
|
||||
event,
|
||||
entry,
|
||||
}: EntryEditorCustomFieldsProps) {
|
||||
return (
|
||||
<Fragment>
|
||||
{Object.keys(customFields).map((fieldKey) => {
|
||||
const key = `${event.id}-${fieldKey}`;
|
||||
const key = `${entry.id}-${fieldKey}`;
|
||||
const fieldName = `custom-${fieldKey}`;
|
||||
const initialValue = event.custom[fieldKey] ?? '';
|
||||
const initialValue = entry.custom[fieldKey] ?? '';
|
||||
const { backgroundColor, color } = getAccessibleColour(customFields[fieldKey].colour);
|
||||
const labelText = customFields[fieldKey].label;
|
||||
|
||||
|
||||
@@ -44,6 +44,16 @@ function QuickAddBlock({ previousEventId, parentBlock, backgroundColor }: QuickA
|
||||
);
|
||||
};
|
||||
|
||||
const addMilestone = () => {
|
||||
addEntry(
|
||||
{ type: SupportedEntry.Milestone, parent: parentBlock },
|
||||
{
|
||||
lastEventId: previousEventId,
|
||||
after: previousEventId,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const addBlock = () => {
|
||||
if (parentBlock !== null) {
|
||||
return;
|
||||
@@ -76,6 +86,11 @@ function QuickAddBlock({ previousEventId, parentBlock, backgroundColor }: QuickA
|
||||
Delay
|
||||
</Toolbar.Button>
|
||||
|
||||
<Toolbar.Button render={<Button size='small' variant='subtle-white' />} onClick={addMilestone}>
|
||||
<IoAdd />
|
||||
Milestone
|
||||
</Toolbar.Button>
|
||||
|
||||
{parentBlock === null && (
|
||||
<Toolbar.Button render={<Button size='small' variant='subtle-white' />} onClick={addBlock}>
|
||||
<IoAdd />
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
@use '../blockMixins' as *;
|
||||
|
||||
.milestone {
|
||||
@include block-styling;
|
||||
|
||||
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%);
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 2rem 8rem 1fr auto auto;
|
||||
align-items: center;
|
||||
height: $secondary-block-height;
|
||||
gap: 0.5rem;
|
||||
|
||||
&.hasCursor {
|
||||
outline: 1px solid $block-cursor-color;
|
||||
}
|
||||
}
|
||||
|
||||
.binder {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
background-color: var(--user-bg, $block-bg);
|
||||
}
|
||||
|
||||
.drag {
|
||||
@include drag-style;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { MouseEvent, useCallback, useRef } from 'react';
|
||||
import { IoCheckmarkDone, IoClose, IoReorderTwo } from 'react-icons/io5';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { EntryId } from 'ontime-types';
|
||||
|
||||
import Button from '../../../common/components/buttons/Button';
|
||||
import Input from '../../../common/components/input/input/Input';
|
||||
import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
|
||||
import { useEntryActions } from '../../../common/hooks/useEntryAction';
|
||||
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||
import { useEventSelection } from '../useEventSelection';
|
||||
|
||||
import style from './RundownMilestone.module.scss';
|
||||
|
||||
interface RundownMilestoneProps {
|
||||
colour: string;
|
||||
cue: string;
|
||||
entryId: EntryId;
|
||||
hasCursor: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default function RundownMilestone({ colour, cue, entryId, hasCursor, title }: RundownMilestoneProps) {
|
||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||
const { updateEntry, deleteEntry } = useEntryActions();
|
||||
const { selectedEvents, setSelectedBlock } = useEventSelection();
|
||||
|
||||
const {
|
||||
attributes: dragAttributes,
|
||||
listeners: dragListeners,
|
||||
setNodeRef,
|
||||
isDragging,
|
||||
transform,
|
||||
transition,
|
||||
} = useSortable({
|
||||
id: entryId,
|
||||
data: {
|
||||
type: 'milestone',
|
||||
},
|
||||
animateLayoutChanges: () => false,
|
||||
});
|
||||
|
||||
const handleFocusClick = (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
|
||||
// event.button === 2 is a right-click
|
||||
// disable selection if the user selected events and right clicks
|
||||
// so the context menu shows up
|
||||
if (selectedEvents.size > 1 && event.button === 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// UI indexes are 1 based
|
||||
setSelectedBlock({ id: entryId });
|
||||
};
|
||||
|
||||
const handleUpdate = (field: 'cue' | 'title', value: string) => {
|
||||
updateEntry({ id: entryId, [field]: value });
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteEntry([entryId]);
|
||||
};
|
||||
|
||||
const dragStyle = {
|
||||
zIndex: isDragging ? 2 : 'inherit',
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
const binderColours = colour && getAccessibleColour(colour);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx([style.milestone, hasCursor ? style.hasCursor : null])}
|
||||
ref={setNodeRef}
|
||||
onClick={handleFocusClick}
|
||||
style={{ ...dragStyle, '--user-bg': colour }}
|
||||
data-testid='rundown-milestone'
|
||||
>
|
||||
<div className={style.binder}>
|
||||
<span
|
||||
className={style.drag}
|
||||
style={{ ...binderColours }}
|
||||
ref={handleRef}
|
||||
{...dragAttributes}
|
||||
{...dragListeners}
|
||||
>
|
||||
<IoReorderTwo />
|
||||
</span>
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface MilestoneTextInputProps {
|
||||
field: 'cue' | 'title';
|
||||
initialValue: string;
|
||||
placeholder?: string;
|
||||
submitHandler: (field: 'cue' | 'title', value: string) => void;
|
||||
}
|
||||
|
||||
function MilestoneTextInput({ field, initialValue, placeholder, submitHandler }: MilestoneTextInputProps) {
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
||||
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
||||
submitOnEnter: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<Input
|
||||
id={field}
|
||||
ref={ref}
|
||||
fluid
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
OntimeDelay,
|
||||
OntimeEntry,
|
||||
OntimeEvent,
|
||||
OntimeMilestone,
|
||||
PlayableEvent,
|
||||
RundownEntries,
|
||||
SupportedEntry,
|
||||
@@ -83,10 +84,10 @@ function processEntry(
|
||||
processedData.groupColour = entry.colour;
|
||||
} else {
|
||||
// for delays and blocks, we insert the group metadata
|
||||
if ((entry as OntimeEvent | OntimeDelay).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
|
||||
processedData.groupId = (entry as OntimeEvent | OntimeDelay).parent;
|
||||
if ((entry as OntimeEvent | OntimeDelay).parent === null) {
|
||||
processedData.groupId = (entry as OntimeEvent | OntimeDelay | OntimeMilestone).parent;
|
||||
if ((entry as OntimeEvent | OntimeDelay | OntimeMilestone).parent === null) {
|
||||
// if the entry has no parent, it cannot have a group colour
|
||||
processedData.groupColour = undefined;
|
||||
}
|
||||
|
||||
+45
-1
@@ -1,7 +1,15 @@
|
||||
import { RefObject, useEffect } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { RowModel, Table } from '@tanstack/react-table';
|
||||
import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeBlock, OntimeEntry, Rundown } from 'ontime-types';
|
||||
import {
|
||||
isOntimeBlock,
|
||||
isOntimeDelay,
|
||||
isOntimeEvent,
|
||||
isOntimeMilestone,
|
||||
OntimeBlock,
|
||||
OntimeEntry,
|
||||
Rundown,
|
||||
} from 'ontime-types';
|
||||
import { colourToHex, cssOrHexToColour } from 'ontime-utils';
|
||||
|
||||
import { RUNDOWN } from '../../../../common/api/constants';
|
||||
@@ -13,6 +21,7 @@ import { usePersistedCuesheetOptions } from '../../cuesheet.options';
|
||||
import BlockRow from './BlockRow';
|
||||
import DelayRow from './DelayRow';
|
||||
import EventRow from './EventRow';
|
||||
import MilestoneRow from './MilestoneRow';
|
||||
import { cleanup } from './rowObserver';
|
||||
|
||||
interface CuesheetBodyProps {
|
||||
@@ -90,6 +99,41 @@ export default function CuesheetBody({ rowModel, selectedRef, table }: CuesheetB
|
||||
}
|
||||
return <DelayRow key={key} duration={delayVal} parentBgColour={parentBgColour} />;
|
||||
}
|
||||
if (isOntimeMilestone(entry)) {
|
||||
if (isPast && hidePast) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let rowBgColour: string | undefined;
|
||||
if (entry.colour) {
|
||||
// the colour is user defined and might be invalid
|
||||
const accessibleBackgroundColor = cssOrHexToColour(getAccessibleColour(entry.colour).backgroundColor);
|
||||
if (accessibleBackgroundColor !== null) {
|
||||
rowBgColour = colourToHex({
|
||||
...accessibleBackgroundColor,
|
||||
alpha: accessibleBackgroundColor.alpha * 0.25,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let parentBgColour: string | null = null;
|
||||
if (entry.parent) {
|
||||
const rundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const parentEntry = rundown?.entries[entry.parent];
|
||||
parentBgColour = (parentEntry as OntimeBlock).colour ?? null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MilestoneRow
|
||||
key={key}
|
||||
isPast={isPast}
|
||||
parentBgColour={parentBgColour}
|
||||
rowBgColour={rowBgColour}
|
||||
rowId={row.id}
|
||||
table={table}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (isOntimeEvent(entry)) {
|
||||
eventIndex++;
|
||||
const isSelected = key === selectedEventId;
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
@import "../CuesheetTable.module.scss";
|
||||
|
||||
.milestoneRow {
|
||||
background: color-mix(in srgb, transparent 92%, var(--user-bg, $gray-500) 8%);
|
||||
border-left: 4px solid var(--user-bg, $gray-500);
|
||||
|
||||
&:hover {
|
||||
outline: 1px solid $blue-500;
|
||||
outline-offset: -1px;
|
||||
background: color-mix(in srgb, transparent 80%, var(--user-bg, $gray-500) 20%);
|
||||
}
|
||||
|
||||
td {
|
||||
background-color: $gray-1250;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { IoEllipsisHorizontal } from 'react-icons/io5';
|
||||
import { useSessionStorage } from '@mantine/hooks';
|
||||
import { flexRender, Table } from '@tanstack/react-table';
|
||||
import { OntimeEntry } from 'ontime-types';
|
||||
|
||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||
import { cx, enDash } from '../../../../common/utils/styleUtils';
|
||||
import { AppMode, sessionKeys } from '../../../../ontimeConfig';
|
||||
import { usePersistedCuesheetOptions } from '../../cuesheet.options';
|
||||
|
||||
import style from './MilestoneRow.module.scss';
|
||||
|
||||
interface MilestoneRowProps {
|
||||
isPast: boolean;
|
||||
parentBgColour: string | null;
|
||||
rowBgColour?: string;
|
||||
rowId: string;
|
||||
table: Table<OntimeEntry>;
|
||||
}
|
||||
|
||||
export default function MilestoneRow({ isPast, parentBgColour, rowBgColour, rowId, table }: MilestoneRowProps) {
|
||||
const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn);
|
||||
const [cuesheetMode] = useSessionStorage<AppMode>({
|
||||
key: sessionKeys.cuesheetMode,
|
||||
defaultValue: AppMode.Edit,
|
||||
});
|
||||
|
||||
return (
|
||||
<tr
|
||||
className={cx([style.milestoneRow, Boolean(parentBgColour) && style.hasParent])}
|
||||
style={{
|
||||
opacity: `${isPast ? '0.2' : '1'}`,
|
||||
'--user-bg': parentBgColour ?? 'transparent',
|
||||
}}
|
||||
data-testid='cuesheet-milestone'
|
||||
>
|
||||
{cuesheetMode === AppMode.Edit && (
|
||||
<td className={style.actionColumn} tabIndex={-1} role='cell'>
|
||||
<IconButton aria-label='Options' variant='subtle-white' size='small' onClick={() => undefined}>
|
||||
<IoEllipsisHorizontal />
|
||||
</IconButton>
|
||||
</td>
|
||||
)}
|
||||
{!hideIndexColumn && (
|
||||
<td className={style.indexColumn} tabIndex={-1} role='cell'>
|
||||
{enDash}
|
||||
</td>
|
||||
)}
|
||||
{table
|
||||
.getRow(rowId)
|
||||
.getVisibleCells()
|
||||
.map((cell) => {
|
||||
const canRender =
|
||||
cell.column.id !== 'duration' && cell.column.id !== 'timeStart' && cell.column.id !== 'timeEnd';
|
||||
return (
|
||||
<td
|
||||
key={cell.id}
|
||||
style={{
|
||||
width: `calc(var(--col-${cell.column.id}-size) * 1px)`,
|
||||
backgroundColor: rowBgColour,
|
||||
}}
|
||||
tabIndex={-1}
|
||||
role='cell'
|
||||
>
|
||||
{canRender && flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user