Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): implement Freeze for Rundown UI #1228

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
7 changes: 7 additions & 0 deletions apps/client/src/common/api/rundown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,10 @@ export async function requestDelete(eventIds: string[]): Promise<AxiosResponse<M
export async function requestDeleteAll(): Promise<AxiosResponse<MessageResponse>> {
return axios.delete(`${rundownPath}/all`);
}

/**
* HTTP request to freeze rundown
*/
export async function requestToggleRundownFreeze(frozen: boolean): Promise<AxiosResponse<MessageResponse>> {
return axios.post(`${rundownPath}/frozen`, { frozen });
}
27 changes: 27 additions & 0 deletions apps/client/src/common/hooks/useEventAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
requestPostEvent,
requestPutEvent,
requestReorderEvent,
requestToggleRundownFreeze,
SwapEntry,
} from '../api/rundown';
import { logAxiosError } from '../api/utils';
Expand Down Expand Up @@ -604,6 +605,31 @@ export const useEventAction = () => {
[_swapEvents],
);

/**
* Calls mutation to freeze events
* @private
*/
const _toggleFreezeEvents = useMutation({
mutationFn: requestToggleRundownFreeze,
onSettled: () => {
queryClient.invalidateQueries({ queryKey: RUNDOWN });
},
});

/**
* Freezes all changes to events
*/
const toggleFreezeEvents = useCallback(
async (frozen: boolean) => {
try {
await _toggleFreezeEvents.mutateAsync(frozen);
} catch (error) {
logAxiosError('Error freezing events', error);
}
},
[_toggleFreezeEvents],
);

return {
addEvent,
applyDelay,
Expand All @@ -616,5 +642,6 @@ export const useEventAction = () => {
updateEvent,
updateTimer,
updateCustomField,
toggleFreezeEvents,
};
};
13 changes: 12 additions & 1 deletion apps/client/src/common/stores/appModeStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,18 @@ export enum AppMode {
const appModeKey = 'ontime-app-mode';

function getModeFromSession() {
return sessionStorage.getItem(appModeKey) === AppMode.Run ? AppMode.Run : AppMode.Edit;
const appModeFromSessionStorage = sessionStorage.getItem(appModeKey);

switch (appModeFromSessionStorage) {
case AppMode.Run:
return AppMode.Run;
case AppMode.Edit:
return AppMode.Edit;
case AppMode.Freeze:
return AppMode.Freeze;
default:
return AppMode.Run;
}
Comment on lines +14 to +23
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the domain, run and freeze are not mutually exclusive. It is not ideal to build our logic in a way that goes agains the domain

Another challenge I see here, is that the data provenance between the AppMode and the Freeze is different. AppMode is a local setting for the user, while freeze is a session setting.
I am unsure it makes sense for us to mix the concerns here, although I understand the convenience

I wonder if the correct place to consume this is from the runtime store. we could create a feature selector for it and consume it in the components instead of passing it as props.

The other data is passed to the blocks as props, since we maintain the relationship of list > item, this is not the case here. Performance wise we would also prefer consuming this data as low down in the tree as possible

}

function persistModeToSession(mode: AppMode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Input } from '@chakra-ui/react';

import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
import { useEventAction } from '../../../common/hooks/useEventAction';
import { AppMode, useAppMode } from '../../../common/stores/appModeStore';
import { cx } from '../../../common/utils/styleUtils';

import style from './TitleEditor.module.scss';
Expand All @@ -17,6 +18,9 @@ interface TitleEditorProps {
export default function EditableBlockTitle(props: TitleEditorProps) {
const { title, eventId, placeholder, className } = props;
const { updateEvent } = useEventAction();
const appMode = useAppMode((state) => state.mode);
const isRundownFrozen = appMode === AppMode.Freeze;

const ref = useRef<HTMLInputElement | null>(null);
const submitCallback = useCallback(
(text: string) => {
Expand Down Expand Up @@ -51,6 +55,7 @@ export default function EditableBlockTitle(props: TitleEditorProps) {
fontWeight='600'
letterSpacing='0.25px'
paddingLeft='0'
isDisabled={isRundownFrozen}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircle
import TooltipActionBtn from '../../../../common/components/buttons/TooltipActionBtn';
import { useEventAction } from '../../../../common/hooks/useEventAction';
import { setEventPlayback } from '../../../../common/hooks/useSocket';
import { AppMode, useAppMode } from '../../../../common/stores/appModeStore';
import { tooltipDelayMid } from '../../../../ontimeConfig';

import style from '../EventBlock.module.scss';
Expand Down Expand Up @@ -39,6 +40,8 @@ interface EventBlockPlaybackProps {
const EventBlockPlayback = (props: EventBlockPlaybackProps) => {
const { eventId, skip, isPlaying, isPaused, loaded, disablePlayback } = props;
const { updateEvent } = useEventAction();
const appMode = useAppMode((state) => state.mode);
const isRundownFrozen = appMode === AppMode.Freeze;

const toggleSkip = (event: MouseEvent) => {
event.stopPropagation();
Expand Down Expand Up @@ -100,7 +103,7 @@ const EventBlockPlayback = (props: EventBlockPlaybackProps) => {
{...blockBtnStyle}
clickHandler={toggleSkip}
tabIndex={-1}
isDisabled={loaded}
isDisabled={isRundownFrozen || loaded}
/>
<TooltipActionBtn
variant='ontime-subtle-white'
Expand Down
5 changes: 5 additions & 0 deletions apps/client/src/features/rundown/event-editor/EventEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import CopyTag from '../../../common/components/copy-tag/CopyTag';
import { useEventAction } from '../../../common/hooks/useEventAction';
import useCustomFields from '../../../common/hooks-query/useCustomFields';
import useRundown from '../../../common/hooks-query/useRundown';
import { AppMode, useAppMode } from '../../../common/stores/appModeStore';
import { getAccessibleColour } from '../../../common/utils/styleUtils';
import { useEventSelection } from '../useEventSelection';

Expand All @@ -28,6 +29,8 @@ export default function EventEditor() {
const { order, rundown } = data;
const { updateEvent } = useEventAction();
const [_searchParams, setSearchParams] = useSearchParams();
const appMode = useAppMode((state) => state.mode);
const isRundownFrozen = appMode === AppMode.Freeze;

const [event, setEvent] = useState<OntimeEvent | null>(null);

Expand Down Expand Up @@ -88,6 +91,7 @@ export default function EventEditor() {
timerType={event.timerType}
timeWarning={event.timeWarning}
timeDanger={event.timeDanger}
isRundownFrozen={isRundownFrozen}
/>
<EventEditorTitles
key={`${event.id}-titles`}
Expand All @@ -97,6 +101,7 @@ export default function EventEditor() {
note={event.note}
colour={event.colour}
handleSubmit={handleSubmit}
isRundownFrozen={isRundownFrozen}
/>
<div className={style.column}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface EventEditorTimesProps {
timerType: TimerType;
timeWarning: number;
timeDanger: number;
isRundownFrozen: boolean;
}

type HandledActions = 'timerType' | 'endAction' | 'isPublic' | 'timeWarning' | 'timeDanger';
Expand All @@ -41,6 +42,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
timerType,
timeWarning,
timeDanger,
isRundownFrozen,
} = props;
const { updateEvent } = useEventAction();

Expand Down Expand Up @@ -102,6 +104,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
value={timerType}
onChange={(event) => handleSubmit('timerType', event.target.value)}
variant='ontime'
disabled={isRundownFrozen}
>
<option value={TimerType.CountDown}>Count down</option>
<option value={TimerType.CountUp}>Count up</option>
Expand All @@ -123,6 +126,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
value={endAction}
onChange={(event) => handleSubmit('endAction', event.target.value)}
variant='ontime'
disabled={isRundownFrozen}
>
<option value={EndAction.None}>None</option>
<option value={EndAction.Stop}>Stop</option>
Expand All @@ -135,7 +139,13 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
<div>
<span className={style.inputLabel}>Event Visibility</span>
<label className={style.switchLabel}>
<Switch size='md' isChecked={isPublic} onChange={() => handleSubmit('isPublic', isPublic)} variant='ontime' />
<Switch
size='md'
isChecked={isPublic}
onChange={() => handleSubmit('isPublic', isPublic)}
variant='ontime'
disabled={isRundownFrozen}
/>
{isPublic ? 'Public' : 'Private'}
</label>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ interface EventEditorTitlesProps {
title: string;
note: string;
colour: string;
isRundownFrozen: boolean;
handleSubmit: (field: EditorUpdateFields, value: string) => void;
}

const EventEditorTitles = (props: EventEditorTitlesProps) => {
const { eventId, cue, title, note, colour, handleSubmit } = props;
const { eventId, cue, title, note, colour, handleSubmit, isRundownFrozen } = props;

const cueSubmitHandler = (_field: string, newValue: string) => {
handleSubmit('cue', sanitiseCue(newValue));
Expand All @@ -42,14 +43,33 @@ const EventEditorTitles = (props: EventEditorTitlesProps) => {
readOnly
/>
</div>
<EventTextInput field='cue' label='Cue' initialValue={cue} submitHandler={cueSubmitHandler} maxLength={10} />
<EventTextInput
field='cue'
label='Cue'
initialValue={cue}
submitHandler={cueSubmitHandler}
maxLength={10}
disabled={isRundownFrozen}
/>
</div>
<div>
<label className={style.inputLabel}>Colour</label>
<SwatchSelect name='colour' value={colour} handleChange={handleSubmit} />
</div>
<EventTextInput field='title' label='Title' initialValue={title} submitHandler={handleSubmit} />
<EventTextArea field='note' label='Note' initialValue={note} submitHandler={handleSubmit} />
<EventTextInput
field='title'
label='Title'
initialValue={title}
submitHandler={handleSubmit}
disabled={isRundownFrozen}
/>
<EventTextArea
field='note'
label='Note'
initialValue={note}
submitHandler={handleSubmit}
disabled={isRundownFrozen}
/>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ interface CountedTextAreaProps {
initialValue: string;
style?: CSSProperties;
submitHandler: (field: EditorUpdateFields, value: string) => void;
disabled?: boolean;
}

export default function EventTextArea(props: CountedTextAreaProps) {
const { className, field, label, initialValue, style: givenStyles, submitHandler } = props;
const { className, field, label, initialValue, style: givenStyles, submitHandler, disabled } = props;
const ref = useRef<HTMLInputElement | null>(null);
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);

Expand All @@ -40,6 +41,7 @@ export default function EventTextArea(props: CountedTextAreaProps) {
variant='ontime-filled'
data-testid='input-textarea'
value={value}
isDisabled={disabled}
onChange={onChange}
onBlur={onBlur}
onKeyDown={onKeyDown}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ interface EventTextInputProps extends InputProps {
label: string;
initialValue: string;
submitHandler: (field: EditorUpdateFields, value: string) => void;
disabled?: boolean;
}

export default function EventTextInput(props: EventTextInputProps) {
const { field, label, initialValue, submitHandler, maxLength } = props;
const { field, label, initialValue, submitHandler, maxLength, disabled } = props;
const ref = useRef<HTMLInputElement | null>(null);
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);

Expand All @@ -39,6 +40,7 @@ export default function EventTextInput(props: EventTextInputProps) {
onBlur={onBlur}
onKeyDown={onKeyDown}
autoComplete='off'
disabled={disabled}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
.header {
padding-inline: calc(2rem - 6px + 0.5rem) calc(1rem + 2px);

display: flex;
align-items: center;
justify-content: space-between;
column-gap: 0.5rem;
asharonbaltazar marked this conversation as resolved.
Show resolved Hide resolved
}
25 changes: 23 additions & 2 deletions apps/client/src/features/rundown/rundown-header/RundownHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
import { Button, ButtonGroup } from '@chakra-ui/react';

import { useEventAction } from '../../../common/hooks/useEventAction';
import { AppMode, useAppMode } from '../../../common/stores/appModeStore';

import RundownMenu from './RundownMenu';

import style from './RundownHeader.module.scss';

export default function RundownHeader() {
const { toggleFreezeEvents } = useEventAction();

const appMode = useAppMode((state) => state.mode);
const setAppMode = useAppMode((state) => state.setMode);
const setRunMode = () => setAppMode(AppMode.Run);
const setEditMode = () => setAppMode(AppMode.Edit);

const toggleAppMode = async (appMode: AppMode) => {
if (appMode === AppMode.Freeze) {
await toggleFreezeEvents(true);
} else {
await toggleFreezeEvents(false);
}
setAppMode(appMode);
};

const setRunMode = () => toggleAppMode(AppMode.Run);
const setEditMode = () => toggleAppMode(AppMode.Edit);
const setFreezeMode = () => toggleAppMode(AppMode.Freeze);

return (
<div className={style.header}>
Expand All @@ -21,6 +35,13 @@ export default function RundownHeader() {
<Button size='sm' variant={appMode === AppMode.Edit ? 'ontime-filled' : 'ontime-subtle'} onClick={setEditMode}>
Edit
</Button>
<Button
size='sm'
variant={appMode === AppMode.Freeze ? 'ontime-filled' : 'ontime-subtle'}
onClick={setFreezeMode}
>
Freeze
</Button>
</ButtonGroup>
<RundownMenu />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ export default function RundownMenu() {
leftIcon={<IoTrash />}
onClick={onOpen}
color='#FA5656'
isDisabled={appMode === 'run'}
isDisabled={appMode !== 'edit'}
>
Clear rundown
</Button>

<AlertDialog variant='ontime' isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
<AlertDialogOverlay>
<AlertDialogContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@

.timeAction {
opacity: 0.4;
cursor: pointer;
padding-right: 0.5em;

&.active {
opacity: 1;
color: var(--status-color-active-override, $active-indicator);
}
&:disabled {
cursor: not-allowed;
}
.fourtyfive {
transform: rotate(-45deg);
}
Expand Down
Loading
Loading