diff --git a/client/package.json b/client/package.json index 89a21abaa..f3f7c9ae5 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "ontime-ui", - "version": "1.3.0", + "version": "1.6.0", "private": true, "dependencies": { "@chakra-ui/react": "^2.0.0-next.3", @@ -14,6 +14,8 @@ "axios": "^0.25.0", "color": "^4.2.3", "framer-motion": "^6.3.3", + "jotai": "^1.7.8", + "luxon": "^3.0.1", "react": "^18.1.0", "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.1.0", diff --git a/client/src/App.jsx b/client/src/App.jsx index e545b38d3..249adea40 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -11,7 +11,7 @@ import AppRouter from './AppRouter'; // Load Open Sans typeface require('typeface-open-sans'); -const queryClient = new QueryClient(); +export const ontimeQueryClient = new QueryClient(); function App() { @@ -46,7 +46,7 @@ function App() { return ( - +
diff --git a/client/src/common/api/ontimeApi.js b/client/src/common/api/ontimeApi.js index 91213cf8a..59d554f32 100644 --- a/client/src/common/api/ontimeApi.js +++ b/client/src/common/api/ontimeApi.js @@ -19,7 +19,12 @@ export const ontimePlaceholderInfo = { * @type {{pinCode: null}} */ export const ontimePlaceholderSettings = { + app: 'ontime', + version: 1, + serverPort: 4001, + lock: null, pinCode: null, + timeFormat: '24', }; /** @@ -223,9 +228,7 @@ export const downloadEvents = async () => { filename = headerLine.substring(startFileNameIndex, endFileNameIndex); } - const url = window.URL.createObjectURL( - new Blob([response.data], { type: 'application/json' }) - ); + const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/json' })); const link = document.createElement('a'); link.href = url; link.setAttribute('download', filename); @@ -252,4 +255,5 @@ export const uploadEvents = async (file) => { * @description HTTP request to upload events * @return {Promise} */ -export const uploadEventsWithPath = async (filepath) => axios.post(`${ontimeURL}/dbpath`, { path: filepath }); +export const uploadEventsWithPath = async (filepath) => + axios.post(`${ontimeURL}/dbpath`, { path: filepath }); diff --git a/client/src/common/atoms/LocalEventSettings.js b/client/src/common/atoms/LocalEventSettings.js new file mode 100644 index 000000000..9e02d372a --- /dev/null +++ b/client/src/common/atoms/LocalEventSettings.js @@ -0,0 +1,20 @@ +import { atomWithStorage, selectAtom } from 'jotai/utils'; + +export const eventSettingsAtom = atomWithStorage('ontime-eventSettings', { + showQuickEntry: false, + startTimeIsLastEnd: false, + defaultPublic: false, +}); + +export const showQuickEntryAtom = selectAtom( + eventSettingsAtom, + (settings) => settings.showQuickEntry +); +export const startTimeIsLastEndAtom = selectAtom( + eventSettingsAtom, + (settings) => settings.startTimeIsLastEnd +); +export const defaultPublicAtom = selectAtom( + eventSettingsAtom, + (settings) => settings.defaultPublic +); diff --git a/client/src/common/components/views/TodayItem.jsx b/client/src/common/components/views/TodayItem.jsx index 5e6d1984d..1cbcf4e6f 100644 --- a/client/src/common/components/views/TodayItem.jsx +++ b/client/src/common/components/views/TodayItem.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { stringFromMillis } from '../../utils/time'; +import { formatTime } from '../../utils/time'; import style from './Paginator.module.scss'; @@ -9,8 +9,8 @@ export default function TodayItem(props) { const { selected, timeStart, timeEnd, title, backstageEvent, colour } = props; // Format timers - const start = stringFromMillis(timeStart, false) || ''; - const end = stringFromMillis(timeEnd, false) || ''; + const start = formatTime(timeStart, { format: 'hh:mm' }); + const end = formatTime(timeEnd, { format: 'hh:mm' }); // user colours const userColour = colour !== '' ? colour : 'transparent'; diff --git a/client/src/common/context/LocalEventSettingsContext.jsx b/client/src/common/context/LocalEventSettingsContext.jsx deleted file mode 100644 index 6d8624bfc..000000000 --- a/client/src/common/context/LocalEventSettingsContext.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, { createContext, useState } from 'react'; - -export const LocalEventSettingsContext = createContext({ - showQuickEntry: false, - starTimeIsLastEnd: true, - defaultPublic: true, - - setShowQuickEntry: () => undefined, - setStarTimeIsLastEnd: () => undefined, - setDefaultPublic: () => undefined, -}); - -export const LocalEventSettingsProvider = ({ children }) => { - const [showQuickEntry, setShowQuickEntry] = useState(false); - const [starTimeIsLastEnd, setStarTimeIsLastEnd] = useState(true); - const [defaultPublic, setDefaultPublic] = useState(false); - - return ( - - {children} - - ); -}; diff --git a/client/src/common/utils/__tests__/eventsManager.test.js b/client/src/common/utils/__tests__/eventsManager.test.js index 10ff97f87..b81a70097 100644 --- a/client/src/common/utils/__tests__/eventsManager.test.js +++ b/client/src/common/utils/__tests__/eventsManager.test.js @@ -373,10 +373,9 @@ describe('test formatEvents function', () => { isNext: false, colour: "", }, - ] - const parsed = formatEventList(testEvent, selectedId, nextId, true); + const parsed = formatEventList(testEvent, selectedId, nextId, { showEnd: true }); expect(parsed).toStrictEqual(expected); }); @@ -403,7 +402,7 @@ describe('test formatEvents function', () => { ] - const parsed = formatEventList(testEvent, selectedId, nextId, true); + const parsed = formatEventList(testEvent, selectedId, nextId, { showEnd: true }); expect(parsed).toStrictEqual(expected); }); @@ -431,7 +430,7 @@ describe('test formatEvents function', () => { ] - const parsed = formatEventList(testEvent, selectedId, nextId, true); + const parsed = formatEventList(testEvent, selectedId, nextId, { showEnd: true }); expect(parsed).toStrictEqual(expected); }); -}) \ No newline at end of file +}); diff --git a/client/src/common/utils/__tests__/time.test.js b/client/src/common/utils/__tests__/time.test.js new file mode 100644 index 000000000..856c10707 --- /dev/null +++ b/client/src/common/utils/__tests__/time.test.js @@ -0,0 +1,29 @@ +import { formatTime } from '../time'; + +describe('formatTime()', () => { + test('parses 24h strings', () => { + const ms = 13 * 60 * 60 * 1000; + const options = { + showSeconds: true, + format: 'irrelevant', + }; + const time = formatTime(ms, options, () => '24'); + expect(time).toStrictEqual('13:00:00'); + }); + + test('parses same string in 12h strings', () => { + const ms = 13 * 60 * 60 * 1000; + const options = { + showSeconds: true, + format: 'hh:mm:ss a', + }; + const time = formatTime(ms, options, () => '12'); + expect(time).toStrictEqual('01:00:00 PM'); + }); + + test('handles null times', () => { + const ms = null; + const time = formatTime(ms); + expect(time).toStrictEqual('...'); + }); +}); diff --git a/client/src/common/utils/eventsManager.js b/client/src/common/utils/eventsManager.js index 2c7a70138..408c81c3c 100644 --- a/client/src/common/utils/eventsManager.js +++ b/client/src/common/utils/eventsManager.js @@ -1,4 +1,4 @@ -import { stringFromMillis } from './time'; +import { formatTime } from './time'; /** * @description From a list of events, returns only events of type event with calculated delays @@ -58,27 +58,29 @@ export const trimEventlist = (events, selectedId, limit) => { * @param {Object[]} events - given events * @param {string} selectedId - id of currently selected event * @param {string} nextId - id of next event - * @param {boolean} [showEnd] - whether to show the end time + * @param {object} [options] + * @param {boolean} [options.showEnd] - whether to show the end time * @returns {Object[]} Formatted list of events [{time: -, title: -, isNow, isNext}] */ -export const formatEventList = (events, selectedId, nextId, showEnd = false) => { +export const formatEventList = (events, selectedId, nextId, options) => { if (events == null) return []; + const { showEnd = false } = options; const givenEvents = [...events]; // format list const formattedEvents = []; - for (const g of givenEvents) { - const start = stringFromMillis(g.timeStart, false); - const end = stringFromMillis(g.timeEnd, false); + for (const event of givenEvents) { + const start = formatTime(event.timeStart) + const end = formatTime(event.timeEnd); formattedEvents.push({ - id: g.id, + id: event.id, time: showEnd ? `${start} - ${end}` : start, - title: g.title, - isNow: g.id === selectedId, - isNext: g.id === nextId, - colour: g.colour, + title: event.title, + isNow: event.id === selectedId, + isNext: event.id === nextId, + colour: event.colour, }); } diff --git a/client/src/common/utils/time.js b/client/src/common/utils/time.js index 75fcc5db6..e7383203a 100644 --- a/client/src/common/utils/time.js +++ b/client/src/common/utils/time.js @@ -1,3 +1,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 @@ -26,12 +31,7 @@ export const nowInMillis = () => { * @param {string} ifNull - what to return if value is null * @returns {string} String representing time 00:12:02 */ -export const stringFromMillis = ( - ms, - showSeconds = true, - delim = ':', - ifNull = '...' -) => { +export const stringFromMillis = (ms, showSeconds = true, delim = ':', ifNull = '...') => { if (ms == null || isNaN(ms)) return ifNull; const isNegative = ms < 0 ? '-' : ''; const millis = Math.abs(ms); @@ -54,18 +54,34 @@ export const stringFromMillis = ( }; /** - * @description Converts an excel date to milliseconds - * @argument {string} excelDate - excel string date - * @returns {number} - time in milliseconds + * @description Resolves format from url and store + * @return {string|undefined} */ -export const excelDateStringToMillis = (excelDate) => { - const date = new Date(excelDate); - if (date instanceof Date && !isNaN(date)) { - const h = date.getHours(); - const m = date.getMinutes(); - const s = date.getSeconds(); +export const resolveTimeFormat = () => { + const params = new URL(document.location).searchParams; + const urlOptions = params.get('format'); + const settings = ontimeQueryClient.getQueryData(APP_SETTINGS); - return h * mth + m * mtm + s * mts; - } - return 0; + return urlOptions || settings?.timeFormat; +}; + +/** + /** + * @description utility function to format a date in 12 or 24 hour format + * @param {number} milliseconds + * @param {object} [options] + * @param {boolean} [options.showSeconds] + * @param {string} [options.format] + * @param {function} resolver + * @return {string} + */ +export const formatTime = (milliseconds, options, resolver = resolveTimeFormat) => { + if (milliseconds === null) { + return '...'; + } + const timeFormat = resolver(); + const { showSeconds = false, format: formatString = 'hh:mm a' } = options || {}; + return timeFormat === '12' + ? DateTime.fromMillis(milliseconds).toUTC().toFormat(formatString) + : stringFromMillis(milliseconds, showSeconds); }; diff --git a/client/src/features/editors/Editor.jsx b/client/src/features/editors/Editor.jsx index 6310a859b..00e59e9ef 100644 --- a/client/src/features/editors/Editor.jsx +++ b/client/src/features/editors/Editor.jsx @@ -1,10 +1,9 @@ -import React, { lazy, useEffect } from 'react'; +import React, { lazy } from 'react'; import { useDisclosure } from '@chakra-ui/hooks'; import { Box } from '@chakra-ui/layout'; import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary'; import ModalManager from 'features/modals/ModalManager'; -import { LocalEventSettingsProvider } from '../../common/context/LocalEventSettingsContext'; import { LoggingProvider } from '../../common/context/LoggingContext'; import MenuBar from '../menu/MenuBar'; @@ -19,28 +18,24 @@ export default function Editor() { const { isOpen, onOpen, onClose } = useDisclosure(); // Set window title - useEffect(() => { - document.title = 'ontime - Editor'; - }, []); + document.title = 'ontime - Editor'; return ( - - - - -
- - - - - - - - - -
-
+ + + +
+ + + + + + + + + +
); } diff --git a/client/src/features/editors/EntryBlock/EntryBlock.jsx b/client/src/features/editors/EntryBlock/EntryBlock.jsx index d0018f39a..8e89a09e4 100644 --- a/client/src/features/editors/EntryBlock/EntryBlock.jsx +++ b/client/src/features/editors/EntryBlock/EntryBlock.jsx @@ -1,9 +1,13 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Checkbox } from '@chakra-ui/react'; import { Tooltip } from '@chakra-ui/tooltip'; +import { useAtomValue } from 'jotai'; import PropTypes from 'prop-types'; -import { LocalEventSettingsContext } from '../../../common/context/LocalEventSettingsContext'; +import { + defaultPublicAtom, + startTimeIsLastEndAtom, +} from '../../../common/atoms/LocalEventSettings'; import style from './EntryBlock.module.scss'; @@ -16,13 +20,14 @@ export default function EntryBlock(props) { disableAddDelay = true, disableAddBlock, } = props; - const { starTimeIsLastEnd, defaultPublic } = useContext(LocalEventSettingsContext); - const [doStartTime, setStartTime] = useState(starTimeIsLastEnd); + const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom); + const defaultPublic = useAtomValue(defaultPublicAtom); + const [doStartTime, setStartTime] = useState(startTimeIsLastEnd); const [doPublic, setPublic] = useState(defaultPublic); useEffect(() => { - setStartTime(starTimeIsLastEnd); - }, [starTimeIsLastEnd]); + setStartTime(startTimeIsLastEnd); + }, [startTimeIsLastEnd]); useEffect(() => { setPublic(defaultPublic); @@ -36,7 +41,7 @@ export default function EntryBlock(props) { onClick={() => eventsHandler( 'add', - { type: 'event', after: previousId, isPublic: doPublic }, + { type: 'event', after: previousId, isPublic: doPublic }, { startIsLastEnd: doStartTime ? previousId : undefined } ) } @@ -68,7 +73,9 @@ export default function EntryBlock(props) { size='sm' colorScheme='blue' isChecked={doStartTime} - onChange={(e) => setStartTime(e.target.checked)} + onChange={(e) => { + setStartTime(e.target.checked); + }} > Start time is last end @@ -93,4 +100,3 @@ EntryBlock.propTypes = { disableAddDelay: PropTypes.bool, disableAddBlock: PropTypes.bool, }; - diff --git a/client/src/features/editors/list/EventList.jsx b/client/src/features/editors/list/EventList.jsx index 4a99d5a4b..8e7015e7c 100644 --- a/client/src/features/editors/list/EventList.jsx +++ b/client/src/features/editors/list/EventList.jsx @@ -2,9 +2,10 @@ import React, { createRef, useCallback, useContext, useEffect, useState } from ' import { DragDropContext, Droppable } from 'react-beautiful-dnd'; import Empty from 'common/components/state/Empty'; import { useSocket } from 'common/context/socketContext'; +import { useAtomValue } from 'jotai'; +import { showQuickEntryAtom } from '../../../common/atoms/LocalEventSettings'; import { CursorContext } from '../../../common/context/CursorContext'; -import { LocalEventSettingsContext } from '../../../common/context/LocalEventSettingsContext'; import EntryBlock from '../EntryBlock/EntryBlock'; import EventListItem from './EventListItem'; @@ -19,21 +20,24 @@ export default function EventList(props) { const [selectedId, setSelectedId] = useState(null); const [nextId, setNextId] = useState(null); const cursorRef = createRef(); - const { showQuickEntry } = useContext(LocalEventSettingsContext); + const showQuickEntry = useAtomValue(showQuickEntryAtom); - const insertAtCursor = useCallback((type, cursor) => { - if (cursor === -1) { - eventsHandler('add', { type: type }); - } else { - const previousEvent = events[cursor]; - const nextEvent = events[cursor + 1]; - if (type === 'event') { - eventsHandler('add', { type: type, after: previousEvent.id }); - } else if (previousEvent?.type !== type && nextEvent?.type !== type) { - eventsHandler('add', { type: type, after: previousEvent.id }); + const insertAtCursor = useCallback( + (type, cursor) => { + if (cursor === -1) { + eventsHandler('add', { type: type }); + } else { + const previousEvent = events[cursor]; + const nextEvent = events[cursor + 1]; + if (type === 'event') { + eventsHandler('add', { type: type, after: previousEvent.id }); + } else if (previousEvent?.type !== type && nextEvent?.type !== type) { + eventsHandler('add', { type: type, after: previousEvent.id }); + } } - } - },[events, eventsHandler]) + }, + [events, eventsHandler] + ); // Handle keyboard shortcuts const handleKeyPress = useCallback( @@ -54,19 +58,19 @@ export default function EventList(props) { if (e.key === 'e' || e.key === 'E') { e.preventDefault(); if (cursor == null) return; - insertAtCursor('event', cursor) + insertAtCursor('event', cursor); } // D if (e.key === 'd' || e.key === 'D') { e.preventDefault(); if (cursor == null) return; - insertAtCursor('delay', cursor) + insertAtCursor('delay', cursor); } // B if (e.key === 'b' || e.key === 'B') { e.preventDefault(); if (cursor == null) return; - insertAtCursor('block', cursor) + insertAtCursor('block', cursor); } } }, diff --git a/client/src/features/editors/list/EventListItem.jsx b/client/src/features/editors/list/EventListItem.jsx index 5349b9434..c5f1bb195 100644 --- a/client/src/features/editors/list/EventListItem.jsx +++ b/client/src/features/editors/list/EventListItem.jsx @@ -1,7 +1,11 @@ import React, { memo, useCallback, useContext } from 'react'; +import { useAtomValue } from 'jotai'; import PropTypes from 'prop-types'; -import { LocalEventSettingsContext } from '../../../common/context/LocalEventSettingsContext'; +import { + defaultPublicAtom, + startTimeIsLastEndAtom, +} from '../../../common/atoms/LocalEventSettings'; import { LoggingContext } from '../../../common/context/LoggingContext'; import BlockBlock from '../BlockBlock/BlockBlock'; import DelayBlock from '../DelayBlock/DelayBlock'; @@ -19,19 +23,11 @@ const areEqual = (prevProps, nextProps) => { }; const EventListItem = (props) => { - const { - type, - index, - eventIndex, - data, - selected, - next, - eventsHandler, - delay, - previousEnd, - } = props; + const { type, index, eventIndex, data, selected, next, eventsHandler, delay, previousEnd } = + props; const { emitError } = useContext(LoggingContext); - const { starTimeIsLastEnd, defaultPublic } = useContext(LocalEventSettingsContext); + const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom); + const defaultPublic = useAtomValue(defaultPublicAtom); /** * @description calculates duration from given options @@ -56,7 +52,7 @@ const EventListItem = (props) => { after: data.id, isPublic: defaultPublic, }, - { startIsLastEnd: starTimeIsLastEnd ? data.id : undefined } + { startIsLastEnd: startTimeIsLastEnd ? data.id : undefined } ); break; case 'delay': @@ -103,7 +99,7 @@ const EventListItem = (props) => { break; } }, - [calculateDuration, data, defaultPublic, emitError, eventsHandler, starTimeIsLastEnd] + [calculateDuration, data, defaultPublic, emitError, eventsHandler, startTimeIsLastEnd] ); switch (type) { @@ -147,5 +143,5 @@ EventListItem.propTypes = { next: PropTypes.bool, eventsHandler: PropTypes.func, delay: PropTypes.number, - previousEnd: PropTypes.number -} \ No newline at end of file + previousEnd: PropTypes.number, +}; diff --git a/client/src/features/modals/AppSettingsModal.jsx b/client/src/features/modals/AppSettingsModal.jsx index 57d137a30..32083ca3b 100644 --- a/client/src/features/modals/AppSettingsModal.jsx +++ b/client/src/features/modals/AppSettingsModal.jsx @@ -1,22 +1,32 @@ -import React, { useCallback, useContext, useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { IconButton } from '@chakra-ui/button'; import { ModalBody } from '@chakra-ui/modal'; -import { Checkbox, FormControl, FormLabel, Input, PinInput, PinInputField } from '@chakra-ui/react'; +import { + Checkbox, + FormControl, + FormLabel, + Input, + PinInput, + PinInputField, + Select, +} from '@chakra-ui/react'; import { FiEye } from '@react-icons/all-files/fi/FiEye'; import { FiX } from '@react-icons/all-files/fi/FiX'; import { APP_SETTINGS } from 'common/api/apiConstants'; import { getSettings, ontimePlaceholderSettings, postSettings } from 'common/api/ontimeApi'; import { useFetch } from 'common/hooks/useFetch'; +import { useAtom } from 'jotai'; +import { eventSettingsAtom } from '../../common/atoms/LocalEventSettings'; import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn'; -import { LocalEventSettingsContext } from '../../common/context/LocalEventSettingsContext'; import { LoggingContext } from '../../common/context/LoggingContext'; import { inputProps } from './modalHelper'; import SubmitContainer from './SubmitContainer'; import style from './Modals.module.scss'; -const version = require('../../../package.json').version + +const version = require('../../../package.json').version; export default function AppSettingsModal() { const { data, status, refetch } = useFetch(APP_SETTINGS, getSettings); @@ -26,18 +36,8 @@ export default function AppSettingsModal() { const [submitting, setSubmitting] = useState(false); const [hidePin, setHidePin] = useState(true); - const { - showQuickEntry, - setShowQuickEntry, - starTimeIsLastEnd, - setStarTimeIsLastEnd, - defaultPublic, - setDefaultPublic, - } = useContext(LocalEventSettingsContext); - - const [doShowQuickEntry, setDoShowQuickEntry] = useState(showQuickEntry); - const [doStarTimeIsLastEnd, setDoStarTimeIsLastEnd] = useState(starTimeIsLastEnd); - const [doDefaultPublic, setDoDefaultPublic] = useState(defaultPublic); + const [eventSettings, saveEventSettings] = useAtom(eventSettingsAtom); + const [formSettings, setFormSettings] = useState(eventSettings); /** * Set formdata from server state @@ -47,117 +47,72 @@ export default function AppSettingsModal() { if (changed) return; setFormData({ pinCode: data.pinCode, + timeFormat: data.timeFormat, }); }, [changed, data]); - /** - * Set formdata from context - */ - useEffect(() => { - if (showQuickEntry == null) return; - setDoShowQuickEntry(showQuickEntry); - }, [showQuickEntry]); - - useEffect(() => { - if (starTimeIsLastEnd == null) return; - setDoStarTimeIsLastEnd(starTimeIsLastEnd); - }, [starTimeIsLastEnd]); - - useEffect(() => { - if (defaultPublic == null) return; - setDoDefaultPublic(defaultPublic); - }, [defaultPublic]); - /** * Validate and submit data */ - const submitHandler = useCallback( - async (event) => { - event.preventDefault(); - setSubmitting(true); + const submitHandler = async (event) => { + event.preventDefault(); + setSubmitting(true); - // set context - setShowQuickEntry(doShowQuickEntry); - setStarTimeIsLastEnd(doStarTimeIsLastEnd); - setDefaultPublic(doDefaultPublic); + // set context + saveEventSettings(formSettings); + const validation = { isValid: false }; - const f = formData; - - // we might not have changed this - if (f.pinCode !== data.pinCode) { - const e = { status: false, message: '' }; - - // Validate fields - if (f.pinCode === '' || f.pinCode == null) { - e.status = true; - e.message += 'App pin code removed'; - } else { - e.status = true; - e.message += 'App pin code added'; - } - - // set fields with error - if (!e.status) { - emitError(`Invalid Input: ${e.message}`); - } else { - await postSettings(formData); - await refetch(); - emitWarning(e.message); - setChanged(false); - } + // we might not have changed this + if (formData.pinCode !== data.pinCode) { + // Validate fields + if (formData.pinCode === '' || formData.pinCode == null) { + validation.isValid = true; + validation.message += 'App pin code removed'; + } else { + validation.isValid = true; + validation.message += 'App pin code added'; } - setSubmitting(false); - setChanged(false); - }, - [ - data.pinCode, - doDefaultPublic, - doShowQuickEntry, - doStarTimeIsLastEnd, - emitError, - emitWarning, - formData, - refetch, - setDefaultPublic, - setShowQuickEntry, - setStarTimeIsLastEnd, - ] - ); + } + + if (formData.timeFormat !== data.timeFormat) { + if (formData.timeFormat === '12' || formData.timeFormat === '24') { + validation.isValid = true; + } + } + + // set fields with error + if (!validation.isValid) { + emitError(`Invalid Input: ${validation.message}`); + } else { + await postSettings(formData); + await refetch(); + validation?.message && emitWarning(validation.message); + } + setSubmitting(false); + setChanged(false); + }; /** * Reverts local state equals to server state */ - const revert = useCallback(async () => { + const revert = async () => { setChanged(false); - await refetch(); - // set from context - setDoShowQuickEntry(showQuickEntry); - setDoStarTimeIsLastEnd(starTimeIsLastEnd); - setDoDefaultPublic(defaultPublic); - }, [defaultPublic, refetch, showQuickEntry, starTimeIsLastEnd]); + setFormSettings(eventSettings); + await refetch(); + }; /** * Handles change of input field in local state * @param {string} field - object parameter to update * @param {string} value - new object parameter value */ - const handleChange = useCallback( - (field, value) => { - const temp = { ...formData }; - temp[field] = value; - setFormData(temp); - setChanged(true); - }, - [formData] - ); - - /** - * Sets changed flag to true - */ - const handleContextChange = useCallback(() => { + const handleChange = (field, value) => { + const temp = { ...formData }; + temp[field] = value; + setFormData(temp); setChanged(true); - }, []); + }; const disableModal = status !== 'success'; @@ -201,6 +156,7 @@ export default function AppSettingsModal() {
+
+ + + Time format + +
+ 12 / 24 hour format (viewers only for now) +
+
+ +
+
Create Event Default Settings
{ - setDoShowQuickEntry(e.target.checked); - handleContextChange(); + setFormSettings((prev) => ({ ...prev, showQuickEntry: e.target.checked })); + setChanged(true); }} > Show quick entry on hover { - setDoStarTimeIsLastEnd(e.target.checked); - handleContextChange(); + setFormSettings((prev) => ({ ...prev, startTimeIsLastEnd: e.target.checked })); + setChanged(true); }} > Start time is last end { - setDoDefaultPublic(e.target.checked); - handleContextChange(); + setFormSettings((prev) => ({ ...prev, defaultPublic: e.target.checked })); + setChanged(true); }} > Event default public diff --git a/client/src/features/table/Table.module.scss b/client/src/features/table/Table.module.scss index 25c2ecc3f..310f7fc61 100644 --- a/client/src/features/table/Table.module.scss +++ b/client/src/features/table/Table.module.scss @@ -32,7 +32,7 @@ grid-template-areas: 'name playback running time actions' 'now playback running time actions'; - grid-template-columns: 1fr auto 10em 10em auto; + grid-template-columns: 1fr auto 10em 12.5em auto; align-items: center; padding: 0.25em 1em; diff --git a/client/src/features/table/TableHeader.jsx b/client/src/features/table/TableHeader.jsx index 4520cb2ec..be35fc26e 100644 --- a/client/src/features/table/TableHeader.jsx +++ b/client/src/features/table/TableHeader.jsx @@ -10,7 +10,7 @@ import { useSocket } from '../../common/context/socketContext'; import { TableSettingsContext } from '../../common/context/TableSettingsContext'; import { useFetch } from '../../common/hooks/useFetch'; import { formatDisplay } from '../../common/utils/dateConfig'; -import { stringFromMillis } from '../../common/utils/time'; +import { formatTime } from '../../common/utils/time'; import PlaybackIcon from './tableElements/PlaybackIcon'; @@ -97,7 +97,10 @@ export default function TableHeader() { // prepare presentation variables const timerNow = `${timer.running < 0 ? '-' : ''}${formatDisplay(timer.running)}`; - + const timeNow = formatTime(timer.clock, { + showSeconds: true, + format: 'hh:mm:ss a', + }); return (
{data?.title || ''}
@@ -115,7 +118,7 @@ export default function TableHeader() {
Time Now
- {stringFromMillis(timer.clock)} + {timeNow}
diff --git a/client/src/features/viewers/ViewWrapper.jsx b/client/src/features/viewers/ViewWrapper.jsx index 2af2cddf9..b75d09d83 100644 --- a/client/src/features/viewers/ViewWrapper.jsx +++ b/client/src/features/viewers/ViewWrapper.jsx @@ -1,12 +1,11 @@ /* eslint-disable react/display-name */ import React, { useEffect, useState } from 'react'; -import { EVENT_TABLE, EVENTS_TABLE } from 'common/api/apiConstants'; -import { fetchEvent } from 'common/api/eventApi'; -import { fetchAllEvents } from 'common/api/eventsApi'; -import { useSocket } from 'common/context/socketContext'; -import { useFetch } from 'common/hooks/useFetch'; -import { stringFromMillis } from '../../common/utils/time'; +import { EVENT_TABLE, EVENTS_TABLE } from '../../common/api/apiConstants'; +import { fetchEvent } from '../../common/api/eventApi'; +import { fetchAllEvents } from '../../common/api/eventsApi'; +import { useSocket } from '../../common/context/socketContext'; +import { useFetch } from '../../common/hooks/useFetch'; const withSocket = (Component) => { return (props) => { @@ -30,9 +29,9 @@ const withSocket = (Component) => { visible: false, }); const [timer, setTimer] = useState({ - clock: null, - running: null, - isNegative: null, + clock: 0, + running: 0, + isNegative: false, startedAt: null, expectedFinish: null, }); @@ -67,7 +66,9 @@ const withSocket = (Component) => { // Ask for update on load useEffect(() => { - if (socket == null) return; + if (!socket) { + return; + } // Handle timer messages socket.on('messages-timer', (data) => { @@ -154,12 +155,12 @@ const withSocket = (Component) => { // Filter events only to pass down useEffect(() => { - if (eventsData == null) return; + if (!eventsData) { + return; + } // filter just events with title if (Array.isArray(eventsData)) { - const pe = eventsData.filter( - (d) => d.type === 'event' && d.title !== '' && d.isPublic - ); + const pe = eventsData.filter((d) => d.type === 'event' && d.title !== '' && d.isPublic); setPublicEvents(pe); // everything goes backstage @@ -169,10 +170,13 @@ const withSocket = (Component) => { // Set general data useEffect(() => { - if (genData == null) return; + if (!genData) { + return; + } setGeneral(genData); }, [genData]); + /********************************************/ /*** + titleManager ***/ /*** WRAP INFORMATION RELATED TO TITLES ***/ @@ -221,9 +225,6 @@ const withSocket = (Component) => { const timeManager = { ...timer, finished: playback === 'start' && timer.isNegative && timer.startedAt, - clock: stringFromMillis(timer.clock), - clockMs: timer.clock, - clockNoSeconds: stringFromMillis(timer.clock, false), playstate: playback, }; diff --git a/client/src/features/viewers/backstage/StageManager.jsx b/client/src/features/viewers/backstage/StageManager.jsx index f4fc13e53..1d9439249 100644 --- a/client/src/features/viewers/backstage/StageManager.jsx +++ b/client/src/features/viewers/backstage/StageManager.jsx @@ -8,10 +8,16 @@ import { AnimatePresence, motion } from 'framer-motion'; import PropTypes from 'prop-types'; import { getEventsWithDelay } from '../../../common/utils/eventsManager'; +import { formatTime } from '../../../common/utils/time'; import { titleVariants } from '../common/animation'; import style from './StageManager.module.scss'; +const formatOptions = { + showSeconds: true, + format: 'hh:mm:ss a', +}; + export default function StageManager(props) { const { publ, title, time, backstageEvents, selectedId, general } = props; const [filteredEvents, setFilteredEvents] = useState(null); @@ -41,6 +47,8 @@ export default function StageManager(props) { if (time.isNegative) stageTimer = `-${stageTimer}`; } + const clock = formatTime(time.clock, formatOptions); + return (
@@ -118,7 +126,7 @@ export default function StageManager(props) {
Time Now
-
{time.clock}
+
{clock}
diff --git a/client/src/features/viewers/countdown/Countdown.jsx b/client/src/features/viewers/countdown/Countdown.jsx index 7fb997aa9..48722ab0c 100644 --- a/client/src/features/viewers/countdown/Countdown.jsx +++ b/client/src/features/viewers/countdown/Countdown.jsx @@ -6,19 +6,24 @@ import NavLogo from '../../../common/components/nav/NavLogo'; import Empty from '../../../common/components/state/Empty'; import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig'; import getDelayTo from '../../../common/utils/getDelayTo'; -import { stringFromMillis } from '../../../common/utils/time'; +import { formatTime } from '../../../common/utils/time'; import { fetchTimerData, sanitiseTitle, timerMessages } from './countdown.helpers'; import style from './Countdown.module.scss'; +const formatOptions = { + showSeconds: true, + format: 'hh:mm:ss a', +}; + export default function Countdown(props) { - const [searchParams] = useSearchParams(); const { backstageEvents, time, selectedId } = props; const [follow, setFollow] = useState(null); const [runningTimer, setRunningTimer] = useState(0); const [runningMessage, setRunningMessage] = useState(''); const [delay, setDelay] = useState(0); + const [searchParams] = useSearchParams(); // Set window title useEffect(() => { @@ -46,8 +51,8 @@ export default function Countdown(props) { if (typeof followThis !== 'undefined') { setFollow(followThis); const idx = backstageEvents.findIndex((event) => event.id === followThis.id); - const delay = getDelayTo(backstageEvents, idx); - setDelay(delay); + const delayToEvent = getDelayTo(backstageEvents, idx); + setDelay(delayToEvent); } }, [backstageEvents, searchParams]); @@ -73,6 +78,16 @@ export default function Countdown(props) { const isSelected = useMemo(() => runningMessage === timerMessages.running, [runningMessage]); + const clock = formatTime(time.clock, formatOptions); + const startTime = + follow === null + ? '...' + : formatTime(follow.timeStart + delay, formatOptions); + const endTime = + follow === null + ? '...' + : formatTime(follow.timeEnd + delay, formatOptions); + return (
@@ -100,19 +115,17 @@ export default function Countdown(props) {
Time Now
- {time.clock} + {clock}
Start Time
0 ? style.delayed : ''}`}> - {stringFromMillis(follow.timeStart + delay)} + {startTime}
End Time
- 0 ? style.delayed : ''}`}> - {stringFromMillis(follow.timeEnd + delay)} - + 0 ? style.delayed : ''}`}>{endTime}
{runningMessage}
@@ -126,7 +139,7 @@ export default function Countdown(props) { isSelected || time.waiting )} -
{follow.title || 'Untitled Event'}
+
{follow?.title || 'Untitled Event'}
)}
@@ -137,4 +150,5 @@ Countdown.propTypes = { backstageEvents: PropTypes.array, time: PropTypes.object, selectedId: PropTypes.string, + settings: PropTypes.object, }; diff --git a/client/src/features/viewers/countdown/__tests__/Countdown.test.js b/client/src/features/viewers/countdown/__tests__/Countdown.test.js index 90110fab5..c5e65b541 100644 --- a/client/src/features/viewers/countdown/__tests__/Countdown.test.js +++ b/client/src/features/viewers/countdown/__tests__/Countdown.test.js @@ -35,7 +35,7 @@ describe('fetchTimerData() function', () => { const startMockValue = 10000; const timeNow = 1000; const follow = { id: 'anotherevent', timeStart: startMockValue }; - const time = { clockMs: timeNow }; + const time = { clock: timeNow }; const { message, timer } = fetchTimerData(time, follow, 'notthesameevent'); expect(message).toBe(timerMessages.toStart); @@ -48,7 +48,7 @@ describe('fetchTimerData() function', () => { const timeNow = 15000; const followId = 'testId'; const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue }; - const time = { clockMs: timeNow, running: endMockValue - startMockValue }; + const time = { clock: timeNow, running: endMockValue - startMockValue }; const { message, timer } = fetchTimerData(time, follow, 'notthesameevent'); expect(message).toBe(timerMessages.waiting); @@ -61,7 +61,7 @@ describe('fetchTimerData() function', () => { const timeNow = 30000; const followId = 'testId'; const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue }; - const time = { clockMs: timeNow, running: endMockValue - startMockValue }; + const time = { clock: timeNow, running: endMockValue - startMockValue }; const { message, timer } = fetchTimerData(time, follow, 'notthesameevent'); expect(message).toBe(timerMessages.ended); @@ -74,7 +74,7 @@ describe('fetchTimerData() function', () => { const timeNow = 15000; const followId = 'testId'; const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue }; - const time = { clockMs: timeNow, running: DAY_TO_MS + endMockValue - startMockValue }; + const time = { clock: timeNow, running: DAY_TO_MS + endMockValue - startMockValue }; const { message, timer } = fetchTimerData(time, follow, 'notthesameevent'); expect(message).toBe(timerMessages.waiting); @@ -87,7 +87,7 @@ describe('fetchTimerData() function', () => { const timeNow = 15000; const followId = 'testId'; const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue }; - const time = { clockMs: timeNow, running: DAY_TO_MS + endMockValue - startMockValue }; + const time = { clock: timeNow, running: DAY_TO_MS + endMockValue - startMockValue }; const { message, timer } = fetchTimerData(time, follow, followId); expect(message).toBe(timerMessages.running); @@ -100,7 +100,7 @@ describe('fetchTimerData() function', () => { const timeNow = 2000; const followId = 'testId'; const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue }; - const time = { clockMs: timeNow, running: DAY_TO_MS + endMockValue - startMockValue }; + const time = { clock: timeNow, running: DAY_TO_MS + endMockValue - startMockValue }; const { message, timer } = fetchTimerData(time, follow, 'notthesameevent'); expect(message).toBe(timerMessages.toStart); diff --git a/client/src/features/viewers/countdown/countdown.helpers.js b/client/src/features/viewers/countdown/countdown.helpers.js index 885636c34..b1c193175 100644 --- a/client/src/features/viewers/countdown/countdown.helpers.js +++ b/client/src/features/viewers/countdown/countdown.helpers.js @@ -34,22 +34,22 @@ export const fetchTimerData = (time, follow, selectedId) => { // check that is not running message = time.playstate === 'pause' ? timerMessages.waiting : timerMessages.running; timer = time.running; - } else if (time.clockMs < follow.timeStart) { + } else if (time.clock < follow.timeStart) { // if it hasnt started, we count to start message = timerMessages.toStart; - timer = millisToSeconds(follow.timeStart - time.clockMs); - } else if (follow.timeStart <= time.clockMs && time.clockMs <= follow.timeEnd) { + timer = millisToSeconds(follow.timeStart - time.clock); + } else if (follow.timeStart <= time.clock && time.clock <= follow.timeEnd) { // if it has started, we show running timer message = timerMessages.waiting; timer = time.running; } else { if (follow.timeStart > follow.timeEnd) { // ends day after - if (follow.timeStart > time.clockMs ) { + if (follow.timeStart > time.clock ) { // if it hasnt started, we count to start message = timerMessages.toStart; - timer = millisToSeconds(follow.timeStart - time.clockMs); - } else if (follow.timeStart <= time.clockMs) { + timer = millisToSeconds(follow.timeStart - time.clock); + } else if (follow.timeStart <= time.clock) { // if it has started, we show running timer message = timerMessages.waiting; timer = time.running; diff --git a/client/src/features/viewers/foh/Public.jsx b/client/src/features/viewers/foh/Public.jsx index bbc8ec322..d7acc83e6 100644 --- a/client/src/features/viewers/foh/Public.jsx +++ b/client/src/features/viewers/foh/Public.jsx @@ -6,10 +6,16 @@ import TitleSide from 'common/components/views/TitleSide'; import { AnimatePresence, motion } from 'framer-motion'; import PropTypes from 'prop-types'; +import { formatTime } from '../../../common/utils/time'; import { titleVariants } from '../common/animation'; import style from './Public.module.scss'; +const formatOptions = { + showSeconds: true, + format: 'hh:mm:ss a', +}; + export default function Public(props) { const { publ, publicTitle, time, events, publicSelectedId, general } = props; const [pageNumber, setPageNumber] = useState(0); @@ -23,7 +29,7 @@ export default function Public(props) { // Format messages const showPubl = publ.text !== '' && publ.visible; - // motion + const clock = formatTime(time.clock, formatOptions); return (
@@ -78,12 +84,12 @@ export default function Public(props) {
Today
{pageNumber > 1 && - [...Array(pageNumber).keys()].map((i) => ( -
- ))} + [...Array(pageNumber).keys()].map((i) => ( +
+ ))}
-
+
Public message
{publ.text}
Time Now
-
{time.clock}
+
{clock}
@@ -116,11 +118,7 @@ export default function Public(props) {
{general.url != null && general.url !== '' && ( - + )}
diff --git a/client/src/features/viewers/production/Pip.jsx b/client/src/features/viewers/production/Pip.jsx index 7b89bf8fc..f340dca29 100644 --- a/client/src/features/viewers/production/Pip.jsx +++ b/client/src/features/viewers/production/Pip.jsx @@ -7,20 +7,27 @@ import { formatDisplay } from 'common/utils/dateConfig'; import { AnimatePresence, motion } from 'framer-motion'; import PropTypes from 'prop-types'; +import { formatTime } from '../../../common/utils/time'; + import style from './Pip.module.scss'; +const formatOptions = { + showSeconds: true, + format: 'hh:mm:ss a', +}; + export default function Pip(props) { const { time, backstageEvents, selectedId, general } = props; const [size, setSize] = useState(''); - const ref = useRef(null); + const pipAreaRef = useRef(null); const [filteredEvents, setFilteredEvents] = useState(null); const [pageNumber, setPageNumber] = useState(0); const [currentPage, setCurrentPage] = useState(0); - // calculcate pip size + // calculate pip size useLayoutEffect(() => { - const h = ref.current.clientHeight; - const w = ref.current.clientWidth; + const h = pipAreaRef.current.clientHeight; + const w = pipAreaRef.current.clientWidth; setSize(`${w} x ${h}`); }, []); @@ -51,11 +58,12 @@ export default function Pip(props) { }, [backstageEvents]); // Format messages - const showInfo = - general.backstageInfo !== '' && general.backstageInfo != null; + const showInfo = general.backstageInfo !== '' && general.backstageInfo != null; let stageTimer = formatDisplay(Math.abs(time.running), true); if (time.isNegative) stageTimer = `-${stageTimer}`; + const clock = formatTime(time.clock, formatOptions); + return (
@@ -67,12 +75,12 @@ export default function Pip(props) {
Today
{pageNumber > 1 && - [...Array(pageNumber).keys()].map((i) => ( -
- ))} + [...Array(pageNumber).keys()].map((i) => ( +
+ ))}
-
+
{size}
@@ -100,11 +108,7 @@ export default function Pip(props) {
{general.url != null && general.url !== '' && ( - + )}
@@ -113,7 +117,7 @@ export default function Pip(props) {
Time Now
-
{time.clock}
+
{clock}
diff --git a/client/src/features/viewers/studio/StudioClock.jsx b/client/src/features/viewers/studio/StudioClock.jsx index 499a10c2e..d04829a6a 100644 --- a/client/src/features/viewers/studio/StudioClock.jsx +++ b/client/src/features/viewers/studio/StudioClock.jsx @@ -9,13 +9,18 @@ import { getEventsWithDelay, trimEventlist, } from '../../../common/utils/eventsManager'; +import { formatTime, stringFromMillis } from '../../../common/utils/time'; import style from './StudioClock.module.scss'; +const formatOptions = { + showSeconds: false, + format: 'hh:mm', +}; + export default function StudioClock(props) { const { title, time, backstageEvents, selectedId, nextId, onAir } = props; - const { fontSize, ref } = useFitText({ maxFontSize: 500 }); - const [, , secondsNow] = time.clock.split(':'); + const { fontSize: titleFontSize, ref: titleRef } = useFitText({ maxFontSize: 500 }); const [schedule, setSchedule] = useState([]); const activeIndicators = [...Array(12).keys()]; @@ -28,26 +33,32 @@ export default function StudioClock(props) { }, []); // Prepare event list - // Todo: useMemo() useEffect(() => { if (backstageEvents == null) return; + const delayed = getEventsWithDelay(backstageEvents); const events = delayed.filter((e) => e.type === 'event'); const trimmed = trimEventlist(events, selectedId, MAX_TITLES); - const formatted = formatEventList(trimmed, selectedId, nextId); + const formatted = formatEventList(trimmed, selectedId, nextId, { + showEnd: false, + }); setSchedule(formatted); - }, [backstageEvents, selectedId, nextId]); + }, [backstageEvents, nextId, selectedId] ); + + const clock = formatTime(time.clock, formatOptions); + + const [, , secondsNow] = stringFromMillis(time.clock).split(':'); return (
-
{time.clockNoSeconds}
+
{clock}
{title.titleNext}
diff --git a/client/src/features/viewers/studio/StudioClock.module.scss b/client/src/features/viewers/studio/StudioClock.module.scss index 692321e24..797d54465 100644 --- a/client/src/features/viewers/studio/StudioClock.module.scss +++ b/client/src/features/viewers/studio/StudioClock.module.scss @@ -89,6 +89,13 @@ $cyan-idle: #0aa; line-height: 0.8em; } + .timeAA { + color: $red-active; + font-size: calc(#{$clock-size} / 4.5); + margin-top: calc(50% - calc(#{$clock-size} / 7)); + line-height: 0.8em; + } + .nextTitle:after, .nextCountdown:after, .nextCountdown__overtime:after { diff --git a/client/src/features/viewers/timer/Timer.jsx b/client/src/features/viewers/timer/Timer.jsx index da631e60c..f50f99608 100644 --- a/client/src/features/viewers/timer/Timer.jsx +++ b/client/src/features/viewers/timer/Timer.jsx @@ -7,8 +7,15 @@ import TitleCard from 'common/components/views/TitleCard'; import { AnimatePresence, motion } from 'framer-motion'; import PropTypes from 'prop-types'; +import { formatTime } from '../../../common/utils/time'; + import style from './Timer.module.scss'; +const formatOptions = { + showSeconds: true, + format: 'hh:mm:ss a', +}; + export default function Timer(props) { const { general, pres, title, time } = props; const [elapsed, setElapsed] = useState(true); @@ -32,22 +39,12 @@ export default function Timer(props) { } }, [searchParams]); + const clock = formatTime(time.clock, formatOptions); + const showOverlay = pres.text !== '' && pres.visible; const isPlaying = time.playstate !== 'pause'; const normalisedTime = Math.max(time.running, 0); - // show timer if end message is empty - const endMessage = - general.endMessage == null || general.endMessage === '' ? ( - - ) : ( - general.endMessage - ); - // motion const titleVariants = { hidden: { @@ -65,16 +62,8 @@ export default function Timer(props) { }; return ( -
-
+
+
{pres.text}
@@ -82,12 +71,18 @@ export default function Timer(props) {
Time Now
-
{time.clock}
+
{clock}
{time.finished ? ( -
{endMessage}
+
+ {general.endMessage == null || general.endMessage === '' ? ( + + ) : ( + general.endMessage + )} +
) : (
@@ -96,11 +91,7 @@ export default function Timer(props) {
{!time.finished && ( -
+
{ const version = data.settings.version; const serverPort = data.settings.serverPort; const pinCode = data.settings.pinCode; + const timeFormat = data.settings.timeFormat; // send object with network information res.status(200).send({ version, serverPort, pinCode, + timeFormat, }); }; @@ -226,9 +228,18 @@ export const postSettings = async (req, res) => { pin = req.body?.pinCode; } } + + let timeFormat = data.settings.timeFormat; + if (typeof req.body?.timeFormat === 'string') { + if (req.body?.timeFormat === '12' || req.body?.timeFormat === '24') { + timeFormat = req.body.timeFormat; + } + } + data.settings = { ...data.settings, pinCode: pin, + timeFormat: timeFormat, }; await db.write(); res.sendStatus(200); diff --git a/server/src/models/dataModel.js b/server/src/models/dataModel.js index 23f58ae65..9d469332f 100644 --- a/server/src/models/dataModel.js +++ b/server/src/models/dataModel.js @@ -13,6 +13,7 @@ export const dbModelv1 = { serverPort: 4001, lock: null, pinCode: null, + timeFormat: '24', }, aliases: [], userFields: { diff --git a/server/src/utils/__tests__/eventUtils.test.js b/server/src/utils/__tests__/eventUtils.test.js index 6b91d3829..99310c7e0 100644 --- a/server/src/utils/__tests__/eventUtils.test.js +++ b/server/src/utils/__tests__/eventUtils.test.js @@ -51,4 +51,4 @@ describe('getPreviousPlayable()', () => { expect(id).toBe(null); }); }); -}); \ No newline at end of file +}); diff --git a/server/src/utils/__tests__/parser.tests.js b/server/src/utils/__tests__/parser.tests.js index dc2f7da21..90a38275e 100644 --- a/server/src/utils/__tests__/parser.tests.js +++ b/server/src/utils/__tests__/parser.tests.js @@ -184,6 +184,7 @@ describe('test json parser with valid def', () => { settings: { app: 'ontime', version: 1, + timeFormat: '24', }, }; @@ -386,6 +387,7 @@ describe('test corrupt data', () => { version: 1, serverPort: 4001, lock: null, + timeFormat: '24', }, }; @@ -408,6 +410,7 @@ describe('test corrupt data', () => { version: 1, serverPort: 4001, lock: null, + timeFormat: '24', }, }; @@ -424,6 +427,7 @@ describe('test corrupt data', () => { version: 1, serverPort: 4001, lock: null, + timeFormat: '24', }, }; diff --git a/server/src/utils/parserUtils_v1.js b/server/src/utils/parserUtils_v1.js index 573d0c058..7968d996c 100644 --- a/server/src/utils/parserUtils_v1.js +++ b/server/src/utils/parserUtils_v1.js @@ -98,6 +98,7 @@ export const parseSettings_v1 = (data, enforce) => { const settings = { lock: s.lock || null, pinCode: s.pinCode || null, + timeFormat: s.timeFormat || '24', }; // write to db