mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-28 02:19:07 +00:00
feat: table mode in editor
This commit is contained in:
@@ -57,6 +57,7 @@ interface RundownProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
||||||
|
// invoke the compiler for the component
|
||||||
'use memo';
|
'use memo';
|
||||||
|
|
||||||
const { order, entries, id } = data;
|
const { order, entries, id } = data;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { AppMode, sessionKeys } from '../../ontimeConfig';
|
|||||||
import RundownEntryEditor from './entry-editor/RundownEntryEditor';
|
import RundownEntryEditor from './entry-editor/RundownEntryEditor';
|
||||||
import FinderPlacement from './placements/FinderPlacement';
|
import FinderPlacement from './placements/FinderPlacement';
|
||||||
import { RundownContextMenu } from './rundown-context-menu/RundownContextMenu';
|
import { RundownContextMenu } from './rundown-context-menu/RundownContextMenu';
|
||||||
import RundownWrapper from './RundownWrapper';
|
import RundownWrapper, { RundownViewMode } from './RundownWrapper';
|
||||||
|
|
||||||
import style from './RundownExport.module.scss';
|
import style from './RundownExport.module.scss';
|
||||||
|
|
||||||
@@ -26,6 +26,10 @@ function RundownExport() {
|
|||||||
key: sessionKeys.editorMode,
|
key: sessionKeys.editorMode,
|
||||||
defaultValue: AppMode.Edit,
|
defaultValue: AppMode.Edit,
|
||||||
});
|
});
|
||||||
|
const [viewMode, setViewMode] = useSessionStorage<RundownViewMode>({
|
||||||
|
key: 'rundown-view-mode',
|
||||||
|
defaultValue: 'list',
|
||||||
|
});
|
||||||
const isSmallDevice = useIsSmallDevice();
|
const isSmallDevice = useIsSmallDevice();
|
||||||
|
|
||||||
if (isSmallDevice && isExtracted) {
|
if (isSmallDevice && isExtracted) {
|
||||||
@@ -41,7 +45,7 @@ function RundownExport() {
|
|||||||
<div className={style.rundown}>
|
<div className={style.rundown}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<RundownContextMenu>
|
<RundownContextMenu>
|
||||||
<RundownWrapper isSmallDevice />
|
<RundownWrapper isSmallDevice viewMode={viewMode} setViewMode={setViewMode} />
|
||||||
</RundownContextMenu>
|
</RundownContextMenu>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,7 +54,7 @@ function RundownExport() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideSideBar = isExtracted && editorMode === 'run';
|
const hideSideBar = (isExtracted && editorMode === 'run') || viewMode === 'table';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProtectRoute permission='editor'>
|
<ProtectRoute permission='editor'>
|
||||||
@@ -62,7 +66,7 @@ function RundownExport() {
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{!isExtracted && <Editor.CornerExtract onClick={(event) => handleLinks('rundown', event)} />}
|
{!isExtracted && <Editor.CornerExtract onClick={(event) => handleLinks('rundown', event)} />}
|
||||||
<RundownContextMenu>
|
<RundownContextMenu>
|
||||||
<RundownWrapper />
|
<RundownWrapper viewMode={viewMode} setViewMode={setViewMode} />
|
||||||
</RundownContextMenu>
|
</RundownContextMenu>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Editor.Panel>
|
</Editor.Panel>
|
||||||
|
|||||||
@@ -1,27 +1,34 @@
|
|||||||
import Empty from '../../common/components/state/Empty';
|
import Empty from '../../common/components/state/Empty';
|
||||||
import { useRundownWithMetadata } from '../../common/hooks-query/useRundown';
|
import { useRundownWithMetadata } from '../../common/hooks-query/useRundown';
|
||||||
|
import EditorEditModal from '../../views/cuesheet/cuesheet-edit-modal/EditorEditModal';
|
||||||
|
|
||||||
import RundownHeader from './rundown-header/RundownHeader';
|
import RundownHeader from './rundown-header/RundownHeader';
|
||||||
import RundownHeaderMobile from './rundown-header/RundownHeaderMobile';
|
import RundownHeaderMobile from './rundown-header/RundownHeaderMobile';
|
||||||
|
import RundownTable from './rundown-table/RundownTable';
|
||||||
import Rundown from './Rundown';
|
import Rundown from './Rundown';
|
||||||
|
|
||||||
import styles from './Rundown.module.scss';
|
import styles from './Rundown.module.scss';
|
||||||
|
|
||||||
|
export type RundownViewMode = 'list' | 'table';
|
||||||
|
|
||||||
interface RundownWrapperProps {
|
interface RundownWrapperProps {
|
||||||
isSmallDevice?: boolean;
|
isSmallDevice?: boolean;
|
||||||
|
viewMode: RundownViewMode;
|
||||||
|
setViewMode: (mode: RundownViewMode) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RundownWrapper({ isSmallDevice }: RundownWrapperProps) {
|
export default function RundownWrapper({ isSmallDevice, viewMode, setViewMode }: RundownWrapperProps) {
|
||||||
const { data, status, rundownMetadata } = useRundownWithMetadata();
|
const { data, status, rundownMetadata } = useRundownWithMetadata();
|
||||||
|
|
||||||
|
const isLoading = status !== 'success' || !data || !rundownMetadata;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.rundownWrapper}>
|
<div className={styles.rundownWrapper}>
|
||||||
{isSmallDevice ? <RundownHeaderMobile /> : <RundownHeader />}
|
{isSmallDevice ? <RundownHeaderMobile /> : <RundownHeader viewMode={viewMode} setViewMode={setViewMode} />}
|
||||||
{status === 'success' && data && rundownMetadata ? (
|
{isLoading && <Empty text='Connecting to server' />}
|
||||||
<Rundown data={data} rundownMetadata={rundownMetadata} />
|
{!isLoading && viewMode === 'list' && <Rundown data={data} rundownMetadata={rundownMetadata} />}
|
||||||
) : (
|
{!isLoading && viewMode === 'table' && <RundownTable />}
|
||||||
<Empty text='Connecting to server' />
|
<EditorEditModal />
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,15 @@ import RundownMenu from './RundownMenu';
|
|||||||
|
|
||||||
import style from './RundownHeader.module.scss';
|
import style from './RundownHeader.module.scss';
|
||||||
|
|
||||||
|
type RundownViewMode = 'list' | 'table';
|
||||||
|
|
||||||
|
interface RundownHeaderProps {
|
||||||
|
viewMode: RundownViewMode;
|
||||||
|
setViewMode: (mode: RundownViewMode) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export default memo(RundownHeader);
|
export default memo(RundownHeader);
|
||||||
function RundownHeader() {
|
function RundownHeader({ viewMode, setViewMode }: RundownHeaderProps) {
|
||||||
const [editorMode, setEditorMode] = useSessionStorage({ key: sessionKeys.editorMode, defaultValue: AppMode.Edit });
|
const [editorMode, setEditorMode] = useSessionStorage({ key: sessionKeys.editorMode, defaultValue: AppMode.Edit });
|
||||||
|
|
||||||
const { offsetMode } = useOffsetMode();
|
const { offsetMode } = useOffsetMode();
|
||||||
@@ -33,6 +40,13 @@ function RundownHeader() {
|
|||||||
setOffsetMode(newValue);
|
setOffsetMode(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleViewMode = (mode: RundownViewMode[]) => {
|
||||||
|
// we need to stop user from deselecting a mode
|
||||||
|
const newValue = mode.at(0);
|
||||||
|
if (!newValue) return;
|
||||||
|
setViewMode(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Toolbar.Root className={style.header}>
|
<Toolbar.Root className={style.header}>
|
||||||
<ToggleGroup value={[editorMode]} onValueChange={toggleAppMode} className={style.group}>
|
<ToggleGroup value={[editorMode]} onValueChange={toggleAppMode} className={style.group}>
|
||||||
@@ -55,6 +69,17 @@ function RundownHeader() {
|
|||||||
</Toolbar.Button>
|
</Toolbar.Button>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
|
|
||||||
|
<Editor.Separator className={style.separator} />
|
||||||
|
|
||||||
|
<ToggleGroup value={[viewMode]} onValueChange={toggleViewMode} className={style.group}>
|
||||||
|
<Toolbar.Button render={<Toggle />} value='list' className={style.radioButton}>
|
||||||
|
List
|
||||||
|
</Toolbar.Button>
|
||||||
|
<Toolbar.Button render={<Toggle />} value='table' className={style.radioButton}>
|
||||||
|
Table
|
||||||
|
</Toolbar.Button>
|
||||||
|
</ToggleGroup>
|
||||||
|
|
||||||
<RundownMenu />
|
<RundownMenu />
|
||||||
</Toolbar.Root>
|
</Toolbar.Root>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Toolbar } from '@base-ui/react/toolbar';
|
||||||
|
import type { Column } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
||||||
|
import {
|
||||||
|
ColumnSettings,
|
||||||
|
ViewSettings,
|
||||||
|
} from '../../../views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableSettings';
|
||||||
|
|
||||||
|
import { usePersistedRundownOptions } from '../rundown.options';
|
||||||
|
|
||||||
|
import style from '../../../views/cuesheet/cuesheet-table/cuesheet-table-settings/CuesheetTableSettings.module.scss';
|
||||||
|
|
||||||
|
interface EditorTableSettingsProps {
|
||||||
|
columns: Column<ExtendedEntry, unknown>[];
|
||||||
|
handleResetResizing: () => void;
|
||||||
|
handleResetReordering: () => void;
|
||||||
|
handleClearToggles: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EditorTableSettings({
|
||||||
|
columns,
|
||||||
|
handleResetResizing,
|
||||||
|
handleResetReordering,
|
||||||
|
handleClearToggles,
|
||||||
|
}: EditorTableSettingsProps) {
|
||||||
|
const options = usePersistedRundownOptions();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Toolbar.Root className={style.tableSettings}>
|
||||||
|
<ViewSettings optionsStore={options} />
|
||||||
|
<ColumnSettings
|
||||||
|
columns={columns}
|
||||||
|
handleResetResizing={handleResetResizing}
|
||||||
|
handleResetReordering={handleResetReordering}
|
||||||
|
handleClearToggles={handleClearToggles}
|
||||||
|
/>
|
||||||
|
{/* No mode toggle - already in RundownHeader */}
|
||||||
|
{/* No share modal - cuesheet only */}
|
||||||
|
</Toolbar.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import { useSessionStorage } from '@mantine/hooks';
|
||||||
|
|
||||||
|
import EmptyPage from '../../../common/components/state/EmptyPage';
|
||||||
|
import useCustomFields from '../../../common/hooks-query/useCustomFields';
|
||||||
|
import { AppMode, sessionKeys } from '../../../ontimeConfig';
|
||||||
|
import CuesheetDnd from '../../../views/cuesheet/cuesheet-dnd/CuesheetDnd';
|
||||||
|
import CuesheetTable from '../../../views/cuesheet/cuesheet-table/CuesheetTable';
|
||||||
|
|
||||||
|
import { makeRundownColumns } from './makeRundownColumns';
|
||||||
|
|
||||||
|
export default memo(RundownTable);
|
||||||
|
function RundownTable() {
|
||||||
|
const { data: customFields, status: customFieldStatus } = useCustomFields();
|
||||||
|
|
||||||
|
const [editorMode] = useSessionStorage({
|
||||||
|
key: sessionKeys.editorMode,
|
||||||
|
defaultValue: AppMode.Edit,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = useMemo(() => makeRundownColumns(customFields), [customFields]);
|
||||||
|
|
||||||
|
const isLoading = !customFields || customFieldStatus === 'pending';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CuesheetDnd columns={columns} tableRoot='editor'>
|
||||||
|
{isLoading ? (
|
||||||
|
<EmptyPage text='Loading...' />
|
||||||
|
) : (
|
||||||
|
<CuesheetTable columns={columns} cuesheetMode={editorMode} tableRoot='editor' />
|
||||||
|
)}
|
||||||
|
</CuesheetDnd>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import type { CustomFields } from 'ontime-types';
|
||||||
|
|
||||||
|
import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
||||||
|
import { AppMode } from '../../../ontimeConfig';
|
||||||
|
import { makeCuesheetColumns } from '../../../views/cuesheet/cuesheet-table/cuesheet-table-elements/cuesheetColsFactory';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates column definitions for the rundown table
|
||||||
|
* Reuses cuesheetColsFactory with preset=undefined for full access
|
||||||
|
*/
|
||||||
|
export function makeRundownColumns(customFields: CustomFields): ColumnDef<ExtendedEntry>[] {
|
||||||
|
// When preset=undefined, factory defaults to fullRead=true, fullWrite=true
|
||||||
|
// canWrite is determined by editorMode (AppMode.Edit vs AppMode.Run)
|
||||||
|
return makeCuesheetColumns(customFields, AppMode.Edit, undefined);
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { CustomFields } from 'ontime-types';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
type OptionValues = {
|
||||||
|
hideTableSeconds: boolean;
|
||||||
|
hideIndexColumn: boolean;
|
||||||
|
showDelayedTimes: boolean;
|
||||||
|
hideDelays: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultOptions: OptionValues = {
|
||||||
|
hideTableSeconds: false,
|
||||||
|
hideIndexColumn: false,
|
||||||
|
showDelayedTimes: false,
|
||||||
|
hideDelays: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RundownOptionKeys = keyof OptionValues;
|
||||||
|
|
||||||
|
export interface RundownOptions extends OptionValues {
|
||||||
|
setOption: <K extends RundownOptionKeys>(key: K, value: OptionValues[K]) => void;
|
||||||
|
toggleOption: (key: RundownOptionKeys) => void;
|
||||||
|
resetOptions: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePersistedRundownOptions = create<RundownOptions>()(
|
||||||
|
persist(
|
||||||
|
(set) => {
|
||||||
|
return {
|
||||||
|
...defaultOptions,
|
||||||
|
setOption: (key, value) => set((state) => ({ ...state, [key]: value })),
|
||||||
|
toggleOption: (key) => set((state) => ({ ...state, [key]: !state[key] })),
|
||||||
|
resetOptions: () => set(defaultOptions),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'editor-options',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const rundownDefaultColumns = [
|
||||||
|
{ value: 'flag', label: 'Flag' },
|
||||||
|
{ value: 'cue', label: 'Cue' },
|
||||||
|
{ value: 'title', label: 'Title' },
|
||||||
|
{ value: 'timeStart', label: 'Time start' },
|
||||||
|
{ value: 'timeEnd', label: 'Time end' },
|
||||||
|
{ value: 'duration', label: 'Duration' },
|
||||||
|
{ value: 'note', label: 'Note' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function makeRundownCustomColumns(customFields: CustomFields) {
|
||||||
|
return Object.entries(customFields).map(([key, field]) => {
|
||||||
|
return {
|
||||||
|
value: `custom-${key}`,
|
||||||
|
label: field.label,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -15,10 +15,15 @@ import { useColumnOrder } from '../cuesheet-table/useColumnManager';
|
|||||||
|
|
||||||
interface CuesheetDndProps {
|
interface CuesheetDndProps {
|
||||||
columns: ColumnDef<ExtendedEntry>[];
|
columns: ColumnDef<ExtendedEntry>[];
|
||||||
|
tableRoot?: 'editor' | 'cuesheet';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CuesheetDnd({ columns, children }: PropsWithChildren<CuesheetDndProps>) {
|
export default function CuesheetDnd({
|
||||||
const { columnOrder, saveColumnOrder } = useColumnOrder(columns);
|
columns,
|
||||||
|
tableRoot = 'cuesheet',
|
||||||
|
children,
|
||||||
|
}: PropsWithChildren<CuesheetDndProps>) {
|
||||||
|
const { columnOrder, saveColumnOrder } = useColumnOrder(columns, tableRoot);
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
import Modal from '../../../common/components/modal/Modal';
|
||||||
|
import CuesheetEntryEditor from '../../../features/rundown/entry-editor/CuesheetEventEditor';
|
||||||
|
|
||||||
|
import { useEditorEditModal } from './useEditorEditModal';
|
||||||
|
|
||||||
|
export default memo(EditorEditModal);
|
||||||
|
function EditorEditModal() {
|
||||||
|
const entryId = useEditorEditModal((state) => state.selectedEntryId);
|
||||||
|
const closeModal = useEditorEditModal((state) => state.clearSelection);
|
||||||
|
|
||||||
|
if (entryId === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen
|
||||||
|
onClose={closeModal}
|
||||||
|
title='Edit entry'
|
||||||
|
showCloseButton
|
||||||
|
bodyElements={<CuesheetEntryEditor entryId={entryId} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { EntryId } from 'ontime-types';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
interface SelectedEntryState {
|
||||||
|
selectedEntryId: EntryId | null;
|
||||||
|
setEditableEntry: (entryId: EntryId) => void;
|
||||||
|
clearSelection: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useEditorEditModal = create<SelectedEntryState>((set) => ({
|
||||||
|
selectedEntryId: null,
|
||||||
|
setEditableEntry: (entryId: EntryId) => set({ selectedEntryId: entryId }),
|
||||||
|
clearSelection: () => set({ selectedEntryId: null }),
|
||||||
|
}));
|
||||||
@@ -10,6 +10,8 @@ import { useEntryActions } from '../../../common/hooks/useEntryAction';
|
|||||||
import { useSelectedEventId } from '../../../common/hooks/useSocket';
|
import { useSelectedEventId } from '../../../common/hooks/useSocket';
|
||||||
import { useFlatRundownWithMetadata } from '../../../common/hooks-query/useRundown';
|
import { useFlatRundownWithMetadata } from '../../../common/hooks-query/useRundown';
|
||||||
import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
||||||
|
import { usePersistedRundownOptions } from '../../../features/rundown/rundown.options';
|
||||||
|
import EditorTableSettings from '../../../features/rundown/rundown-table/EditorTableSettings';
|
||||||
import { AppMode } from '../../../ontimeConfig';
|
import { AppMode } from '../../../ontimeConfig';
|
||||||
import { usePersistedCuesheetOptions } from '../cuesheet.options';
|
import { usePersistedCuesheetOptions } from '../cuesheet.options';
|
||||||
|
|
||||||
@@ -19,6 +21,7 @@ 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 EditorTableMenu from './cuesheet-table-menu/EditorTableMenu';
|
||||||
import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings';
|
import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings';
|
||||||
import { useColumnOrder, useColumnSizes, useColumnVisibility } from './useColumnManager';
|
import { useColumnOrder, useColumnSizes, useColumnVisibility } from './useColumnManager';
|
||||||
|
|
||||||
@@ -27,14 +30,17 @@ import style from './CuesheetTable.module.scss';
|
|||||||
interface CuesheetTableProps {
|
interface CuesheetTableProps {
|
||||||
columns: ColumnDef<ExtendedEntry>[];
|
columns: ColumnDef<ExtendedEntry>[];
|
||||||
cuesheetMode: AppMode;
|
cuesheetMode: AppMode;
|
||||||
|
tableRoot?: 'editor' | 'cuesheet';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CuesheetTable({ columns, cuesheetMode }: CuesheetTableProps) {
|
export default function CuesheetTable({ columns, cuesheetMode, tableRoot = 'cuesheet' }: CuesheetTableProps) {
|
||||||
const { data, status } = useFlatRundownWithMetadata();
|
const { data, status } = useFlatRundownWithMetadata();
|
||||||
const { updateEntry, updateTimer } = useEntryActions();
|
const { updateEntry, updateTimer } = useEntryActions();
|
||||||
const showDelayedTimes = usePersistedCuesheetOptions((state) => state.showDelayedTimes);
|
|
||||||
const hideTableSeconds = usePersistedCuesheetOptions((state) => state.hideTableSeconds);
|
const useOptions = tableRoot === 'editor' ? usePersistedRundownOptions : usePersistedCuesheetOptions;
|
||||||
const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn);
|
const showDelayedTimes = useOptions((state) => state.showDelayedTimes);
|
||||||
|
const hideTableSeconds = useOptions((state) => state.hideTableSeconds);
|
||||||
|
const hideIndexColumn = useOptions((state) => state.hideIndexColumn);
|
||||||
|
|
||||||
const { selectedEventId } = useSelectedEventId();
|
const { selectedEventId } = useSelectedEventId();
|
||||||
|
|
||||||
@@ -79,9 +85,9 @@ export default function CuesheetTable({ columns, cuesheetMode }: CuesheetTablePr
|
|||||||
[cuesheetMode, data, hideIndexColumn, hideTableSeconds, showDelayedTimes, updateEntry, updateTimer],
|
[cuesheetMode, data, hideIndexColumn, hideTableSeconds, showDelayedTimes, updateEntry, updateTimer],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { columnOrder, resetColumnOrder } = useColumnOrder(columns);
|
const { columnOrder, resetColumnOrder } = useColumnOrder(columns, tableRoot);
|
||||||
const { columnSizing, setColumnSizing } = useColumnSizes();
|
const { columnSizing, setColumnSizing } = useColumnSizes(tableRoot);
|
||||||
const { columnVisibility, setColumnVisibility } = useColumnVisibility();
|
const { columnVisibility, setColumnVisibility } = useColumnVisibility(tableRoot);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
@@ -143,9 +149,13 @@ export default function CuesheetTable({ columns, cuesheetMode }: CuesheetTablePr
|
|||||||
return <EmptyPage text='Loading...' />;
|
return <EmptyPage text='Loading...' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// control components need different implementations for handling permissions
|
||||||
|
const TableRootSettings = tableRoot === 'editor' ? EditorTableSettings : CuesheetTableSettings;
|
||||||
|
const TableMenu = tableRoot === 'editor' ? EditorTableMenu : CuesheetTableMenu;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CuesheetTableSettings
|
<TableRootSettings
|
||||||
columns={allLeafColumns}
|
columns={allLeafColumns}
|
||||||
handleResetResizing={resetColumnResizing}
|
handleResetResizing={resetColumnResizing}
|
||||||
handleResetReordering={resetColumnOrder}
|
handleResetReordering={resetColumnOrder}
|
||||||
@@ -249,7 +259,7 @@ export default function CuesheetTable({ columns, cuesheetMode }: CuesheetTablePr
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CuesheetTableMenu />
|
<TableMenu />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -15,3 +15,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.delayRowHidden {
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 0;
|
||||||
|
height: 1px;
|
||||||
|
line-height: 1px;
|
||||||
|
font-size: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,16 @@ function DelayRow({ duration, injectedStyles, ...virtuosoProps }: DelayRowProps)
|
|||||||
const hideDelays = usePersistedCuesheetOptions((state) => state.hideDelays);
|
const hideDelays = usePersistedCuesheetOptions((state) => state.hideDelays);
|
||||||
|
|
||||||
if (hideDelays || duration === 0) {
|
if (hideDelays || duration === 0) {
|
||||||
return null;
|
return (
|
||||||
|
<tr
|
||||||
|
className={`${style.delayRow} ${style.delayRowHidden}`}
|
||||||
|
data-testid='cuesheet-delay-hidden'
|
||||||
|
style={injectedStyles}
|
||||||
|
{...virtuosoProps}
|
||||||
|
>
|
||||||
|
<td />
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const delayTime = millisToDelayString(duration, 'expanded');
|
const delayTime = millisToDelayString(duration, 'expanded');
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
import { IoAdd, IoArrowDown, IoArrowUp, IoDuplicateOutline, IoOptions, IoTrash } from 'react-icons/io5';
|
||||||
|
import { SupportedEntry } from 'ontime-types';
|
||||||
|
|
||||||
|
import { PositionedDropdownMenu } from '../../../../common/components/dropdown-menu/DropdownMenu';
|
||||||
|
import { useEntryActions } from '../../../../common/hooks/useEntryAction';
|
||||||
|
import { useCuesheetEditModal } from '../../cuesheet-edit-modal/useCuesheetEditModal';
|
||||||
|
|
||||||
|
import { useCuesheetTableMenu } from './useCuesheetTableMenu';
|
||||||
|
|
||||||
|
export default memo(EditorTableMenu);
|
||||||
|
|
||||||
|
function EditorTableMenu() {
|
||||||
|
const { isOpen, entryId, entryIndex, parentId, flag, position, closeMenu } = useCuesheetTableMenu();
|
||||||
|
const { addEntry, clone, deleteEntry, move, updateEntry } = useEntryActions();
|
||||||
|
const showModal = useCuesheetEditModal((state) => state.setEditableEntry);
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PositionedDropdownMenu
|
||||||
|
isOpen
|
||||||
|
onClose={closeMenu}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: 'Edit...',
|
||||||
|
onClick: () => showModal(entryId),
|
||||||
|
icon: IoOptions,
|
||||||
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: flag ? 'Remove flag' : 'Add flag',
|
||||||
|
onClick: () => updateEntry({ id: entryId, flag: !flag }),
|
||||||
|
icon: IoDuplicateOutline,
|
||||||
|
disabled: flag === null,
|
||||||
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: 'Add event above',
|
||||||
|
onClick: () => addEntry({ type: SupportedEntry.Event, parent: parentId }, { before: entryId }),
|
||||||
|
icon: IoAdd,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: 'Add event below',
|
||||||
|
onClick: () => addEntry({ type: SupportedEntry.Event, parent: parentId }, { after: entryId }),
|
||||||
|
icon: IoAdd,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: 'Clone event',
|
||||||
|
onClick: () => clone(entryId),
|
||||||
|
icon: IoDuplicateOutline,
|
||||||
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: 'Move up',
|
||||||
|
onClick: () => move(entryId, 'up'),
|
||||||
|
icon: IoArrowUp,
|
||||||
|
disabled: entryIndex < 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: 'Move down',
|
||||||
|
onClick: () => move(entryId, 'down'),
|
||||||
|
icon: IoArrowDown,
|
||||||
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
label: 'Delete',
|
||||||
|
onClick: () => deleteEntry([entryId]),
|
||||||
|
icon: IoTrash,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
position={position}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
+18
-6
@@ -15,7 +15,7 @@ import { PresetContext } from '../../../../common/context/PresetContext';
|
|||||||
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
import type { ExtendedEntry } from '../../../../common/utils/rundownMetadata';
|
||||||
import { cx } from '../../../../common/utils/styleUtils';
|
import { cx } from '../../../../common/utils/styleUtils';
|
||||||
import { AppMode, sessionKeys } from '../../../../ontimeConfig';
|
import { AppMode, sessionKeys } from '../../../../ontimeConfig';
|
||||||
import { usePersistedCuesheetOptions } from '../../cuesheet.options';
|
import { CuesheetOptions, usePersistedCuesheetOptions } from '../../cuesheet.options';
|
||||||
import { useCuesheetPermissions } from '../../useTablePermissions';
|
import { useCuesheetPermissions } from '../../useTablePermissions';
|
||||||
|
|
||||||
import CuesheetShareModal from './CuesheetShareModal';
|
import CuesheetShareModal from './CuesheetShareModal';
|
||||||
@@ -29,6 +29,17 @@ interface CuesheetTableSettingsProps {
|
|||||||
handleClearToggles: () => void;
|
handleClearToggles: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ViewSettingsProps {
|
||||||
|
optionsStore: CuesheetOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColumnSettingsProps {
|
||||||
|
columns: Column<ExtendedEntry, unknown>[];
|
||||||
|
handleResetResizing: () => void;
|
||||||
|
handleResetReordering: () => void;
|
||||||
|
handleClearToggles: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export default function CuesheetTableSettings({
|
export default function CuesheetTableSettings({
|
||||||
columns,
|
columns,
|
||||||
handleResetResizing,
|
handleResetResizing,
|
||||||
@@ -37,6 +48,7 @@ export default function CuesheetTableSettings({
|
|||||||
}: CuesheetTableSettingsProps) {
|
}: CuesheetTableSettingsProps) {
|
||||||
const canShare = useCuesheetPermissions((state) => state.canShare);
|
const canShare = useCuesheetPermissions((state) => state.canShare);
|
||||||
const preset = use(PresetContext);
|
const preset = use(PresetContext);
|
||||||
|
const options = usePersistedCuesheetOptions();
|
||||||
|
|
||||||
const [cuesheetMode, setCuesheetMode] = useSessionStorage({
|
const [cuesheetMode, setCuesheetMode] = useSessionStorage({
|
||||||
key: preset ? `${preset.alias}${sessionKeys.cuesheetMode}` : sessionKeys.cuesheetMode,
|
key: preset ? `${preset.alias}${sessionKeys.cuesheetMode}` : sessionKeys.cuesheetMode,
|
||||||
@@ -52,7 +64,7 @@ export default function CuesheetTableSettings({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Toolbar.Root className={style.tableSettings}>
|
<Toolbar.Root className={style.tableSettings}>
|
||||||
<ViewSettings />
|
<ViewSettings optionsStore={options} />
|
||||||
<ColumnSettings
|
<ColumnSettings
|
||||||
columns={columns}
|
columns={columns}
|
||||||
handleResetResizing={handleResetResizing}
|
handleResetResizing={handleResetResizing}
|
||||||
@@ -78,8 +90,8 @@ export default function CuesheetTableSettings({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ViewSettings() {
|
export function ViewSettings({ optionsStore }: ViewSettingsProps) {
|
||||||
const options = usePersistedCuesheetOptions();
|
const options = optionsStore;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover.Root>
|
<Popover.Root>
|
||||||
@@ -137,12 +149,12 @@ function ViewSettings() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ColumnSettings({
|
export function ColumnSettings({
|
||||||
columns,
|
columns,
|
||||||
handleResetResizing,
|
handleResetResizing,
|
||||||
handleResetReordering,
|
handleResetReordering,
|
||||||
handleClearToggles,
|
handleClearToggles,
|
||||||
}: CuesheetTableSettingsProps) {
|
}: ColumnSettingsProps) {
|
||||||
return (
|
return (
|
||||||
<Popover.Root>
|
<Popover.Root>
|
||||||
<Popover.Trigger
|
<Popover.Trigger
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useLocalStorage } from '@mantine/hooks';
|
import { useLocalStorage } from '@mantine/hooks';
|
||||||
import { ColumnDef, ColumnSizingState, Updater } from '@tanstack/react-table';
|
import { ColumnDef, ColumnSizingState, Updater } from '@tanstack/react-table';
|
||||||
|
|
||||||
@@ -6,15 +6,11 @@ import { debounce } from '../../../common/utils/debounce';
|
|||||||
import { makeStageKey } from '../../../common/utils/localStorage';
|
import { makeStageKey } from '../../../common/utils/localStorage';
|
||||||
import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
|
||||||
|
|
||||||
const tableSizesKey = makeStageKey('cuesheet-sizes');
|
type TableRoot = 'editor' | 'cuesheet';
|
||||||
const tableHiddenKey = makeStageKey('cuesheet-hidden');
|
|
||||||
const tableOrderKey = makeStageKey('cuesheet-order');
|
|
||||||
|
|
||||||
const saveSizesToStorage = debounce((sizes: Record<string, number>) => {
|
export function useColumnSizes(tableRoot: TableRoot = 'cuesheet') {
|
||||||
localStorage.setItem(tableSizesKey, JSON.stringify(sizes));
|
const tableSizesKey = useMemo(() => makeStageKey(`${tableRoot}-table-sizes`), [tableRoot]);
|
||||||
}, 500);
|
|
||||||
|
|
||||||
export function useColumnSizes() {
|
|
||||||
const [columnSizing, setColumnSizingState] = useState<Record<string, number>>(() => {
|
const [columnSizing, setColumnSizingState] = useState<Record<string, number>>(() => {
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(tableSizesKey);
|
const stored = localStorage.getItem(tableSizesKey);
|
||||||
@@ -26,8 +22,11 @@ export function useColumnSizes() {
|
|||||||
|
|
||||||
// save sizes to localStorage whenever they change (debounced)
|
// save sizes to localStorage whenever they change (debounced)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const saveSizesToStorage = debounce((sizes: Record<string, number>) => {
|
||||||
|
localStorage.setItem(tableSizesKey, JSON.stringify(sizes));
|
||||||
|
}, 500);
|
||||||
saveSizesToStorage(columnSizing);
|
saveSizesToStorage(columnSizing);
|
||||||
}, [columnSizing]);
|
}, [columnSizing, tableSizesKey]);
|
||||||
|
|
||||||
const setColumnSizing = useCallback((sizesOrUpdater: Updater<ColumnSizingState>) => {
|
const setColumnSizing = useCallback((sizesOrUpdater: Updater<ColumnSizingState>) => {
|
||||||
setColumnSizingState(sizesOrUpdater);
|
setColumnSizingState(sizesOrUpdater);
|
||||||
@@ -39,7 +38,9 @@ export function useColumnSizes() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useColumnOrder(columns: ColumnDef<ExtendedEntry>[]) {
|
export function useColumnOrder(columns: ColumnDef<ExtendedEntry>[], tableRoot: TableRoot = 'cuesheet') {
|
||||||
|
const tableOrderKey = useMemo(() => makeStageKey(`${tableRoot}-table-order`), [tableRoot]);
|
||||||
|
|
||||||
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({
|
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({
|
||||||
key: tableOrderKey,
|
key: tableOrderKey,
|
||||||
defaultValue: columns.map((col) => col.id as string),
|
defaultValue: columns.map((col) => col.id as string),
|
||||||
@@ -64,7 +65,9 @@ export function useColumnOrder(columns: ColumnDef<ExtendedEntry>[]) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useColumnVisibility() {
|
export function useColumnVisibility(tableRoot: TableRoot = 'cuesheet') {
|
||||||
|
const tableHiddenKey = useMemo(() => makeStageKey(`${tableRoot}-table-hidden`), [tableRoot]);
|
||||||
|
|
||||||
const [columnVisibility, setColumnVisibility] = useLocalStorage({
|
const [columnVisibility, setColumnVisibility] = useLocalStorage({
|
||||||
key: tableHiddenKey,
|
key: tableHiddenKey,
|
||||||
defaultValue: {},
|
defaultValue: {},
|
||||||
|
|||||||
Reference in New Issue
Block a user