mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-13 19:33:46 +00:00
v2 event editor (#265)
* style: add cursor indicator in all blocks * style: prevent reflows on width * style: harmonise white range * ux: focus elements on cursor nav * ux: prevent focus on indicators * refactor: convert to typescript * fix: missing dependency * chore: add linter rules for dependency arrays * style: style editable component * style: tweak input styles * style: tweak switch styles * style: style tweaks
This commit is contained in:
+2
-1
@@ -11,9 +11,10 @@
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"eslint-config-prettier",
|
||||
"plugin:@tanstack/eslint-plugin-query/recommended"
|
||||
"plugin:@tanstack/eslint-plugin-query/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"react",
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-jest": "^27.0.4",
|
||||
"eslint-plugin-react": "^7.31.8",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||
"eslint-plugin-testing-library": "^5.6.4",
|
||||
"jsdom": "^20.0.0",
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import { Suspense, useCallback, useEffect } from 'react';
|
||||
import { Suspense, useEffect } from 'react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { ChakraProvider } from '@chakra-ui/react';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
@@ -19,7 +19,7 @@ import('typeface-open-sans');
|
||||
function App() {
|
||||
const { isElectron, sendToElectron } = useElectronEvent();
|
||||
|
||||
const handleKeyPress = useCallback((event:KeyboardEvent) => {
|
||||
const handleKeyPress = (event:KeyboardEvent) => {
|
||||
// handle held key
|
||||
if (event.repeat) return;
|
||||
// check if the alt key is pressed
|
||||
@@ -29,7 +29,7 @@ function App() {
|
||||
sendToElectron('set-window', 'show-dev');
|
||||
}
|
||||
}
|
||||
},[]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isElectron) {
|
||||
@@ -40,7 +40,7 @@ function App() {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
}
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ChakraProvider resetCSS theme={theme}>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
@use '../../../../theme/v2Styles' as *;
|
||||
|
||||
.delayInput {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: $element-spacing;
|
||||
align-items: center;
|
||||
color: $ontime-delay-text;
|
||||
font-size: $text-body-size;
|
||||
}
|
||||
|
||||
.inputField {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Input } from '@chakra-ui/react';
|
||||
import { clamp } from 'common/utils/math';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import style from './DelayInput.module.scss';
|
||||
|
||||
@@ -81,8 +80,3 @@ export default function DelayInput(props: DelayInputProps) {
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
DelayInput.propTypes = {
|
||||
submitHandler: PropTypes.func,
|
||||
value: PropTypes.number,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { Input, Textarea } from '@chakra-ui/react';
|
||||
|
||||
import { EventEditorSubmitActions } from '../../../../features/event-editor/EventEditor';
|
||||
import { Size } from '../../../models/UtilTypes';
|
||||
|
||||
import useReactiveTextInput from './useReactiveTextInput';
|
||||
@@ -9,9 +10,9 @@ interface TextInputProps {
|
||||
isTextArea?: boolean;
|
||||
isFullHeight?: boolean;
|
||||
size?: Size;
|
||||
field: string;
|
||||
field: EventEditorSubmitActions;
|
||||
initialText?: string;
|
||||
submitHandler: (field: string, newValue: string) => void;
|
||||
submitHandler: (field: EventEditorSubmitActions, newValue: string) => void;
|
||||
}
|
||||
|
||||
export default function TextInput(props: TextInputProps) {
|
||||
@@ -20,7 +21,7 @@ export default function TextInput(props: TextInputProps) {
|
||||
|
||||
const submitCallback = useCallback((newValue: string) =>
|
||||
submitHandler(field, newValue)
|
||||
, [field]);
|
||||
,[field, submitHandler]);
|
||||
|
||||
const textInputProps = useReactiveTextInput(initialText, submitCallback, { submitOnEnter: true });
|
||||
const textAreaProps = useReactiveTextInput(initialText, submitCallback);
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function useReactiveTextInput(
|
||||
break;
|
||||
}
|
||||
},
|
||||
[initialText, handleSubmit, text],
|
||||
[initialText, options?.submitOnEnter, handleSubmit, text],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -61,7 +61,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => {
|
||||
return () => {
|
||||
socket.off('logger');
|
||||
};
|
||||
}, [socket]);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Utility function sends message over socket
|
||||
@@ -86,7 +86,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => {
|
||||
setLogData((currentLog) => currentLog.slice(1));
|
||||
}
|
||||
},
|
||||
[logData.length, setLogData, socket],
|
||||
[logData.length, setLogData],
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -60,7 +60,7 @@ export const useEventAction = () => {
|
||||
emitError(`Error fetching data: ${error.message}`);
|
||||
}
|
||||
},
|
||||
[_addEventMutation, emitError],
|
||||
[_addEventMutation, emitError, queryClient],
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,5 +14,5 @@ export const useKeyDown = (callback: () => void, targetKey: string) => {
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, [onKeyDown]);
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function useSubscription<T>(topic: string, initialState: T, reque
|
||||
return () => {
|
||||
socket.off(topic);
|
||||
};
|
||||
}, [requestString, socket, topic]);
|
||||
}, [requestString, topic]);
|
||||
|
||||
return [state, setState] as const;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export type TimeEntryField = 'timeStart' |'timeEnd' | 'durationOverride';
|
||||
|
||||
/**
|
||||
* @description Milliseconds in a day
|
||||
*/
|
||||
@@ -12,7 +14,7 @@ export const calculateDuration = (start: number, end: number): number =>
|
||||
/**
|
||||
* @description Checks which field the value relates to
|
||||
*/
|
||||
export const handleTimeEntry = (field: string, val: number, timeStart: number, timeEnd: number): {start: number, end: number, durationOverride: boolean} => {
|
||||
export const handleTimeEntry = (field: TimeEntryField, val: number, timeStart: number, timeEnd: number): {start: number, end: number, durationOverride: boolean} => {
|
||||
let start = timeStart;
|
||||
let end = timeEnd;
|
||||
let durationOverride = false;
|
||||
@@ -30,7 +32,7 @@ export const handleTimeEntry = (field: string, val: number, timeStart: number, t
|
||||
/**
|
||||
* @description Validates time entry
|
||||
*/
|
||||
export const validateEntry = (field: string, value: number, timeStart: number, timeEnd: number): { value: boolean, catch: string } => {
|
||||
export const validateEntry = (field: TimeEntryField, value: number, timeStart: number, timeEnd: number): { value: boolean, catch: string } => {
|
||||
const validate = { value: true, catch: '' };
|
||||
|
||||
// 1. if one of times is not entered, anything goes
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
@use '../../theme/ontimeColours' as *;
|
||||
@use '../../theme/v2Styles' as *;
|
||||
|
||||
$menu-width: 48px;
|
||||
$rundown-width: 46em;
|
||||
$playback-width: 450px;
|
||||
|
||||
@mixin absolute-top-right($distance) {
|
||||
position: absolute;
|
||||
top: $distance;
|
||||
right: $distance;
|
||||
cursor: pointer;
|
||||
color: #f2f2f2;
|
||||
color: $ui-white;
|
||||
}
|
||||
|
||||
.corner {
|
||||
@@ -26,7 +30,7 @@
|
||||
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-columns: 48px 46em 450px auto;
|
||||
grid-template-columns: $menu-width $rundown-width $playback-width auto;
|
||||
grid-template-areas:
|
||||
'sett even play info'
|
||||
'sett even mess info';
|
||||
@@ -50,7 +54,7 @@
|
||||
.mainContainer {
|
||||
height: 100%;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-columns: 48px 48em auto;
|
||||
grid-template-columns: $menu-width $rundown-width auto;
|
||||
|
||||
.info {
|
||||
visibility: hidden;
|
||||
@@ -63,7 +67,7 @@
|
||||
.mainContainer {
|
||||
height: 100%;
|
||||
grid-template-rows: 100%;
|
||||
grid-template-columns: 48px 48em;
|
||||
grid-template-columns: $menu-width $rundown-width;
|
||||
grid-template-areas:
|
||||
'sett even';
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.left {
|
||||
|
||||
+31
-18
@@ -10,20 +10,23 @@ import { useEventAction } from 'common/hooks/useEventAction';
|
||||
import { millisToMinutes } from 'common/utils/dateConfig';
|
||||
import getDelayTo from 'common/utils/getDelayTo';
|
||||
import { stringFromMillis } from 'common/utils/time';
|
||||
import { calculateDuration, validateEntry } from 'common/utils/timesManager';
|
||||
import { calculateDuration, TimeEntryField, validateEntry } from 'common/utils/timesManager';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
import CopyTag from '../../common/components/copy-tag/CopyTag';
|
||||
import useRundown from '../../common/hooks-query/useRundown';
|
||||
import { OntimeEvent } from '../../common/models/EventTypes';
|
||||
|
||||
import style from './EventEditor.module.scss';
|
||||
|
||||
export type EventEditorSubmitActions = keyof OntimeEvent | 'durationOverride';
|
||||
|
||||
export default function EventEditor() {
|
||||
const [openId] = useAtom(editorEventId);
|
||||
const { data } = useRundown();
|
||||
const { emitWarning, emitError } = useContext(LoggingContext);
|
||||
const { updateEvent } = useEventAction();
|
||||
const [event, setEvent] = useState(null);
|
||||
const [event, setEvent] = useState<OntimeEvent | null>(null);
|
||||
const [delay, setDelay] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -33,28 +36,34 @@ export default function EventEditor() {
|
||||
|
||||
const eventIndex = data.findIndex((event) => event.id === openId);
|
||||
if (eventIndex > -1) {
|
||||
setDelay(getDelayTo(data, eventIndex));
|
||||
setEvent(data[eventIndex]);
|
||||
const event = data[eventIndex];
|
||||
if (event.type === 'event') {
|
||||
setDelay(getDelayTo(data, eventIndex));
|
||||
setEvent(data[eventIndex] as OntimeEvent);
|
||||
}
|
||||
}
|
||||
}, [data, event, openId]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(field, value) => {
|
||||
const newEventData = { id: event.id };
|
||||
(field: EventEditorSubmitActions, value: any) => {
|
||||
if (event === null) {
|
||||
return;
|
||||
}
|
||||
const newEventData: Partial<OntimeEvent> = { id: event.id };
|
||||
switch (field) {
|
||||
case 'durationOverride': {
|
||||
// duration defines timeEnd
|
||||
newEventData.timeEnd = event.timeStart += value;
|
||||
newEventData.timeEnd = event.timeStart += value as number;
|
||||
break;
|
||||
}
|
||||
case 'timeStart': {
|
||||
newEventData.duration = calculateDuration(value, event.timeEnd);
|
||||
newEventData.timeStart = value;
|
||||
newEventData.duration = calculateDuration(value as number, event.timeEnd);
|
||||
newEventData.timeStart = value as number;
|
||||
break;
|
||||
}
|
||||
case 'timeEnd': {
|
||||
newEventData.duration = calculateDuration(event.timeStart, value);
|
||||
newEventData.timeEnd = value;
|
||||
newEventData.duration = calculateDuration(event.timeStart, value as number);
|
||||
newEventData.timeEnd = value as number;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -73,22 +82,26 @@ export default function EventEditor() {
|
||||
[emitError, event, updateEvent],
|
||||
);
|
||||
|
||||
const timerValidationHandler = useCallback(
|
||||
(entry, val) => {
|
||||
const timerValidationHandler = useCallback((entry: TimeEntryField, val: number) => {
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
const valid = validateEntry(entry, val, event.timeStart, event.timeEnd);
|
||||
if (!valid.value) {
|
||||
emitWarning(`Time Input Warning: ${valid.catch}`);
|
||||
}
|
||||
return valid.value;
|
||||
},
|
||||
[emitWarning, event?.timeStart, event?.timeEnd],
|
||||
[event, emitWarning],
|
||||
);
|
||||
|
||||
const togglePublic = useCallback(
|
||||
(currentValue) => {
|
||||
const togglePublic = useCallback((currentValue: boolean) => {
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
updateEvent({ id: event.id, isPublic: !currentValue });
|
||||
},
|
||||
[event?.id, updateEvent],
|
||||
[event, updateEvent],
|
||||
);
|
||||
|
||||
if (!event) {
|
||||
@@ -197,7 +210,7 @@ export default function EventEditor() {
|
||||
handleChange={(value) => handleSubmit('colour', value)}
|
||||
/>
|
||||
<Button
|
||||
rightIcon={<IoBan />}
|
||||
leftIcon={<IoBan />}
|
||||
onClick={() => handleSubmit('colour', '')}
|
||||
variant='ontime-subtle'
|
||||
size='sm'
|
||||
@@ -81,7 +81,7 @@ export default function MenuBar(props: MenuBarProps) {
|
||||
}
|
||||
}
|
||||
},
|
||||
[isElectron, isSettingsOpen, onSettingsClose, onSettingsOpen],
|
||||
[isSettingsOpen, onSettingsClose, onSettingsOpen],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -93,7 +93,7 @@ export default function MenuBar(props: MenuBarProps) {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
}
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
}, [handleKeyPress, isElectron]);
|
||||
|
||||
return (
|
||||
<VStack>
|
||||
|
||||
@@ -41,7 +41,7 @@ const RundownMenu = () => {
|
||||
break;
|
||||
}
|
||||
},
|
||||
[toggleCursorLocked],
|
||||
[addEvent, deleteAllEvents],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -75,7 +75,7 @@ export default function AliasesModal() {
|
||||
}
|
||||
setSubmitting(false);
|
||||
},
|
||||
[aliases, refetch],
|
||||
[aliases, emitError, refetch],
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -74,7 +74,7 @@ export default function Rundown(props) {
|
||||
if (event.altKey && (!event.ctrlKey || !event.shiftKey)) {
|
||||
// Arrow down
|
||||
if (event.keyCode === 40) {
|
||||
if (cursor < entries.length - 2) moveCursorDown();
|
||||
if (cursor < entries.length - 1) moveCursorDown();
|
||||
}
|
||||
// Arrow up
|
||||
if (event.keyCode === 38) {
|
||||
@@ -121,7 +121,7 @@ export default function Rundown(props) {
|
||||
block: 'nearest',
|
||||
inline: 'start',
|
||||
});
|
||||
}, [cursor]);
|
||||
}, [cursorRef]);
|
||||
|
||||
// if selected event
|
||||
// or cursor settings changed
|
||||
@@ -143,7 +143,7 @@ export default function Rundown(props) {
|
||||
// move cursor
|
||||
moveCursorTo(gotoIndex);
|
||||
}
|
||||
}, [data.selectedEventId, isCursorLocked, moveCursorTo]);
|
||||
}, [data.selectedEventId, entries, isCursorLocked, moveCursorTo]);
|
||||
|
||||
// DND
|
||||
const handleOnDragEnd = useCallback(
|
||||
|
||||
@@ -132,16 +132,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
break;
|
||||
}
|
||||
},
|
||||
[
|
||||
addEvent,
|
||||
calculateDuration,
|
||||
data,
|
||||
defaultPublic,
|
||||
deleteEvent,
|
||||
emitError,
|
||||
startTimeIsLastEnd,
|
||||
updateEvent,
|
||||
],
|
||||
[addEvent, data, defaultPublic, deleteEvent, emitError, moveCursorTo, openId, setOpenId, startTimeIsLastEnd, updateEvent],
|
||||
);
|
||||
|
||||
if (data.type === 'event') {
|
||||
@@ -168,9 +159,19 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
/>
|
||||
);
|
||||
} else if (data.type === 'block') {
|
||||
return <BlockBlock index={index} data={data} actionHandler={actionHandler} />;
|
||||
return <BlockBlock
|
||||
index={index}
|
||||
data={data}
|
||||
hasCursor={hasCursor}
|
||||
actionHandler={actionHandler}
|
||||
/>;
|
||||
} else if (data.type === 'delay') {
|
||||
return <DelayBlock index={index} data={data} actionHandler={actionHandler} />;
|
||||
return <DelayBlock
|
||||
index={index}
|
||||
data={data}
|
||||
hasCursor={hasCursor}
|
||||
actionHandler={actionHandler}
|
||||
/>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ $block-text-color: $gray-50;
|
||||
$block-bg: $gray-1200;
|
||||
$block-box-shadow: rgba(0, 0, 0, 0.5) 0 0 3px 2px;
|
||||
$secondary-block-height: 40px;
|
||||
$block-cursor-color: $blue-400;
|
||||
|
||||
@mixin block-styling() {
|
||||
box-sizing: content-box;
|
||||
@@ -21,7 +22,7 @@ $secondary-block-height: 40px;
|
||||
}
|
||||
|
||||
@mixin block-spacing() {
|
||||
padding: 4px 10px 4px 2px;
|
||||
padding: 4px 8px 4px 2px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
@@ -34,4 +35,8 @@ $secondary-block-height: 40px;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
grid-template-columns: 32px 1fr auto;
|
||||
align-items: center;
|
||||
height: $secondary-block-height;
|
||||
|
||||
&.hasCursor {
|
||||
outline: 1px solid $block-cursor-color;
|
||||
}
|
||||
}
|
||||
|
||||
.drag {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
|
||||
|
||||
import { OntimeBlock, OntimeEvent } from '../../../common/models/EventTypes';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
import BlockActionMenu from '../event-block/composite/BlockActionMenu';
|
||||
import { EventItemActions } from '../RundownEntry';
|
||||
|
||||
@@ -10,17 +12,30 @@ import style from './BlockBlock.module.scss';
|
||||
interface BlockBlockProps {
|
||||
index: number;
|
||||
data: OntimeBlock;
|
||||
hasCursor: boolean;
|
||||
actionHandler: (action: EventItemActions, payload?: number | { field: keyof OntimeEvent, value: unknown }) => void;
|
||||
}
|
||||
|
||||
export default function BlockBlock(props: BlockBlockProps) {
|
||||
const { index, data, actionHandler } = props;
|
||||
const { index, data, hasCursor, actionHandler } = props;
|
||||
const onFocusRef = useRef<null | HTMLSpanElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasCursor) {
|
||||
onFocusRef?.current?.focus();
|
||||
}
|
||||
}, [hasCursor])
|
||||
|
||||
const blockClasses = cx([
|
||||
style.block,
|
||||
hasCursor ? style.hasCursor : null,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Draggable key={data.id} draggableId={data.id} index={index}>
|
||||
{(provided) => (
|
||||
<div className={style.block} {...provided.draggableProps} ref={provided.innerRef}>
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<div className={blockClasses} {...provided.draggableProps} ref={provided.innerRef}>
|
||||
<span className={style.drag} {...provided.dragHandleProps} ref={onFocusRef}>
|
||||
<IoReorderTwo />
|
||||
</span>
|
||||
<BlockActionMenu
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
align-items: center;
|
||||
height: $secondary-block-height;
|
||||
gap: 8px;
|
||||
|
||||
&.hasCursor {
|
||||
outline: 1px solid $block-cursor-color;
|
||||
}
|
||||
}
|
||||
|
||||
.drag {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { Button, HStack } from '@chakra-ui/react';
|
||||
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
|
||||
@@ -8,6 +8,7 @@ import { useEventAction } from 'common/hooks/useEventAction';
|
||||
import { millisToMinutes } from 'common/utils/dateConfig';
|
||||
|
||||
import { OntimeDelay, OntimeEvent } from '../../../common/models/EventTypes';
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
import BlockActionMenu from '../event-block/composite/BlockActionMenu';
|
||||
import { EventItemActions } from '../RundownEntry';
|
||||
|
||||
@@ -16,12 +17,20 @@ import style from './DelayBlock.module.scss';
|
||||
interface DelayBlockProps {
|
||||
data: OntimeDelay,
|
||||
index: number;
|
||||
hasCursor: boolean;
|
||||
actionHandler: (action: EventItemActions, payload?: number | { field: keyof OntimeEvent, value: unknown }) => void;
|
||||
}
|
||||
|
||||
export default function DelayBlock(props: DelayBlockProps) {
|
||||
const { data, index, actionHandler } = props;
|
||||
const { data, index, hasCursor, actionHandler } = props;
|
||||
const { applyDelay, updateEvent } = useEventAction();
|
||||
const onFocusRef = useRef<null | HTMLSpanElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasCursor) {
|
||||
onFocusRef?.current?.focus();
|
||||
}
|
||||
}, [hasCursor])
|
||||
|
||||
const applyDelayHandler = useCallback(() => {
|
||||
applyDelay(data.id);
|
||||
@@ -39,13 +48,18 @@ export default function DelayBlock(props: DelayBlockProps) {
|
||||
[data.id, updateEvent],
|
||||
);
|
||||
|
||||
const blockClasses = cx([
|
||||
style.delay,
|
||||
hasCursor ? style.hasCursor : null,
|
||||
]);
|
||||
|
||||
const delayValue = data.duration != null ? millisToMinutes(data.duration) : undefined;
|
||||
|
||||
return (
|
||||
<Draggable key={data.id} draggableId={data.id} index={index}>
|
||||
{(provided) => (
|
||||
<div className={style.delay} {...provided.draggableProps} ref={provided.innerRef}>
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<div className={blockClasses} {...provided.draggableProps} ref={provided.innerRef}>
|
||||
<span className={style.drag} {...provided.dragHandleProps} ref={onFocusRef}>
|
||||
<IoReorderTwo />
|
||||
</span>
|
||||
<DelayInput
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
}
|
||||
|
||||
&.hasCursor {
|
||||
outline: 1px solid $blue-400;
|
||||
outline: 1px solid $block-cursor-color;
|
||||
}
|
||||
|
||||
&.skip {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { Editable, EditableInput, EditablePreview, Tooltip } from '@chakra-ui/react';
|
||||
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
|
||||
@@ -81,6 +81,7 @@ export default function EventBlock(props: EventBlockProps) {
|
||||
const [openId, setOpenId] = useAtom(editorEventId);
|
||||
const { updateEvent } = useEventAction();
|
||||
const [blockTitle, setBlockTitle] = useState<string>(title || '');
|
||||
const onFocusRef = useRef<null | HTMLSpanElement>(null);
|
||||
|
||||
const binderColours = colour && getAccessibleColour(colour);
|
||||
|
||||
@@ -90,6 +91,12 @@ export default function EventBlock(props: EventBlockProps) {
|
||||
setBlockTitle(title);
|
||||
}, [title]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasCursor) {
|
||||
onFocusRef?.current?.focus();
|
||||
}
|
||||
}, [hasCursor]);
|
||||
|
||||
const handleTitle = useCallback(
|
||||
(text: string) => {
|
||||
if (text === title) {
|
||||
@@ -101,7 +108,7 @@ export default function EventBlock(props: EventBlockProps) {
|
||||
|
||||
updateEvent({ id: eventId, title: cleanVal });
|
||||
},
|
||||
[updateEvent, title],
|
||||
[title, updateEvent, eventId],
|
||||
);
|
||||
|
||||
const eventIsPlaying = selected && playback === 'start';
|
||||
@@ -133,7 +140,7 @@ export default function EventBlock(props: EventBlockProps) {
|
||||
tabIndex={-1}
|
||||
onClick={() => actionHandler('set-cursor', index)}
|
||||
>
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<span className={style.drag} {...provided.dragHandleProps} ref={onFocusRef}>
|
||||
<IoReorderTwo />
|
||||
</span>
|
||||
{eventIndex}
|
||||
@@ -184,6 +191,7 @@ export default function EventBlock(props: EventBlockProps) {
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<Editable
|
||||
variant='ontime'
|
||||
value={blockTitle}
|
||||
className={`${style.eventTitle} ${!title || title === '' ? style.noTitle : ''}`}
|
||||
placeholder='Event title'
|
||||
@@ -198,21 +206,25 @@ export default function EventBlock(props: EventBlockProps) {
|
||||
<div className={selected ? style.progressBg : `${style.progressBg} ${style.hidden}`}>
|
||||
<EventBlockProgressBar playback={playback} />
|
||||
</div>
|
||||
<div className={style.eventStatus}>
|
||||
<div className={style.eventStatus} tabIndex={-1}
|
||||
>
|
||||
<Tooltip
|
||||
label='Next event'
|
||||
isDisabled={!next}
|
||||
shouldWrapChildren {...tooltipProps}
|
||||
{...tooltipProps}
|
||||
>
|
||||
<IoPlaySkipForward
|
||||
className={`${style.statusIcon} ${next ? style.active : ''}`} />
|
||||
<span>
|
||||
<IoPlaySkipForward
|
||||
className={`${style.statusIcon} ${next ? style.active : ''}`} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
label={`${isPublic ? 'Event is public' : 'Event is private'}`}
|
||||
{...tooltipProps}
|
||||
shouldWrapChildren
|
||||
>
|
||||
<IoPeople className={`${style.statusIcon} ${isPublic ? style.active : ''}`} />
|
||||
<span>
|
||||
<IoPeople className={`${style.statusIcon} ${isPublic ? style.active : ''}`} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function QuickAddBlock(props: QuickAddBlockProps) {
|
||||
}
|
||||
}
|
||||
|
||||
}, [addEvent, doPublic, doStartTime, emitError, previousId, previousEventId]);
|
||||
}, [defaultPublic, previousId, previousEventId, addEvent, emitError]);
|
||||
|
||||
return (
|
||||
<div className={style.quickAdd}>
|
||||
|
||||
@@ -50,7 +50,7 @@ const withSocket = (Component) => {
|
||||
socket.on('publicselected-id', (data) => {
|
||||
setPublicSelectedId(data);
|
||||
});
|
||||
}, [socket]);
|
||||
}, []);
|
||||
|
||||
|
||||
const publicEvents = useMemo(() => {
|
||||
|
||||
@@ -20,6 +20,7 @@ $gray-900: #4c4c4c;
|
||||
$gray-1000: #404040;
|
||||
$gray-1050: #303030;
|
||||
$gray-1100: #2d2d2d;
|
||||
$gray-1250: #262626;
|
||||
$gray-1200: #202020;
|
||||
$gray-1300: #1a1a1a;
|
||||
$gray-1350: #101010;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export const ontimeEditable = {
|
||||
input: {
|
||||
borderRadius: '3px',
|
||||
width: '100%',
|
||||
_focus: {
|
||||
border: '1px solid #578AF4', // $blue-500
|
||||
boxShadow: 'none',
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
width: '100%',
|
||||
border: '1px solid transparent', // $blue-500
|
||||
},
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
export const ontimeSelect = {
|
||||
field: {
|
||||
color: '#9d9d9d', // $gray-500
|
||||
color: '#e2e2e2', // $gray-200
|
||||
borderRadius: '3px',
|
||||
fontWeight: '400',
|
||||
background: '#2d2d2d', // $gray-1100
|
||||
background: '#262626', // $gray-1100
|
||||
border: '1px solid transparent',
|
||||
_hover: {
|
||||
background: '#404040', // $gray-1000
|
||||
@@ -15,6 +15,6 @@ export const ontimeSelect = {
|
||||
},
|
||||
},
|
||||
icon: {
|
||||
color: '#9d9d9d', // $gray-500
|
||||
color: '#e2e2e2', // $gray-200
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,9 +2,13 @@ export const ontimeSwitch = {
|
||||
container: { },
|
||||
track: {
|
||||
background: '#2d2d2d', // $gray-1100
|
||||
border: '1px solid transparent',
|
||||
_checked: {
|
||||
background: `#2B5ABC`, // $blue-700
|
||||
},
|
||||
_focus: {
|
||||
border: '1px solid #578AF4', // $blue-500
|
||||
}
|
||||
},
|
||||
thumb: {},
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
const commonStyles = {
|
||||
borderRadius: '3px',
|
||||
fontWeight: '400',
|
||||
backgroundColor: '#2d2d2d', // $gray-1100
|
||||
backgroundColor: '#262626', // $gray-1250
|
||||
color: '#e2e2e2', // $gray-200
|
||||
border: '1px solid transparent',
|
||||
_hover: {
|
||||
backgroundColor: '#404040', // $gray-1000
|
||||
backgroundColor: '#2d2d2d', // $gray-1100
|
||||
},
|
||||
_focus: {
|
||||
backgroundColor: '#404040', // $gray-1000
|
||||
backgroundColor: '#2d2d2d', // $gray-1000
|
||||
color: '#f6f6f6', // $gray-50
|
||||
border: '1px solid #578AF4', // $blue-500
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ontimeButtonSubtleWhite,
|
||||
} from './ontimeButton';
|
||||
import { ontimeCheckboxOnDark } from './ontimeCheckbox';
|
||||
import { ontimeEditable } from './ontimeEditable';
|
||||
import { ontimeMenuOnDark } from './ontimeMenu';
|
||||
import { ontimeSelect } from './ontimeSelect';
|
||||
import { ontimeSwitch } from './ontimeSwitch';
|
||||
@@ -36,19 +37,13 @@ const theme = extendTheme({
|
||||
},
|
||||
},
|
||||
Editable: {
|
||||
baseStyle: {
|
||||
input: {
|
||||
borderRadius: '2px',
|
||||
width: '100%',
|
||||
},
|
||||
preview: {
|
||||
width: '100%',
|
||||
},
|
||||
variants: {
|
||||
'ontime': { ...ontimeEditable },
|
||||
},
|
||||
},
|
||||
Input: {
|
||||
baseStyle: {
|
||||
borderRadius: '2px',
|
||||
borderRadius: '3px',
|
||||
border: '1px',
|
||||
},
|
||||
variants: {
|
||||
|
||||
@@ -2980,6 +2980,11 @@ eslint-plugin-jest@^27.0.4:
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "^5.10.0"
|
||||
|
||||
eslint-plugin-react-hooks@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3"
|
||||
integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==
|
||||
|
||||
eslint-plugin-react@^7.31.8:
|
||||
version "7.31.8"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.31.8.tgz#3a4f80c10be1bcbc8197be9e8b641b2a3ef219bf"
|
||||
|
||||
Reference in New Issue
Block a user