mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 15:09:10 +00:00
fix(cuesheet): infinite render loop on resizing columns
This commit is contained in:
committed by
Carlos Valente
parent
304832ecde
commit
7bbdab7dd9
@@ -11,14 +11,14 @@ import {
|
|||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
||||||
import useColumnManager from '../cuesheet-table/useColumnManager';
|
import { useColumnOrder } from '../cuesheet-table/useColumnManager';
|
||||||
|
|
||||||
interface CuesheetDndProps {
|
interface CuesheetDndProps {
|
||||||
columns: ColumnDef<ExtendedEntry>[];
|
columns: ColumnDef<ExtendedEntry>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CuesheetDnd({ columns, children }: PropsWithChildren<CuesheetDndProps>) {
|
export default function CuesheetDnd({ columns, children }: PropsWithChildren<CuesheetDndProps>) {
|
||||||
const { columnOrder, saveColumnOrder } = useColumnManager(columns);
|
const { columnOrder, saveColumnOrder } = useColumnOrder(columns);
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
|||||||
import { AppMode } from '../../../ontimeConfig';
|
import { AppMode } from '../../../ontimeConfig';
|
||||||
import { usePersistedCuesheetOptions } from '../cuesheet.options';
|
import { usePersistedCuesheetOptions } from '../cuesheet.options';
|
||||||
|
|
||||||
import CuesheetHeader from './cuesheet-table-elements/CuesheetHeader';
|
import { CuesheetHeader, SortableCuesheetHeader } from './cuesheet-table-elements/CuesheetHeader';
|
||||||
import DelayRow from './cuesheet-table-elements/DelayRow';
|
import DelayRow from './cuesheet-table-elements/DelayRow';
|
||||||
import EventRow from './cuesheet-table-elements/EventRow';
|
import EventRow from './cuesheet-table-elements/EventRow';
|
||||||
import GroupRow from './cuesheet-table-elements/GroupRow';
|
import GroupRow from './cuesheet-table-elements/GroupRow';
|
||||||
import MilestoneRow from './cuesheet-table-elements/MilestoneRow';
|
import MilestoneRow from './cuesheet-table-elements/MilestoneRow';
|
||||||
import CuesheetTableMenu from './cuesheet-table-menu/CuesheetTableMenu';
|
import CuesheetTableMenu from './cuesheet-table-menu/CuesheetTableMenu';
|
||||||
import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings';
|
import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings';
|
||||||
import useColumnManager from './useColumnManager';
|
import { useColumnOrder, useColumnSizes, useColumnVisibility } from './useColumnManager';
|
||||||
|
|
||||||
import style from './CuesheetTable.module.scss';
|
import style from './CuesheetTable.module.scss';
|
||||||
|
|
||||||
@@ -79,8 +79,9 @@ export default function CuesheetTable({ columns, cuesheetMode }: CuesheetTablePr
|
|||||||
[cuesheetMode, data, hideIndexColumn, hideTableSeconds, showDelayedTimes, updateEntry, updateTimer],
|
[cuesheetMode, data, hideIndexColumn, hideTableSeconds, showDelayedTimes, updateEntry, updateTimer],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { columnVisibility, columnOrder, columnSizing, resetColumnOrder, setColumnVisibility, setColumnSizing } =
|
const { columnOrder, resetColumnOrder } = useColumnOrder(columns);
|
||||||
useColumnManager(columns);
|
const { columnSizing, setColumnSizing } = useColumnSizes();
|
||||||
|
const { columnVisibility, setColumnVisibility } = useColumnVisibility();
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
@@ -237,11 +238,14 @@ export default function CuesheetTable({ columns, cuesheetMode }: CuesheetTablePr
|
|||||||
TableHead: (virtuosoProps) => <thead className={style.tableHeader} {...virtuosoProps} />,
|
TableHead: (virtuosoProps) => <thead className={style.tableHeader} {...virtuosoProps} />,
|
||||||
}}
|
}}
|
||||||
fixedHeaderContent={() => {
|
fixedHeaderContent={() => {
|
||||||
return table
|
return table.getHeaderGroups().map((headerGroup) => {
|
||||||
.getHeaderGroups()
|
const HeaderComponent = table.getState().columnSizingInfo.isResizingColumn
|
||||||
.map((headerGroup) => (
|
? CuesheetHeader
|
||||||
<CuesheetHeader key={headerGroup.id} cuesheetMode={cuesheetMode} headerGroup={headerGroup} />
|
: SortableCuesheetHeader;
|
||||||
));
|
|
||||||
|
// if the table is being resized, we render non-sortable headers to avoid performance issues
|
||||||
|
return <HeaderComponent key={headerGroup.id} cuesheetMode={cuesheetMode} headerGroup={headerGroup} />;
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
+46
-3
@@ -7,7 +7,7 @@ import { getAccessibleColour } from '../../../../common/utils/styleUtils';
|
|||||||
import { AppMode } from '../../../../ontimeConfig';
|
import { AppMode } from '../../../../ontimeConfig';
|
||||||
import { usePersistedCuesheetOptions } from '../../cuesheet.options';
|
import { usePersistedCuesheetOptions } from '../../cuesheet.options';
|
||||||
|
|
||||||
import { SortableCell } from './SortableCell';
|
import { Draggable, SortableCell, TableCell } from './SortableCell';
|
||||||
|
|
||||||
import style from '../CuesheetTable.module.scss';
|
import style from '../CuesheetTable.module.scss';
|
||||||
|
|
||||||
@@ -16,8 +16,9 @@ interface CuesheetHeaderProps {
|
|||||||
cuesheetMode: AppMode;
|
cuesheetMode: AppMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHeaderProps) {
|
export function SortableCuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHeaderProps) {
|
||||||
const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn);
|
const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{cuesheetMode === AppMode.Edit && <th className={style.actionColumn} tabIndex={-1} />}
|
{cuesheetMode === AppMode.Edit && <th className={style.actionColumn} tabIndex={-1} />}
|
||||||
@@ -43,8 +44,10 @@ export default function CuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHe
|
|||||||
return (
|
return (
|
||||||
<SortableCell
|
<SortableCell
|
||||||
key={header.column.columnDef.id}
|
key={header.column.columnDef.id}
|
||||||
header={header}
|
columnId={header.column.id}
|
||||||
|
colSpan={header.colSpan}
|
||||||
injectedStyles={{ width: `calc(var(--header-${header?.id}-size) * 1px)`, ...customStyles }}
|
injectedStyles={{ width: `calc(var(--header-${header?.id}-size) * 1px)`, ...customStyles }}
|
||||||
|
draggable={<Draggable header={header} />}
|
||||||
>
|
>
|
||||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
</SortableCell>
|
</SortableCell>
|
||||||
@@ -54,3 +57,43 @@ export default function CuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHe
|
|||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function CuesheetHeader({ headerGroup, cuesheetMode }: CuesheetHeaderProps) {
|
||||||
|
const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={headerGroup.id}>
|
||||||
|
{cuesheetMode === AppMode.Edit && <th className={style.actionColumn} tabIndex={-1} />}
|
||||||
|
{!hideIndexColumn && (
|
||||||
|
<th className={style.indexColumn} tabIndex={-1}>
|
||||||
|
#
|
||||||
|
</th>
|
||||||
|
)}
|
||||||
|
{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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableCell
|
||||||
|
key={header.column.columnDef.id}
|
||||||
|
columnId={header.column.id}
|
||||||
|
colSpan={header.colSpan}
|
||||||
|
injectedStyles={{ width: `calc(var(--header-${header?.id}-size) * 1px)`, ...customStyles }}
|
||||||
|
draggable={<Draggable header={header} />}
|
||||||
|
>
|
||||||
|
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
+30
-11
@@ -8,16 +8,16 @@ import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
|||||||
import style from '../CuesheetTable.module.scss';
|
import style from '../CuesheetTable.module.scss';
|
||||||
|
|
||||||
interface SortableCellProps {
|
interface SortableCellProps {
|
||||||
header: Header<ExtendedEntry, unknown>;
|
columnId: string;
|
||||||
|
colSpan: number;
|
||||||
injectedStyles: CSSProperties;
|
injectedStyles: CSSProperties;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
draggable: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SortableCell({ header, injectedStyles, children }: SortableCellProps) {
|
export function SortableCell({ columnId, colSpan, injectedStyles, children, draggable }: SortableCellProps) {
|
||||||
const { column, colSpan } = header;
|
|
||||||
|
|
||||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||||
id: column.id,
|
id: columnId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// build drag styles
|
// build drag styles
|
||||||
@@ -34,12 +34,31 @@ export function SortableCell({ header, injectedStyles, children }: SortableCellP
|
|||||||
<div {...attributes} {...listeners}>
|
<div {...attributes} {...listeners}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<div
|
{draggable}
|
||||||
onDoubleClick={() => header.column.resetSize()}
|
|
||||||
onMouseDown={header.getResizeHandler()}
|
|
||||||
onTouchStart={header.getResizeHandler()}
|
|
||||||
className={style.resizer}
|
|
||||||
/>
|
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function TableCell({ colSpan, injectedStyles, children, draggable }: SortableCellProps) {
|
||||||
|
return (
|
||||||
|
<th style={injectedStyles} colSpan={colSpan} tabIndex={-1}>
|
||||||
|
<div>{children}</div>
|
||||||
|
{draggable}
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DraggableProps {
|
||||||
|
header: Header<ExtendedEntry, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Draggable({ header }: DraggableProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onDoubleClick={() => header.column.resetSize()}
|
||||||
|
onMouseDown={header.getResizeHandler()}
|
||||||
|
onTouchStart={header.getResizeHandler()}
|
||||||
|
className={style.resizer}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useLocalStorage } from '@mantine/hooks';
|
import { useLocalStorage } from '@mantine/hooks';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef, ColumnSizingState, Updater } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { debounce } from '../../../common/utils/debounce';
|
import { debounce } from '../../../common/utils/debounce';
|
||||||
import { makeStageKey } from '../../../common/utils/localStorage';
|
import { makeStageKey } from '../../../common/utils/localStorage';
|
||||||
@@ -14,17 +14,8 @@ const saveSizesToStorage = debounce((sizes: Record<string, number>) => {
|
|||||||
localStorage.setItem(tableSizesKey, JSON.stringify(sizes));
|
localStorage.setItem(tableSizesKey, JSON.stringify(sizes));
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
export default function useColumnManager(columns: ColumnDef<ExtendedEntry>[]) {
|
export function useColumnSizes() {
|
||||||
const [columnVisibility, setColumnVisibility] = useLocalStorage({
|
const [columnSizing, setColumnSizingState] = useState<Record<string, number>>(() => {
|
||||||
key: tableHiddenKey,
|
|
||||||
defaultValue: {},
|
|
||||||
});
|
|
||||||
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({
|
|
||||||
key: tableOrderKey,
|
|
||||||
defaultValue: columns.map((col) => col.id as string),
|
|
||||||
});
|
|
||||||
|
|
||||||
const [columnSizing, setColumnSizingState] = useState(() => {
|
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(tableSizesKey);
|
const stored = localStorage.getItem(tableSizesKey);
|
||||||
return stored ? JSON.parse(stored) : {};
|
return stored ? JSON.parse(stored) : {};
|
||||||
@@ -38,10 +29,22 @@ export default function useColumnManager(columns: ColumnDef<ExtendedEntry>[]) {
|
|||||||
saveSizesToStorage(columnSizing);
|
saveSizesToStorage(columnSizing);
|
||||||
}, [columnSizing]);
|
}, [columnSizing]);
|
||||||
|
|
||||||
const setColumnSizing = useCallback((sizes: typeof columnSizing) => {
|
const setColumnSizing = useCallback((sizesOrUpdater: Updater<ColumnSizingState>) => {
|
||||||
setColumnSizingState(sizes);
|
setColumnSizingState(sizesOrUpdater);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
columnSizing,
|
||||||
|
setColumnSizing,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useColumnOrder(columns: ColumnDef<ExtendedEntry>[]) {
|
||||||
|
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({
|
||||||
|
key: tableOrderKey,
|
||||||
|
defaultValue: columns.map((col) => col.id as string),
|
||||||
|
});
|
||||||
|
|
||||||
// update column order if columns change
|
// update column order if columns change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newColumns = columns.map((col) => col.id as string);
|
const newColumns = columns.map((col) => col.id as string);
|
||||||
@@ -55,12 +58,20 @@ export default function useColumnManager(columns: ColumnDef<ExtendedEntry>[]) {
|
|||||||
}, [columns, saveColumnOrder]);
|
}, [columns, saveColumnOrder]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
columnVisibility,
|
|
||||||
columnOrder,
|
columnOrder,
|
||||||
columnSizing,
|
|
||||||
resetColumnOrder,
|
|
||||||
setColumnVisibility,
|
|
||||||
saveColumnOrder,
|
saveColumnOrder,
|
||||||
setColumnSizing,
|
resetColumnOrder,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useColumnVisibility() {
|
||||||
|
const [columnVisibility, setColumnVisibility] = useLocalStorage({
|
||||||
|
key: tableHiddenKey,
|
||||||
|
defaultValue: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
columnVisibility,
|
||||||
|
setColumnVisibility,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user