mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-09 01:13:55 +00:00
Feat/table part1 (#197)
* feat(csv): export data as csv file * feat(table): toggle fullscreen * ux: coordinate tooltip open delay * feat(excelDates): update tests * refactor: folder structure
This commit is contained in:
@@ -7,6 +7,8 @@ import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
|
||||
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
export default function ActionButtons(props) {
|
||||
const { showAdd, showDelay, showBlock, actionHandler } = props;
|
||||
|
||||
@@ -17,7 +19,7 @@ export default function ActionButtons(props) {
|
||||
|
||||
return (
|
||||
<Menu isLazy lazyBehavior='unmount'>
|
||||
<Tooltip label='Add ...' delay={500}>
|
||||
<Tooltip label='Add ...' delay={tooltipDelayMid}>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label='Options'
|
||||
|
||||
@@ -4,10 +4,12 @@ import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IoPause } from '@react-icons/all-files/io5/IoPause';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
export default function PauseIconBtn(props) {
|
||||
const { clickhandler, active, disabled, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Pause timer' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<Tooltip label='Pause timer' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPause size='24px' />}
|
||||
colorScheme='orange'
|
||||
|
||||
@@ -4,10 +4,12 @@ import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
export default function RollIconBtn(props) {
|
||||
const { clickhandler, active, disabled, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Roll mode' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<Tooltip label='Roll mode' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoTimeOutline size='24px' />}
|
||||
colorScheme='blue'
|
||||
|
||||
@@ -4,10 +4,12 @@ import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
export default function StartIconBtn(props) {
|
||||
const { clickhandler, active, disabled, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Start timer' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<Tooltip label='Start timer' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPlay size='24px' />}
|
||||
colorScheme='green'
|
||||
|
||||
@@ -4,9 +4,9 @@ import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function TooltipActionBtn(props) {
|
||||
const { clickHandler, icon, color, size='xs', tooltip, ...rest } = props;
|
||||
const { clickHandler, icon, color, size='xs', tooltip, openDelay = 0, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label={tooltip}>
|
||||
<Tooltip label={tooltip} openDelay={openDelay}>
|
||||
<IconButton
|
||||
aria-label={tooltip}
|
||||
size={size}
|
||||
@@ -23,5 +23,6 @@ TooltipActionBtn.propTypes = {
|
||||
icon: PropTypes.element,
|
||||
color: PropTypes.string,
|
||||
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
|
||||
tooltip: PropTypes.string
|
||||
tooltip: PropTypes.string,
|
||||
openDelay: PropTypes.number
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
export default function TransportIconBtn(props) {
|
||||
const { clickHandler, icon, tooltip, disabled, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label={tooltip} openDelay={500} shouldWrapChildren={disabled}>
|
||||
<Tooltip label={tooltip} openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={icon}
|
||||
colorScheme='white'
|
||||
|
||||
@@ -4,10 +4,12 @@ import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
export default function UnloadIconBtn(props) {
|
||||
const { clickHandler, disabled, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Unload event' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<Tooltip label='Unload event' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoStop size='22px' />}
|
||||
colorScheme='red'
|
||||
|
||||
@@ -2,11 +2,14 @@ import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Image } from '@chakra-ui/react';
|
||||
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
||||
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
||||
import navlogo from 'assets/images/logos/LOGO-72.png';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import useFullscreen from '../../hooks/useFullscreen';
|
||||
|
||||
import navigatorConstants from './navigatorConstants';
|
||||
|
||||
import style from './NavLogo.module.scss';
|
||||
@@ -22,6 +25,7 @@ const navButtonStyle = {
|
||||
export default function NavLogo(props) {
|
||||
const { isHidden } = props;
|
||||
const [showNav, setShowNav] = useState(false);
|
||||
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setShowNav((prev) => !prev);
|
||||
@@ -37,16 +41,6 @@ export default function NavLogo(props) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleFullscreen = useCallback(() => {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen();
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// attach the event listener
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
@@ -83,8 +77,8 @@ export default function NavLogo(props) {
|
||||
>
|
||||
<IconButton
|
||||
aria-label='Toggle Fullscreen'
|
||||
icon={<IoExpand />}
|
||||
onClick={toggleFullscreen}
|
||||
icon={isFullScreen ? <IoContract /> : <IoExpand />}
|
||||
onClick={toggleFullScreen}
|
||||
{...navButtonStyle}
|
||||
/>
|
||||
</motion.div>
|
||||
@@ -96,10 +90,7 @@ export default function NavLogo(props) {
|
||||
className={style.nav}
|
||||
>
|
||||
{navigatorConstants.map((route) => (
|
||||
<Link
|
||||
to={route.url}
|
||||
key={route.url}
|
||||
{...tabProps}>
|
||||
<Link to={route.url} key={route.url} {...tabProps}>
|
||||
{route.label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
|
||||
export default function useFullscreen() {
|
||||
const [isFullScreen, setFullScreen] = useState(document.fullscreenElement);
|
||||
|
||||
useEffect(() => {
|
||||
const handleChange = () => {
|
||||
setFullScreen(document.fullscreenElement);
|
||||
};
|
||||
document.addEventListener('fullscreenchange', handleChange, { passive: true });
|
||||
document.addEventListener('resize', handleChange, { passive: true });
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('fullscreenchange', handleChange, { passive: true });
|
||||
document.removeEventListener('resize', handleChange, { passive: true });
|
||||
}; }, []);
|
||||
|
||||
const toggleFullScreen = useCallback(() => {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen();
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
setFullScreen(document.fullscreenElement);
|
||||
}, []);
|
||||
|
||||
return { isFullScreen, toggleFullScreen };
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import { mth, mtm, mts } from './timeConstants';
|
||||
|
||||
export const timeFormat = 'HH:mm';
|
||||
export const timeFormatSeconds = 'HH:mm:ss';
|
||||
|
||||
const mts = 1000; // millis to seconds
|
||||
const mtm = 1000 * 60; // millis to minutes
|
||||
const mth = 1000 * 60 * 60; // millis to hours
|
||||
|
||||
/**
|
||||
* another go at simpler string formatting (counters)
|
||||
* @description Converts seconds to string representing time
|
||||
|
||||
@@ -3,9 +3,8 @@ import { DateTime } from 'luxon';
|
||||
import { ontimeQueryClient } from '../../App';
|
||||
import { APP_SETTINGS } from '../api/apiConstants';
|
||||
|
||||
const mts = 1000; // millis to seconds
|
||||
const mtm = 1000 * 60; // millis to minutes
|
||||
const mth = 1000 * 60 * 60; // millis to hours
|
||||
import { mth, mtm, mts } from './timeConstants';
|
||||
|
||||
|
||||
/**
|
||||
* Returns current time in milliseconds
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* millis to seconds
|
||||
* @type {number}
|
||||
*/
|
||||
export const mts = 1000;
|
||||
|
||||
/**
|
||||
* millis to minutes
|
||||
* @type {number}
|
||||
*/
|
||||
export const mtm = 1000 * 60;
|
||||
|
||||
|
||||
/**
|
||||
* millis to hours
|
||||
* @type {number}
|
||||
*/
|
||||
export const mth = 1000 * 60 * 60;
|
||||
|
||||
/**
|
||||
* milliseconds in a day
|
||||
* @type {number}
|
||||
*/
|
||||
export const DAY_TO_MS = 86400000;
|
||||
@@ -5,13 +5,15 @@ import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
import style from './MessageControl.module.scss';
|
||||
|
||||
export default function InputRow(props) {
|
||||
const { label, placeholder, text, visible, actionHandler, changeHandler } = props;
|
||||
|
||||
return (
|
||||
<div className={visible && style.inputRowActive}>
|
||||
<div className={`${visible ? style.inputRowActive: ''}`}>
|
||||
<span className={style.label}>{label}</span>
|
||||
<div className={style.inputItems}>
|
||||
<Editable
|
||||
@@ -21,10 +23,10 @@ export default function InputRow(props) {
|
||||
className={style.inline}
|
||||
color={text === '' ? '#666' : 'inherit'}
|
||||
>
|
||||
<EditablePreview className={style.padleft} />
|
||||
<EditablePreview className={`${style.padleft} ${style.fullWidth}`} />
|
||||
<EditableInput className={style.padleft} />
|
||||
</Editable>
|
||||
<Tooltip label={visible ? 'Make invisible' : 'Make visible'} openDelay={500}>
|
||||
<Tooltip label={visible ? 'Make invisible' : 'Make visible'} openDelay={tooltipDelayMid}>
|
||||
<IconButton
|
||||
aria-label='Toggle visibility'
|
||||
size='sm'
|
||||
|
||||
@@ -5,6 +5,8 @@ import { IoMicOffOutline } from '@react-icons/all-files/io5/IoMicOffOutline';
|
||||
import { IoMicSharp } from '@react-icons/all-files/io5/IoMicSharp';
|
||||
import { useSocket } from 'common/context/socketContext';
|
||||
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
import InputRow from './InputRow';
|
||||
|
||||
import style from './MessageControl.module.scss';
|
||||
@@ -122,7 +124,7 @@ export default function MessageControl() {
|
||||
/>
|
||||
</div>
|
||||
<div className={style.onAirToggle}>
|
||||
<Tooltip label={onAir ? 'Go Off Air' : 'Go On Air'} openDelay={500}>
|
||||
<Tooltip label={onAir ? 'Go Off Air' : 'Go On Air'} openDelay={tooltipDelayMid}>
|
||||
<IconButton
|
||||
className={style.btn}
|
||||
size='md'
|
||||
|
||||
@@ -36,10 +36,15 @@
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inputRowActive {
|
||||
.label {
|
||||
color: $light-bg;
|
||||
}
|
||||
|
||||
.inline {
|
||||
background-color: $light-bg-transparent;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import TimerDisplay from 'common/components/countdown/TimerDisplay';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { stringFromMillis } from '../../../common/utils/time';
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
import style from './PlaybackControl.module.scss';
|
||||
|
||||
@@ -69,7 +70,7 @@ const PlaybackTimer = (props) => {
|
||||
</>
|
||||
)}
|
||||
<div className={style.btn}>
|
||||
<Tooltip label='Remove 1 minute' openDelay={500} shouldWrapChildren={disableButtons}>
|
||||
<Tooltip label='Remove 1 minute' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
||||
<Button
|
||||
{...incrementProps}
|
||||
disabled={disableButtons}
|
||||
@@ -79,7 +80,7 @@ const PlaybackTimer = (props) => {
|
||||
-1
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip label='Add 1 minute' openDelay={500} shouldWrapChildren={disableButtons}>
|
||||
<Tooltip label='Add 1 minute' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
||||
<Button
|
||||
{...incrementProps}
|
||||
disabled={disableButtons}
|
||||
@@ -89,7 +90,7 @@ const PlaybackTimer = (props) => {
|
||||
+1
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip label='Remove 5 minutes' openDelay={500} shouldWrapChildren={disableButtons}>
|
||||
<Tooltip label='Remove 5 minutes' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
||||
<Button
|
||||
{...incrementProps}
|
||||
disabled={disableButtons}
|
||||
@@ -99,7 +100,7 @@ const PlaybackTimer = (props) => {
|
||||
-5
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip label='Add 5 minutes' openDelay={500} shouldWrapChildren={disableButtons}>
|
||||
<Tooltip label='Add 5 minutes' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
||||
<Button
|
||||
{...incrementProps}
|
||||
disabled={disableButtons}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
defaultPublicAtom,
|
||||
startTimeIsLastEndAtom,
|
||||
} from '../../../common/atoms/LocalEventSettings';
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
import style from './EntryBlock.module.scss';
|
||||
|
||||
@@ -35,7 +36,7 @@ export default function EntryBlock(props) {
|
||||
|
||||
return (
|
||||
<div className={`${style.create} ${visible ? style.visible : ''}`}>
|
||||
<Tooltip label='Add Event' openDelay={300}>
|
||||
<Tooltip label='Add Event' openDelay={tooltipDelayMid}>
|
||||
<span
|
||||
className={style.createEvent}
|
||||
onClick={() =>
|
||||
@@ -50,7 +51,7 @@ export default function EntryBlock(props) {
|
||||
E{showKbd && <span className={style.keyboard}>Alt + E</span>}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip label='Add Delay' openDelay={300}>
|
||||
<Tooltip label='Add Delay' openDelay={tooltipDelayMid}>
|
||||
<span
|
||||
className={`${style.createDelay} ${disableAddDelay ? style.disabled : ''}`}
|
||||
onClick={() => eventsHandler('add', { type: 'delay', after: previousId })}
|
||||
@@ -59,7 +60,7 @@ export default function EntryBlock(props) {
|
||||
D{showKbd && <span className={style.keyboard}>Alt + D</span>}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip label='Add Block' openDelay={300}>
|
||||
<Tooltip label='Add Block' openDelay={tooltipDelayMid}>
|
||||
<span
|
||||
className={`${style.createBlock} ${disableAddBlock ? style.disabled : ''}`}
|
||||
onClick={() => eventsHandler('add', { type: 'block', after: previousId })}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { getAliases, postAliases } from '../../common/api/ontimeApi';
|
||||
import { LoggingContext } from '../../common/context/LoggingContext';
|
||||
import { validateAlias } from '../../common/utils/aliases';
|
||||
import { handleLinks, host } from '../../common/utils/linkUtils';
|
||||
import { tooltipDelayFast } from '../../ontimeConfig';
|
||||
|
||||
import SubmitContainer from './SubmitContainer';
|
||||
|
||||
@@ -252,7 +253,7 @@ export default function AliasesModal() {
|
||||
isInvalid={alias.urlError}
|
||||
onChange={(event) => handleChange(index, 'pathAndParams', event.target.value)}
|
||||
/>
|
||||
<Tooltip label={`Test /${alias.pathAndParams}`} openDelay={500}>
|
||||
<Tooltip label={`Test /${alias.pathAndParams}`} openDelay={tooltipDelayFast}>
|
||||
<a
|
||||
href='#!'
|
||||
target='_blank'
|
||||
@@ -260,7 +261,7 @@ export default function AliasesModal() {
|
||||
onClick={(e) => handleLinks(e, alias.pathAndParams)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label='Enable alias' openDelay={500}>
|
||||
<Tooltip label='Enable alias' openDelay={tooltipDelayFast}>
|
||||
<IconButton
|
||||
aria-label='Enable alias'
|
||||
size='xs'
|
||||
@@ -270,7 +271,7 @@ export default function AliasesModal() {
|
||||
onClick={() => setEnabled(alias.id, !alias.enabled)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label='Delete alias' openDelay={500}>
|
||||
<Tooltip label='Delete alias' openDelay={tooltipDelayFast}>
|
||||
<IconButton
|
||||
aria-label='Delete alias'
|
||||
size='xs'
|
||||
|
||||
@@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { TableSettingsContext } from '../../common/context/TableSettingsContext';
|
||||
import { useLocalStorage } from '../../common/hooks/useLocalStorage';
|
||||
import { tooltipDelayFast } from '../../ontimeConfig';
|
||||
|
||||
import SortableCell from './tableElements/SortableCell';
|
||||
import TableSettings from './tableElements/TableSettings';
|
||||
@@ -184,7 +185,7 @@ export default function OntimeTable({ tableData, userFields, handleUpdate, selec
|
||||
>
|
||||
<tr {...restHeaderGroupProps}>
|
||||
<th className={style.indexColumn}>
|
||||
<Tooltip label='Event Order' openDelay={300}>
|
||||
<Tooltip label='Event Order' openDelay={tooltipDelayFast}>
|
||||
#
|
||||
</Tooltip>
|
||||
</th>
|
||||
|
||||
@@ -222,6 +222,12 @@ $text-theme-dark: white;
|
||||
background-color: $cell-theme-light;
|
||||
border: 1px solid $bg-theme-light;
|
||||
}
|
||||
.actionText:hover,
|
||||
.actionIcon:hover,
|
||||
.actionDisabled:hover {
|
||||
color: black;
|
||||
transition: 300ms;
|
||||
}
|
||||
}
|
||||
|
||||
.tableWrapper__dark {
|
||||
@@ -237,6 +243,12 @@ $text-theme-dark: white;
|
||||
background-color: $cell-theme-dark;
|
||||
border: 1px solid $bg-theme-dark;
|
||||
}
|
||||
.actionText:hover,
|
||||
.actionIcon:hover,
|
||||
.actionDisabled:hover {
|
||||
color: white;
|
||||
transition: 300ms;
|
||||
}
|
||||
}
|
||||
|
||||
.timer {
|
||||
@@ -257,12 +269,21 @@ svg {
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
.actionIcon {
|
||||
@mixin action-element() {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.actionIcon {
|
||||
@include action-element();
|
||||
}
|
||||
|
||||
.actionText {
|
||||
@include action-element();
|
||||
font-size: 0.65em;
|
||||
}
|
||||
|
||||
.actionDisabled {
|
||||
cursor: pointer;
|
||||
@include action-element();
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,32 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Divider } from '@chakra-ui/layout';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiSettings } from '@react-icons/all-files/fi/FiSettings';
|
||||
import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
|
||||
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
||||
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
||||
import { IoMoon } from '@react-icons/all-files/io5/IoMoon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { EVENT_TABLE } from '../../common/api/apiConstants';
|
||||
import { fetchEvent } from '../../common/api/eventApi';
|
||||
import { useSocket } from '../../common/context/socketContext';
|
||||
import { TableSettingsContext } from '../../common/context/TableSettingsContext';
|
||||
import { useFetch } from '../../common/hooks/useFetch';
|
||||
import useFullscreen from '../../common/hooks/useFullscreen';
|
||||
import { formatDisplay } from '../../common/utils/dateConfig';
|
||||
import { formatTime } from '../../common/utils/time';
|
||||
import { tooltipDelayFast } from '../../ontimeConfig';
|
||||
|
||||
import PlaybackIcon from './tableElements/PlaybackIcon';
|
||||
|
||||
import style from './Table.module.scss';
|
||||
|
||||
export default function TableHeader() {
|
||||
export default function TableHeader({ handleCSVExport }) {
|
||||
const { followSelected, showSettings, toggleTheme, toggleSettings, toggleFollow } =
|
||||
useContext(TableSettingsContext);
|
||||
const { data } = useFetch(EVENT_TABLE, fetchEvent);
|
||||
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
||||
|
||||
const socket = useSocket();
|
||||
const [timer, setTimer] = useState({
|
||||
@@ -101,6 +108,7 @@ export default function TableHeader() {
|
||||
showSeconds: true,
|
||||
format: 'hh:mm:ss a',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={style.header}>
|
||||
<div className={style.headerName}>{data?.title || ''}</div>
|
||||
@@ -121,22 +129,41 @@ export default function TableHeader() {
|
||||
<span className={style.timer}>{timeNow}</span>
|
||||
</div>
|
||||
<div className={style.headerActions}>
|
||||
<Tooltip openDelay={300} label='Follow selected'>
|
||||
<Tooltip openDelay={tooltipDelayFast} label='Follow selected'>
|
||||
<span className={followSelected ? style.actionIcon : style.actionDisabled}>
|
||||
<FiTarget onClick={() => toggleFollow()} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip openDelay={300} label='Show settings'>
|
||||
<Tooltip openDelay={tooltipDelayFast} label='Show settings'>
|
||||
<span className={showSettings ? style.actionIcon : style.actionDisabled}>
|
||||
<FiSettings onClick={() => toggleSettings()} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip openDelay={300} label='Toggle dark mode'>
|
||||
<Tooltip openDelay={tooltipDelayFast} label='Toggle dark mode'>
|
||||
<span className={style.actionIcon}>
|
||||
<IoMoon onClick={() => toggleTheme()} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip openDelay={tooltipDelayFast} label='Toggle Fullscreen'>
|
||||
<span className={style.actionIcon}>
|
||||
{isFullScreen ? (
|
||||
<IoContract onClick={() => toggleFullScreen()} />
|
||||
) : (
|
||||
<IoExpand onClick={() => toggleFullScreen()} />
|
||||
)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Divider />
|
||||
<Tooltip openDelay={tooltipDelayFast} label='Export to CSV'>
|
||||
<span className={style.actionText} onClick={() => handleCSVExport(data)}>
|
||||
CSV
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TableHeader.propTypes = {
|
||||
handleCSVExport: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import useMutateEvents from '../../common/hooks/useMutateEvents';
|
||||
|
||||
import OntimeTable from './OntimeTable';
|
||||
import TableHeader from './TableHeader';
|
||||
import { makeCSV, makeTable } from './utils';
|
||||
|
||||
import style from './Table.module.scss';
|
||||
|
||||
@@ -22,9 +23,7 @@ export default function TableWrapper() {
|
||||
const { theme } = useContext(TableSettingsContext);
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Cuesheet';
|
||||
}, []);
|
||||
document.title = 'ontime - Cuesheet';
|
||||
|
||||
/**
|
||||
* Handle incoming data from socket
|
||||
@@ -46,47 +45,68 @@ export default function TableWrapper() {
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
const handleUpdate = useCallback(async (rowIndex, accessor, payload) => {
|
||||
if (rowIndex == null || accessor == null || payload == null) {
|
||||
return;
|
||||
}
|
||||
const handleUpdate = useCallback(
|
||||
async (rowIndex, accessor, payload) => {
|
||||
if (rowIndex == null || accessor == null || payload == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if value is the same
|
||||
const event = tableData[rowIndex];
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
// check if value is the same
|
||||
const event = tableData[rowIndex];
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event[accessor] === payload) {
|
||||
return;
|
||||
}
|
||||
// check if value is valid
|
||||
// as of now, the fields do not have any validation
|
||||
if (typeof payload !== 'string') {
|
||||
return;
|
||||
}
|
||||
if (event[accessor] === payload) {
|
||||
return;
|
||||
}
|
||||
// check if value is valid
|
||||
// as of now, the fields do not have any validation
|
||||
if (typeof payload !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
// cleanup
|
||||
const cleanVal = payload.trim();
|
||||
const mutationObject = {
|
||||
id: event.id,
|
||||
[accessor]: cleanVal,
|
||||
};
|
||||
// cleanup
|
||||
const cleanVal = payload.trim();
|
||||
const mutationObject = {
|
||||
id: event.id,
|
||||
[accessor]: cleanVal,
|
||||
};
|
||||
|
||||
// submit
|
||||
try {
|
||||
await mutation.mutateAsync(mutationObject);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [mutation, tableData]);
|
||||
// submit
|
||||
try {
|
||||
await mutation.mutateAsync(mutationObject);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
[mutation, tableData]
|
||||
);
|
||||
|
||||
const exportHandler = useCallback(
|
||||
(headerData) => {
|
||||
if (!headerData || !tableData || !userFields) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sheetData = makeTable(headerData, tableData, userFields);
|
||||
const csvContent = makeCSV(sheetData);
|
||||
const encodedUri = encodeURI(csvContent);
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', encodedUri);
|
||||
link.setAttribute('download', 'ontime export.csv');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
},
|
||||
[tableData, userFields]
|
||||
);
|
||||
|
||||
if (typeof tableData === 'undefined' || typeof userFields === 'undefined') {
|
||||
return <span>loading...</span>;
|
||||
}
|
||||
return (
|
||||
<div className={theme === 'dark' ? style.tableWrapper__dark : style.tableWrapper}>
|
||||
<TableHeader />
|
||||
<TableHeader handleCSVExport={exportHandler} />
|
||||
<OntimeTable
|
||||
tableData={tableData}
|
||||
userFields={userFields}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`makeTable() returns array of arrays with given fields 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
"Ontime · Schedule Template",
|
||||
],
|
||||
Array [
|
||||
"Event Name",
|
||||
"",
|
||||
],
|
||||
Array [
|
||||
"Event URL",
|
||||
"",
|
||||
],
|
||||
Array [],
|
||||
Array [
|
||||
"Time Start",
|
||||
"Time End",
|
||||
"Event Title",
|
||||
"Presenter Name",
|
||||
"Event Subtitle",
|
||||
"Is Public? (x)",
|
||||
"Notes",
|
||||
"Colour",
|
||||
"user0:test",
|
||||
],
|
||||
Array [
|
||||
"00:00:00",
|
||||
"00:00:00",
|
||||
"test title 1",
|
||||
"",
|
||||
"",
|
||||
"x",
|
||||
"",
|
||||
"",
|
||||
"test",
|
||||
"test",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
],
|
||||
]
|
||||
`;
|
||||
@@ -0,0 +1,93 @@
|
||||
import { makeCSV, makeTable, parseField } from '../utils';
|
||||
|
||||
describe('parseField()', () => {
|
||||
it('returns a string from given millis on timeStart and TimeEnd', () => {
|
||||
const testData1 = 1000;
|
||||
const testData2 = 60000;
|
||||
expect(parseField('timeStart', testData1)).toBe('00:00:01');
|
||||
expect(parseField('timeEnd', testData2)).toBe('00:01:00');
|
||||
expect(parseField('timeEnd', testData2)).toBe('00:01:00');
|
||||
});
|
||||
|
||||
describe('returns an x when isPublic is truthy, empty string otherwise', () => {
|
||||
const testTruthy = [1, true, 'x', 'test'];
|
||||
const testFalsy = ['', null, undefined, false, 0];
|
||||
|
||||
testTruthy.forEach((value) => {
|
||||
test(`${value}`, () => {
|
||||
expect(parseField('isPublic', value)).toBe('x');
|
||||
});
|
||||
});
|
||||
testFalsy.forEach((value) => {
|
||||
test(`${value}`, () => {
|
||||
expect(parseField('isPublic', value)).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an empty string on undefined fields', () => {
|
||||
expect(parseField('presenter', undefined)).toBe('');
|
||||
});
|
||||
|
||||
describe('simply returns any other value in any other field', () => {
|
||||
const testFields = [
|
||||
{ field: 'nothing', value: 123 },
|
||||
{ field: 'title', value: 'test' },
|
||||
{ field: 'presenter', value: 'test' },
|
||||
{ field: 'subtitle', value: 'test' },
|
||||
{ field: 'notes', value: 'test' },
|
||||
{ field: 'colour', value: 'test' },
|
||||
{ field: 'user0', value: 'test' },
|
||||
{ field: 'user1', value: 'test' },
|
||||
{ field: 'user2', value: 'test' },
|
||||
{ field: 'user3', value: 'test' },
|
||||
{ field: 'user4', value: 'test' },
|
||||
{ field: 'user5', value: 'test' },
|
||||
{ field: 'user6', value: 'test' },
|
||||
{ field: 'user7', value: 'test' },
|
||||
{ field: 'user8', value: 'test' },
|
||||
{ field: 'user9', value: 'test' },
|
||||
];
|
||||
|
||||
testFields.forEach((testCase) => {
|
||||
test(`${testCase.field}:${testCase.value}`, () => {
|
||||
expect(parseField(testCase.field, testCase.value)).toBe(testCase.value);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeTable()', () => {
|
||||
it('returns array of arrays with given fields', () => {
|
||||
const headerData = {};
|
||||
const tableData = [
|
||||
{
|
||||
title: 'test title 1',
|
||||
presenter: '',
|
||||
timeStart: 0,
|
||||
timeEnd: 0,
|
||||
isPublic: 'x',
|
||||
user0: 'test',
|
||||
user1: 'test',
|
||||
},
|
||||
];
|
||||
const userFields = {
|
||||
user0: 'test',
|
||||
};
|
||||
|
||||
const table = makeTable(headerData, tableData, userFields);
|
||||
expect(table).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('make CSV()', () => {
|
||||
it('joins an array of arrays with commas and newlines', () => {
|
||||
const testdata = [['field'], ['after newline', 'after comma'], ['', 'after empty']];
|
||||
expect(makeCSV(testdata)).toMatchInlineSnapshot(`
|
||||
"data:text/csv;charset=utf-8,field
|
||||
after newline,after comma
|
||||
,after empty
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -6,12 +6,14 @@ import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
||||
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { tooltipDelayFast } from '../../../ontimeConfig';
|
||||
|
||||
export default function PlaybackIcon(props) {
|
||||
const { state } = props;
|
||||
|
||||
if (state === 'stop') {
|
||||
return (
|
||||
<Tooltip openDelay={300} label='Timer Stopped' shouldWrapChildren>
|
||||
<Tooltip openDelay={tooltipDelayFast} label='Timer Stopped' shouldWrapChildren>
|
||||
<IoStop />
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -19,7 +21,7 @@ export default function PlaybackIcon(props) {
|
||||
|
||||
if (state === 'start') {
|
||||
return (
|
||||
<Tooltip openDelay={300} label='Timer Playing' shouldWrapChildren>
|
||||
<Tooltip openDelay={tooltipDelayFast} label='Timer Playing' shouldWrapChildren>
|
||||
<IoPlay />
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -27,7 +29,7 @@ export default function PlaybackIcon(props) {
|
||||
|
||||
if (state === 'pause') {
|
||||
return (
|
||||
<Tooltip openDelay={300} label='Timer Paused' shouldWrapChildren>
|
||||
<Tooltip openDelay={tooltipDelayFast} label='Timer Paused' shouldWrapChildren>
|
||||
<IoPause />
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -35,7 +37,7 @@ export default function PlaybackIcon(props) {
|
||||
|
||||
if (state === 'roll') {
|
||||
return (
|
||||
<Tooltip openDelay={300} label='Timer Rolling' shouldWrapChildren>
|
||||
<Tooltip openDelay={tooltipDelayFast} label='Timer Rolling' shouldWrapChildren>
|
||||
<IoTimeOutline />
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { tooltipDelayFast } from '../../../ontimeConfig';
|
||||
|
||||
import styles from '../Table.module.scss';
|
||||
|
||||
export default function SortableCell({ column }) {
|
||||
@@ -29,7 +31,7 @@ export default function SortableCell({ column }) {
|
||||
return (
|
||||
<th {...restColumn} ref={setNodeRef} style={{...dragStyle}} className={isDragging ? styles.dragging: ''}>
|
||||
<div {...attributes} {...listeners}>
|
||||
<Tooltip label={column.Header} openDelay={300}>
|
||||
<Tooltip label={column.Header} openDelay={tooltipDelayFast}>
|
||||
{column.render('Header')}
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* @description parses a field for export
|
||||
* @param {string} field
|
||||
* @param {*} data
|
||||
* @return {string}
|
||||
*/
|
||||
import { stringFromMillis } from '../../common/utils/time';
|
||||
|
||||
export const parseField = (field, data) => {
|
||||
let val;
|
||||
switch (field) {
|
||||
case 'timeStart':
|
||||
case 'timeEnd':
|
||||
val = stringFromMillis(data);
|
||||
break;
|
||||
case 'isPublic':
|
||||
val = data ? 'x' : '';
|
||||
break;
|
||||
default:
|
||||
val = data;
|
||||
break;
|
||||
}
|
||||
if (typeof data === 'undefined') {
|
||||
return ''
|
||||
}
|
||||
return val;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Creates an array of arrays usable by xlsx for export
|
||||
* @param {object} headerData
|
||||
* @param {array} tableData
|
||||
* @param {object} userFields
|
||||
* @return {(string[])[]}
|
||||
*/
|
||||
export const makeTable = (headerData, tableData, userFields) => {
|
||||
const data = [
|
||||
['Ontime · Schedule Template'],
|
||||
['Event Name', headerData?.title || ''],
|
||||
['Event URL', headerData?.url || ''],
|
||||
[],
|
||||
];
|
||||
|
||||
const fieldOrder = [
|
||||
'timeStart',
|
||||
'timeEnd',
|
||||
'title',
|
||||
'presenter',
|
||||
'subtitle',
|
||||
'isPublic',
|
||||
'notes',
|
||||
'colour',
|
||||
'user0',
|
||||
'user1',
|
||||
'user2',
|
||||
'user3',
|
||||
'user4',
|
||||
'user5',
|
||||
'user6',
|
||||
'user7',
|
||||
'user8',
|
||||
'user9',
|
||||
];
|
||||
|
||||
const fieldTitles = [
|
||||
'Time Start',
|
||||
'Time End',
|
||||
'Event Title',
|
||||
'Presenter Name',
|
||||
'Event Subtitle',
|
||||
'Is Public? (x)',
|
||||
'Notes',
|
||||
'Colour',
|
||||
];
|
||||
|
||||
for (const field in userFields) {
|
||||
const fieldValue = userFields[field];
|
||||
const displayName = `${field}${
|
||||
fieldValue !== field && fieldValue !== '' ? `:${fieldValue}` : ''
|
||||
}`;
|
||||
fieldTitles.push(displayName);
|
||||
}
|
||||
|
||||
data.push(fieldTitles);
|
||||
|
||||
tableData.forEach((entry) => {
|
||||
const row = [];
|
||||
fieldOrder.forEach((field) => row.push(parseField(field, entry[field])));
|
||||
data.push(row);
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Converts an array of arrays to a csv file
|
||||
* @param {array[]} arrayOfArrays
|
||||
* @return {string}
|
||||
*/
|
||||
export const makeCSV = (arrayOfArrays) => {
|
||||
let csvData = 'data:text/csv;charset=utf-8,';
|
||||
arrayOfArrays.forEach((rowArray) => {
|
||||
const row = rowArray.join(',');
|
||||
csvData += `${row}\n`;
|
||||
});
|
||||
return csvData;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DAY_TO_MS } from '../../../../../../server/src/classes/classUtils';
|
||||
import { millisToSeconds } from '../../../../common/utils/dateConfig';
|
||||
import { DAY_TO_MS } from '../../../../common/utils/timeConstants';
|
||||
import { fetchTimerData, sanitiseTitle, timerMessages } from '../countdown.helpers';
|
||||
|
||||
describe('sanitiseTitle() function', () => {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const tooltipDelaySlow = 1000;
|
||||
export const tooltipDelayMid = 500
|
||||
export const tooltipDelayFast = 300;
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "1.6.0",
|
||||
"version": "1.7.0",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ import { router as ontimeRouter } from './routes/ontimeRouter.js';
|
||||
import { router as playbackRouter } from './routes/playbackRouter.js';
|
||||
|
||||
// Global Objects
|
||||
import { EventTimer } from './classes/EventTimer.js';
|
||||
import { EventTimer } from './classes/timer/EventTimer.js';
|
||||
// Start OSC server
|
||||
import { initiateOSC, shutdownOSCServer } from './controllers/OscController.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
@@ -3,10 +3,10 @@ import { Server } from 'socket.io';
|
||||
import { DAY_TO_MS, getSelectionByRoll, replacePlaceholder, updateRoll } from './classUtils.js';
|
||||
import { OSCIntegration } from './integrations/Osc.js';
|
||||
import { HTTPIntegration } from './integrations/Http.js';
|
||||
import { cleanURL } from '../utils/url.js';
|
||||
import getRandomName from '../utils/getRandomName.js';
|
||||
import { generateId } from '../utils/generate_id.js';
|
||||
import { stringFromMillis } from '../utils/time.js';
|
||||
import { cleanURL } from '../../utils/url.js';
|
||||
import getRandomName from '../../utils/getRandomName.js';
|
||||
import { generateId } from '../../utils/generate_id.js';
|
||||
import { stringFromMillis } from '../../utils/time.js';
|
||||
|
||||
/*
|
||||
* Class EventTimer adds functions specific to APP
|
||||
@@ -1,4 +1,4 @@
|
||||
import { stringFromMillis } from '../utils/time.js';
|
||||
import { stringFromMillis } from '../../utils/time.js';
|
||||
|
||||
/**
|
||||
* @description Implements simple countdown timer functions
|
||||
@@ -0,0 +1,28 @@
|
||||
import { parseExcelDate } from '../time';
|
||||
|
||||
describe('parseExcelDate', () => {
|
||||
it('parses a valid date string as expected from excel', () => {
|
||||
const millis = parseExcelDate('1899-12-30T07:00:00.000Z');
|
||||
expect(millis).not.toBe(0);
|
||||
});
|
||||
|
||||
describe('parses a time string that passes validation', () => {
|
||||
const validFields = ['10:00:00', '10:00'];
|
||||
validFields.forEach((field) => {
|
||||
it(`handles ${field}`, () => {
|
||||
const millis = parseExcelDate(field);
|
||||
expect(millis).not.toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('returns 0 on other strings', () => {
|
||||
const invalidFields = ['10', 'test', ''];
|
||||
invalidFields.forEach((field) => {
|
||||
it(`handles ${field}`, () => {
|
||||
const millis = parseExcelDate(field);
|
||||
expect(millis).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
parseSettings_v1,
|
||||
parseUserFields_v1,
|
||||
} from './parserUtils_v1.js';
|
||||
import { excelDateStringToMillis } from './time.js';
|
||||
import { parseExcelDate } from './time.js';
|
||||
import { generateId } from './generate_id.js';
|
||||
|
||||
export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
@@ -79,9 +79,9 @@ export const parseExcel_v1 = async (excelData) => {
|
||||
eventData.url = column;
|
||||
eventUrlNext = false;
|
||||
} else if (j === timeStartIndex) {
|
||||
event.timeStart = excelDateStringToMillis(column);
|
||||
event.timeStart = parseExcelDate(column);
|
||||
} else if (j === timeEndIndex) {
|
||||
event.timeEnd = excelDateStringToMillis(column);
|
||||
event.timeEnd = parseExcelDate(column);
|
||||
} else if (j === titleIndex) {
|
||||
event.title = column;
|
||||
} else if (j === presenterIndex) {
|
||||
|
||||
+104
-23
@@ -2,22 +2,6 @@ const mts = 1000; // millis to seconds
|
||||
const mtm = 1000 * 60; // millis to minutes
|
||||
const mth = 1000 * 60 * 60; // millis to hours
|
||||
|
||||
/**
|
||||
* Returns current time in milliseconds
|
||||
* @returns {number}
|
||||
*/
|
||||
export const nowInMillis = () => {
|
||||
const now = new Date();
|
||||
|
||||
// extract milliseconds since midnight
|
||||
let elapsed = now.getHours() * 3600000;
|
||||
elapsed += now.getMinutes() * 60000;
|
||||
elapsed += now.getSeconds() * 1000;
|
||||
elapsed += now.getMilliseconds();
|
||||
|
||||
return elapsed;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Converts milliseconds to string representing time
|
||||
* @param {number} ms - time in milliseconds
|
||||
@@ -51,17 +35,114 @@ export const stringFromMillis = (ms, showSeconds = true, delim = ':', ifNull = '
|
||||
|
||||
/**
|
||||
* @description Converts an excel date to milliseconds
|
||||
* @argument {string} excelDate - excel string date
|
||||
* @argument {string} date - excel string date
|
||||
* @returns {number} - time in milliseconds
|
||||
*/
|
||||
export const excelDateStringToMillis = (excelDate) => {
|
||||
export const dateToMillis = (date) => {
|
||||
const h = date.getHours();
|
||||
const m = date.getMinutes();
|
||||
const s = date.getSeconds();
|
||||
|
||||
return h * mth + m * mtm + s * mts;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Parses an excel date using the correct parser
|
||||
* @param {string} excelDate
|
||||
* @returns {number} - time in milliseconds
|
||||
|
||||
*/
|
||||
export const parseExcelDate = (excelDate) => {
|
||||
// attempt converting to date object
|
||||
const date = new Date(excelDate);
|
||||
if (date instanceof Date && !isNaN(date)) {
|
||||
const h = date.getHours();
|
||||
const m = date.getMinutes();
|
||||
const s = date.getSeconds();
|
||||
|
||||
return h * mth + m * mtm + s * mts;
|
||||
return dateToMillis(date);
|
||||
} else if (isTimeString(excelDate)) {
|
||||
return forgivingStringToMillis(excelDate);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const timeFormat = 'HH:mm';
|
||||
export const timeFormatSeconds = 'HH:mm:ss';
|
||||
|
||||
/**
|
||||
* @description Validates a time string
|
||||
* @param {string} string - time string "23:00:12"
|
||||
* @returns {boolean} string represents time
|
||||
*/
|
||||
export const isTimeString = (string) => {
|
||||
// ^ # Start of string
|
||||
// (?: # Try to match...
|
||||
// (?: # Try to match...
|
||||
// ([01]?\d|2[0-3]): # HH:
|
||||
// )? # (optionally).
|
||||
// ([0-5]?\d): # MM: (required)
|
||||
// )? # (entire group optional, so either HH:MM:, MM: or nothing)
|
||||
// ([0-5]?\d) # SS (required)
|
||||
// $ # End of string
|
||||
|
||||
const regex = /^(?:(?:([01]?\d|2[0-3])[:,.])?([0-5]?\d)[:,.])?([0-5]?\d)$/;
|
||||
return regex.test(string);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description safe parse string to int, copied from client code
|
||||
* @param valueAsString
|
||||
* @return {number}
|
||||
*/
|
||||
const parse = (valueAsString) => {
|
||||
const parsed = parseInt(valueAsString, 10);
|
||||
if (isNaN(parsed)) {
|
||||
return 0;
|
||||
}
|
||||
return Math.abs(parsed);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Parses a time string to millis, copied from client code
|
||||
* @param {string} value - time string
|
||||
* @param {boolean} fillLeft - autofill left = hours / right = seconds
|
||||
* @returns {number} - time string in millis
|
||||
*/
|
||||
export const forgivingStringToMillis = (value, fillLeft = true) => {
|
||||
let millis = 0;
|
||||
|
||||
// split string at known separators : , .
|
||||
const separatorRegex = /[\s,:.]+/;
|
||||
const [first, second, third] = value.split(separatorRegex);
|
||||
|
||||
if (first != null && second != null && third != null) {
|
||||
// if string has three sections, treat as [hours] [minutes] [seconds]
|
||||
millis = parse(first) * mth;
|
||||
millis += parse(second) * mtm;
|
||||
millis += parse(third) * mts;
|
||||
} else if (first != null && second == null && third == null) {
|
||||
// if string has one section,
|
||||
// could be a complete string like 121010 - 12:10:10
|
||||
if (first.length === 6) {
|
||||
const hours = first.substring(0, 2);
|
||||
const minutes = first.substring(2, 4);
|
||||
const seconds = first.substring(4);
|
||||
millis = parse(hours) * mth;
|
||||
millis += parse(minutes) * mtm;
|
||||
millis += parse(seconds) * mts;
|
||||
} else {
|
||||
// otherwise lets treat as [minutes]
|
||||
millis = parse(first) * mtm;
|
||||
}
|
||||
}
|
||||
if (first != null && second != null && third == null) {
|
||||
// if string has two sections
|
||||
if (fillLeft) {
|
||||
// treat as [hours] [minutes]
|
||||
millis = parse(first) * mth;
|
||||
millis += parse(second) * mtm;
|
||||
} else {
|
||||
// treat as [minutes] [seconds]
|
||||
millis = parse(first) * mtm;
|
||||
millis += parse(second) * mts;
|
||||
}
|
||||
}
|
||||
return millis;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user