mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-20 22:49:18 +00:00
refactor: restructure element composition
This commit is contained in:
committed by
Carlos Valente
parent
67fa747aae
commit
8f30c0a7df
@@ -0,0 +1,154 @@
|
|||||||
|
$table-font-size: 1rem;
|
||||||
|
$table-header-font-size: calc(1rem - 2px);
|
||||||
|
|
||||||
|
.cuesheetContainer {
|
||||||
|
grid-area: table;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
padding-bottom: 640px; // allow focus to reach last elements
|
||||||
|
}
|
||||||
|
|
||||||
|
.cuesheet {
|
||||||
|
font-size: $table-font-size;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
tr {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
margin: 1px;
|
||||||
|
font-weight: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
text-align: left;
|
||||||
|
position: relative;
|
||||||
|
@include ellipsis-overflow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableHeader,
|
||||||
|
.eventRow {
|
||||||
|
.indexColumn {
|
||||||
|
min-width: 3em; // allow for 3-digit numbers
|
||||||
|
text-align: right;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: $table-header-font-size;
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background-color: $gray-1300;
|
||||||
|
}
|
||||||
|
.actionColumn {
|
||||||
|
width: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableHeader {
|
||||||
|
position: sticky;
|
||||||
|
top: 0px;
|
||||||
|
z-index: 10;
|
||||||
|
background-color: $ui-black;
|
||||||
|
font-size: $table-header-font-size;
|
||||||
|
color: $label-gray;}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: $gray-1300;
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.resizer {
|
||||||
|
width: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventRow {
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
outline: 1px solid $blue-700;
|
||||||
|
outline-offset: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
background-color: $gray-1250;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.skip {
|
||||||
|
text-decoration: line-through;
|
||||||
|
opacity: $opacity-disabled !important; // fighting inline styles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blockRow {
|
||||||
|
width: 100%;
|
||||||
|
background-color: $gray-1350;
|
||||||
|
font-size: 1rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
|
||||||
|
td {
|
||||||
|
align-self: flex-end;
|
||||||
|
position: sticky;
|
||||||
|
left: 1rem;
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delayRow {
|
||||||
|
width: 100%;
|
||||||
|
color: $ontime-delay-text;
|
||||||
|
|
||||||
|
td {
|
||||||
|
position: sticky;
|
||||||
|
left: 47.5%; // center of the screen, ish
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
&:first-letter {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.check {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
@include ellipsis-overflow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delayedTime {
|
||||||
|
color: $ontime-delay-text;
|
||||||
|
font-size: calc(1rem - 2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizer {
|
||||||
|
cursor: col-resize;
|
||||||
|
opacity: $opacity-disabled;
|
||||||
|
display: inline-block;
|
||||||
|
width: 0;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
background-color: $action-blue;
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
touch-action: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
import { useCurrentBlockId } from '../../../../common/hooks/useSocket';
|
||||||
|
|
||||||
|
import style from '../CuesheetTable.module.scss';
|
||||||
|
|
||||||
|
interface BlockRowProps {
|
||||||
|
hidePast: boolean;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BlockRow(props: BlockRowProps) {
|
||||||
|
const { hidePast, title } = props;
|
||||||
|
const { currentBlockId } = useCurrentBlockId();
|
||||||
|
|
||||||
|
if (hidePast && !currentBlockId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr className={style.blockRow}>
|
||||||
|
<td>{title}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(BlockRow);
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
import { horizontalListSortingStrategy, SortableContext } from '@dnd-kit/sortable';
|
||||||
|
import { flexRender, HeaderGroup } from '@tanstack/react-table';
|
||||||
|
import { OntimeRundownEntry } from 'ontime-types';
|
||||||
|
|
||||||
|
import { getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||||
|
|
||||||
|
import { SortableCell } from './SortableCell';
|
||||||
|
|
||||||
|
import style from '../CuesheetTable.module.scss';
|
||||||
|
|
||||||
|
interface CuesheetHeaderProps {
|
||||||
|
headerGroups: HeaderGroup<OntimeRundownEntry>[];
|
||||||
|
showIndexColumn: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CuesheetHeader(props: CuesheetHeaderProps) {
|
||||||
|
const { headerGroups, showIndexColumn } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<thead className={style.tableHeader}>
|
||||||
|
{headerGroups.map((headerGroup) => {
|
||||||
|
const key = headerGroup.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={headerGroup.id}>
|
||||||
|
<th className={style.indexColumn}>{showIndexColumn && '#'}</th>
|
||||||
|
<th className={style.actionColumn} />
|
||||||
|
<SortableContext key={key} items={headerGroup.headers} strategy={horizontalListSortingStrategy}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
const width = header.getSize();
|
||||||
|
// @ts-expect-error -- we inject this into react-table
|
||||||
|
const customBackground = header.column.columnDef?.meta?.colour;
|
||||||
|
|
||||||
|
let customStyles = {};
|
||||||
|
if (customBackground) {
|
||||||
|
const customColour = getAccessibleColour(customBackground);
|
||||||
|
customStyles = { backgroundColor: customColour.backgroundColor, color: customColour.color };
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SortableCell key={header.column.columnDef.id} header={header} style={{ width, ...customStyles }}>
|
||||||
|
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
</SortableCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SortableContext>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</thead>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
import { millisToDelayString } from '../../../../common/utils/dateConfig';
|
||||||
|
|
||||||
|
import style from '../CuesheetTable.module.scss';
|
||||||
|
|
||||||
|
interface DelayRowProps {
|
||||||
|
duration: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DelayRow(props: DelayRowProps) {
|
||||||
|
const { duration } = props;
|
||||||
|
const delayTime = millisToDelayString(duration, 'expanded');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr className={style.delayRow}>
|
||||||
|
<td>{delayTime}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(DelayRow);
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { memo, MutableRefObject, PropsWithChildren, useLayoutEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||||
|
|
||||||
|
import style from '../CuesheetTable.module.scss';
|
||||||
|
|
||||||
|
interface EventRowProps {
|
||||||
|
eventIndex: number;
|
||||||
|
showIndexColumn: boolean;
|
||||||
|
isPast?: boolean;
|
||||||
|
selectedRef?: MutableRefObject<HTMLTableRowElement | null>;
|
||||||
|
skip?: boolean;
|
||||||
|
colour?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EventRow(props: PropsWithChildren<EventRowProps>) {
|
||||||
|
const { children, eventIndex, isPast, selectedRef, skip, colour, showIndexColumn } = props;
|
||||||
|
const ownRef = useRef<HTMLTableRowElement>(null);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
const textColour = getAccessibleColour(colour);
|
||||||
|
const bgColour = textColour.backgroundColor;
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setIsVisible(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: null,
|
||||||
|
threshold: 0.01,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRefCurrent = ownRef.current;
|
||||||
|
if (selectedRef) {
|
||||||
|
setIsVisible(true);
|
||||||
|
} else if (handleRefCurrent) {
|
||||||
|
observer.observe(handleRefCurrent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (handleRefCurrent) {
|
||||||
|
observer.unobserve(handleRefCurrent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [ownRef, selectedRef]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
className={cx([style.eventRow, skip ?? style.skip])}
|
||||||
|
style={{ opacity: `${isPast ? '0.2' : '1'}` }}
|
||||||
|
ref={selectedRef ?? ownRef}
|
||||||
|
>
|
||||||
|
<td className={style.indexColumn} style={{ backgroundColor: bgColour, color: textColour.color }}>
|
||||||
|
{showIndexColumn && eventIndex}
|
||||||
|
</td>
|
||||||
|
{isVisible ? children : null}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(EventRow);
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
import { memo, useCallback, useRef } from 'react';
|
||||||
|
|
||||||
|
import { AutoTextArea } from '../../../../common/components/input/auto-text-area/AutoTextArea';
|
||||||
|
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
||||||
|
|
||||||
|
interface MultiLineCellProps {
|
||||||
|
initialValue: string;
|
||||||
|
handleUpdate: (newValue: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MultiLineCell = (props: MultiLineCellProps) => {
|
||||||
|
const { initialValue, handleUpdate } = props;
|
||||||
|
const ref = useRef<HTMLInputElement | null>(null);
|
||||||
|
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
|
||||||
|
|
||||||
|
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
||||||
|
submitOnCtrlEnter: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AutoTextArea
|
||||||
|
inputref={ref}
|
||||||
|
rows={1}
|
||||||
|
size='sm'
|
||||||
|
style={{ padding: 0 }}
|
||||||
|
transition='none'
|
||||||
|
variant='ontime-transparent'
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(MultiLineCell);
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
import { memo, useCallback, useRef } from 'react';
|
||||||
|
import { Input } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
||||||
|
|
||||||
|
interface SingleLineCellProps {
|
||||||
|
initialValue: string;
|
||||||
|
handleUpdate: (newValue: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SingleLineCell = (props: SingleLineCellProps) => {
|
||||||
|
const { initialValue, handleUpdate } = props;
|
||||||
|
const ref = useRef<HTMLInputElement | null>(null);
|
||||||
|
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
|
||||||
|
|
||||||
|
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
||||||
|
submitOnCtrlEnter: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
ref={ref}
|
||||||
|
size='sx'
|
||||||
|
variant='ontime-transparent'
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
spellCheck={false}
|
||||||
|
autoComplete='off'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(SingleLineCell);
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
import { CSSProperties, ReactNode } from 'react';
|
||||||
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import { Header } from '@tanstack/react-table';
|
||||||
|
import { OntimeRundownEntry } from 'ontime-types';
|
||||||
|
|
||||||
|
import styles from '../CuesheetTable.module.scss';
|
||||||
|
|
||||||
|
interface SortableCellProps {
|
||||||
|
header: Header<OntimeRundownEntry, unknown>;
|
||||||
|
style: CSSProperties;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SortableCell({ header, style, children }: SortableCellProps) {
|
||||||
|
const { column, colSpan } = header;
|
||||||
|
|
||||||
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||||
|
id: column.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// build drag styles
|
||||||
|
const dragStyle = {
|
||||||
|
...style,
|
||||||
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
transform: CSS.Translate.toString(transform),
|
||||||
|
transition,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<th ref={setNodeRef} style={dragStyle} colSpan={colSpan}>
|
||||||
|
<div {...attributes} {...listeners}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
{...{
|
||||||
|
onMouseDown: header.getResizeHandler(),
|
||||||
|
onTouchStart: header.getResizeHandler(),
|
||||||
|
}}
|
||||||
|
className={styles.resizer}
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { CellContext, ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { CustomFields, isOntimeEvent, OntimeEvent, OntimeRundownEntry } from 'ontime-types';
|
||||||
|
|
||||||
|
import DelayIndicator from '../../../../common/components/delay-indicator/DelayIndicator';
|
||||||
|
import RunningTime from '../../../../features/viewers/common/running-time/RunningTime';
|
||||||
|
import { useCuesheetOptions } from '../../cuesheet.options';
|
||||||
|
|
||||||
|
import MultiLineCell from './MultiLineCell';
|
||||||
|
import SingleLineCell from './SingleLineCell';
|
||||||
|
|
||||||
|
import style from '../CuesheetTable.module.scss';
|
||||||
|
|
||||||
|
function MakeTimer({ getValue, row: { original } }: CellContext<OntimeRundownEntry, unknown>) {
|
||||||
|
const { showDelayedTimes, hideTableSeconds } = useCuesheetOptions();
|
||||||
|
const cellValue = (getValue() as number | null) ?? 0;
|
||||||
|
const delayValue = (original as OntimeEvent)?.delay ?? 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={style.time}>
|
||||||
|
<DelayIndicator delayValue={delayValue} />
|
||||||
|
<RunningTime value={cellValue} hideSeconds={hideTableSeconds} />
|
||||||
|
{delayValue !== 0 && showDelayedTimes && (
|
||||||
|
<RunningTime className={style.delayedTime} value={cellValue + delayValue} hideSeconds={hideTableSeconds} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MakeDuration({ getValue }: CellContext<OntimeRundownEntry, unknown>) {
|
||||||
|
const { hideTableSeconds } = useCuesheetOptions();
|
||||||
|
const cellValue = (getValue() as number | null) ?? 0;
|
||||||
|
|
||||||
|
return <RunningTime value={cellValue} hideSeconds={hideTableSeconds} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MakeMultiLineField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||||
|
const update = useCallback(
|
||||||
|
(newValue: string) => {
|
||||||
|
// @ts-expect-error -- we inject this into react-table
|
||||||
|
table.options.meta?.handleUpdate(row.index, column.id, newValue);
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- we skip table.options.meta since the reference seems unstable
|
||||||
|
[column.id, row.index],
|
||||||
|
);
|
||||||
|
|
||||||
|
const event = row.original;
|
||||||
|
if (!isOntimeEvent(event)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialValue = event[column.id as keyof OntimeRundownEntry] ?? '';
|
||||||
|
|
||||||
|
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MakeSingleLineField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||||
|
const update = useCallback(
|
||||||
|
(newValue: string) => {
|
||||||
|
// @ts-expect-error -- we inject this into react-table
|
||||||
|
table.options.meta?.handleUpdate(row.index, column.id, newValue);
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- we skip table.options.meta since the reference seems unstable
|
||||||
|
[column.id, row.index],
|
||||||
|
);
|
||||||
|
|
||||||
|
const event = row.original;
|
||||||
|
if (!isOntimeEvent(event)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialValue = event[column.id as keyof OntimeRundownEntry] ?? '';
|
||||||
|
|
||||||
|
return <SingleLineCell initialValue={initialValue} handleUpdate={update} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||||
|
const update = useCallback(
|
||||||
|
(newValue: string) => {
|
||||||
|
// @ts-expect-error -- we inject this into react-table
|
||||||
|
table.options.meta?.handleUpdateCustom(row.index, column.id, newValue);
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- we skip table.options.meta since the reference seems unstable
|
||||||
|
[column.id, row.index],
|
||||||
|
);
|
||||||
|
|
||||||
|
const event = row.original;
|
||||||
|
if (!isOntimeEvent(event)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialValue = event.custom[column.id] ?? '';
|
||||||
|
|
||||||
|
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<OntimeRundownEntry>[] {
|
||||||
|
const dynamicCustomFields = Object.keys(customFields).map((key) => ({
|
||||||
|
accessorKey: key,
|
||||||
|
id: key,
|
||||||
|
header: customFields[key].label,
|
||||||
|
meta: { colour: customFields[key].colour },
|
||||||
|
cell: MakeCustomField,
|
||||||
|
size: 250,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
accessorKey: 'cue',
|
||||||
|
id: 'cue',
|
||||||
|
header: 'Cue',
|
||||||
|
cell: (row) => row.getValue(),
|
||||||
|
size: 75,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'timeStart',
|
||||||
|
id: 'timeStart',
|
||||||
|
header: 'Start',
|
||||||
|
cell: MakeTimer,
|
||||||
|
size: 75,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'timeEnd',
|
||||||
|
id: 'timeEnd',
|
||||||
|
header: 'End',
|
||||||
|
cell: MakeTimer,
|
||||||
|
size: 75,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'duration',
|
||||||
|
id: 'duration',
|
||||||
|
header: 'Duration',
|
||||||
|
cell: MakeDuration,
|
||||||
|
size: 75,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'title',
|
||||||
|
id: 'title',
|
||||||
|
header: 'Title',
|
||||||
|
cell: MakeSingleLineField,
|
||||||
|
size: 250,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'note',
|
||||||
|
id: 'note',
|
||||||
|
header: 'Note',
|
||||||
|
cell: MakeMultiLineField,
|
||||||
|
size: 250,
|
||||||
|
},
|
||||||
|
...dynamicCustomFields,
|
||||||
|
];
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
.tableSettings {
|
||||||
|
grid-area: settings;
|
||||||
|
padding-inline: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 5rem;
|
||||||
|
font-size: $inner-section-text-size;
|
||||||
|
|
||||||
|
@media (max-width: $small-screen) {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
column-gap: 1rem;
|
||||||
|
row-gap: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
import { memo, ReactNode } from 'react';
|
||||||
|
import { Button, Checkbox } from '@chakra-ui/react';
|
||||||
|
import { Column } from '@tanstack/react-table';
|
||||||
|
import { OntimeRundownEntry } from 'ontime-types';
|
||||||
|
|
||||||
|
import * as Editor from '../../../../features/editors/editor-utils/EditorUtils';
|
||||||
|
|
||||||
|
import style from './CuesheetTableSettings.module.scss';
|
||||||
|
|
||||||
|
// reusable button styles
|
||||||
|
const buttonProps = {
|
||||||
|
size: 'xs',
|
||||||
|
variant: 'ontime-subtle',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CuesheetTableSettingsProps {
|
||||||
|
columns: Column<OntimeRundownEntry, unknown>[];
|
||||||
|
handleResetResizing: () => void;
|
||||||
|
handleResetReordering: () => void;
|
||||||
|
handleClearToggles: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CuesheetTableSettings(props: CuesheetTableSettingsProps) {
|
||||||
|
const { columns, handleResetResizing, handleResetReordering, handleClearToggles } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.tableSettings}>
|
||||||
|
<div>
|
||||||
|
<Editor.Label className={style.sectionTitle}>Toggle column visibility</Editor.Label>
|
||||||
|
<div className={style.row}>
|
||||||
|
{columns.map((column) => {
|
||||||
|
const columnHeader = column.columnDef.header;
|
||||||
|
const visible = column.getIsVisible();
|
||||||
|
return (
|
||||||
|
<label key={`${column.id}-${visible}`} className={style.option}>
|
||||||
|
<Checkbox
|
||||||
|
variant='ontime-ondark'
|
||||||
|
defaultChecked={visible}
|
||||||
|
onChange={column.getToggleVisibilityHandler()}
|
||||||
|
/>
|
||||||
|
{columnHeader as ReactNode}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={style.column}>
|
||||||
|
<Editor.Label className={style.sectionTitle}>Reset Options</Editor.Label>
|
||||||
|
<div className={style.row}>
|
||||||
|
<Button onClick={handleClearToggles} {...buttonProps}>
|
||||||
|
Show All
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleResetResizing} {...buttonProps}>
|
||||||
|
Reset Resizing
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleResetReordering} {...buttonProps}>
|
||||||
|
Reset Reordering
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(CuesheetTableSettings);
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { useLocalStorage } from '@mantine/hooks';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { OntimeRundownEntry } from 'ontime-types';
|
||||||
|
|
||||||
|
export default function useColumnManager(columns: ColumnDef<OntimeRundownEntry>[]) {
|
||||||
|
const [columnVisibility, setColumnVisibility] = useLocalStorage({ key: 'table-hidden', defaultValue: {} });
|
||||||
|
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({
|
||||||
|
key: 'table-order',
|
||||||
|
defaultValue: columns.map((col) => col.id as string),
|
||||||
|
});
|
||||||
|
const [columnSizing, setColumnSizing] = useLocalStorage({ key: 'table-sizes', defaultValue: {} });
|
||||||
|
|
||||||
|
// if the columns change, we update the dataset
|
||||||
|
useEffect(() => {
|
||||||
|
let shouldReplace = false;
|
||||||
|
const newColumns: string[] = [];
|
||||||
|
|
||||||
|
// iterate through columns to see if there are new ids
|
||||||
|
columns.forEach((column) => {
|
||||||
|
const columnnId = column.id as string;
|
||||||
|
if (!shouldReplace && !columnOrder.includes(columnnId)) {
|
||||||
|
shouldReplace = true;
|
||||||
|
}
|
||||||
|
newColumns.push(columnnId);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (shouldReplace) {
|
||||||
|
saveColumnOrder(newColumns);
|
||||||
|
}
|
||||||
|
}, [columnOrder, columns, saveColumnOrder]);
|
||||||
|
|
||||||
|
const resetColumnOrder = useCallback(() => {
|
||||||
|
saveColumnOrder(columns.map((col) => col.id as string));
|
||||||
|
}, [columns, saveColumnOrder]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
columnVisibility,
|
||||||
|
columnOrder,
|
||||||
|
columnSizing,
|
||||||
|
resetColumnOrder,
|
||||||
|
setColumnVisibility,
|
||||||
|
saveColumnOrder,
|
||||||
|
setColumnSizing,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user