mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-17 13:23:35 +00:00
refactor: improve editor and cuesheet headers
- consistent behaviour for app mode - improve navigation
This commit is contained in:
committed by
Carlos Valente
parent
79bcc179ba
commit
6facd6a666
@@ -1,10 +1,12 @@
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useSessionStorage } from '@mantine/hooks';
|
||||
import { useTableNav } from '@table-nav/react';
|
||||
import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||
import { OntimeEntry, TimeField } from 'ontime-types';
|
||||
|
||||
import { useEntryActions } from '../../../common/hooks/useEntryAction';
|
||||
import { useFollowSelected } from '../../../common/hooks/useFollowComponent';
|
||||
import { AppMode,sessionKeys } from '../../../ontimeConfig';
|
||||
import { usePersistedCuesheetOptions } from '../cuesheet.options';
|
||||
|
||||
import CuesheetBody from './cuesheet-table-elements/CuesheetBody';
|
||||
@@ -22,11 +24,14 @@ interface CuesheetTableProps {
|
||||
|
||||
export default function CuesheetTable({ data, columns }: CuesheetTableProps) {
|
||||
const { updateEntry, updateTimer } = useEntryActions();
|
||||
const followPlayback = usePersistedCuesheetOptions((state) => state.followPlayback);
|
||||
const showDelayedTimes = usePersistedCuesheetOptions((state) => state.showDelayedTimes);
|
||||
const hideTableSeconds = usePersistedCuesheetOptions((state) => state.hideTableSeconds);
|
||||
const [cuesheetMode] = useSessionStorage({
|
||||
key: sessionKeys.cuesheetMode,
|
||||
defaultValue: AppMode.Edit,
|
||||
});
|
||||
|
||||
const { selectedRef, scrollRef } = useFollowSelected(followPlayback);
|
||||
const { selectedRef, scrollRef } = useFollowSelected(cuesheetMode === AppMode.Run);
|
||||
|
||||
const { listeners } = useTableNav();
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { IoEllipsisHorizontal } from 'react-icons/io5';
|
||||
import { useSessionStorage } from '@mantine/hooks';
|
||||
import { flexRender, Table } from '@tanstack/react-table';
|
||||
import { EntryId, OntimeEntry } from 'ontime-types';
|
||||
|
||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||
import { useCurrentBlockId } from '../../../../common/hooks/useSocket';
|
||||
import { AppMode, sessionKeys } from '../../../../ontimeConfig';
|
||||
import { usePersistedCuesheetOptions } from '../../cuesheet.options';
|
||||
import { useCuesheetTableMenu } from '../cuesheet-table-menu/useCuesheetTableMenu';
|
||||
|
||||
@@ -22,7 +24,10 @@ export default function BlockRow({ blockId, colour, hidePast, rowId, rowIndex, t
|
||||
const { currentBlockId } = useCurrentBlockId();
|
||||
|
||||
const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn);
|
||||
const showActionMenu = usePersistedCuesheetOptions((state) => state.showActionMenu);
|
||||
const [cuesheetMode] = useSessionStorage<AppMode>({
|
||||
key: sessionKeys.cuesheetMode,
|
||||
defaultValue: AppMode.Edit,
|
||||
});
|
||||
const openMenu = useCuesheetTableMenu((store) => store.openMenu);
|
||||
|
||||
if (hidePast && !currentBlockId) {
|
||||
@@ -31,7 +36,7 @@ export default function BlockRow({ blockId, colour, hidePast, rowId, rowIndex, t
|
||||
|
||||
return (
|
||||
<tr className={style.blockRow} style={{ '--user-bg': colour }} data-testid='cuesheet-block'>
|
||||
{showActionMenu && (
|
||||
{cuesheetMode === AppMode.Edit && (
|
||||
<td className={style.actionColumn} tabIndex={-1} role='cell'>
|
||||
<IconButton
|
||||
aria-label='Options'
|
||||
|
||||
+7
-2
@@ -1,8 +1,10 @@
|
||||
import { horizontalListSortingStrategy, SortableContext } from '@dnd-kit/sortable';
|
||||
import { useSessionStorage } from '@mantine/hooks';
|
||||
import { flexRender, HeaderGroup } from '@tanstack/react-table';
|
||||
import { OntimeEntry } from 'ontime-types';
|
||||
|
||||
import { getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||
import { AppMode, sessionKeys } from '../../../../ontimeConfig';
|
||||
import { usePersistedCuesheetOptions } from '../../cuesheet.options';
|
||||
|
||||
import { SortableCell } from './SortableCell';
|
||||
@@ -15,7 +17,10 @@ interface CuesheetHeaderProps {
|
||||
|
||||
export default function CuesheetHeader({ headerGroups }: CuesheetHeaderProps) {
|
||||
const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn);
|
||||
const showActionMenu = usePersistedCuesheetOptions((state) => state.showActionMenu);
|
||||
const [cuesheetMode] = useSessionStorage<AppMode>({
|
||||
key: sessionKeys.cuesheetMode,
|
||||
defaultValue: AppMode.Edit,
|
||||
});
|
||||
|
||||
return (
|
||||
<thead className={style.tableHeader}>
|
||||
@@ -24,7 +29,7 @@ export default function CuesheetHeader({ headerGroups }: CuesheetHeaderProps) {
|
||||
|
||||
return (
|
||||
<tr key={headerGroup.id}>
|
||||
{showActionMenu && <th className={style.actionColumn} tabIndex={-1} />}
|
||||
{cuesheetMode === AppMode.Edit && <th className={style.actionColumn} tabIndex={-1} />}
|
||||
{!hideIndexColumn && (
|
||||
<th className={style.indexColumn} tabIndex={-1}>
|
||||
#
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { MutableRefObject, useEffect, useRef } from 'react';
|
||||
import { IoEllipsisHorizontal } from 'react-icons/io5';
|
||||
import { useSessionStorage } from '@mantine/hooks';
|
||||
import { flexRender, Table } from '@tanstack/react-table';
|
||||
import { OntimeEntry, OntimeEvent, RGBColour } from 'ontime-types';
|
||||
import { colourToHex, cssOrHexToColour } from 'ontime-utils';
|
||||
|
||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||
import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils';
|
||||
import { AppMode, sessionKeys } from '../../../../ontimeConfig';
|
||||
import { usePersistedCuesheetOptions } from '../../cuesheet.options';
|
||||
import { useCuesheetTableMenu } from '../cuesheet-table-menu/useCuesheetTableMenu';
|
||||
|
||||
@@ -44,8 +46,10 @@ export default function EventRow({
|
||||
firstAfterBlock,
|
||||
}: EventRowProps) {
|
||||
const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn);
|
||||
const showActionMenu = usePersistedCuesheetOptions((state) => state.showActionMenu);
|
||||
const ownRef = useRef<HTMLTableRowElement>(null);
|
||||
const [cuesheetMode] = useSessionStorage<AppMode>({
|
||||
key: sessionKeys.cuesheetMode,
|
||||
defaultValue: AppMode.Edit,
|
||||
}); const ownRef = useRef<HTMLTableRowElement>(null);
|
||||
|
||||
const isVisible = useVisibleRowsStore((state) => state.visibleRows.has(rowId));
|
||||
|
||||
@@ -86,7 +90,7 @@ export default function EventRow({
|
||||
ref={selectedRef ?? ownRef}
|
||||
data-testid='cuesheet-event'
|
||||
>
|
||||
{showActionMenu && (
|
||||
{cuesheetMode === AppMode.Edit && (
|
||||
<td className={style.actionColumn} tabIndex={-1} role='cell'>
|
||||
<IconButton
|
||||
aria-label='Options'
|
||||
|
||||
+3
-2
@@ -1,3 +1,4 @@
|
||||
import { Toolbar } from '@base-ui-components/react/toolbar';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
|
||||
import Button from '../../../../common/components/buttons/Button';
|
||||
@@ -17,10 +18,10 @@ function CuesheetShareModal() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant='subtle' onClick={handler.open}>
|
||||
<Toolbar.Button onClick={handler.open} render={<Button />}>
|
||||
<RotatedLink />
|
||||
Share...
|
||||
</Button>
|
||||
</Toolbar.Button>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={handler.close}
|
||||
|
||||
+54
-2
@@ -5,7 +5,7 @@
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
font-size: $inner-section-text-size;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
@@ -39,4 +39,56 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.apart {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/** styles should match RundownHeader.module.scss */
|
||||
.group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding-inline: 2px;
|
||||
background: $gray-1100;
|
||||
border-radius: $component-border-radius-md;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
/** styles should match RundownHeader.module.scss */
|
||||
.radioButton {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: calc(2rem - 4px); // padding inline * 2
|
||||
padding-inline: 1em;
|
||||
border: 1px solid transparent;
|
||||
border-radius: $component-border-radius-md;
|
||||
color: $gray-400;
|
||||
font-size: calc(1rem - 2px);
|
||||
font-weight: 600;
|
||||
|
||||
&:focus-visible {
|
||||
background: transparent;
|
||||
outline: 1px solid $blue-500;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $gray-1000;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $gray-1100;
|
||||
}
|
||||
|
||||
&[data-pressed] {
|
||||
background: $blue-700;
|
||||
color: $ui-white;
|
||||
|
||||
&:hover:not(:disabled):not(:active) {
|
||||
background: $blue-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+96
-81
@@ -1,6 +1,10 @@
|
||||
import { memo, ReactNode } from 'react';
|
||||
import { IoChevronDown, IoLocate, IoOptions, IoSettingsOutline } from 'react-icons/io5';
|
||||
import { IoChevronDown, IoOptions, IoSettingsOutline } from 'react-icons/io5';
|
||||
import { Popover } from '@base-ui-components/react/popover';
|
||||
import { Toggle } from '@base-ui-components/react/toggle';
|
||||
import { ToggleGroup } from '@base-ui-components/react/toggle-group';
|
||||
import { Toolbar } from '@base-ui-components/react/toolbar';
|
||||
import { useSessionStorage } from '@mantine/hooks';
|
||||
import type { Column } from '@tanstack/react-table';
|
||||
import { OntimeEntry } from 'ontime-types';
|
||||
|
||||
@@ -8,6 +12,8 @@ import Button from '../../../../common/components/buttons/Button';
|
||||
import Checkbox from '../../../../common/components/checkbox/Checkbox';
|
||||
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
|
||||
import PopoverContents from '../../../../common/components/popover/Popover';
|
||||
import { cx } from '../../../../common/utils/styleUtils';
|
||||
import { AppMode, sessionKeys } from '../../../../ontimeConfig';
|
||||
import { usePersistedCuesheetOptions } from '../../cuesheet.options';
|
||||
|
||||
import CuesheetShareModal from './CuesheetShareModal';
|
||||
@@ -28,36 +34,40 @@ function CuesheetTableSettings({
|
||||
handleResetReordering,
|
||||
handleClearToggles,
|
||||
}: CuesheetTableSettingsProps) {
|
||||
return (
|
||||
<div className={style.tableSettings}>
|
||||
<div className={style.inline}>
|
||||
<ViewSettings />
|
||||
<ColumnSettings
|
||||
columns={columns}
|
||||
handleResetResizing={handleResetResizing}
|
||||
handleResetReordering={handleResetReordering}
|
||||
handleClearToggles={handleClearToggles}
|
||||
/>
|
||||
</div>
|
||||
const [cuesheetMode, setCuesheetMode] = useSessionStorage({
|
||||
key: sessionKeys.cuesheetMode,
|
||||
defaultValue: AppMode.Edit,
|
||||
});
|
||||
|
||||
<div className={style.inline}>
|
||||
<ViewSettingsFollowButton />
|
||||
<Editor.Separator orientation='vertical' />
|
||||
<CuesheetShareModal />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ViewSettingsFollowButton() {
|
||||
const followPlayback = usePersistedCuesheetOptions((state) => state.followPlayback);
|
||||
const toggle = usePersistedCuesheetOptions((state) => state.toggleOption);
|
||||
const toggleCuesheetMode = (mode: AppMode[]) => {
|
||||
// we need to stop user from deselecting a mode
|
||||
const newValue = mode.at(0);
|
||||
if (!newValue) return;
|
||||
setCuesheetMode(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button variant={followPlayback ? 'primary' : 'subtle'} onClick={() => toggle('followPlayback')}>
|
||||
<IoLocate />
|
||||
{followPlayback ? 'Following playback' : 'Follow playback'}
|
||||
</Button>
|
||||
<Toolbar.Root className={style.tableSettings}>
|
||||
<ViewSettings />
|
||||
<ColumnSettings
|
||||
columns={columns}
|
||||
handleResetResizing={handleResetResizing}
|
||||
handleResetReordering={handleResetReordering}
|
||||
handleClearToggles={handleClearToggles}
|
||||
/>
|
||||
|
||||
<ToggleGroup value={[cuesheetMode]} onValueChange={toggleCuesheetMode} className={cx([style.group, style.apart])}>
|
||||
<Toolbar.Button render={<Toggle />} value={AppMode.Run} className={style.radioButton}>
|
||||
Run
|
||||
</Toolbar.Button>
|
||||
<Toolbar.Button render={<Toggle />} value={AppMode.Edit} className={style.radioButton}>
|
||||
Edit
|
||||
</Toolbar.Button>
|
||||
</ToggleGroup>
|
||||
|
||||
<Editor.Separator orientation='vertical' />
|
||||
<CuesheetShareModal />
|
||||
</Toolbar.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,59 +78,60 @@ function ViewSettings() {
|
||||
<Popover.Root>
|
||||
<Popover.Trigger
|
||||
render={
|
||||
<Button variant='ghosted-white'>
|
||||
<IoSettingsOutline /> Settings
|
||||
<IoChevronDown />
|
||||
</Button>
|
||||
<Toolbar.Button
|
||||
render={
|
||||
<Button variant='ghosted-white'>
|
||||
<IoSettingsOutline /> Settings
|
||||
<IoChevronDown />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PopoverContents align='start' className={style.column}>
|
||||
<Editor.Label className={style.sectionTitle}>Element visibility</Editor.Label>
|
||||
<Editor.Label className={style.option}>
|
||||
<Checkbox
|
||||
defaultChecked={options.showActionMenu}
|
||||
onCheckedChange={(checked) => options.setOption('showActionMenu', checked)}
|
||||
/>
|
||||
Show action menu
|
||||
</Editor.Label>
|
||||
<Editor.Label className={style.option}>
|
||||
<Checkbox
|
||||
defaultChecked={options.hideTableSeconds}
|
||||
onCheckedChange={(checked) => options.setOption('hideTableSeconds', checked)}
|
||||
/>
|
||||
Hide seconds in table
|
||||
</Editor.Label>
|
||||
<Editor.Label className={style.option}>
|
||||
<Checkbox
|
||||
defaultChecked={options.hidePast}
|
||||
onCheckedChange={(checked) => options.setOption('hidePast', checked)}
|
||||
/>
|
||||
Hide past events
|
||||
</Editor.Label>
|
||||
<Editor.Label className={style.option}>
|
||||
<Checkbox
|
||||
defaultChecked={options.hideIndexColumn}
|
||||
onCheckedChange={(checked) => options.setOption('hideIndexColumn', checked)}
|
||||
/>
|
||||
Hide index column
|
||||
</Editor.Label>
|
||||
<PopoverContents align='start' className={style.inline}>
|
||||
<div className={style.column}>
|
||||
<Editor.Label className={style.sectionTitle}>Element visibility</Editor.Label>
|
||||
<Editor.Label className={style.option}>
|
||||
<Checkbox
|
||||
defaultChecked={options.hideTableSeconds}
|
||||
onCheckedChange={(checked) => options.setOption('hideTableSeconds', checked)}
|
||||
/>
|
||||
Hide seconds in table
|
||||
</Editor.Label>
|
||||
<Editor.Label className={style.option}>
|
||||
<Checkbox
|
||||
defaultChecked={options.hidePast}
|
||||
onCheckedChange={(checked) => options.setOption('hidePast', checked)}
|
||||
/>
|
||||
Hide past events
|
||||
</Editor.Label>
|
||||
<Editor.Label className={style.option}>
|
||||
<Checkbox
|
||||
defaultChecked={options.hideIndexColumn}
|
||||
onCheckedChange={(checked) => options.setOption('hideIndexColumn', checked)}
|
||||
/>
|
||||
Hide index column
|
||||
</Editor.Label>
|
||||
</div>
|
||||
|
||||
<Editor.Label className={style.sectionTitle}>Table Behaviour</Editor.Label>
|
||||
<Editor.Label className={style.option}>
|
||||
<Checkbox
|
||||
defaultChecked={options.showDelayedTimes}
|
||||
onCheckedChange={(checked) => options.setOption('showDelayedTimes', checked)}
|
||||
/>
|
||||
Show delayed times
|
||||
</Editor.Label>
|
||||
<Editor.Label className={style.option}>
|
||||
<Checkbox
|
||||
defaultChecked={options.hideDelays}
|
||||
onCheckedChange={(checked) => options.setOption('hideDelays', checked)}
|
||||
/>
|
||||
Hide delay entries
|
||||
</Editor.Label>
|
||||
<div className={style.column}>
|
||||
<Editor.Label className={style.sectionTitle}>Table Behaviour</Editor.Label>
|
||||
<Editor.Label className={style.option}>
|
||||
<Checkbox
|
||||
defaultChecked={options.showDelayedTimes}
|
||||
onCheckedChange={(checked) => options.setOption('showDelayedTimes', checked)}
|
||||
/>
|
||||
Show delayed times
|
||||
</Editor.Label>
|
||||
<Editor.Label className={style.option}>
|
||||
<Checkbox
|
||||
defaultChecked={options.hideDelays}
|
||||
onCheckedChange={(checked) => options.setOption('hideDelays', checked)}
|
||||
/>
|
||||
Hide delay entries
|
||||
</Editor.Label>
|
||||
</div>
|
||||
</PopoverContents>
|
||||
</Popover.Root>
|
||||
);
|
||||
@@ -136,10 +147,14 @@ function ColumnSettings({
|
||||
<Popover.Root>
|
||||
<Popover.Trigger
|
||||
render={
|
||||
<Button variant='ghosted-white'>
|
||||
<IoOptions /> View
|
||||
<IoChevronDown />
|
||||
</Button>
|
||||
<Toolbar.Button
|
||||
render={
|
||||
<Button variant='ghosted-white'>
|
||||
<IoOptions /> View
|
||||
<IoChevronDown />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PopoverContents align='start' className={style.inline}>
|
||||
|
||||
@@ -2,9 +2,7 @@ import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
type OptionValues = {
|
||||
showActionMenu: boolean;
|
||||
hideTableSeconds: boolean;
|
||||
followPlayback: boolean;
|
||||
hidePast: boolean;
|
||||
hideIndexColumn: boolean;
|
||||
showDelayedTimes: boolean;
|
||||
@@ -12,9 +10,7 @@ type OptionValues = {
|
||||
};
|
||||
|
||||
const defaultOptions: OptionValues = {
|
||||
showActionMenu: false,
|
||||
hideTableSeconds: false,
|
||||
followPlayback: false,
|
||||
hidePast: false,
|
||||
hideIndexColumn: false,
|
||||
showDelayedTimes: false,
|
||||
|
||||
Reference in New Issue
Block a user