v2 styling (#263)

* feat: implement progress bar on running event

* refactor: use event action hook

* refactor: align backend data

* style: v2 style tweaks

* style: v2 style tweaks + ts <Info>

* fix: fix delay increment calls

* style: v2 style tweaks + ts <Playback>

* style: v2 style tweaks + ts <EventEditor>

* style: v2 style tweaks + ts <MessageControl>

* fix: naming issue with message control socket endpoint

* chore: upgrade style dependencies

* style: v2 style tweaks + ts <MenuBar>

* style: v2 style tweaks + ts <RundownMenu>

* style: v2 style tweaks + ts <Playback>

* fix: add selected and next information to EventTimer

* style: v2 style tweaks + ts <EventBlock>

* style: v2 style tweaks + ts <DelayBlock>

* style: v2 style tweaks + ts <BlockBlock>

* style: v2 style tweaks + ts <QuickEntry>

* refactor: extract <TextInput> logic

* chore: upgrade build dependencies

* chore: folder structure refactor

* fix: issue with dependency path in electron

* chore: update version in demo db
This commit is contained in:
Carlos Valente
2022-11-27 14:56:09 +01:00
committed by GitHub
parent 3626b5b357
commit d486d78594
104 changed files with 3027 additions and 3217 deletions
@@ -1,27 +0,0 @@
import { useEffect, useRef } from 'react';
import { Textarea } from '@chakra-ui/react';
import autosize from 'autosize/dist/autosize';
export const AutoTextArea = (props) => {
const ref = useRef();
useEffect(() => {
const node = ref.current;
autosize(ref.current);
return () => {
autosize.destroy(node);
};
}, []);
return (
<Textarea
overflow='hidden'
w='100%'
resize='none'
ref={ref}
transition='height none'
{...props}
/>
);
};
@@ -1,106 +0,0 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Input, Textarea } from '@chakra-ui/react';
import PropTypes from 'prop-types';
export default function TextInput(props) {
const { isTextArea, size = 'sm', field, initialText = '', submitHandler } = props;
const inputRef = useRef(null);
const [text, setText] = useState(initialText);
useEffect(() => {
if (typeof initialText === 'undefined') {
setText('');
} else {
setText(initialText);
}
}, [initialText]);
/**
* @description Handles Input value change
* @param {string} newValue
*/
const handleChange = useCallback(
(newValue) => {
if (newValue !== text) {
setText(newValue);
}
},
[text]
);
/**
* @description Handles submit events
* @param {string} valueToSubmit
*/
const handleSubmit = useCallback(
(valueToSubmit) => {
// No need to update if it hasn't changed
if (valueToSubmit === initialText) {
return;
}
const cleanVal = valueToSubmit.trim();
submitHandler(field, cleanVal);
if (cleanVal !== valueToSubmit) {
setText(cleanVal);
}
},
[field, initialText, submitHandler]
);
/**
* @description Resets input value to given
*/
const resetValue = useCallback(async () => {
setText(initialText);
},[initialText])
/**
* @description Handles common keys for submit and cancel
* @param {KeyboardEvent} event
*/
const keyHandler = useCallback(
(event) => {
if (event.key === 'Escape') {
resetValue();
} else if (event.key === 'Enter') {
if (!isTextArea) {
handleSubmit(text);
}
}
},
[resetValue, isTextArea, handleSubmit, text]
);
return isTextArea ? (
<Textarea
ref={inputRef}
size={size}
variant='filled'
value={text}
onChange={(event) => handleChange(event.target.value)}
onBlur={(event) => handleSubmit(event.target.value)}
onKeyDown={(event) => keyHandler(event)}
data-testid='input-textarea'
/>
) : (
<Input
ref={inputRef}
size={size}
variant='filled'
value={text}
onChange={(event) => handleChange(event.target.value)}
onBlur={(event) => handleSubmit(event.target.value)}
onKeyDown={(event) => keyHandler(event)}
data-testid='input-textfield'
/>
);
}
TextInput.propTypes = {
isTextArea: PropTypes.bool,
size: PropTypes.string,
field: PropTypes.string.isRequired,
initialText: PropTypes.string,
submitHandler: PropTypes.func,
};
@@ -1,197 +0,0 @@
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { Button, Input, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react';
import { LoggingContext } from 'common/context/LoggingContext';
import { forgivingStringToMillis } from 'common/utils/dateConfig';
import { stringFromMillis } from 'common/utils/time';
import PropTypes from 'prop-types';
import { tooltipDelayFast } from '../../../ontimeConfig';
import style from './TimeInput.module.scss';
export default function TimeInput(props) {
const { name, submitHandler, time = 0, delay, placeholder, validationHandler, previousEnd } = props;
const { emitError } = useContext(LoggingContext);
const inputRef = useRef(null);
const [value, setValue] = useState('');
/**
* @description Resets input value to given
*/
const resetValue = useCallback(() => {
// Todo: check if change is necessary
try {
setValue(stringFromMillis(time + delay));
} catch (error) {
emitError(`Unable to parse date: ${error.text}`);
}
}, [delay, emitError, time]);
/**
* @description Selects input text on focus
*/
const handleFocus = useCallback(() => {
inputRef.current.select();
}, []);
/**
* @description Submit handler
* @param {string} newValue
*/
const handleSubmit = useCallback(
(newValue) => {
// Check if there is anything there
if (newValue === '') {
return false;
}
let newValMillis = 0;
// check for known aliases
if (newValue === 'p' || newValue === 'prev' || newValue === 'previous') {
// string to pass should be the time of the end before
if (previousEnd != null) {
newValMillis = previousEnd;
}
} else if (
newValue.startsWith('+') ||
newValue.startsWith('p+') ||
newValue.startsWith('p +')
) {
// string to pass should add to the end before
const val = newValue.substring(1);
newValMillis = previousEnd + forgivingStringToMillis(val);
} else {
// convert entered value to milliseconds
newValMillis = forgivingStringToMillis(newValue);
}
// Time now and time submittedVal
const originalMillis = time + delay;
// check if time is different from before
if (newValMillis === originalMillis) return false;
// validate with parent
if (!validationHandler(name, newValMillis)) return false;
// update entry
submitHandler(name, newValMillis);
return true;
},
[delay, name, previousEnd, submitHandler, time, validationHandler]
);
/**
* @description Prepare time fields
* @param {string} value string to be parsed
*/
const validateAndSubmit = useCallback(
(newValue) => {
const success = handleSubmit(newValue);
if (success) {
const ms = forgivingStringToMillis(newValue);
setValue(stringFromMillis(ms + delay));
} else {
resetValue();
}
},
[delay, handleSubmit, resetValue]
);
/**
* @description Handles common keys for submit and cancel
* @param {KeyboardEvent} event
*/
const onKeyDownHandler = useCallback(
(event) => {
if (event.key === 'Enter') {
inputRef.current.blur();
validateAndSubmit(event.target.value);
} else if (event.key === 'Tab') {
validateAndSubmit(event.target.value);
}
if (event.key === 'Escape') {
inputRef.current.blur();
resetValue();
}
},
[resetValue, validateAndSubmit]
);
useEffect(() => {
if (time == null) return;
resetValue();
}, [emitError, resetValue, time]);
const isDelayed = delay != null && delay !== 0;
/*
<IconButton
size='sm'
icon="s"
aria-label='automate'
colorScheme='white'
style={{ borderRadius: '2px', width: 'min-content' }}
tabIndex={-1}
variant='ghost'
/>
*/
const buttonInitial = () => {
if (name === 'timeStart') return 'S';
if (name === 'timeEnd') return 'E';
if (name === 'duration') return 'D';
}
const buttonTooltip = () => {
if (name === 'timeStart') return 'Start';
if (name === 'timeEnd') return 'End';
if (name === 'duration') return 'Duration';
}
return (
<InputGroup size='sm' className={`${style.timeInput} ${isDelayed ? style.delayed : ''}`}>
<InputLeftElement width='fit-content'>
<Tooltip label={buttonTooltip()} openDelay={tooltipDelayFast}>
<Button
size='sm'
variant='filled'
tabIndex={-1}
backgroundColor='#303030'
color='#fffffa'
borderRadius='2px 0 0 2px'
border={isDelayed ? "1px solid #d69e2e55" : "1px solid transparent"}
>
{buttonInitial()}
</Button>
</Tooltip>
</InputLeftElement>
<Input
ref={inputRef}
data-testid='time-input'
className={style.inputField}
type='text'
placeholder={placeholder}
variant='filled'
onFocus={handleFocus}
onChange={(event) => setValue(event.target.value)}
onBlur={resetValue}
onKeyDown={onKeyDownHandler}
value={value}
maxLength={8}
/>
</InputGroup>
);
}
TimeInput.propTypes = {
name: PropTypes.string,
submitHandler: PropTypes.func,
time: PropTypes.number,
delay: PropTypes.number,
placeholder: PropTypes.string,
validationHandler: PropTypes.func,
previousEnd: PropTypes.number,
};
@@ -1,47 +0,0 @@
@use '../../../theme/main' as *;
@mixin input-field {
border: 1px solid transparent;
background-color: $input-bg;
font-size: 1em;
letter-spacing: 1px;
&:hover {
background-color: $input-hover-bg;
}
}
.timeInput {
width: fit-content !important;
.inputField {
@include input-field;
width: 7.5em;
padding: 0 0 0 2.6em;
}
&.delayed {
.inputField {
border: $input-delayed-border;
background-color: $input-bg-delayed;
&:hover {
background-color: $input-hover-bg-delayed;
}
}
}
}
.delayInput {
display: flex;
align-items: center;
.inputField {
@include input-field;
}
.label {
padding-left: 8px;
font-size: 14px;
}
}
@@ -0,0 +1,34 @@
import { useEffect, useRef } from 'react';
import { Textarea, TextareaProps } from '@chakra-ui/react';
// @ts-expect-error no types from library
import autosize from 'autosize/dist/autosize';
interface AutoTextAreaProps extends TextareaProps {
isDark?: boolean;
}
export const AutoTextArea = (props: AutoTextAreaProps) => {
const { isDark, ...rest } = props;
const ref = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
const node = ref.current;
autosize(ref.current);
return () => {
autosize.destroy(node);
};
}, []);
return (
<Textarea
overflow='hidden'
w='100%'
resize='none'
ref={ref}
transition='height none'
variant={isDark ? 'ontime-filled' : 'ontime-filled-onlight'}
{...rest}
/>
);
};
@@ -1,5 +1,3 @@
@use '../../../theme/main' as *;
input[type="color"] {
appearance: none;
cursor: pointer;
@@ -12,7 +12,7 @@ export default function ColourInput(props: ColourInputProps) {
return (
<Input
size='sm'
variant='filled'
variant='ontime-filled'
className={style.colourInput}
type='color'
value={value}
@@ -0,0 +1,9 @@
.delayInput {
display: flex;
gap: 8px;
align-items: center;
}
.inputField {
text-align: center;
}
@@ -3,24 +3,25 @@ import { Input } from '@chakra-ui/react';
import { clamp } from 'common/utils/math';
import PropTypes from 'prop-types';
import style from './TimeInput.module.scss';
import style from './DelayInput.module.scss';
const inputProps = {
const inputStyleProps = {
width: 20,
backgroundColor: 'rgba(255,255,255,0.13)',
color: '#fff',
border: '1px solid #ecc94b55',
variant: 'filled',
borderRadius: '3px',
placeholder: '-',
textAlign: 'center',
size: 'sm',
color: '#E69056',
variant: 'ontime-filled',
};
export default function DelayInput(props) {
const { submitHandler, value } = props;
interface DelayInputProps {
submitHandler: (value: number) => void;
value?: number;
}
export default function DelayInput(props: DelayInputProps) {
const { submitHandler, value = 0 } = props;
const [_value, setValue] = useState(value);
const inputRef = useRef(null);
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (value == null) return;
@@ -32,7 +33,8 @@ export default function DelayInput(props) {
* @param {string} value string to be parsed
*/
const validate = useCallback(
(newValue) => {
(newValue?: string) => {
console.log('debug', newValue, typeof newValue);
if (newValue === '') setValue(0);
const delayValue = clamp(Number(newValue), -60, 60);
@@ -41,42 +43,42 @@ export default function DelayInput(props) {
submitHandler(delayValue);
},
[submitHandler, value]
[submitHandler, value],
);
/**
* @description Handles common keys for submit and cancel
* @param {KeyboardEvent} event
*/
const onKeyDownHandler = useCallback((event) => {
if (event.key === 'Enter') {
inputRef.current.blur();
validate(event.target.value);
} else if (event.key === 'Escape') {
inputRef.current.blur();
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 labelText = `${Math.abs(value) > 1 ? 'minutes' : 'minute'} ${
value >= 0 ? 'delayed' : 'ahead'
const labelText = `${Math.abs(value) !== 1 ? 'minutes' : 'minute'} ${
value !== undefined && value >= 0 ? 'delayed' : 'ahead'
}`;
return (
<div className={style.delayInput}>
<label className={style.delayInput}>
<Input
{...inputStyleProps}
ref={inputRef}
data-testid='delay-input'
className={style.inputField}
{...inputProps}
value={_value}
onChange={(event) => setValue(event.target.value)}
onBlur={() => setValue(value)}
onKeyDown={onKeyDownHandler}
onChange={(event) => setValue(Number(event.target.value))}
onBlur={(event) => validate(event.target.value)}
onKeyDown={(event) => onKeyDownHandler(event.key)}
type='number'
/>
<span className={style.label}>{labelText}</span>
</div>
{labelText}
</label>
);
}
@@ -0,0 +1,46 @@
import { useCallback, useRef } from 'react';
import { Input, Textarea } from '@chakra-ui/react';
import { Size } from '../../../models/UtilTypes';
import useReactiveTextInput from './useReactiveTextInput';
interface TextInputProps {
isTextArea?: boolean;
isFullHeight?: boolean;
size?: Size;
field: string;
initialText?: string;
submitHandler: (field: string, newValue: string) => void;
}
export default function TextInput(props: TextInputProps) {
const { isTextArea, isFullHeight, size = 'sm', field, initialText = '', submitHandler } = props;
const inputRef = useRef(null);
const submitCallback = useCallback((newValue: string) =>
submitHandler(field, newValue)
, [field]);
const textInputProps = useReactiveTextInput(initialText, submitCallback, { submitOnEnter: true });
const textAreaProps = useReactiveTextInput(initialText, submitCallback);
return isTextArea ? (
<Textarea
ref={inputRef}
size={size}
variant='ontime-filled'
{...textAreaProps}
style={{ height: isFullHeight ? '100%' : undefined }}
data-testid='input-textarea'
/>
) : (
<Input
ref={inputRef}
size={size}
variant='ontime-filled'
{...textInputProps}
data-testid='input-textfield'
/>
);
}
@@ -9,7 +9,8 @@ describe('TextInput component', () => {
it('renders correctly', () => {
const testField = 'test';
const testText = 'Test 123';
render(<TextInput field={testField} initialText={testText} />);
const submitHandler = vi.fn();
render(<TextInput field={testField} initialText={testText} submitHandler={submitHandler} />);
const input = screen.getByTestId('input-textfield');
expect(input).toBeInTheDocument();
@@ -19,7 +20,9 @@ describe('TextInput component', () => {
it('Handles renders as textarea', () => {
const testField = 'test';
const testText = 'Test 123';
render(<TextInput field={testField} initialText={testText} isTextArea />);
const submitHandler = vi.fn();
render(<TextInput field={testField} initialText={testText} isTextArea
submitHandler={submitHandler} />);
const input = screen.getByTestId('input-textarea');
expect(input).toBeInTheDocument();
@@ -87,7 +90,7 @@ describe('TextInput component', () => {
it('handles undefined value', () => {
const testField = 'test';
const expected = '';
render(<TextInput field={testField} />);
render(<TextInput field={testField} submitHandler={vi.fn()} />);
const input = screen.getByTestId('input-textfield');
expect(input).toBeInTheDocument();
expect(input).toHaveValue(expected);
@@ -0,0 +1,88 @@
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
interface UseReactiveTextInputReturn {
value: string;
onChange: (event: ChangeEvent) => void;
onBlur: (event: ChangeEvent) => void;
onKeyDown: (event: KeyboardEvent) => void;
}
export default function useReactiveTextInput(
initialText: string,
submitCallback: (newValue: string) => void,
options?: {
submitOnEnter?: boolean;
},
): UseReactiveTextInputReturn {
const [text, setText] = useState(initialText);
useEffect(() => {
if (typeof initialText === 'undefined') {
setText('');
} else {
setText(initialText);
}
}, [initialText]);
/**
* @description Handles Input value change
* @param {string} newValue
*/
const handleChange = useCallback(
(newValue: string) => {
if (newValue !== text) {
setText(newValue);
}
},
[text],
);
/**
* @description Handles submit events
* @param {string} valueToSubmit
*/
const handleSubmit = useCallback(
(valueToSubmit: string) => {
// No need to update if it hasn't changed
if (valueToSubmit === initialText) {
return;
}
const cleanVal = valueToSubmit.trim();
submitCallback(cleanVal);
if (cleanVal !== valueToSubmit) {
setText(cleanVal);
}
},
[initialText, submitCallback],
);
/**
* @description Handles common keys for submit and cancel
* @param {string} key
*/
const keyHandler = useCallback(
(key: string) => {
switch (key) {
case 'Escape':
setText(initialText);
break;
case 'Enter':
if (options?.submitOnEnter) {
handleSubmit(text);
}
break;
}
},
[initialText, handleSubmit, text],
);
return {
value: text,
onChange: (event) => handleChange((event.target as HTMLInputElement).value),
onBlur: (event) => handleSubmit((event.target as HTMLInputElement).value),
onKeyDown: (event) => keyHandler(event.key),
};
}
@@ -0,0 +1,174 @@
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { Button, Input, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react';
import { LoggingContext } from 'common/context/LoggingContext';
import { forgivingStringToMillis } from 'common/utils/dateConfig';
import { stringFromMillis } from 'common/utils/time';
import PropTypes from 'prop-types';
import { tooltipDelayFast } from '../../../../ontimeConfig';
import style from './TimeInput.module.scss';
export default function TimeInput(props) {
const {
name, submitHandler, time = 0, delay, placeholder, validationHandler, previousEnd,
} = props;
const { emitError } = useContext(LoggingContext);
const inputRef = useRef(null);
const [value, setValue] = useState('');
/**
* @description Resets input value to given
*/
const resetValue = useCallback(() => {
// Todo: check if change is necessary
try {
setValue(stringFromMillis(time + delay));
} catch (error) {
emitError(`Unable to parse date: ${error.text}`);
}
}, [delay, emitError, time]);
/**
* @description Selects input text on focus
*/
const handleFocus = useCallback(() => {
inputRef.current.select();
}, []);
/**
* @description Submit handler
* @param {string} newValue
*/
const handleSubmit = useCallback((newValue) => {
// Check if there is anything there
if (newValue === '') {
return false;
}
let newValMillis = 0;
// check for known aliases
if (newValue === 'p' || newValue === 'prev' || newValue === 'previous') {
// string to pass should be the time of the end before
if (previousEnd != null) {
newValMillis = previousEnd;
}
} else if (newValue.startsWith('+') || newValue.startsWith('p+') || newValue.startsWith('p +')) {
// string to pass should add to the end before
const val = newValue.substring(1);
newValMillis = previousEnd + forgivingStringToMillis(val);
} else {
// convert entered value to milliseconds
newValMillis = forgivingStringToMillis(newValue);
}
// Time now and time submittedVal
const originalMillis = time + delay;
// check if time is different from before
if (newValMillis === originalMillis) return false;
// validate with parent
if (!validationHandler(name, newValMillis)) return false;
// update entry
submitHandler(name, newValMillis);
return true;
}, [delay, name, previousEnd, submitHandler, time, validationHandler]);
/**
* @description Prepare time fields
* @param {string} value string to be parsed
*/
const validateAndSubmit = useCallback((newValue) => {
const success = handleSubmit(newValue);
if (success) {
const ms = forgivingStringToMillis(newValue);
setValue(stringFromMillis(ms + delay));
} else {
resetValue();
}
}, [delay, handleSubmit, resetValue]);
/**
* @description Handles common keys for submit and cancel
* @param {KeyboardEvent} event
*/
const onKeyDownHandler = useCallback((event) => {
if (event.key === 'Enter') {
inputRef.current.blur();
validateAndSubmit(event.target.value);
} else if (event.key === 'Tab') {
validateAndSubmit(event.target.value);
}
if (event.key === 'Escape') {
inputRef.current.blur();
resetValue();
}
}, [resetValue, validateAndSubmit]);
useEffect(() => {
if (time == null) return;
resetValue();
}, [emitError, resetValue, time]);
const isDelayed = delay != null && delay !== 0;
const ButtonInitial = () => {
if (name === 'timeStart') return 'S';
if (name === 'timeEnd') return 'E';
if (name === 'duration') return 'D';
};
const ButtonTooltip = () => {
if (name === 'timeStart') return 'Start';
if (name === 'timeEnd') return 'End';
if (name === 'duration') return 'Duration';
};
return (
<InputGroup size='sm' className={`${style.timeInput} ${isDelayed ? style.delayed : ''}`}>
<InputLeftElement width='fit-content'>
<Tooltip label={<ButtonTooltip />} openDelay={tooltipDelayFast} variant='ontime-ondark'>
<Button
size='sm'
variant='ontime-subtle-white'
className={`${style.inputButton} ${isDelayed ? style.delayed : ''}`}
tabIndex={-1}
border={isDelayed ? '1px solid #E69056' : '1px solid transparent'}
borderRight='1px solid transparent'
borderRadius='2px 0 0 2px'
>
<ButtonInitial />
</Button>
</Tooltip>
</InputLeftElement>
<Input
ref={inputRef}
data-testid='time-input'
className={style.inputField}
type='text'
placeholder={placeholder}
variant='ontime-filled'
onFocus={handleFocus}
onChange={(event) => setValue(event.target.value)}
onBlur={resetValue}
onKeyDown={onKeyDownHandler}
value={value}
maxLength={8}
/>
</InputGroup>
);
}
TimeInput.propTypes = {
name: PropTypes.string,
submitHandler: PropTypes.func,
time: PropTypes.number,
delay: PropTypes.number,
placeholder: PropTypes.string,
validationHandler: PropTypes.func,
previousEnd: PropTypes.number,
};
@@ -0,0 +1,19 @@
$input-font-size: 15px;
$input-delayed-border-color: #E69056;
.timeInput {
width: fit-content !important;
.inputField {
font-size: $input-font-size;
letter-spacing: 1px;
width: 7.5em;
padding: 0 0 0 2.6em;
}
&.delayed {
.inputField {
border: 1px solid $input-delayed-border-color;
}
}
}