* chore: upgrade local build documentation

* style: keep scrolling event in screen

* feat: delay is time entry

* refactor: remove unused

* refactor: remove unused

* refactor: batch store updates

* refactor: virtually remove cap on events

* refactor: style and behaviour tweaks to event block

* style: tweaks on schedules

* chore: remove sentry from server

* style: reorder menu

* chore: update docs
This commit is contained in:
Carlos Valente
2023-04-14 10:13:46 +02:00
committed by GitHub
parent 94a1369d64
commit 770d12888d
48 changed files with 531 additions and 376 deletions
@@ -1,13 +1,24 @@
@use '../../../../theme/v2Styles' as *;
$input-font-size: 15px;
.delayInput {
display: flex;
gap: $element-spacing;
align-items: center;
color: $ontime-delay-text;
font-size: $text-body-size;
.inputField {
font-size: $input-font-size;
letter-spacing: 1px;
max-width: 7em;
padding-left: 16px;
color: $ontime-delay-text
}
}
.inputField {
text-align: center;
}
.delayOptions {
display: flex;
flex-direction: column;
}
@@ -1,88 +1,134 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Input } from '@chakra-ui/react';
import { KeyboardEvent, useEffect, useRef, useState } from 'react';
import { Input, Radio, RadioGroup } from '@chakra-ui/react';
import { millisToString } from 'ontime-utils';
import { clamp } from '../../../utils/math';
import { useEventAction } from '../../../hooks/useEventAction';
import { forgivingStringToMillis } from '../../../utils/dateConfig';
import style from './DelayInput.module.scss';
const inputStyleProps = {
width: 20,
placeholder: '-',
size: 'sm',
color: '#E69056',
variant: 'ontime-filled',
fontSize: '15px',
letterSpacing: '0.3px',
};
interface DelayInputProps {
submitHandler: (value: number) => void;
value?: number;
eventId: string;
duration: number;
}
export default function DelayInput(props: DelayInputProps) {
const { submitHandler, value = 0 } = props;
const [_value, setValue] = useState(value);
const { eventId, duration } = props;
const { updateEvent } = useEventAction();
const [value, setValue] = useState<string>('');
const inputRef = useRef<HTMLInputElement | null>(null);
// avoid wrong submit on cancel
let ignoreChange = false;
useEffect(() => {
if (!value) {
if (typeof duration === undefined) {
return;
}
setValue(value);
}, [value]);
setValue(millisToString(duration));
}, [duration]);
/**
* @description Prepare delay value for update
* @param {string} value string to be parsed
* @param {string} newValue string to be parsed
*/
const validate = useCallback(
(newValue?: string) => {
if (newValue === '') setValue(0);
const delayValue = clamp(Number(newValue), -60, 60);
if (delayValue === value) return;
setValue(delayValue);
const validateAndSubmit = (newValue: string) => {
if (ignoreChange) {
ignoreChange = false;
return;
}
submitHandler(delayValue);
},
[submitHandler, value],
);
const isNegative = newValue.startsWith('-');
let newMillis = forgivingStringToMillis(newValue);
if (isNegative) {
newMillis = newMillis * -1;
}
if (newMillis === duration) {
return;
}
submitChange(newMillis);
setValue(millisToString(newMillis));
};
const submitChange = (value: number) => {
updateEvent({
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 = useCallback(
(key: string) => {
if (key === 'Enter') {
inputRef.current?.blur();
validate(inputRef.current?.value);
} else if (key === 'Escape') {
inputRef.current?.blur();
setValue(value);
}
},
[validate, value],
);
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') {
ignoreChange = true;
setValue(millisToString(duration));
inputRef.current?.blur();
}
};
const labelText = `${Math.abs(value) !== 1 ? 'minutes' : 'minute'} ${
value !== undefined && value >= 0 ? 'delayed' : 'ahead'
}`;
/**
* @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 (
<label className={style.delayInput}>
<div className={style.delayInput}>
<Input
{...inputStyleProps}
size='sm'
ref={inputRef}
data-testid='delay-input'
className={style.inputField}
value={_value}
onChange={(event) => setValue(Number(event.target.value))}
onBlur={(event) => validate(event.target.value)}
onKeyDown={(event) => onKeyDownHandler(event.key)}
type='number'
type='text'
placeholder='-'
variant='ontime-filled'
onFocus={handleFocus}
onChange={(event) => setValue(event.target.value)}
onBlur={(event) => validateAndSubmit(event.target.value)}
onKeyDown={onKeyDownHandler}
value={value}
maxLength={9}
/>
{labelText}
</label>
<RadioGroup
className={style.delayOptions}
onChange={handleSlipChange}
value={checkedOption}
variant='ontime-block'
size='sm'
>
<Radio value='add'>Add time</Radio>
<Radio value='subtract'>Subtract time</Radio>
</RadioGroup>
</div>
);
}
@@ -25,18 +25,22 @@ export default function TimeInput(props: TimeInputProps) {
const { name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0, warning } = props;
const { emitError } = useEmitLog();
const inputRef = useRef<HTMLInputElement | null>(null);
const [value, setValue] = useState('');
const [value, setValue] = useState<string>('');
// avoid wrong submit on cancel
let ignoreChange = false;
/**
* @description Resets input value to given
*/
const resetValue = useCallback(() => {
try {
setValue(millisToString(time + delay));
// eslint-disable-next-line -- we use ignore change to stop submit on cancel
ignoreChange = true;
setValue(millisToString(time));
} catch (error) {
emitError(`Unable to parse date: ${error}`);
}
}, [delay, emitError, time]);
}, [emitError, time]);
/**
* @description Selects input text on focus
@@ -73,11 +77,8 @@ export default function TimeInput(props: TimeInputProps) {
newValMillis = forgivingStringToMillis(newValue);
}
// Time now and time submittedVal
const originalMillis = time + delay;
// check if time is different from before
if (newValMillis === originalMillis) return false;
if (newValMillis === time) return false;
// validate with parent
if (!validationHandler(name, newValMillis)) return false;
@@ -87,7 +88,7 @@ export default function TimeInput(props: TimeInputProps) {
return true;
},
[delay, name, previousEnd, submitHandler, time, validationHandler],
[name, previousEnd, submitHandler, time, validationHandler],
);
/**
@@ -96,10 +97,16 @@ export default function TimeInput(props: TimeInputProps) {
*/
const validateAndSubmit = useCallback(
(newValue: string) => {
if (ignoreChange) {
// eslint-disable-next-line -- we use this to prevent a wrong submit
ignoreChange = false;
return;
}
const success = handleSubmit(newValue);
if (success) {
const ms = forgivingStringToMillis(newValue);
setValue(millisToString(ms + delay));
const delayed = name === 'timeEnd' ? Math.max(0, ms + delay) : Math.max(0, ms + delay);
setValue(millisToString(delayed));
} else {
resetValue();
}
@@ -139,8 +146,6 @@ export default function TimeInput(props: TimeInputProps) {
resetValue();
}, [emitError, resetValue, time]);
const isDelayed = delay != null && delay !== 0;
const ButtonInitial = () => {
if (name === 'timeStart') return 'S';
if (name === 'timeEnd') return 'E';
@@ -155,6 +160,7 @@ export default function TimeInput(props: TimeInputProps) {
return '';
};
const isDelayed = delay !== 0;
const inputClasses = cx([style.timeInput, isDelayed ? style.delayed : null]);
const buttonClasses = cx([style.inputButton, isDelayed ? style.delayed : null, warning ? style.warn : null]);
@@ -32,7 +32,7 @@
}
&:not(:last-child) {
padding-bottom: clamp(16px, 1.5vw, 24px);
padding-bottom: 8px;
}
&--past {
@@ -6,7 +6,7 @@ import { useInterval } from '../../hooks/useInterval';
interface ScheduleContextState {
events: OntimeEvent[];
paginatedEvents: OntimeEvent[];
selectedEventId: string;
selectedEventId: string | null;
numPages: number;
visiblePage: number;
isBackstage: boolean;
@@ -16,22 +16,20 @@ const ScheduleContext = createContext<ScheduleContextState | undefined>(undefine
interface ScheduleProviderProps {
events: OntimeEvent[];
selectedEventId: string;
selectedEventId: string | null;
isBackstage?: boolean;
eventsPerPage?: number;
time?: number;
}
export const ScheduleProvider = (
{
children,
events,
selectedEventId,
isBackstage = false,
eventsPerPage = 4,
time = 10,
}: PropsWithChildren<ScheduleProviderProps>) => {
export const ScheduleProvider = ({
children,
events,
selectedEventId,
isBackstage = false,
eventsPerPage = 8,
time = 10,
}: PropsWithChildren<ScheduleProviderProps>) => {
const [visiblePage, setVisiblePage] = useState(0);
const numPages = Math.ceil(events.length / eventsPerPage);
@@ -90,6 +90,7 @@ export const useCuesheet = () => {
export const setEventPlayback = {
loadEvent: (eventId: string) => socketSendJson('loadid', eventId),
startEvent: (eventId: string) => socketSendJson('startid', eventId),
start: () => socketSendJson('start'),
pause: () => socketSendJson('pause'),
};
@@ -2,9 +2,9 @@ import {
forgivingStringToMillis,
formatDisplay,
isTimeString,
millisToDelayString,
millisToMinutes,
millisToSeconds,
timeStringToMillis,
} from '../dateConfig';
describe('test string from formatDisplay function', () => {
@@ -170,88 +170,6 @@ describe('test millisToMinutes function', () => {
});
});
describe('test timeStringToMillis function', () => {
it('test with null', () => {
const t = { val: null, result: 0 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 00:00:00', () => {
const t = { val: '00:00:00', result: 0 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with -00:00:00', () => {
const t = { val: '-00:00:00', result: 0 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 00:00:01', () => {
const t = { val: '00:00:01', result: 1000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with -00:00:01', () => {
const t = { val: '-00:00:01', result: 1000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 01:00:01', () => {
const t = { val: '01:00:01', result: 3601000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 24:00:01', () => {
const t = { val: '24:00:01', result: 86401000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 00:00:5', () => {
const t = { val: '00:00:5', result: 5000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 00:1:00', () => {
const t = { val: '00:1:00', result: 60000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 1:00:00', () => {
const t = { val: '1:00:00', result: 3600000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 1', () => {
const t = { val: '1', result: 1000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 120', () => {
const t = { val: '120', result: 120000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 56', () => {
const t = { val: '56', result: 56000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 2:3', () => {
const t = { val: '2:3', result: 123000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 02:3', () => {
const t = { val: '02:3', result: 123000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
it('test with 2:03', () => {
const t = { val: '2:03', result: 123000 };
expect(timeStringToMillis(t.val)).toBe(t.result);
});
});
describe('test isTimeString() function', () => {
it('it validates time strings', () => {
const ts = ['2', '2:10', '2:10:22'];
@@ -450,3 +368,59 @@ describe('test forgivingStringToMillis()', () => {
});
});
});
describe('millisToDelayString()', () => {
it('returns null for null values', () => {
expect(millisToDelayString(null)).toBeNull();
});
it('returns null 0', () => {
expect(millisToDelayString(0)).toBeNull();
});
describe('converts values in seconds', () => {
it(`shows a simple string with value in seconds`, () => {
expect(millisToDelayString(10000)).toBe('+10sec');
});
it(`... and its negative counterpart`, () => {
expect(millisToDelayString(-10000)).toBe('-10sec');
});
const underAMinute = [1, 500, 1000, 6000, 55000, 59999];
underAMinute.forEach((value) => {
it(`handles ${value}`, () => {
expect(millisToDelayString(value)?.endsWith('sec')).toBe(true);
});
});
expect(millisToDelayString(null)).toBeNull();
});
describe('converts values in minutes', () => {
it(`shows a simple string with value in minutes`, () => {
expect(millisToDelayString(720000)).toBe('+12min');
});
it(`... and its negative counterpart`, () => {
expect(millisToDelayString(-720000)).toBe('-12min');
});
it(`shows a simple string with value in minutes and seconds`, () => {
expect(millisToDelayString(630000)).toBe('+00:10:30');
});
it(`... and its negative counterpart`, () => {
expect(millisToDelayString(-630000)).toBe('-00:10:30');
});
const underAnHour = [60000, 360000, 720000];
underAnHour.forEach((value) => {
it(`handles ${value}`, () => {
expect(millisToDelayString(value)?.endsWith('min')).toBe(true);
});
});
});
describe('converts values with full time string', () => {
it(`positive added time`, () => {
expect(millisToDelayString(45015000)).toBe('+12:30:15');
});
it(`negative added time`, () => {
expect(millisToDelayString(-45015000)).toBe('-12:30:15');
});
});
});
+19 -14
View File
@@ -1,3 +1,5 @@
import { formatFromMillis } from 'ontime-utils';
import { mth, mtm, mts } from './timeConstants';
export const timeFormat = 'HH:mm';
@@ -47,20 +49,6 @@ export const millisToMinutes = (millis: number): number => {
return millis < 0 ? Math.ceil(millis / mtm) : Math.floor(millis / mtm);
};
/**
* @description Converts timestring to milliseconds
* @param {string} string - time string "23:00:12"
* @returns {number} Amount in milliseconds
*/
export const timeStringToMillis = (string: string): number => {
if (typeof string !== 'string') return 0;
const time = string.split(':');
if (time.length === 1) return Math.abs(time[0] * mts);
if (time.length === 2) return Math.abs(time[0]) * mtm + time[1] * mts;
if (time.length === 3) return Math.abs(time[0]) * mth + time[1] * mtm + time[2] * mts;
return 0;
};
/**
* @description Validates a time string
* @param {string} string - time string "23:00:12"
@@ -150,3 +138,20 @@ export const forgivingStringToMillis = (value: string, fillLeft = true): number
return millis;
};
export function millisToDelayString(millis: number | null): undefined | string | null {
if (millis == null || millis === 0) {
return null;
}
const isNegative = millis < 0;
const absMillis = Math.abs(millis);
if (absMillis < mtm) {
return `${isNegative ? '-' : '+'}${formatFromMillis(absMillis, 's')}sec`;
} else if (absMillis < mth && absMillis % mtm === 0) {
return `${isNegative ? '-' : '+'}${formatFromMillis(absMillis, 'm')}min`;
} else {
return `${isNegative ? '-' : '+'}${formatFromMillis(absMillis, 'HH:mm:ss')}`;
}
}
+2 -6
View File
@@ -1,6 +1,5 @@
import { DateTime } from 'luxon';
import { Settings } from 'ontime-types';
import { millisToString } from 'ontime-utils';
import { formatFromMillis, millisToString } from 'ontime-utils';
import { APP_SETTINGS } from '../api/apiConstants';
import { ontimeQueryClient } from '../queryClient';
@@ -39,7 +38,6 @@ type FormatOptions = {
};
/**
/**
* @description utility function to format a date in 12 or 24 hour format
* @param {number | null} milliseconds
* @param {object} [options]
@@ -54,7 +52,5 @@ export const formatTime = (milliseconds: number | null, options: FormatOptions,
}
const timeFormat = resolver();
const { showSeconds = false, format: formatString = 'hh:mm a' } = options || {};
return timeFormat === '12'
? DateTime.fromMillis(milliseconds).toUTC().toFormat(formatString)
: millisToString(milliseconds, showSeconds);
return timeFormat === '12' ? formatFromMillis(milliseconds, formatString) : millisToString(milliseconds, showSeconds);
};