mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-17 21:24:11 +00:00
refactor: table mode and context
This commit is contained in:
committed by
Carlos Valente
parent
8c22968bc9
commit
c5045ad82e
@@ -5,7 +5,6 @@
|
||||
|
||||
.rundownContainer {
|
||||
margin-top: 1rem;
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ import { useEntryCopy } from '../../common/stores/entryCopyStore';
|
||||
import { lastMetadataKey, RundownMetadataObject } from '../../common/utils/rundownMetadata';
|
||||
import { AppMode, sessionKeys } from '../../ontimeConfig';
|
||||
|
||||
import { useRundownEntryActions } from './context/RundownActionsContext';
|
||||
import { useEntryActionsContext } from '../../common/context/EntryActionsContext';
|
||||
import QuickAddButtons from './entry-editor/quick-add-buttons/QuickAddButtons';
|
||||
import QuickAddInline from './entry-editor/quick-add-cursor/QuickAddInline';
|
||||
import RundownGroup from './rundown-group/RundownGroup';
|
||||
@@ -72,7 +72,7 @@ export default function Rundown({ data, rundownMetadata }: RundownProps) {
|
||||
});
|
||||
const collapsedGroupSet = useMemo(() => new Set(collapsedGroups), [collapsedGroups]);
|
||||
|
||||
const { addEntry, clone, deleteEntry, move, reorderEntry } = useRundownEntryActions();
|
||||
const { addEntry, clone, deleteEntry, move, reorderEntry } = useEntryActionsContext();
|
||||
const setEntryCopyId = useEntryCopy((state) => state.setEntryCopyId);
|
||||
|
||||
// cursor
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.rundownExport {
|
||||
height: 100%;
|
||||
flex: 1 1 auto; /* flex-grow: 1, flex-shrink: 1, flex-basis: auto */
|
||||
flex: 1 1 0; /* flex-grow: 1, flex-shrink: 1, flex-basis: 0 */
|
||||
min-width: 0;
|
||||
|
||||
&.extracted {
|
||||
.list {
|
||||
@@ -20,6 +21,11 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.rundownRoot {
|
||||
height: calc(100% - 1.5rem);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
height: inherit;
|
||||
|
||||
@@ -5,18 +5,23 @@ import * as Editor from '../../common/components/editor-utils/EditorUtils';
|
||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||
import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu';
|
||||
import ProtectRoute from '../../common/components/protect-route/ProtectRoute';
|
||||
import { EntryActionsProvider } from '../../common/context/EntryActionsContext';
|
||||
import { useEntryActions } from '../../common/hooks/useEntryAction';
|
||||
import { useIsSmallDevice } from '../../common/hooks/useIsSmallDevice';
|
||||
import { handleLinks } from '../../common/utils/linkUtils';
|
||||
import { cx } from '../../common/utils/styleUtils';
|
||||
import { getIsNavigationLocked } from '../../externals';
|
||||
import { AppMode, sessionKeys } from '../../ontimeConfig';
|
||||
import EditorEditModal from '../../views/cuesheet/cuesheet-edit-modal/EditorEditModal';
|
||||
|
||||
import { RundownActionsProvider } from './context/RundownActionsContext';
|
||||
import RundownEntryEditor from './entry-editor/RundownEntryEditor';
|
||||
import FinderPlacement from './placements/FinderPlacement';
|
||||
import { RundownContextMenu } from './rundown-context-menu/RundownContextMenu';
|
||||
import RundownWrapper, { RundownViewMode } from './RundownWrapper';
|
||||
import RundownHeader from './rundown-header/RundownHeader';
|
||||
import RundownHeaderMobile from './rundown-header/RundownHeaderMobile';
|
||||
import RundownTable from './rundown-table/RundownTable';
|
||||
import RundownList from './RundownList';
|
||||
import { DEFAULT_RUNDOWN_VIEW_MODE, RUNDOWN_VIEW_MODE_STORAGE_KEY, RundownViewMode } from './rundownViewMode';
|
||||
|
||||
import style from './RundownExport.module.scss';
|
||||
|
||||
@@ -29,15 +34,15 @@ function RundownExport() {
|
||||
defaultValue: AppMode.Edit,
|
||||
});
|
||||
const [viewMode, setViewMode] = useSessionStorage<RundownViewMode>({
|
||||
key: 'rundown-view-mode',
|
||||
defaultValue: 'list',
|
||||
key: RUNDOWN_VIEW_MODE_STORAGE_KEY,
|
||||
defaultValue: DEFAULT_RUNDOWN_VIEW_MODE,
|
||||
});
|
||||
const isSmallDevice = useIsSmallDevice();
|
||||
const entryActions = useEntryActions();
|
||||
|
||||
if (isSmallDevice && isExtracted) {
|
||||
return (
|
||||
<RundownActionsProvider actions={entryActions}>
|
||||
<EntryActionsProvider actions={entryActions}>
|
||||
<ProtectRoute permission='editor'>
|
||||
<div
|
||||
className={cx([style.rundownExport, style.extracted])}
|
||||
@@ -48,20 +53,20 @@ function RundownExport() {
|
||||
<ViewNavigationMenu suppressSettings />
|
||||
<div className={style.rundown}>
|
||||
<ErrorBoundary>
|
||||
<RundownWrapper isSmallDevice viewMode={viewMode} setViewMode={setViewMode} />
|
||||
<RundownRoot isSmallDevice viewMode={viewMode} setViewMode={setViewMode} />
|
||||
<RundownContextMenu />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</ProtectRoute>
|
||||
</RundownActionsProvider>
|
||||
</EntryActionsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const hideSideBar = (isExtracted && editorMode === 'run') || viewMode === 'table';
|
||||
|
||||
return (
|
||||
<RundownActionsProvider actions={entryActions}>
|
||||
<EntryActionsProvider actions={entryActions}>
|
||||
<ProtectRoute permission='editor'>
|
||||
<div className={cx([style.rundownExport, isExtracted && style.extracted])} data-testid='panel-rundown'>
|
||||
<FinderPlacement />
|
||||
@@ -70,7 +75,7 @@ function RundownExport() {
|
||||
<Editor.Panel className={style.list}>
|
||||
<ErrorBoundary>
|
||||
{!isExtracted && <Editor.CornerExtract onClick={(event) => handleLinks('rundown', event)} />}
|
||||
<RundownWrapper viewMode={viewMode} setViewMode={setViewMode} />
|
||||
<RundownRoot viewMode={viewMode} setViewMode={setViewMode} />
|
||||
<RundownContextMenu />
|
||||
</ErrorBoundary>
|
||||
</Editor.Panel>
|
||||
@@ -84,6 +89,22 @@ function RundownExport() {
|
||||
</div>
|
||||
</div>
|
||||
</ProtectRoute>
|
||||
</RundownActionsProvider>
|
||||
</EntryActionsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
interface RundownRootProps {
|
||||
isSmallDevice?: boolean;
|
||||
viewMode: RundownViewMode;
|
||||
setViewMode: (mode: RundownViewMode) => void;
|
||||
}
|
||||
|
||||
function RundownRoot({ isSmallDevice, viewMode, setViewMode }: RundownRootProps) {
|
||||
return (
|
||||
<div className={style.rundownRoot}>
|
||||
{isSmallDevice ? <RundownHeaderMobile /> : <RundownHeader viewMode={viewMode} setViewMode={setViewMode} />}
|
||||
{viewMode === 'list' ? <RundownList /> : <RundownTable />}
|
||||
<EditorEditModal />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import Empty from '../../common/components/state/Empty';
|
||||
import { useRundownEditor } from '../../common/hooks/useSocket';
|
||||
import { useRundownWithMetadata } from '../../common/hooks-query/useRundown';
|
||||
|
||||
import Rundown from './Rundown';
|
||||
|
||||
export default memo(RundownList);
|
||||
function RundownList() {
|
||||
const { data, status, rundownMetadata } = useRundownWithMetadata();
|
||||
const featureData = useRundownEditor();
|
||||
|
||||
const isLoading = status !== 'success' || !data || !rundownMetadata;
|
||||
|
||||
if (isLoading) {
|
||||
return <Empty text='Connecting to server' />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Rundown
|
||||
order={data.order}
|
||||
entries={data.entries}
|
||||
id={data.id}
|
||||
rundownMetadata={rundownMetadata}
|
||||
featureData={featureData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import Empty from '../../common/components/state/Empty';
|
||||
import { useRundownWithMetadata } from '../../common/hooks-query/useRundown';
|
||||
import EditorEditModal from '../../views/cuesheet/cuesheet-edit-modal/EditorEditModal';
|
||||
|
||||
import RundownHeader from './rundown-header/RundownHeader';
|
||||
import RundownHeaderMobile from './rundown-header/RundownHeaderMobile';
|
||||
import RundownTable from './rundown-table/RundownTable';
|
||||
import Rundown from './Rundown';
|
||||
|
||||
import styles from './Rundown.module.scss';
|
||||
|
||||
export type RundownViewMode = 'list' | 'table';
|
||||
|
||||
interface RundownWrapperProps {
|
||||
isSmallDevice?: boolean;
|
||||
viewMode: RundownViewMode;
|
||||
setViewMode: (mode: RundownViewMode) => void;
|
||||
}
|
||||
|
||||
export default function RundownWrapper({ isSmallDevice, viewMode, setViewMode }: RundownWrapperProps) {
|
||||
const { data, status, rundownMetadata } = useRundownWithMetadata();
|
||||
|
||||
const isLoading = status !== 'success' || !data || !rundownMetadata;
|
||||
|
||||
return (
|
||||
<div className={styles.rundownWrapper}>
|
||||
{isSmallDevice ? <RundownHeaderMobile /> : <RundownHeader viewMode={viewMode} setViewMode={setViewMode} />}
|
||||
{isLoading && <Empty text='Connecting to server' />}
|
||||
{!isLoading && viewMode === 'list' && <Rundown data={data} rundownMetadata={rundownMetadata} />}
|
||||
{!isLoading && viewMode === 'table' && <RundownTable />}
|
||||
<EditorEditModal />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import { useCallback, useRef } from 'react';
|
||||
|
||||
import Input from '../../../common/components/input/input/Input';
|
||||
import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
import { useRundownEntryActions } from '../context/RundownActionsContext';
|
||||
|
||||
import style from './TitleEditor.module.scss';
|
||||
|
||||
@@ -15,7 +15,7 @@ interface TitleEditorProps {
|
||||
}
|
||||
|
||||
export default function TitleEditor({ title, entryId, placeholder, className }: TitleEditorProps) {
|
||||
const { updateEntry } = useRundownEntryActions();
|
||||
const { updateEntry } = useEntryActionsContext();
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
const submitCallback = useCallback(
|
||||
(text: string) => {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { createContext, PropsWithChildren, useContext } from 'react';
|
||||
|
||||
import { useEntryActions } from '../../../common/hooks/useEntryAction';
|
||||
|
||||
type EntryActionsContextValue = ReturnType<typeof useEntryActions>;
|
||||
const RundownActionsContext = createContext<EntryActionsContextValue | null>(null);
|
||||
|
||||
interface RundownActionsProviderProps extends PropsWithChildren {
|
||||
actions: EntryActionsContextValue;
|
||||
}
|
||||
|
||||
export function RundownActionsProvider({ children, actions }: RundownActionsProviderProps) {
|
||||
return <RundownActionsContext.Provider value={actions}>{children}</RundownActionsContext.Provider>;
|
||||
}
|
||||
|
||||
export function useRundownEntryActions(): EntryActionsContextValue {
|
||||
const context = useContext(RundownActionsContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useRundownEntryActions must be used within RundownActionsProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import { OntimeEvent } from 'ontime-types';
|
||||
|
||||
import * as Editor from '../../../common/components/editor-utils/EditorUtils';
|
||||
import AppLink from '../../../common/components/link/app-link/AppLink';
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
import useCustomFields from '../../../common/hooks-query/useCustomFields';
|
||||
import { useRundownEntryActions } from '../context/RundownActionsContext';
|
||||
|
||||
import EntryEditorCustomFields from './composite/EventEditorCustomFields';
|
||||
import EventEditorTimes from './composite/EventEditorTimes';
|
||||
@@ -22,7 +22,7 @@ interface EventEditorProps {
|
||||
|
||||
export default function EventEditor({ event }: EventEditorProps) {
|
||||
const { data: customFields } = useCustomFields();
|
||||
const { updateEntry } = useRundownEntryActions();
|
||||
const { updateEntry } = useEntryActionsContext();
|
||||
|
||||
const isEditor = window.location.pathname.includes('editor');
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ import { millisToString } from 'ontime-utils';
|
||||
import * as Editor from '../../../common/components/editor-utils/EditorUtils';
|
||||
import SwatchSelect from '../../../common/components/input/colour-input/SwatchSelect';
|
||||
import AppLink from '../../../common/components/link/app-link/AppLink';
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
import useCustomFields from '../../../common/hooks-query/useCustomFields';
|
||||
import { getOffsetState } from '../../../common/utils/offset';
|
||||
import { cx, enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||
import TextLikeInput from '../../../views/cuesheet/cuesheet-table/cuesheet-table-elements/TextLikeInput';
|
||||
import { useRundownEntryActions } from '../context/RundownActionsContext';
|
||||
|
||||
import EntryEditorCustomFields from './composite/EventEditorCustomFields';
|
||||
import EventTextArea from './composite/EventTextArea';
|
||||
@@ -28,7 +28,7 @@ interface GroupEditorProps {
|
||||
|
||||
export default function GroupEditor({ group }: GroupEditorProps) {
|
||||
const { data: customFields } = useCustomFields();
|
||||
const { updateEntry } = useRundownEntryActions();
|
||||
const { updateEntry } = useEntryActionsContext();
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(field: GroupEditorUpdateTextFields | GroupEditorUpdateMaybeNumberFields, value: string | MaybeNumber) => {
|
||||
|
||||
@@ -5,8 +5,8 @@ import * as Editor from '../../../common/components/editor-utils/EditorUtils';
|
||||
import SwatchSelect from '../../../common/components/input/colour-input/SwatchSelect';
|
||||
import Input from '../../../common/components/input/input/Input';
|
||||
import AppLink from '../../../common/components/link/app-link/AppLink';
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
import useCustomFields from '../../../common/hooks-query/useCustomFields';
|
||||
import { useRundownEntryActions } from '../context/RundownActionsContext';
|
||||
|
||||
import EntryEditorCustomFields from './composite/EventEditorCustomFields';
|
||||
import EventTextArea from './composite/EventTextArea';
|
||||
@@ -22,7 +22,7 @@ interface MilestoneEditorProps {
|
||||
}
|
||||
export default function MilestoneEditor({ milestone }: MilestoneEditorProps) {
|
||||
const { data: customFields } = useCustomFields();
|
||||
const { updateEntry } = useRundownEntryActions();
|
||||
const { updateEntry } = useEntryActionsContext();
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(field: MilestoneEditorUpdateTextFields, value: string) => {
|
||||
|
||||
@@ -8,8 +8,8 @@ import TimeInput from '../../../../common/components/input/time-input/TimeInput'
|
||||
import Select from '../../../../common/components/select/Select';
|
||||
import Switch from '../../../../common/components/switch/Switch';
|
||||
import Tooltip from '../../../../common/components/tooltip/Tooltip';
|
||||
import { useEntryActionsContext } from '../../../../common/context/EntryActionsContext';
|
||||
import { millisToDelayString } from '../../../../common/utils/dateConfig';
|
||||
import { useRundownEntryActions } from '../../context/RundownActionsContext';
|
||||
import TimeInputFlow from '../../time-input-flow/TimeInputFlow';
|
||||
|
||||
import style from '../EntryEditor.module.scss';
|
||||
@@ -46,7 +46,7 @@ function EventEditorTimes({
|
||||
timeWarning,
|
||||
timeDanger,
|
||||
}: EventEditorTimesProps) {
|
||||
const { updateEntry } = useRundownEntryActions();
|
||||
const { updateEntry } = useEntryActionsContext();
|
||||
|
||||
const handleSubmit = (field: HandledActions, value: string | boolean) => {
|
||||
if (field === 'countToEnd') {
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as Editor from '../../../../common/components/editor-utils/EditorUtils'
|
||||
import SwatchSelect from '../../../../common/components/input/colour-input/SwatchSelect';
|
||||
import Input from '../../../../common/components/input/input/Input';
|
||||
import Switch from '../../../../common/components/switch/Switch';
|
||||
import { useRundownEntryActions } from '../../context/RundownActionsContext';
|
||||
import { useEntryActionsContext } from '../../../../common/context/EntryActionsContext';
|
||||
|
||||
import EventTextArea from './EventTextArea';
|
||||
import EntryEditorTextInput from './EventTextInput';
|
||||
@@ -23,7 +23,7 @@ interface EventEditorTitlesProps {
|
||||
|
||||
export default memo(EventEditorTitles);
|
||||
function EventEditorTitles({ eventId, cue, flag, title, note, colour }: EventEditorTitlesProps) {
|
||||
const { updateEntry } = useRundownEntryActions();
|
||||
const { updateEntry } = useEntryActionsContext();
|
||||
|
||||
const cueSubmitHandler = (_field: string, newValue: string) => {
|
||||
updateEntry({ id: eventId, cue: sanitiseCue(newValue) });
|
||||
|
||||
@@ -8,8 +8,8 @@ import IconButton from '../../../../common/components/buttons/IconButton';
|
||||
import Select from '../../../../common/components/select/Select';
|
||||
import Tag from '../../../../common/components/tag/Tag';
|
||||
import Tooltip from '../../../../common/components/tooltip/Tooltip';
|
||||
import { useEntryActionsContext } from '../../../../common/context/EntryActionsContext';
|
||||
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
||||
import { useRundownEntryActions } from '../../context/RundownActionsContext';
|
||||
|
||||
import { eventTriggerOptions } from './eventTrigger.constants';
|
||||
|
||||
@@ -38,7 +38,7 @@ interface EventTriggerFormProps {
|
||||
|
||||
function EventTriggerForm({ eventId, triggers }: EventTriggerFormProps) {
|
||||
const { data: automationSettings } = useAutomationSettings();
|
||||
const { updateEntry } = useRundownEntryActions();
|
||||
const { updateEntry } = useEntryActionsContext();
|
||||
const [automationId, setAutomationId] = useState<string | undefined>(undefined);
|
||||
const [cycleValue, setCycleValue] = useState(TimerLifeCycle.onStart);
|
||||
|
||||
@@ -123,7 +123,7 @@ interface ExistingEventTriggersProps {
|
||||
}
|
||||
|
||||
function ExistingEventTriggers({ eventId, triggers }: ExistingEventTriggersProps) {
|
||||
const { updateEntry } = useRundownEntryActions();
|
||||
const { updateEntry } = useEntryActionsContext();
|
||||
const { data: automationSettings } = useAutomationSettings();
|
||||
|
||||
const handleDelete = useCallback(
|
||||
|
||||
+1
@@ -2,6 +2,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding-left: 0.5rem;
|
||||
|
||||
padding-block: 0.5rem;
|
||||
background: color-mix(in srgb, transparent 90%, var(--user-bg, transparent) 10%);
|
||||
|
||||
@@ -4,8 +4,8 @@ import { Toolbar } from '@base-ui/react/toolbar';
|
||||
import { MaybeString, SupportedEntry } from 'ontime-types';
|
||||
|
||||
import Button from '../../../../common/components/buttons/Button';
|
||||
import { useEntryActionsContext } from '../../../../common/context/EntryActionsContext';
|
||||
import { cx } from '../../../../common/utils/styleUtils';
|
||||
import { useRundownEntryActions } from '../../context/RundownActionsContext';
|
||||
|
||||
import style from './QuickAddButtons.module.scss';
|
||||
|
||||
@@ -17,7 +17,7 @@ interface QuickAddButtonsProps {
|
||||
|
||||
export default memo(QuickAddButtons);
|
||||
function QuickAddButtons({ previousEventId, parentGroup, backgroundColor }: QuickAddButtonsProps) {
|
||||
const { addEntry } = useRundownEntryActions();
|
||||
const { addEntry } = useEntryActionsContext();
|
||||
|
||||
const addEvent = () => {
|
||||
addEntry(
|
||||
|
||||
@@ -4,7 +4,7 @@ import { MaybeString, SupportedEntry } from 'ontime-types';
|
||||
|
||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||
import { DropdownMenu } from '../../../../common/components/dropdown-menu/DropdownMenu';
|
||||
import { useRundownEntryActions } from '../../context/RundownActionsContext';
|
||||
import { useEntryActionsContext } from '../../../../common/context/EntryActionsContext';
|
||||
|
||||
import style from './QuickAddInline.module.scss';
|
||||
|
||||
@@ -16,7 +16,7 @@ interface QuickAddInlineProps {
|
||||
|
||||
export default memo(QuickAddInline);
|
||||
function QuickAddInline({ referenceEntryId, parentGroup, placement }: QuickAddInlineProps) {
|
||||
const { addEntry } = useRundownEntryActions();
|
||||
const { addEntry } = useEntryActionsContext();
|
||||
|
||||
const handleAddEntry = (type: SupportedEntry) => {
|
||||
if (placement === 'before') {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
.radioGroup {
|
||||
color: $gray-900;
|
||||
font-size: calc(1rem - 3px);
|
||||
color: $label-gray;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
line-height: 1.2em;
|
||||
|
||||
&:has([data-checked]) {
|
||||
color: $ui-white;
|
||||
}
|
||||
}
|
||||
|
||||
.radio {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 100%;
|
||||
outline: 0;
|
||||
border: none;
|
||||
|
||||
&[data-unchecked] {
|
||||
background-color: $gray-1200;
|
||||
}
|
||||
|
||||
&[data-checked] {
|
||||
background-color: $gray-700;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $blue-500;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.indicator {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
|
||||
&[data-unchecked] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
border-radius: 100%;
|
||||
width: 0.5em;
|
||||
height: 0.5em;
|
||||
background-color: $ui-white;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Radio } from '@base-ui/react/radio';
|
||||
import { RadioGroup as BaseRadioGroup } from '@base-ui/react/radio-group';
|
||||
|
||||
import style from './BlockRadio.module.scss';
|
||||
|
||||
interface BlockRadioProps<T extends string | number | boolean> extends Omit<BaseRadioGroup.Props, 'onValueChange'> {
|
||||
items: {
|
||||
value: T;
|
||||
label: string;
|
||||
}[];
|
||||
onValueChange?: (value: T) => void;
|
||||
}
|
||||
|
||||
export default function BlockRadio<T extends string | number | boolean>({
|
||||
items,
|
||||
onValueChange,
|
||||
...elementProps
|
||||
}: BlockRadioProps<T>) {
|
||||
return (
|
||||
<BaseRadioGroup
|
||||
onValueChange={(value) => onValueChange?.(value as T)}
|
||||
className={style.radioGroup}
|
||||
{...elementProps}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<label className={style.item} key={item.value.toString()}>
|
||||
<Radio.Root value={item.value.toString()} className={style.radio}>
|
||||
<Radio.Indicator className={style.indicator} />
|
||||
</Radio.Root>
|
||||
{item.label}
|
||||
</label>
|
||||
))}
|
||||
</BaseRadioGroup>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
.delayInput {
|
||||
display: flex;
|
||||
gap: $element-spacing;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inputField {
|
||||
text-align: center;
|
||||
letter-spacing: 1px;
|
||||
max-width: 7em;
|
||||
color: $ontime-delay-text
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { KeyboardEvent, useEffect, useRef, useState } from 'react';
|
||||
import { millisToString, parseUserTime } from 'ontime-utils';
|
||||
|
||||
import Input from '../../../common/components/input/input/Input';
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
|
||||
import BlockRadio from './BlockRadio';
|
||||
|
||||
import style from './DelayInput.module.scss';
|
||||
|
||||
interface DelayInputProps {
|
||||
eventId: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export default function DelayInput({ eventId, duration }: DelayInputProps) {
|
||||
const { updateEntry } = useEntryActionsContext();
|
||||
|
||||
const [value, setValue] = useState<string>('');
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// avoid wrong submit on cancel
|
||||
const ignoreChangeRef = useRef(false);
|
||||
|
||||
// set internal value on duration change
|
||||
useEffect(() => {
|
||||
if (typeof duration === 'undefined') {
|
||||
return;
|
||||
}
|
||||
setValue(millisToString(duration));
|
||||
}, [duration]);
|
||||
|
||||
/**
|
||||
* @description Prepare delay value for update
|
||||
* @param {string} newValue string to be parsed
|
||||
*/
|
||||
const validateAndSubmit = (newValue: string) => {
|
||||
if (ignoreChangeRef.current) {
|
||||
ignoreChangeRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const isNegative = newValue.startsWith('-');
|
||||
let newMillis = parseUserTime(newValue);
|
||||
|
||||
if (isNegative) {
|
||||
newMillis = newMillis * -1;
|
||||
}
|
||||
|
||||
if (newMillis === duration) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitChange(newMillis);
|
||||
setValue(millisToString(newMillis));
|
||||
};
|
||||
|
||||
const submitChange = (value: number) => {
|
||||
updateEntry({
|
||||
id: eventId,
|
||||
duration: value,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Selects input text on focus
|
||||
*/
|
||||
const handleFocus = () => inputRef.current?.select();
|
||||
|
||||
/**
|
||||
* @description Handles common keys for submit and cancel
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
const onKeyDownHandler = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
inputRef.current?.blur();
|
||||
validateAndSubmit((event.target as HTMLInputElement).value);
|
||||
} else if (event.key === 'Tab') {
|
||||
validateAndSubmit((event.target as HTMLInputElement).value);
|
||||
} else if (event.key === 'Escape') {
|
||||
ignoreChangeRef.current = true;
|
||||
setValue(millisToString(duration));
|
||||
inputRef.current?.blur();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description handles direction change to delay
|
||||
* @param newDirection
|
||||
*/
|
||||
const handleSlipChange = (newDirection: 'add' | 'subtract') => {
|
||||
if (newDirection === 'add') {
|
||||
// add time
|
||||
if (duration < 0) {
|
||||
submitChange(duration * -1);
|
||||
}
|
||||
} else if (newDirection === 'subtract') {
|
||||
// subtract time
|
||||
if (duration > 0) {
|
||||
submitChange(duration * -1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const checkedOption = value.startsWith('-') ? 'subtract' : 'add';
|
||||
|
||||
return (
|
||||
<div className={style.delayInput}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
data-testid='delay-input'
|
||||
className={style.inputField}
|
||||
placeholder='-'
|
||||
onFocus={handleFocus}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
onBlur={(event) => validateAndSubmit(event.target.value)}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
value={value}
|
||||
maxLength={9}
|
||||
/>
|
||||
<BlockRadio
|
||||
onValueChange={handleSlipChange}
|
||||
value={checkedOption}
|
||||
items={[
|
||||
{ value: 'add', label: 'Add time' },
|
||||
{ value: 'subtract', label: 'Subtract time' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import { CSS } from '@dnd-kit/utilities';
|
||||
import { OntimeDelay } from 'ontime-types';
|
||||
|
||||
import Button from '../../../common/components/buttons/Button';
|
||||
import DelayInput from '../../../common/components/input/delay-input/DelayInput';
|
||||
import DelayInput from './DelayInput';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
import { useRundownEntryActions } from '../context/RundownActionsContext';
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
|
||||
import style from './RundownDelay.module.scss';
|
||||
|
||||
@@ -19,7 +19,7 @@ interface RundownDelayProps {
|
||||
export default function RundownDelay({ data, hasCursor }: RundownDelayProps) {
|
||||
'use memo';
|
||||
|
||||
const { applyDelay, deleteEntry } = useRundownEntryActions();
|
||||
const { applyDelay, deleteEntry } = useEntryActionsContext();
|
||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||
|
||||
const {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MouseEvent, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { MouseEvent, useEffect, useRef } from 'react';
|
||||
import {
|
||||
IoAdd,
|
||||
IoDuplicateOutline,
|
||||
@@ -15,9 +15,9 @@ import { CSS } from '@dnd-kit/utilities';
|
||||
import { EndAction, EntryId, Playback, TimerType, TimeStrategy } from 'ontime-types';
|
||||
import { isPlaybackActive } from 'ontime-utils';
|
||||
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
import { useContextMenu } from '../../../common/hooks/useContextMenu';
|
||||
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||
import { useRundownEntryActions } from '../context/RundownActionsContext';
|
||||
import { useEventIdSwapping } from '../useEventIdSwapping';
|
||||
import { getSelectionMode, useEventSelection } from '../useEventSelection';
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function RundownEvent({
|
||||
const setSelectedEventId = useEventIdSwapping((state) => state.setSelectedEventId);
|
||||
const clearSelectedEventId = useEventIdSwapping((state) => state.clearSelectedEventId);
|
||||
|
||||
const { updateEntry, batchUpdateEvents, clone, deleteEntry, groupEntries, swapEvents } = useRundownEntryActions();
|
||||
const { updateEntry, batchUpdateEvents, clone, deleteEntry, groupEntries, swapEvents } = useEntryActionsContext();
|
||||
|
||||
const isSelected = useEventSelection((state) => state.selectedEvents.has(eventId));
|
||||
const unselect = useEventSelection((state) => state.unselect);
|
||||
@@ -107,7 +107,6 @@ export default function RundownEvent({
|
||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||
|
||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const [onContextMenu] = useContextMenu<HTMLDivElement>(() =>
|
||||
selectedEvents.size > 1
|
||||
@@ -236,31 +235,6 @@ export default function RundownEvent({
|
||||
}
|
||||
}, [hasCursor]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
root: null,
|
||||
threshold: 1,
|
||||
},
|
||||
);
|
||||
|
||||
const handleRefCurrent = handleRef.current;
|
||||
if (handleRefCurrent) {
|
||||
observer.observe(handleRefCurrent);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (handleRefCurrent) {
|
||||
observer.unobserve(handleRefCurrent);
|
||||
}
|
||||
};
|
||||
}, [handleRef]);
|
||||
|
||||
const blockClasses = cx([
|
||||
style.rundownEvent,
|
||||
skip ? style.skip : null,
|
||||
@@ -308,33 +282,31 @@ export default function RundownEvent({
|
||||
<span className={style.cue}>{cue}</span>
|
||||
</div>
|
||||
|
||||
{isVisible && (
|
||||
<RundownEventInner
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
duration={duration}
|
||||
linkStart={linkStart}
|
||||
countToEnd={countToEnd}
|
||||
timeStrategy={timeStrategy}
|
||||
eventId={eventId}
|
||||
eventIndex={eventIndex}
|
||||
endAction={endAction}
|
||||
timerType={timerType}
|
||||
title={title}
|
||||
note={note}
|
||||
delay={delay}
|
||||
isNext={isNext}
|
||||
skip={skip}
|
||||
loaded={loaded}
|
||||
playback={playback}
|
||||
isRolling={isRolling}
|
||||
dayOffset={dayOffset}
|
||||
isPast={isPast}
|
||||
totalGap={totalGap}
|
||||
isLinkedToLoaded={isLinkedToLoaded}
|
||||
hasTriggers={hasTriggers}
|
||||
/>
|
||||
)}
|
||||
<RundownEventInner
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
duration={duration}
|
||||
linkStart={linkStart}
|
||||
countToEnd={countToEnd}
|
||||
timeStrategy={timeStrategy}
|
||||
eventId={eventId}
|
||||
eventIndex={eventIndex}
|
||||
endAction={endAction}
|
||||
timerType={timerType}
|
||||
title={title}
|
||||
note={note}
|
||||
delay={delay}
|
||||
isNext={isNext}
|
||||
skip={skip}
|
||||
loaded={loaded}
|
||||
playback={playback}
|
||||
isRolling={isRolling}
|
||||
dayOffset={dayOffset}
|
||||
isPast={isPast}
|
||||
totalGap={totalGap}
|
||||
isLinkedToLoaded={isLinkedToLoaded}
|
||||
hasTriggers={hasTriggers}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { memo } from 'react';
|
||||
import {
|
||||
IoArrowDown,
|
||||
IoArrowUp,
|
||||
@@ -76,17 +76,11 @@ function RundownEventInner({
|
||||
isLinkedToLoaded,
|
||||
hasTriggers,
|
||||
}: RundownEventInnerProps) {
|
||||
const [renderInner, setRenderInner] = useState(false);
|
||||
|
||||
const [editorMode] = useSessionStorage({
|
||||
key: sessionKeys.editorMode,
|
||||
defaultValue: AppMode.Edit,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setRenderInner(true);
|
||||
}, []);
|
||||
|
||||
const eventIsPlaying = playback === Playback.Play;
|
||||
const eventIsPaused = playback === Playback.Pause;
|
||||
|
||||
@@ -97,7 +91,7 @@ function RundownEventInner({
|
||||
playBtnStyles._hover = {};
|
||||
}
|
||||
|
||||
return !renderInner ? null : (
|
||||
return (
|
||||
<>
|
||||
<div className={cx([style.eventTimers, editorMode === AppMode.Edit && style.editMode])}>
|
||||
<TimeInputFlow
|
||||
@@ -161,8 +155,7 @@ function RundownEventInner({
|
||||
);
|
||||
}
|
||||
|
||||
function EndActionIcon(props: { action: EndAction; className: string }) {
|
||||
const { action, className } = props;
|
||||
function EndActionIcon({ action, className }: { action: EndAction; className: string }) {
|
||||
const maybeActiveClasses = cx([action !== EndAction.None && style.active, className]);
|
||||
|
||||
if (action === EndAction.LoadNext) {
|
||||
@@ -174,8 +167,7 @@ function EndActionIcon(props: { action: EndAction; className: string }) {
|
||||
return <IoPlay className={className} />;
|
||||
}
|
||||
|
||||
function TimerIcon(props: { type: TimerType; className: string }) {
|
||||
const { type, className } = props;
|
||||
function TimerIcon({ type, className }: { type: TimerType; className: string }) {
|
||||
if (type === TimerType.CountUp) {
|
||||
return <IoArrowUp className={className} />;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { IoPause, IoPlay, IoReload, IoRemoveCircle, IoRemoveCircleOutline } from
|
||||
|
||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||
import Tooltip from '../../../../common/components/tooltip/Tooltip';
|
||||
import { useEntryActionsContext } from '../../../../common/context/EntryActionsContext';
|
||||
import { setEventPlayback } from '../../../../common/hooks/useSocket';
|
||||
import { useRundownEntryActions } from '../../context/RundownActionsContext';
|
||||
|
||||
import style from '../RundownEvent.module.scss';
|
||||
|
||||
@@ -26,7 +26,7 @@ function RundownEventPlayback({
|
||||
loaded,
|
||||
disablePlayback,
|
||||
}: RundownEventPlaybackProps) {
|
||||
const { updateEntry } = useRundownEntryActions();
|
||||
const { updateEntry } = useEntryActionsContext();
|
||||
|
||||
const toggleSkip = (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -18,7 +18,7 @@ import { getOffsetState } from '../../../common/utils/offset';
|
||||
import { cx, getAccessibleColour, timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||
import { formatDuration } from '../../../common/utils/time';
|
||||
import TitleEditor from '../common/TitleEditor';
|
||||
import { useRundownEntryActions } from '../context/RundownActionsContext';
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
import { canDrop } from '../rundown.utils';
|
||||
import { useEventSelection } from '../useEventSelection';
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }:
|
||||
'use memo';
|
||||
|
||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||
const { clone, ungroup, deleteEntry } = useRundownEntryActions();
|
||||
const { clone, ungroup, deleteEntry } = useEntryActionsContext();
|
||||
|
||||
const setSingleEntrySelection = useEventSelection((state) => state.setSingleEntrySelection);
|
||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||
|
||||
@@ -64,3 +64,45 @@
|
||||
.separator {
|
||||
margin-inline: 1rem;
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
color: $gray-400; // Guessing color based on cuesheet
|
||||
}
|
||||
|
||||
.popoverContent {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
color: $gray-400; // Guessing color
|
||||
}
|
||||
|
||||
.popoverContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@@ -3,18 +3,16 @@ import { Toggle } from '@base-ui/react/toggle';
|
||||
import { ToggleGroup } from '@base-ui/react/toggle-group';
|
||||
import { Toolbar } from '@base-ui/react/toolbar';
|
||||
import { useSessionStorage } from '@mantine/hooks';
|
||||
import { OffsetMode } from 'ontime-types';
|
||||
|
||||
import * as Editor from '../../../common/components/editor-utils/EditorUtils';
|
||||
import { setOffsetMode, useOffsetMode } from '../../../common/hooks/useSocket';
|
||||
import { AppMode, sessionKeys } from '../../../ontimeConfig';
|
||||
import { RundownViewMode } from '../rundownViewMode';
|
||||
|
||||
import RundownMenu from './RundownMenu';
|
||||
import RundownSettings from './RundownSettings';
|
||||
|
||||
import style from './RundownHeader.module.scss';
|
||||
|
||||
type RundownViewMode = 'list' | 'table';
|
||||
|
||||
interface RundownHeaderProps {
|
||||
viewMode: RundownViewMode;
|
||||
setViewMode: (mode: RundownViewMode) => void;
|
||||
@@ -24,8 +22,6 @@ export default memo(RundownHeader);
|
||||
function RundownHeader({ viewMode, setViewMode }: RundownHeaderProps) {
|
||||
const [editorMode, setEditorMode] = useSessionStorage({ key: sessionKeys.editorMode, defaultValue: AppMode.Edit });
|
||||
|
||||
const offsetMode = useOffsetMode();
|
||||
|
||||
const toggleAppMode = (mode: AppMode[]) => {
|
||||
// we need to stop user from deselecting a mode
|
||||
const newValue = mode.at(0);
|
||||
@@ -33,20 +29,6 @@ function RundownHeader({ viewMode, setViewMode }: RundownHeaderProps) {
|
||||
setEditorMode(newValue);
|
||||
};
|
||||
|
||||
const toggleOffsetMode = (mode: OffsetMode[]) => {
|
||||
// we need to stop user from deselecting a mode
|
||||
const newValue = mode.at(0);
|
||||
if (!newValue) return;
|
||||
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 (
|
||||
<Toolbar.Root className={style.header}>
|
||||
<ToggleGroup value={[editorMode]} onValueChange={toggleAppMode} className={style.group}>
|
||||
@@ -60,25 +42,7 @@ function RundownHeader({ viewMode, setViewMode }: RundownHeaderProps) {
|
||||
|
||||
<Editor.Separator className={style.separator} />
|
||||
|
||||
<ToggleGroup value={[offsetMode]} onValueChange={toggleOffsetMode} className={style.group}>
|
||||
<Toolbar.Button render={<Toggle />} value={OffsetMode.Absolute} className={style.radioButton}>
|
||||
Absolute
|
||||
</Toolbar.Button>
|
||||
<Toolbar.Button render={<Toggle />} value={OffsetMode.Relative} className={style.radioButton}>
|
||||
Relative
|
||||
</Toolbar.Button>
|
||||
</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>
|
||||
<RundownSettings viewMode={viewMode} setViewMode={setViewMode} />
|
||||
|
||||
<RundownMenu />
|
||||
</Toolbar.Root>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { memo, useCallback } from 'react';
|
||||
import { IoTrash } from 'react-icons/io5';
|
||||
import { IoEllipsisHorizontal, IoList, IoTrash } from 'react-icons/io5';
|
||||
import { Toolbar } from '@base-ui/react/toolbar';
|
||||
import { useDisclosure, useSessionStorage } from '@mantine/hooks';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
|
||||
import Button from '../../../common/components/buttons/Button';
|
||||
import IconButton from '../../../common/components/buttons/IconButton';
|
||||
import Dialog from '../../../common/components/dialog/Dialog';
|
||||
import { AppMode, sessionKeys } from '../../../ontimeConfig';
|
||||
import { useRundownEntryActions } from '../context/RundownActionsContext';
|
||||
import { DropdownMenu } from '../../../common/components/dropdown-menu/DropdownMenu';
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
import useAppSettingsNavigation from '../../app-settings/useAppSettingsNavigation';
|
||||
import { useEventSelection } from '../useEventSelection';
|
||||
|
||||
import style from './RundownHeader.module.scss';
|
||||
@@ -16,11 +18,8 @@ function RundownMenu() {
|
||||
const [isOpen, handlers] = useDisclosure();
|
||||
|
||||
const clearSelectedEvents = useEventSelection((state) => state.clearSelectedEvents);
|
||||
const [editorMode] = useSessionStorage({
|
||||
key: sessionKeys.editorMode,
|
||||
defaultValue: AppMode.Edit,
|
||||
});
|
||||
const { deleteAllEntries } = useRundownEntryActions();
|
||||
const { deleteAllEntries } = useEntryActionsContext();
|
||||
const { setLocation } = useAppSettingsNavigation();
|
||||
|
||||
const deleteAll = useCallback(() => {
|
||||
deleteAllEntries();
|
||||
@@ -30,15 +29,29 @@ function RundownMenu() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toolbar.Button
|
||||
render={<Button variant='subtle-destructive' />}
|
||||
onClick={handlers.open}
|
||||
disabled={editorMode === AppMode.Run}
|
||||
className={style.apart}
|
||||
>
|
||||
<IoTrash />
|
||||
Clear all
|
||||
</Toolbar.Button>
|
||||
<div className={style.apart}>
|
||||
<DropdownMenu
|
||||
render={<Toolbar.Button render={<IconButton variant='subtle-white' aria-label='Rundown menu' />} />}
|
||||
items={[
|
||||
{
|
||||
type: 'item',
|
||||
label: 'Manage Rundowns...',
|
||||
icon: IoList,
|
||||
onClick: () => setLocation('manage'),
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
type: 'destructive',
|
||||
label: 'Clear all',
|
||||
icon: IoTrash,
|
||||
onClick: handlers.open,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IoEllipsisHorizontal />
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
isOpen={isOpen}
|
||||
onClose={handlers.close}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { memo } from 'react';
|
||||
import { Toggle } from '@base-ui/react/toggle';
|
||||
import { ToggleGroup } from '@base-ui/react/toggle-group';
|
||||
import { Toolbar } from '@base-ui/react/toolbar';
|
||||
|
||||
import { RundownViewMode } from '../rundownViewMode';
|
||||
|
||||
import style from './RundownHeader.module.scss';
|
||||
|
||||
interface RundownSettingsProps {
|
||||
viewMode: RundownViewMode;
|
||||
setViewMode: (mode: RundownViewMode) => void;
|
||||
}
|
||||
|
||||
export default memo(RundownSettings);
|
||||
function RundownSettings({ viewMode, setViewMode }: RundownSettingsProps) {
|
||||
const toggleViewMode = (mode: RundownViewMode[]) => {
|
||||
const newValue = mode.at(0);
|
||||
if (!newValue) return;
|
||||
setViewMode(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import Input from '../../../common/components/input/input/Input';
|
||||
import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
|
||||
import { useContextMenu } from '../../../common/hooks/useContextMenu';
|
||||
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||
import { useRundownEntryActions } from '../context/RundownActionsContext';
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
import { useEventSelection } from '../useEventSelection';
|
||||
|
||||
import style from './RundownMilestone.module.scss';
|
||||
@@ -25,7 +25,7 @@ export default function RundownMilestone({ colour, cue, entryId, hasCursor, titl
|
||||
'use memo';
|
||||
|
||||
const handleRef = useRef<null | HTMLSpanElement>(null);
|
||||
const { updateEntry, deleteEntry } = useRundownEntryActions();
|
||||
const { updateEntry, deleteEntry } = useEntryActionsContext();
|
||||
|
||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||
const setSingleEntrySelection = useEventSelection((state) => state.setSingleEntrySelection);
|
||||
|
||||
@@ -6,7 +6,6 @@ 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';
|
||||
@@ -35,8 +34,6 @@ export default function EditorTableSettings({
|
||||
handleResetReordering={handleResetReordering}
|
||||
handleClearToggles={handleClearToggles}
|
||||
/>
|
||||
{/* No mode toggle - already in RundownHeader */}
|
||||
{/* No share modal - cuesheet only */}
|
||||
</Toolbar.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import { EntryId, isOntimeEvent, isOntimeGroup, RundownEntries, SupportedEntry }
|
||||
* ------------------------------------
|
||||
* Due to limitations in dnd-kit we need to flatten the list of entries
|
||||
* This list should also be aware of any elements that are sortable (ie: group ends)
|
||||
*
|
||||
* Note: This creates the FULL structure including all entries and pseudo end-group entries.
|
||||
* For rendering, use filterVisibleEntries() to exclude collapsed items.
|
||||
*/
|
||||
export function makeSortableList(order: EntryId[], entries: RundownEntries): EntryId[] {
|
||||
const flatIds: EntryId[] = [];
|
||||
@@ -31,6 +34,42 @@ export function makeSortableList(order: EntryId[], entries: RundownEntries): Ent
|
||||
return flatIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters sortable list to only include visible entries based on collapsed state
|
||||
* ------------------------------------
|
||||
* Excludes:
|
||||
* - Children of collapsed groups
|
||||
* - End-group markers of collapsed groups
|
||||
*
|
||||
* This is used by Virtuoso for rendering, while DND-kit uses the full sortableData.
|
||||
*/
|
||||
export function filterVisibleEntries(
|
||||
sortableData: EntryId[],
|
||||
entries: RundownEntries,
|
||||
getIsCollapsed: (groupId: EntryId) => boolean,
|
||||
): EntryId[] {
|
||||
return sortableData.filter((entryId) => {
|
||||
// group end pseudo entries are only shown if the group is expanded
|
||||
if (entryId.startsWith('end-')) {
|
||||
const parentId = entryId.split('end-')[1];
|
||||
return !getIsCollapsed(parentId);
|
||||
}
|
||||
|
||||
// retrieve the entry as usual
|
||||
const entry = entries[entryId];
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if entry has a parent and parent is collapsed, filter it out
|
||||
if (entry.type !== SupportedEntry.Group && 'parent' in entry && entry.parent) {
|
||||
return !getIsCollapsed(entry.parent);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a drop operation is valid
|
||||
* Currently only used for validating dropping groups
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export const RUNDOWN_VIEW_MODES = ['list', 'table'] as const;
|
||||
export type RundownViewMode = (typeof RUNDOWN_VIEW_MODES)[number];
|
||||
|
||||
export const DEFAULT_RUNDOWN_VIEW_MODE: RundownViewMode = 'list';
|
||||
export const RUNDOWN_VIEW_MODE_STORAGE_KEY = 'rundown-view-mode';
|
||||
@@ -7,7 +7,7 @@ import IconButton from '../../../common/components/buttons/IconButton';
|
||||
import * as Editor from '../../../common/components/editor-utils/EditorUtils';
|
||||
import TimeInput from '../../../common/components/input/time-input/TimeInput';
|
||||
import Tooltip from '../../../common/components/tooltip/Tooltip';
|
||||
import { useRundownEntryActions } from '../context/RundownActionsContext';
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
|
||||
import TimeInputGroup from './TimeInputGroup';
|
||||
|
||||
@@ -37,7 +37,7 @@ function TimeInputFlow({
|
||||
delay,
|
||||
showLabels,
|
||||
}: TimeInputFlowProps) {
|
||||
const { updateEntry, updateTimer } = useRundownEntryActions();
|
||||
const { updateEntry, updateTimer } = useEntryActionsContext();
|
||||
|
||||
// In sync with EventEditorTimes
|
||||
const handleSubmit = (field: TimeField, value: string) => {
|
||||
|
||||
@@ -12,9 +12,12 @@ interface EventSelectionStore {
|
||||
selectedEvents: Set<EntryId>;
|
||||
anchoredIndex: MaybeNumber;
|
||||
cursor: MaybeString;
|
||||
scrollTargetId: MaybeString;
|
||||
entryMode: 'event' | 'single' | null;
|
||||
setSingleEntrySelection: (selectionArgs: { id: EntryId }) => void;
|
||||
setSelectedEvents: (selectionArgs: { id: EntryId; index: number; selectMode: SelectionMode }) => void;
|
||||
setScrollTargetId: (id: EntryId | null) => void;
|
||||
clearScrollTargetId: () => void;
|
||||
clearSelectedEvents: () => void;
|
||||
clearMultiSelect: () => void;
|
||||
unselect: (id: EntryId) => void;
|
||||
@@ -24,6 +27,7 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
|
||||
selectedEvents: new Set(),
|
||||
anchoredIndex: null,
|
||||
cursor: null,
|
||||
scrollTargetId: null,
|
||||
entryMode: null,
|
||||
setSingleEntrySelection: ({ id }) => {
|
||||
set({ selectedEvents: new Set([id]), anchoredIndex: null, cursor: id, entryMode: 'single' });
|
||||
@@ -99,7 +103,10 @@ export const useEventSelection = create<EventSelectionStore>()((set, get) => ({
|
||||
});
|
||||
}
|
||||
},
|
||||
clearSelectedEvents: () => set({ selectedEvents: new Set(), anchoredIndex: null, cursor: null, entryMode: null }),
|
||||
setScrollTargetId: (id) => set({ scrollTargetId: id }),
|
||||
clearScrollTargetId: () => set({ scrollTargetId: null }),
|
||||
clearSelectedEvents: () =>
|
||||
set({ selectedEvents: new Set(), anchoredIndex: null, cursor: null, entryMode: null, scrollTargetId: null }),
|
||||
clearMultiSelect: () => {
|
||||
const { selectedEvents } = get();
|
||||
const [firstSelected] = selectedEvents;
|
||||
|
||||
Reference in New Issue
Block a user