mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-18 21:54:09 +00:00
Alpha 5 (#1741)
* refactor: align header columns * refactor: improve scrollbar visibility * refactor: center align table elements * refactor: make param elements stateful * fix: issue with collapsed elements not loosing value * fix: prevent search params containing multiple alias references * fix: the issue where a file disappears if it is both migrated and recovered in the same load operation (#1744) * refactor: disable group action for elements in groups * refactor: move context menu items into the event element (#1747) * feat: sheet import new features for v4 (#1730) * import milestone * fixup! import milestone * test: milestone import * add entries to group stop on new group or on group-end type * fixup! add entries to group * cleanup * add event target duration * link start if undefined * add skip import type * extract some to the excel paresing functions * tweaks to presentation * move file --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me> * fix: notify runtimeStore of events bieng groupd * fix: improve authentication and stage detection in demo * chore: ship logo with project * fix: client is referenced by name * fix: prevent reflow in event editor * fix: stale render on selected event due to ref mismatch * Create/Load/Delete multiple rundowns (#1696) * refactor: restore last loaded rundown * refactor: initialise rundown in ProjectService * feat: allow switching rundowns ensure on coordination between the db object and the working object server provide list of rundowns implement switch in the UI implement delete implement new rundown button * fix: render order for floating button * refactor: appropriate names to service * refactor: rundown endpoints * refactor: save last loaded rundown ID * refactor: rundown management UI * refactor: emit refetch all on project load --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me> * feat: recover single event subscription * fixup! feat: sheet import new features for v4 (#1730) * fix: prevent dropping a group inside another * fixup! refactor: move context menu items into the event element (#1747) * fix: prevent stale references to custom fields * fix: propagate updates to all rundowns * refactor: client rundown metadata (#1728) * generate metadata in the hook * move test * ensure there is always a last element * use for-loop * update metadata in useEfect * fully extract metadata generation * use direct assignment * cleanup --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me> * refactor: small imporvement and tests for coerce functions (#1752) * refactor: small imporvement and tests for coerce functions add test `coerceString` add test `coerceBoolean` add test `coerceColour` * remove old todo * fix: consistent quick add behaviour * refactor: create flat rundown with metadata * fix: show add buttons on top * feat: allow editing milestones * refactor: style tweaks to rundown elements refactor: milestones are full width refactor: cuesheet header alignment fix: editor styling in cuesheet * refactor: virtualise table * refactor: improve overscan (#1758) * bump version to 4.0.0-alpha.5 --------- Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk>
This commit is contained in:
-189
@@ -1,189 +0,0 @@
|
||||
import { RefObject, useEffect } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { RowModel, Table } from '@tanstack/react-table';
|
||||
import {
|
||||
isOntimeDelay,
|
||||
isOntimeEvent,
|
||||
isOntimeGroup,
|
||||
isOntimeMilestone,
|
||||
OntimeEntry,
|
||||
OntimeGroup,
|
||||
Rundown,
|
||||
} from 'ontime-types';
|
||||
import { colourToHex, cssOrHexToColour } from 'ontime-utils';
|
||||
|
||||
import { RUNDOWN } from '../../../../common/api/constants';
|
||||
import EmptyTableBody from '../../../../common/components/state/EmptyTableBody';
|
||||
import { useSelectedEventId } from '../../../../common/hooks/useSocket';
|
||||
import { getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||
import { usePersistedCuesheetOptions } from '../../cuesheet.options';
|
||||
|
||||
import DelayRow from './DelayRow';
|
||||
import EventRow from './EventRow';
|
||||
import GroupRow from './GroupRow';
|
||||
import MilestoneRow from './MilestoneRow';
|
||||
import { cleanup } from './rowObserver';
|
||||
|
||||
interface CuesheetBodyProps {
|
||||
rowModel: RowModel<OntimeEntry>;
|
||||
selectedRef: RefObject<HTMLTableRowElement | null>;
|
||||
table: Table<OntimeEntry>;
|
||||
}
|
||||
|
||||
export default function CuesheetBody({ rowModel, selectedRef, table }: CuesheetBodyProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const { selectedEventId } = useSelectedEventId();
|
||||
const hidePast = usePersistedCuesheetOptions((state) => state.hidePast);
|
||||
const hideDelays = usePersistedCuesheetOptions((state) => state.hideDelays);
|
||||
|
||||
let eventIndex = 0;
|
||||
// for the first event, it will be past if there is something selected
|
||||
let isPast = Boolean(selectedEventId);
|
||||
let hadGroup = false;
|
||||
|
||||
// remove the observer when the table unmounts
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cleanup();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (rowModel.rows.length === 0) {
|
||||
return <EmptyTableBody text='No data in rundown' />;
|
||||
}
|
||||
|
||||
return (
|
||||
<tbody>
|
||||
{rowModel.rows.map((row, index) => {
|
||||
const key = row.original.id;
|
||||
const isSelected = selectedEventId === key;
|
||||
const entry = row.original;
|
||||
if (isSelected) {
|
||||
isPast = false;
|
||||
}
|
||||
|
||||
if (isOntimeGroup(entry)) {
|
||||
return (
|
||||
<GroupRow
|
||||
key={key}
|
||||
groupId={entry.id}
|
||||
colour={entry.colour}
|
||||
hidePast={isPast && hidePast}
|
||||
rowId={row.id}
|
||||
rowIndex={row.index}
|
||||
table={table}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (isOntimeDelay(entry)) {
|
||||
if (isPast && hidePast) {
|
||||
return null;
|
||||
}
|
||||
const delayVal = entry.duration;
|
||||
if (hideDelays || delayVal === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let parentBgColour: string | null = null;
|
||||
if (entry.parent) {
|
||||
const rundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const parentEntry = rundown?.entries[entry.parent] as OntimeGroup | undefined;
|
||||
parentBgColour = parentEntry?.colour ?? null;
|
||||
}
|
||||
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 OntimeGroup | undefined)?.colour ?? null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MilestoneRow
|
||||
key={key}
|
||||
entryId={entry.id}
|
||||
isPast={isPast}
|
||||
parentBgColour={parentBgColour}
|
||||
parentId={entry.parent}
|
||||
rowBgColour={rowBgColour}
|
||||
rowId={row.id}
|
||||
rowIndex={index}
|
||||
table={table}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (isOntimeEvent(entry)) {
|
||||
eventIndex++;
|
||||
const isSelected = key === selectedEventId;
|
||||
|
||||
if (isPast && hidePast) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let rowBgColour: string | undefined;
|
||||
if (isSelected) {
|
||||
rowBgColour = '#087A27'; // $active-green
|
||||
} else 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 | undefined;
|
||||
let firstAfterGroup = false;
|
||||
if (entry.parent) {
|
||||
const rundown = queryClient.getQueryData<Rundown>(RUNDOWN);
|
||||
const parentEntry = rundown?.entries[entry.parent] as OntimeGroup | undefined;
|
||||
parentBgColour = parentEntry?.colour;
|
||||
hadGroup = true;
|
||||
} else if (hadGroup) {
|
||||
firstAfterGroup = true;
|
||||
hadGroup = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<EventRow
|
||||
key={row.id}
|
||||
rowId={row.id}
|
||||
event={entry}
|
||||
eventIndex={eventIndex}
|
||||
rowIndex={index}
|
||||
isPast={isPast}
|
||||
selectedRef={isSelected ? selectedRef : undefined}
|
||||
rowBgColour={rowBgColour}
|
||||
parentBgColour={parentBgColour}
|
||||
table={table}
|
||||
firstAfterGroup={firstAfterGroup}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// currently there is no scenario where entryType is not handled above, either way...
|
||||
return null;
|
||||
})}
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
+34
-43
@@ -1,8 +1,8 @@
|
||||
import { CSSProperties } from 'react';
|
||||
import { horizontalListSortingStrategy, SortableContext } from '@dnd-kit/sortable';
|
||||
import { flexRender, HeaderGroup } from '@tanstack/react-table';
|
||||
import { OntimeEntry } from 'ontime-types';
|
||||
|
||||
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
||||
import { getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||
import { AppMode } from '../../../../ontimeConfig';
|
||||
import { usePersistedCuesheetOptions } from '../../cuesheet.options';
|
||||
@@ -12,54 +12,45 @@ import { SortableCell } from './SortableCell';
|
||||
import style from '../CuesheetTable.module.scss';
|
||||
|
||||
interface CuesheetHeaderProps {
|
||||
headerGroups: HeaderGroup<OntimeEntry>[];
|
||||
headerGroup: HeaderGroup<ExtendedEntry>;
|
||||
cuesheetMode: AppMode;
|
||||
}
|
||||
|
||||
export default function CuesheetHeader({ headerGroups, cuesheetMode }: CuesheetHeaderProps) {
|
||||
export default function CuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHeaderProps) {
|
||||
const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn);
|
||||
|
||||
return (
|
||||
<thead className={style.tableHeader}>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const key = headerGroup.id;
|
||||
<tr key={headerGroup.id}>
|
||||
{cuesheetMode === AppMode.Edit && <th className={style.actionColumn} tabIndex={-1} />}
|
||||
{!hideIndexColumn && (
|
||||
<th className={style.indexColumn} tabIndex={-1}>
|
||||
#
|
||||
</th>
|
||||
)}
|
||||
<SortableContext key={headerGroup.id} items={headerGroup.headers} strategy={horizontalListSortingStrategy}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
const customBackground = header.column.columnDef.meta?.colour;
|
||||
const canWrite = header.column.columnDef.meta?.canWrite;
|
||||
|
||||
return (
|
||||
<tr key={headerGroup.id}>
|
||||
{cuesheetMode === AppMode.Edit && <th className={style.actionColumn} tabIndex={-1} />}
|
||||
{!hideIndexColumn && (
|
||||
<th className={style.indexColumn} tabIndex={-1}>
|
||||
#
|
||||
</th>
|
||||
)}
|
||||
<SortableContext key={key} items={headerGroup.headers} strategy={horizontalListSortingStrategy}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
const customBackground = header.column.columnDef.meta?.colour;
|
||||
const canWrite = header.column.columnDef.meta?.canWrite;
|
||||
const customStyles: CSSProperties = {
|
||||
opacity: canWrite ? 1 : 0.6,
|
||||
};
|
||||
if (customBackground) {
|
||||
const customColour = getAccessibleColour(customBackground);
|
||||
customStyles.backgroundColor = customColour.backgroundColor;
|
||||
customStyles.color = customColour.color;
|
||||
}
|
||||
|
||||
const customStyles: CSSProperties = {
|
||||
opacity: canWrite ? 1 : 0.6,
|
||||
};
|
||||
if (customBackground) {
|
||||
const customColour = getAccessibleColour(customBackground);
|
||||
customStyles.backgroundColor = customColour.backgroundColor;
|
||||
customStyles.color = customColour.color;
|
||||
}
|
||||
|
||||
return (
|
||||
<SortableCell
|
||||
key={header.column.columnDef.id}
|
||||
header={header}
|
||||
injectedStyles={{ width: `calc(var(--header-${header?.id}-size) * 1px)`, ...customStyles }}
|
||||
>
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</SortableCell>
|
||||
);
|
||||
})}
|
||||
</SortableContext>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
return (
|
||||
<SortableCell
|
||||
key={header.column.columnDef.id}
|
||||
header={header}
|
||||
injectedStyles={{ width: `calc(var(--header-${header?.id}-size) * 1px)`, ...customStyles }}
|
||||
>
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</SortableCell>
|
||||
);
|
||||
})}
|
||||
</SortableContext>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
+2
-4
@@ -1,15 +1,13 @@
|
||||
@import '../CuesheetTable.module.scss';
|
||||
|
||||
.delayRow {
|
||||
width: calc(100vw - 2rem);
|
||||
color: $ontime-delay-text;
|
||||
border-left: 4px solid var(--user-bg);
|
||||
border-left: 4px solid transparent;
|
||||
|
||||
td {
|
||||
width: 100%;
|
||||
width: calc(100% - 4px);
|
||||
padding-block: 0.5rem;
|
||||
text-align: center;
|
||||
transform: translateX(45%);
|
||||
|
||||
&:first-letter {
|
||||
text-transform: uppercase;
|
||||
|
||||
+10
-12
@@ -1,28 +1,26 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import { millisToDelayString } from '../../../../common/utils/dateConfig';
|
||||
import { usePersistedCuesheetOptions } from '../../cuesheet.options';
|
||||
|
||||
import style from './DelayRow.module.scss';
|
||||
|
||||
interface DelayRowProps {
|
||||
duration: number;
|
||||
parentBgColour: string | null;
|
||||
}
|
||||
|
||||
function DelayRow({ duration, parentBgColour }: DelayRowProps) {
|
||||
function DelayRow({ duration, ...virtuosoProps }: DelayRowProps) {
|
||||
const hideDelays = usePersistedCuesheetOptions((state) => state.hideDelays);
|
||||
|
||||
if (hideDelays || duration === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const delayTime = millisToDelayString(duration, 'expanded');
|
||||
|
||||
return (
|
||||
<tr
|
||||
className={style.delayRow}
|
||||
style={{
|
||||
'--user-bg': parentBgColour ?? 'transparent',
|
||||
}}
|
||||
data-testid='cuesheet-delay'
|
||||
>
|
||||
<td tabIndex={0} role='cell'>
|
||||
{delayTime}
|
||||
</td>
|
||||
<tr className={style.delayRow} data-testid='cuesheet-delay' {...virtuosoProps}>
|
||||
<td tabIndex={0}>{delayTime}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
}
|
||||
|
||||
&.firstAfterGroup {
|
||||
margin-top: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
&.skip {
|
||||
|
||||
+64
-64
@@ -1,89 +1,91 @@
|
||||
import { RefObject, useEffect, useRef } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { IoEllipsisHorizontal } from 'react-icons/io5';
|
||||
import { flexRender, Table } from '@tanstack/react-table';
|
||||
import { OntimeEntry, OntimeEvent, RGBColour, SupportedEntry } from 'ontime-types';
|
||||
import { EntryId, OntimeEntry, RGBColour, SupportedEntry } from 'ontime-types';
|
||||
import { colourToHex, cssOrHexToColour } from 'ontime-utils';
|
||||
|
||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
||||
import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||
import { AppMode } from '../../../../ontimeConfig';
|
||||
import { useCuesheetTableMenu } from '../cuesheet-table-menu/useCuesheetTableMenu';
|
||||
|
||||
import { observeRow, unobserveRow } from './rowObserver';
|
||||
import { useVisibleRowsStore } from './visibleRowsStore';
|
||||
|
||||
import style from './EventRow.module.scss';
|
||||
|
||||
interface EventRowProps {
|
||||
rowId: string;
|
||||
event: OntimeEvent;
|
||||
id: EntryId;
|
||||
eventIndex: number;
|
||||
colour: string;
|
||||
isFirstAfterGroup: boolean;
|
||||
isLoaded: boolean;
|
||||
isPast: boolean;
|
||||
groupColour: string | undefined;
|
||||
flag: boolean;
|
||||
skip: boolean;
|
||||
parent: EntryId | null;
|
||||
rowIndex: number;
|
||||
isPast?: boolean;
|
||||
selectedRef?: RefObject<HTMLTableRowElement | null>;
|
||||
skip?: boolean;
|
||||
colour?: string;
|
||||
rowBgColour?: string;
|
||||
parentBgColour?: string;
|
||||
table: Table<OntimeEntry>;
|
||||
firstAfterGroup: boolean;
|
||||
table: Table<ExtendedEntry<OntimeEntry>>;
|
||||
}
|
||||
|
||||
export default function EventRow({
|
||||
rowId,
|
||||
event,
|
||||
id,
|
||||
eventIndex,
|
||||
rowIndex,
|
||||
colour,
|
||||
isFirstAfterGroup,
|
||||
isLoaded,
|
||||
isPast,
|
||||
selectedRef,
|
||||
rowBgColour,
|
||||
parentBgColour,
|
||||
groupColour,
|
||||
flag,
|
||||
skip,
|
||||
parent,
|
||||
rowIndex,
|
||||
table,
|
||||
firstAfterGroup,
|
||||
...virtuosoProps
|
||||
}: EventRowProps) {
|
||||
const { cuesheetMode, hideIndexColumn } = table.options.meta?.options ?? {
|
||||
cuesheetMode: AppMode.Edit,
|
||||
hideIndexColumn: false,
|
||||
};
|
||||
|
||||
const ownRef = useRef<HTMLTableRowElement>(null);
|
||||
const isVisible = useVisibleRowsStore((state) => state.visibleRows.has(rowId));
|
||||
const openMenu = useCuesheetTableMenu((store) => store.openMenu);
|
||||
|
||||
// register this row with the intersection observer
|
||||
useEffect(() => {
|
||||
const element = ownRef.current;
|
||||
if (element) {
|
||||
element.id = rowId;
|
||||
observeRow(element);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (element) {
|
||||
unobserveRow(element);
|
||||
}
|
||||
};
|
||||
}, [rowId]);
|
||||
|
||||
const { color, backgroundColor } = getAccessibleColour(event.colour);
|
||||
const { color, backgroundColor } = getAccessibleColour(colour);
|
||||
const tmpColour = cssOrHexToColour(color) as RGBColour; // we know this to be a correct colour
|
||||
const mutedText = colourToHex({ ...tmpColour, alpha: tmpColour.alpha * 0.8 });
|
||||
|
||||
const rowBgColour: string | undefined = useMemo(() => {
|
||||
if (isLoaded) {
|
||||
return '#087A27'; // $active-green
|
||||
} else if (colour) {
|
||||
// the colour is user defined and might be invalid
|
||||
const accessibleBackgroundColor = cssOrHexToColour(getAccessibleColour(colour).backgroundColor);
|
||||
if (accessibleBackgroundColor !== null) {
|
||||
return colourToHex({
|
||||
...accessibleBackgroundColor,
|
||||
alpha: accessibleBackgroundColor.alpha * 0.25,
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}, [colour, isLoaded]);
|
||||
|
||||
return (
|
||||
<tr
|
||||
id={rowId}
|
||||
className={cx([
|
||||
style.eventRow,
|
||||
event.skip && style.skip,
|
||||
firstAfterGroup && style.firstAfterGroup,
|
||||
Boolean(parentBgColour) && style.hasParent,
|
||||
skip && style.skip,
|
||||
isFirstAfterGroup && style.firstAfterGroup,
|
||||
parent && style.hasParent,
|
||||
])}
|
||||
style={{
|
||||
opacity: `${isPast ? '0.2' : '1'}`,
|
||||
'--user-bg': parentBgColour ?? 'transparent',
|
||||
'--user-bg': groupColour ?? 'transparent',
|
||||
}}
|
||||
ref={selectedRef ?? ownRef}
|
||||
data-testid='cuesheet-event'
|
||||
{...virtuosoProps}
|
||||
>
|
||||
{cuesheetMode === AppMode.Edit && (
|
||||
<td className={style.actionColumn} tabIndex={-1} role='cell'>
|
||||
@@ -94,7 +96,7 @@ export default function EventRow({
|
||||
onClick={(e) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const yPos = 8 + rect.y + rect.height / 2;
|
||||
openMenu({ x: rect.x, y: yPos }, event.id, SupportedEntry.Event, rowIndex, event.parent, event.flag);
|
||||
openMenu({ x: rect.x, y: yPos }, id, SupportedEntry.Event, rowIndex, parent, flag);
|
||||
}}
|
||||
>
|
||||
<IoEllipsisHorizontal />
|
||||
@@ -106,26 +108,24 @@ export default function EventRow({
|
||||
{eventIndex}
|
||||
</td>
|
||||
)}
|
||||
{isVisible
|
||||
? table
|
||||
.getRow(rowId)
|
||||
.getVisibleCells()
|
||||
.map((cell) => {
|
||||
return (
|
||||
<td
|
||||
key={cell.id}
|
||||
style={{
|
||||
width: `calc(var(--col-${cell.column.id}-size) * 1px)`,
|
||||
backgroundColor: rowBgColour,
|
||||
}}
|
||||
tabIndex={-1}
|
||||
role='cell'
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
{table
|
||||
.getRow(rowId)
|
||||
.getVisibleCells()
|
||||
.map((cell) => {
|
||||
return (
|
||||
<td
|
||||
key={cell.id}
|
||||
style={{
|
||||
width: `calc(var(--col-${cell.column.id}-size) * 1px)`,
|
||||
backgroundColor: rowBgColour,
|
||||
}}
|
||||
tabIndex={-1}
|
||||
role='cell'
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
@import '../CuesheetTable.module.scss';
|
||||
|
||||
.groupRow {
|
||||
margin-top: 1rem;
|
||||
margin-top: 2em;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { IoEllipsisHorizontal } from 'react-icons/io5';
|
||||
import { flexRender, Table } from '@tanstack/react-table';
|
||||
import { EntryId, OntimeEntry, SupportedEntry } from 'ontime-types';
|
||||
import { EntryId, SupportedEntry } from 'ontime-types';
|
||||
|
||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||
import { useCurrentGroupId } from '../../../../common/hooks/useSocket';
|
||||
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
||||
import { AppMode } from '../../../../ontimeConfig';
|
||||
import { useCuesheetTableMenu } from '../cuesheet-table-menu/useCuesheetTableMenu';
|
||||
|
||||
@@ -12,15 +12,12 @@ import style from './GroupRow.module.scss';
|
||||
interface GroupRowProps {
|
||||
groupId: EntryId;
|
||||
colour: string;
|
||||
hidePast: boolean;
|
||||
rowId: string;
|
||||
rowIndex: number;
|
||||
table: Table<OntimeEntry>;
|
||||
table: Table<ExtendedEntry>;
|
||||
}
|
||||
|
||||
export default function GroupRow({ groupId, colour, hidePast, rowId, rowIndex, table }: GroupRowProps) {
|
||||
const { currentGroupId } = useCurrentGroupId();
|
||||
|
||||
export default function GroupRow({ groupId, colour, rowId, rowIndex, table, ...virtuosoProps }: GroupRowProps) {
|
||||
const { cuesheetMode, hideIndexColumn } = table.options.meta?.options ?? {
|
||||
cuesheetMode: AppMode.Edit,
|
||||
hideIndexColumn: false,
|
||||
@@ -28,12 +25,8 @@ export default function GroupRow({ groupId, colour, hidePast, rowId, rowIndex, t
|
||||
|
||||
const openMenu = useCuesheetTableMenu((store) => store.openMenu);
|
||||
|
||||
if (hidePast && !currentGroupId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className={style.groupRow} style={{ '--user-bg': colour }} data-testid='cuesheet-group'>
|
||||
<tr className={style.groupRow} style={{ '--user-bg': colour }} data-testid='cuesheet-group' {...virtuosoProps}>
|
||||
{cuesheetMode === AppMode.Edit && (
|
||||
<td className={style.actionColumn} tabIndex={-1} role='cell'>
|
||||
<IconButton
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
@import "../CuesheetTable.module.scss";
|
||||
|
||||
.milestoneRow {
|
||||
background: color-mix(in srgb, transparent 92%, var(--user-bg, $gray-500) 8%);
|
||||
background: color-mix(in srgb, transparent 98%, var(--user-bg, $gray-500) 2%);
|
||||
border-left: 4px solid var(--user-bg, $gray-500);
|
||||
|
||||
font-style: italic;
|
||||
|
||||
+23
-7
@@ -1,9 +1,11 @@
|
||||
import { IoEllipsisHorizontal } from 'react-icons/io5';
|
||||
import { flexRender, Table } from '@tanstack/react-table';
|
||||
import { EntryId, OntimeEntry, SupportedEntry } from 'ontime-types';
|
||||
import { EntryId, SupportedEntry } from 'ontime-types';
|
||||
import { colourToHex, cssOrHexToColour } from 'ontime-utils';
|
||||
|
||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||
import { cx, enDash } from '../../../../common/utils/styleUtils';
|
||||
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
||||
import { cx, enDash, getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||
import { AppMode } from '../../../../ontimeConfig';
|
||||
import { useCuesheetTableMenu } from '../cuesheet-table-menu/useCuesheetTableMenu';
|
||||
|
||||
@@ -12,12 +14,12 @@ import style from './MilestoneRow.module.scss';
|
||||
interface MilestoneRowProps {
|
||||
entryId: EntryId;
|
||||
isPast: boolean;
|
||||
parentBgColour: string | null;
|
||||
parentBgColour?: string;
|
||||
parentId: EntryId | null;
|
||||
rowBgColour?: string;
|
||||
colour: string;
|
||||
rowId: string;
|
||||
rowIndex: number;
|
||||
table: Table<OntimeEntry>;
|
||||
table: Table<ExtendedEntry>;
|
||||
}
|
||||
|
||||
export default function MilestoneRow({
|
||||
@@ -25,10 +27,11 @@ export default function MilestoneRow({
|
||||
isPast,
|
||||
parentBgColour,
|
||||
parentId,
|
||||
rowBgColour,
|
||||
colour,
|
||||
rowId,
|
||||
rowIndex,
|
||||
table,
|
||||
...virtuosoProps
|
||||
}: MilestoneRowProps) {
|
||||
const { cuesheetMode, hideIndexColumn } = table.options.meta?.options ?? {
|
||||
cuesheetMode: AppMode.Edit,
|
||||
@@ -37,6 +40,18 @@ export default function MilestoneRow({
|
||||
|
||||
const openMenu = useCuesheetTableMenu((store) => store.openMenu);
|
||||
|
||||
let rowBgColour: string | undefined;
|
||||
if (colour) {
|
||||
// the colour is user defined and might be invalid
|
||||
const accessibleBackgroundColor = cssOrHexToColour(getAccessibleColour(colour).backgroundColor);
|
||||
if (accessibleBackgroundColor !== null) {
|
||||
rowBgColour = colourToHex({
|
||||
...accessibleBackgroundColor,
|
||||
alpha: accessibleBackgroundColor.alpha * 0.25,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<tr
|
||||
className={cx([style.milestoneRow, Boolean(parentBgColour) && style.hasParent])}
|
||||
@@ -45,6 +60,7 @@ export default function MilestoneRow({
|
||||
'--user-bg': parentBgColour ?? 'transparent',
|
||||
}}
|
||||
data-testid='cuesheet-milestone'
|
||||
{...virtuosoProps}
|
||||
>
|
||||
{cuesheetMode === AppMode.Edit && (
|
||||
<td className={style.actionColumn} tabIndex={-1} role='cell'>
|
||||
@@ -79,9 +95,9 @@ export default function MilestoneRow({
|
||||
style={{
|
||||
width: `calc(var(--col-${cell.column.id}-size) * 1px)`,
|
||||
backgroundColor: rowBgColour,
|
||||
opacity: canRender ? 1 : 0.4,
|
||||
}}
|
||||
tabIndex={-1}
|
||||
role='cell'
|
||||
>
|
||||
{canRender && flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
|
||||
+6
-7
@@ -2,12 +2,13 @@ import { CSSProperties, ReactNode } from 'react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Header } from '@tanstack/react-table';
|
||||
import { OntimeEntry } from 'ontime-types';
|
||||
|
||||
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
||||
|
||||
import style from '../CuesheetTable.module.scss';
|
||||
|
||||
interface SortableCellProps {
|
||||
header: Header<OntimeEntry, unknown>;
|
||||
header: Header<ExtendedEntry, unknown>;
|
||||
injectedStyles: CSSProperties;
|
||||
children: ReactNode;
|
||||
}
|
||||
@@ -34,11 +35,9 @@ export function SortableCell({ header, injectedStyles, children }: SortableCellP
|
||||
{children}
|
||||
</div>
|
||||
<div
|
||||
{...{
|
||||
onDoubleClick: () => header.column.resetSize(),
|
||||
onMouseDown: header.getResizeHandler(),
|
||||
onTouchStart: header.getResizeHandler(),
|
||||
}}
|
||||
onDoubleClick={() => header.column.resetSize()}
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
className={style.resizer}
|
||||
/>
|
||||
</th>
|
||||
|
||||
+16
-15
@@ -1,9 +1,10 @@
|
||||
import { useCallback } from 'react';
|
||||
import { CellContext, ColumnDef } from '@tanstack/react-table';
|
||||
import { CustomFields, isOntimeDelay, isOntimeEvent, OntimeEntry, TimeStrategy, URLPreset } from 'ontime-types';
|
||||
import { CustomFields, isOntimeDelay, isOntimeEvent, TimeStrategy, URLPreset } from 'ontime-types';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
|
||||
import DelayIndicator from '../../../../common/components/delay-indicator/DelayIndicator';
|
||||
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
||||
import { formatDuration, formatTime } from '../../../../common/utils/time';
|
||||
import { AppMode } from '../../../../ontimeConfig';
|
||||
|
||||
@@ -16,7 +17,7 @@ import MutedText from './MutedText';
|
||||
import SingleLineCell from './SingleLineCell';
|
||||
import TimeInput from './TimeInput';
|
||||
|
||||
function MakeStart({ getValue, row, table, column }: CellContext<OntimeEntry, unknown>) {
|
||||
function MakeStart({ getValue, row, table, column }: CellContext<ExtendedEntry, unknown>) {
|
||||
if (!table.options.meta) {
|
||||
return null;
|
||||
}
|
||||
@@ -55,7 +56,7 @@ function MakeStart({ getValue, row, table, column }: CellContext<OntimeEntry, un
|
||||
);
|
||||
}
|
||||
|
||||
function MakeEnd({ getValue, row, table, column }: CellContext<OntimeEntry, unknown>) {
|
||||
function MakeEnd({ getValue, row, table, column }: CellContext<ExtendedEntry, unknown>) {
|
||||
if (!table.options.meta) {
|
||||
return null;
|
||||
}
|
||||
@@ -95,7 +96,7 @@ function MakeEnd({ getValue, row, table, column }: CellContext<OntimeEntry, unkn
|
||||
);
|
||||
}
|
||||
|
||||
function MakeDuration({ getValue, row, table, column }: CellContext<OntimeEntry, unknown>) {
|
||||
function MakeDuration({ getValue, row, table, column }: CellContext<ExtendedEntry, unknown>) {
|
||||
if (!table.options.meta) {
|
||||
return null;
|
||||
}
|
||||
@@ -126,7 +127,7 @@ function MakeDuration({ getValue, row, table, column }: CellContext<OntimeEntry,
|
||||
);
|
||||
}
|
||||
|
||||
function MakeMultiLineField({ row, column, table }: CellContext<OntimeEntry, unknown>) {
|
||||
function MakeMultiLineField({ row, column, table }: CellContext<ExtendedEntry, unknown>) {
|
||||
const update = useCallback(
|
||||
(newValue: string) => {
|
||||
table.options.meta?.handleUpdate(row.index, column.id, newValue, false);
|
||||
@@ -135,8 +136,8 @@ function MakeMultiLineField({ row, column, table }: CellContext<OntimeEntry, unk
|
||||
);
|
||||
|
||||
// not all entries have all properties (eg groups)
|
||||
const initialValue = row.original[column.id as keyof OntimeEntry];
|
||||
if (initialValue === undefined) {
|
||||
const initialValue = row.original[column.id as keyof ExtendedEntry];
|
||||
if (typeof initialValue !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -148,7 +149,7 @@ function MakeMultiLineField({ row, column, table }: CellContext<OntimeEntry, unk
|
||||
return <MultiLineCell initialValue={initialValue as string} handleUpdate={update} />;
|
||||
}
|
||||
|
||||
function LazyImage({ row, column, table }: CellContext<OntimeEntry, unknown>) {
|
||||
function LazyImage({ row, column, table }: CellContext<ExtendedEntry, unknown>) {
|
||||
const update = useCallback(
|
||||
(newValue: string) => {
|
||||
table.options.meta?.handleUpdate(row.index, column.id, newValue, true);
|
||||
@@ -166,7 +167,7 @@ function LazyImage({ row, column, table }: CellContext<OntimeEntry, unknown>) {
|
||||
return <EditableImage initialValue={initialValue} updateValue={update} readOnly={!canWrite} />;
|
||||
}
|
||||
|
||||
function MakeSingleLineField({ row, column, table }: CellContext<OntimeEntry, unknown>) {
|
||||
function MakeSingleLineField({ row, column, table }: CellContext<ExtendedEntry, unknown>) {
|
||||
const update = useCallback(
|
||||
(newValue: string) => {
|
||||
table.options.meta?.handleUpdate(row.index, column.id, newValue, false);
|
||||
@@ -175,8 +176,8 @@ function MakeSingleLineField({ row, column, table }: CellContext<OntimeEntry, un
|
||||
);
|
||||
|
||||
// not all entries have all properties (eg groups)
|
||||
const initialValue = row.original[column.id as keyof OntimeEntry];
|
||||
if (initialValue === undefined) {
|
||||
const initialValue = row.original[column.id as keyof ExtendedEntry];
|
||||
if (typeof initialValue !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -188,7 +189,7 @@ function MakeSingleLineField({ row, column, table }: CellContext<OntimeEntry, un
|
||||
return <SingleLineCell initialValue={initialValue as string} handleUpdate={update} />;
|
||||
}
|
||||
|
||||
function MakeFlagField({ row }: CellContext<OntimeEntry, unknown>) {
|
||||
function MakeFlagField({ row }: CellContext<ExtendedEntry, unknown>) {
|
||||
const event = row.original;
|
||||
if (!isOntimeEvent(event) || !event.flag) {
|
||||
return null;
|
||||
@@ -196,7 +197,7 @@ function MakeFlagField({ row }: CellContext<OntimeEntry, unknown>) {
|
||||
return <FlagCell />;
|
||||
}
|
||||
|
||||
function MakeCustomField({ row, column, table }: CellContext<OntimeEntry, unknown>) {
|
||||
function MakeCustomField({ row, column, table }: CellContext<ExtendedEntry, unknown>) {
|
||||
const update = useCallback(
|
||||
(newValue: string) => {
|
||||
table.options.meta?.handleUpdate(row.index, column.id, newValue, true);
|
||||
@@ -229,8 +230,8 @@ export function makeCuesheetColumns(
|
||||
customFields: CustomFields,
|
||||
cuesheetMode: AppMode,
|
||||
preset: URLPreset | undefined,
|
||||
): ColumnDef<OntimeEntry>[] {
|
||||
const columnsDef: ColumnDef<OntimeEntry>[] = [];
|
||||
): ColumnDef<ExtendedEntry>[] {
|
||||
const columnsDef: ColumnDef<ExtendedEntry>[] = [];
|
||||
const modeAllowsWrite = cuesheetMode === AppMode.Edit;
|
||||
const fullRead = preset ? preset.options?.read === 'full' : true;
|
||||
const fullWrite = preset ? preset.options?.write === 'full' : true;
|
||||
|
||||
Reference in New Issue
Block a user