Feature: quick start (#369)

* feat: quick start modal

* refactor: end message is part of view settings

* refactor: small tweaks and type improvements
This commit is contained in:
Carlos Valente
2023-04-29 21:20:25 +02:00
committed by GitHub
parent 33b496f98e
commit 38654f8981
41 changed files with 463 additions and 282 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
export const STATIC_PORT = 4001; export const STATIC_PORT = 4001;
// REST stuff // REST stuff
export const EVENTDATA_TABLE = ['eventdata']; export const EVENT_DATA = ['eventdata'];
export const ALIASES = ['aliases']; export const ALIASES = ['aliases'];
export const USERFIELDS = ['userFields']; export const USERFIELDS = ['userFields'];
export const RUNDOWN_TABLE_KEY = 'rundown'; export const RUNDOWN_TABLE_KEY = 'rundown';
+5 -1
View File
@@ -1,5 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import { Alias, OSCSettings, Settings, UserFields, ViewSettings } from 'ontime-types'; import { Alias, EventData, OSCSettings, Settings, UserFields, ViewSettings } from 'ontime-types';
import { apiRepoLatest } from '../../externals'; import { apiRepoLatest } from '../../externals';
import { InfoType } from '../models/Info'; import { InfoType } from '../models/Info';
@@ -169,3 +169,7 @@ export async function getLatestVersion(): Promise<HasUpdate> {
version: res.data.tag_name as string, version: res.data.tag_name as string,
}; };
} }
export async function postNew(initialData: Partial<EventData>) {
return axios.post(`${ontimeURL}/new`, initialData);
}
@@ -19,7 +19,6 @@ export const AutoTextArea = (props: AutoTextAreaProps) => {
}; };
}, []); }, []);
return ( return (
<Textarea <Textarea
overflow='hidden' overflow='hidden'
@@ -27,7 +26,7 @@ export const AutoTextArea = (props: AutoTextAreaProps) => {
resize='none' resize='none'
ref={ref} ref={ref}
transition='height none' transition='height none'
variant={isDark ? 'ontime-filled' : 'ontime-filled-onlight'} variant={isDark ? 'ontime-filled' : 'ontime-filled-on-light'}
{...rest} {...rest}
/> />
); );
@@ -1,13 +1,13 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { EVENTDATA_TABLE } from '../api/apiConstants'; import { EVENT_DATA } from '../api/apiConstants';
import { fetchEventData } from '../api/eventDataApi'; import { fetchEventData } from '../api/eventDataApi';
import { eventDataPlaceholder } from '../models/EventData'; import { eventDataPlaceholder } from '../models/EventData';
export default function useEventData() { export default function useEventData() {
const { data, status, isError, refetch } = useQuery({ const { data, status, isError, refetch } = useQuery({
queryKey: EVENTDATA_TABLE, queryKey: EVENT_DATA,
queryFn: fetchEventData, queryFn: fetchEventData,
placeholderData: eventDataPlaceholder, placeholderData: eventDataPlaceholder,
retry: 5, retry: 5,
@@ -6,5 +6,4 @@ export const eventDataPlaceholder: EventData = {
publicInfo: '', publicInfo: '',
backstageUrl: '', backstageUrl: '',
backstageInfo: '', backstageInfo: '',
endMessage: '',
}; };
@@ -2,4 +2,5 @@ import { ViewSettings } from 'ontime-types';
export const viewsSettingsPlaceholder: ViewSettings = { export const viewsSettingsPlaceholder: ViewSettings = {
overrideStyles: false, overrideStyles: false,
endMessage: '',
}; };
@@ -7,6 +7,7 @@ import MenuBar from '../menu/MenuBar';
import AboutModal from '../modals/about-modal/AboutModal'; import AboutModal from '../modals/about-modal/AboutModal';
import IntegrationModal from '../modals/integration-modal/IntegrationModal'; import IntegrationModal from '../modals/integration-modal/IntegrationModal';
import ModalManager from '../modals/ModalManager'; import ModalManager from '../modals/ModalManager';
import QuickStart from '../modals/quick-start/QuickStart';
import styles from './Editor.module.scss'; import styles from './Editor.module.scss';
@@ -25,6 +26,7 @@ export default function Editor() {
onClose: onIntegrationModalClose, onClose: onIntegrationModalClose,
} = useDisclosure(); } = useDisclosure();
const { isOpen: isAboutModalOpen, onOpen: onAboutModalOpen, onClose: onAboutModalClose } = useDisclosure(); const { isOpen: isAboutModalOpen, onOpen: onAboutModalOpen, onClose: onAboutModalClose } = useDisclosure();
const { isOpen: isQuickStartOpen, onOpen: onQuickStartOpen, onClose: onQuickStartClose } = useDisclosure();
// Set window title // Set window title
useEffect(() => { useEffect(() => {
@@ -34,6 +36,7 @@ export default function Editor() {
return ( return (
<> <>
<ErrorBoundary> <ErrorBoundary>
<QuickStart onClose={onQuickStartClose} isOpen={isQuickStartOpen} />
<UploadModal onClose={onUploadModalClose} isOpen={isUploadModalOpen} /> <UploadModal onClose={onUploadModalClose} isOpen={isUploadModalOpen} />
<IntegrationModal onClose={onIntegrationModalClose} isOpen={isIntegrationModalOpen} /> <IntegrationModal onClose={onIntegrationModalClose} isOpen={isIntegrationModalOpen} />
<AboutModal onClose={onAboutModalClose} isOpen={isAboutModalOpen} /> <AboutModal onClose={onAboutModalClose} isOpen={isAboutModalOpen} />
@@ -52,6 +55,8 @@ export default function Editor() {
onIntegrationOpen={onIntegrationModalOpen} onIntegrationOpen={onIntegrationModalOpen}
isAboutOpen={isAboutModalOpen} isAboutOpen={isAboutModalOpen}
onAboutOpen={onAboutModalOpen} onAboutOpen={onAboutModalOpen}
isQuickStartOpen={isQuickStartOpen}
onQuickStartOpen={onQuickStartOpen}
/> />
</ErrorBoundary> </ErrorBoundary>
</Box> </Box>
+17 -4
View File
@@ -1,12 +1,13 @@
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { VStack } from '@chakra-ui/react'; import { VStack } from '@chakra-ui/react';
import { FiSave } from '@react-icons/all-files/fi/FiSave'; import { IoColorWand } from '@react-icons/all-files/io5/IoColorWand';
import { FiUpload } from '@react-icons/all-files/fi/FiUpload';
import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle'; import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle';
import { IoExtensionPuzzleOutline } from '@react-icons/all-files/io5/IoExtensionPuzzleOutline'; import { IoExtensionPuzzleOutline } from '@react-icons/all-files/io5/IoExtensionPuzzleOutline';
import { IoHelp } from '@react-icons/all-files/io5/IoHelp'; import { IoHelp } from '@react-icons/all-files/io5/IoHelp';
import { IoOptions } from '@react-icons/all-files/io5/IoOptions'; import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay'; import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { IoPushOutline } from '@react-icons/all-files/io5/IoPushOutline';
import { IoSaveOutline } from '@react-icons/all-files/io5/IoSaveOutline';
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline'; import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
import { downloadRundown } from '../../common/api/ontimeApi'; import { downloadRundown } from '../../common/api/ontimeApi';
@@ -27,6 +28,8 @@ interface MenuBarProps {
onIntegrationOpen: () => void; onIntegrationOpen: () => void;
isAboutOpen: boolean; isAboutOpen: boolean;
onAboutOpen: () => void; onAboutOpen: () => void;
isQuickStartOpen: boolean;
onQuickStartOpen: () => void;
} }
const buttonStyle = { const buttonStyle = {
@@ -52,6 +55,8 @@ export default function MenuBar(props: MenuBarProps) {
onIntegrationOpen, onIntegrationOpen,
isAboutOpen, isAboutOpen,
onAboutOpen, onAboutOpen,
isQuickStartOpen,
onQuickStartOpen,
} = props; } = props;
const { isElectron, sendToElectron } = useElectronEvent(); const { isElectron, sendToElectron } = useElectronEvent();
@@ -102,7 +107,15 @@ export default function MenuBar(props: MenuBarProps) {
<div className={style.gap} /> <div className={style.gap} />
<TooltipActionBtn <TooltipActionBtn
{...buttonStyle} {...buttonStyle}
icon={<FiUpload />} icon={<IoColorWand />}
className={isQuickStartOpen ? style.open : ''}
clickHandler={onQuickStartOpen}
tooltip='Quick start'
aria-label='Quick start'
/>
<TooltipActionBtn
{...buttonStyle}
icon={<IoPushOutline />}
className={isUploadOpen ? style.open : ''} className={isUploadOpen ? style.open : ''}
clickHandler={onUploadOpen} clickHandler={onUploadOpen}
tooltip='Upload showfile' tooltip='Upload showfile'
@@ -110,7 +123,7 @@ export default function MenuBar(props: MenuBarProps) {
/> />
<TooltipActionBtn <TooltipActionBtn
{...buttonStyle} {...buttonStyle}
icon={<FiSave />} icon={<IoSaveOutline />}
clickHandler={downloadRundown} clickHandler={downloadRundown}
tooltip='Export showfile' tooltip='Export showfile'
aria-label='Export showfile' aria-label='Export showfile'
@@ -32,7 +32,6 @@ export default function SettingsModal() {
publicInfo: data.publicInfo, publicInfo: data.publicInfo,
backstageUrl: data.backstageUrl, backstageUrl: data.backstageUrl,
backstageInfo: data.backstageInfo, backstageInfo: data.backstageInfo,
endMessage: data.endMessage,
}); });
}, [changed, data]); }, [changed, data]);
@@ -168,23 +167,6 @@ export default function SettingsModal() {
onChange={(event) => handleChange('backstageInfo', event.target.value)} onChange={(event) => handleChange('backstageInfo', event.target.value)}
/> />
</div> </div>
<div className={style.spacedEntry}>
<FormLabel htmlFor='endMessage'>
End Message
<span className={style.labelNote}>
<br />
Shown on presenter view when time is finished
</span>
</FormLabel>
<Input
{...inputProps}
maxLength={30}
name='endMessage'
placeholder='Empty message shows elapsed time'
value={formData.endMessage}
onChange={(event) => handleChange('endMessage', event.target.value)}
/>
</div>
</div> </div>
<SubmitContainer revert={revert} submitting={submitting} changed={changed} status={status} /> <SubmitContainer revert={revert} submitting={submitting} changed={changed} status={status} />
</form> </form>
@@ -1,6 +1,8 @@
@use '../../theme/v2Styles' as *; @use '../../theme/v2Styles' as *;
@use '../../theme/ontimeColours' as *; @use '../../theme/ontimeColours' as *;
$el-padding-with-compensation: 24px; // 16 + 8
@mixin modal-link { @mixin modal-link {
color: $blue-500; color: $blue-500;
transition-property: color; transition-property: color;
@@ -14,7 +16,7 @@
.headerNotes { .headerNotes {
font-size: $text-body-size; font-size: $text-body-size;
width: 100%; width: 100%;
padding: 0 $section-spacing; padding: 0 $el-padding-with-compensation;
color: $modal-note-color; color: $modal-note-color;
margin-bottom: $section-spacing; margin-bottom: $section-spacing;
@@ -24,6 +26,12 @@
} }
} }
.footerNotes {
font-size: $inner-section-text-size;
padding: 0 $el-padding-with-compensation;
color: $modal-note-color;
}
.divider { .divider {
margin: $element-spacing 0; margin: $element-spacing 0;
border: 0; border: 0;
@@ -31,6 +39,7 @@
} }
.sectionContainer { .sectionContainer {
padding: 8px 16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100% height: 100%
@@ -60,7 +69,7 @@
.sectionTitle { .sectionTitle {
font-size: $inner-section-text-size; font-size: $inner-section-text-size;
display: block; display: block;
width: 100%;
&.main { &.main {
font-weight: 600; font-weight: 600;
} }
@@ -83,9 +92,6 @@
.buttonSection { .buttonSection {
margin-top: $section-spacing; margin-top: $section-spacing;
padding-top: $section-spacing;
padding-left: -24px;
border-top: 1px solid $gray-100;
display: flex; display: flex;
gap: $section-spacing; gap: $section-spacing;
} }
@@ -96,7 +102,7 @@
.shiftRight { .shiftRight {
align-self: flex-end; align-self: flex-end;
margin-right: 8px; margin-right: $element-spacing;
} }
.showPointer { .showPointer {
@@ -109,7 +115,7 @@
} }
.padBottom { .padBottom {
padding-bottom: 8px; padding-bottom: $element-spacing;
} }
.logo { .logo {
@@ -117,17 +123,22 @@
height: 48px; height: 48px;
display: inline-block; display: inline-block;
vertical-align: text-bottom; vertical-align: text-bottom;
margin-right: 16px; margin-right: $section-spacing;
} }
.test { .updateSection {
padding-top: 24px; padding-top: $el-padding-with-compensation;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-end; justify-content: flex-end;
gap: 4px; gap: $element-inner-spacing;
.error { .error {
font-size: $error-red; font-size: $error-red;
} }
}
.overflowContainer {
overflow-y: auto;
max-height: 40%;
} }
@@ -1,15 +1,14 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { FormControl, FormLabel, ModalBody } from '@chakra-ui/react'; import { FormControl, FormLabel, Input, ModalBody } from '@chakra-ui/react';
import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp';
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline'; import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
import { useEmitLog } from '@/common/stores/logger';
import { postView } from '../../common/api/ontimeApi'; import { postView } from '../../common/api/ontimeApi';
import EnableBtn from '../../common/components/buttons/EnableBtn'; import EnableBtn from '../../common/components/buttons/EnableBtn';
import useViewSettings from '../../common/hooks-query/useViewSettings'; import useViewSettings from '../../common/hooks-query/useViewSettings';
import { viewsSettingsPlaceholder } from '../../common/models/ViewSettings.type'; import { viewsSettingsPlaceholder } from '../../common/models/ViewSettings.type';
import { useEmitLog } from '../../common/stores/logger';
import { openLink } from '../../common/utils/linkUtils'; import { openLink } from '../../common/utils/linkUtils';
import { inputProps } from '../../features/modals/modalHelper';
import SubmitContainer from './SubmitContainer'; import SubmitContainer from './SubmitContainer';
@@ -83,44 +82,37 @@ export default function ViewsSettingsModal() {
🔥 Changes take effect immediately 🔥 🔥 Changes take effect immediately 🔥
</p> </p>
<form onSubmit={submitHandler}> <form onSubmit={submitHandler}>
<div className={style.hSeparator}>Timer end message</div>
<div className={style.spacedEntry}>
<FormLabel htmlFor='endMessage'>
End Message
<span className={style.labelNote}>
<br />
Shown on presenter view when time is finished
</span>
</FormLabel>
<Input
{...inputProps}
maxLength={50}
name='endMessage'
placeholder='Empty message shows elapsed time'
value={formData.endMessage}
onChange={(event) => handleChange('endMessage', event.target.value)}
/>
</div>
<div className={style.hSeparator}>Style Options</div> <div className={style.hSeparator}>Style Options</div>
<div className={style.blockNotes}> <div className={style.blockNotes}>
<span className={style.inlineFlex}> <span className={style.inlineFlex}>
<IoInformationCircleOutline color='#2b6cb0' fontSize='2em' /> <IoInformationCircleOutline color='#2b6cb0' fontSize='2em' />
CSS Style Overrides CSS Style Overrides
</span> </span>
This feature allows user defined CSS to override the application stylesheets as a way to customise viewers This feature allows user defined CSS to customise viewers appearance. <br />
appearance.
<br />
Currently the feature affects the following views
<br />
<ul className={style.featureList}>
<li>
<IoCheckmarkSharp /> Stage timer
</li>
<li>
<IoCheckmarkSharp /> Clock
</li>
<li>
<IoCheckmarkSharp /> Minimal timer
</li>
<li>
<IoCheckmarkSharp /> Backstage screen
</li>
<li>
<IoCheckmarkSharp /> Public screen
</li>
<li>
<IoCheckmarkSharp /> Countdown
</li>
</ul>
Read more about it in the documentation{' '}
<a <a
href='#!' href='#!'
onClick={() => openLink('https://cpvalente.gitbook.io/ontime/features/custom-styling')} onClick={() => openLink('https://cpvalente.gitbook.io/ontime/features/custom-styling')}
className={style.if} className={style.if}
> >
over at Gitbook For details on the styling and file location please refer to documentation
</a> </a>
</div> </div>
<div className={style.modalFields}> <div className={style.modalFields}>
@@ -50,7 +50,7 @@ export default function UpdateChecker(props: UpdateCheckerProps) {
const disableButton = Boolean(updateMessage && 'version' in updateMessage); const disableButton = Boolean(updateMessage && 'version' in updateMessage);
return ( return (
<div className={styles.test}> <div className={styles.updateSection}>
<Button onClick={versionCheck} variant='ontime-filled' isLoading={isFetching} isDisabled={disableButton}> <Button onClick={versionCheck} variant='ontime-filled' isLoading={isFetching} isDisabled={disableButton}>
Check for updates Check for updates
</Button> </Button>
@@ -1,4 +1,4 @@
import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react'; import { ModalBody, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
import ModalWrapper from '../ModalWrapper'; import ModalWrapper from '../ModalWrapper';
@@ -19,26 +19,28 @@ export default function IntegrationModal(props: IntegrationModalProps) {
return ( return (
<ModalWrapper title='Integration Settings' isOpen={isOpen} onClose={onClose}> <ModalWrapper title='Integration Settings' isOpen={isOpen} onClose={onClose}>
<div className={styles.headerNotes}> <ModalBody>
Manage settings related to protocol integrations <div className={styles.headerNotes}>
<a href={oscDocsUrl} target='_blank' rel='noreferrer'> Manage settings related to protocol integrations
Read the docs <a href={oscDocsUrl} target='_blank' rel='noreferrer'>
</a> Read the docs
</div> </a>
<Tabs variant='ontime' size='sm' isLazy> </div>
<TabList> <Tabs variant='ontime' size='sm' isLazy>
<Tab>OSC</Tab> <TabList>
<Tab>OSC Integration</Tab> <Tab>OSC</Tab>
</TabList> <Tab>OSC Integration</Tab>
<TabPanels> </TabList>
<TabPanel> <TabPanels>
<OscSettings /> <TabPanel>
</TabPanel> <OscSettings />
<TabPanel> </TabPanel>
<OscIntegration /> <TabPanel>
</TabPanel> <OscIntegration />
</TabPanels> </TabPanel>
</Tabs> </TabPanels>
</Tabs>
</ModalBody>
</ModalWrapper> </ModalWrapper>
); );
} }
@@ -17,7 +17,7 @@ export default function OntimeModalFooter(props: OntimeModalFooterProps) {
const disableSubmit = isSubmitting || !isDirty || !isValid; const disableSubmit = isSubmitting || !isDirty || !isValid;
return ( return (
<ModalFooter className={styles.buttonSection} paddingInlineStart={0} paddingInlineEnd={0} paddingBottom={0}> <ModalFooter className={styles.buttonSection}>
<Button isDisabled={disableRevert} variant='ontime-ghost-on-light' size='sm' onClick={handleRevert}> <Button isDisabled={disableRevert} variant='ontime-ghost-on-light' size='sm' onClick={handleRevert}>
Revert to saved Revert to saved
</Button> </Button>
@@ -1,6 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { ModalBody } from '@chakra-ui/react';
import type { OSCSettings, OscSubscription } from 'ontime-types'; import type { OSCSettings, OscSubscription } from 'ontime-types';
import { TimerLifeCycle } from 'ontime-types'; import { TimerLifeCycle } from 'ontime-types';
import { generateId } from 'ontime-utils'; import { generateId } from 'ontime-utils';
@@ -103,27 +102,25 @@ export default function OscIntegration() {
return ( return (
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='oscSubscriptions'> <form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='oscSubscriptions'>
<ModalBody> {subscriptionKeys.map((cycle, idx) => {
{subscriptionKeys.map((cycle, idx) => { return (
return ( <>
<> <OscSubscriptionRow
<OscSubscriptionRow key={cycle}
key={cycle} cycle={cycle as TimerLifeCycle}
cycle={cycle as TimerLifeCycle} title={sectionText[cycle as TimerLifeCycle].title}
title={sectionText[cycle as TimerLifeCycle].title} subtitle={sectionText[cycle as TimerLifeCycle].subtitle}
subtitle={sectionText[cycle as TimerLifeCycle].subtitle} visible={showSection === cycle}
visible={showSection === cycle} setShowSection={setShowSection}
setShowSection={setShowSection} subscriptionOptions={subscriptionState[cycle as TimerLifeCycle]}
subscriptionOptions={subscriptionState[cycle as TimerLifeCycle]} handleDelete={deleteSubscriptionEntry}
handleDelete={deleteSubscriptionEntry} handleAddNew={addNewSubscriptionEntry}
handleAddNew={addNewSubscriptionEntry} register={register}
register={register} />
/> {idx < subscriptionKeys.length - 1 && <hr className={styles.divider} />}
{idx < subscriptionKeys.length - 1 && <hr className={styles.divider} />} </>
</> );
); })}
})}
</ModalBody>
<OntimeModalFooter <OntimeModalFooter
formId='oscSubscriptions' formId='oscSubscriptions'
handleRevert={resetForm} handleRevert={resetForm}
@@ -1,5 +1,5 @@
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FormControl, Input, ModalBody, Switch } from '@chakra-ui/react'; import { FormControl, Input, Switch } from '@chakra-ui/react';
import useOscSettings, { useOscSettingsMutation } from '../../../common/hooks-query/useOscSettings'; import useOscSettings, { useOscSettingsMutation } from '../../../common/hooks-query/useOscSettings';
import { PlaceholderSettings } from '../../../common/models/OscSettings'; import { PlaceholderSettings } from '../../../common/models/OscSettings';
@@ -53,114 +53,108 @@ export default function OscSettings() {
}; };
return ( return (
<> <form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='oscSettings'>
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='oscSettings'> <div className={styles.splitSection}>
<ModalBody> <div>
<div className={styles.splitSection}> <span className={`${styles.sectionTitle} ${styles.main}`}>OSC Input</span>
<div> <span className={styles.sectionSubtitle}>Control Ontime with OSC</span>
<span className={`${styles.sectionTitle} ${styles.main}`}>OSC Input</span> </div>
<span className={styles.sectionSubtitle}>Control Ontime with OSC</span> <Switch {...register('enabledIn')} variant='ontime-on-light' />
</div> </div>
<Switch {...register('enabledIn')} variant='ontime-on-light' />
</div>
<FormControl isInvalid={!!errors.portIn} className={styles.splitSection}> <FormControl isInvalid={!!errors.portIn} className={styles.splitSection}>
<label htmlFor='portIn'> <label htmlFor='portIn'>
<span className={styles.sectionTitle}>Listen on Port</span> <span className={styles.sectionTitle}>Listen on Port</span>
{errors.portIn ? ( {errors.portIn ? (
<span className={styles.error}>{errors.portIn.message}</span> <span className={styles.error}>{errors.portIn.message}</span>
) : ( ) : (
<span className={styles.sectionSubtitle}>Default 8888</span> <span className={styles.sectionSubtitle}>Default 8888</span>
)} )}
</label> </label>
<Input <Input
id='portIn' id='portIn'
placeholder='8888' placeholder='8888'
width='75px' width='75px'
size='sm' size='sm'
textAlign='right' textAlign='right'
maxLength={5} maxLength={5}
variant='ontime-filled-on-light' variant='ontime-filled-on-light'
{...register('portIn', { {...register('portIn', {
required: { value: true, message: 'Required field' }, required: { value: true, message: 'Required field' },
max: { value: 65535, message: 'Port in incorrect range (1024 - 65535)' }, max: { value: 65535, message: 'Port in incorrect range (1024 - 65535)' },
min: { value: 1024, message: 'Port in incorrect range (1024 - 65535)' }, min: { value: 1024, message: 'Port in incorrect range (1024 - 65535)' },
pattern: { pattern: {
value: isOnlyNumbers, value: isOnlyNumbers,
message: 'Value should be numeric', message: 'Value should be numeric',
}, },
})} })}
/> />
</FormControl> </FormControl>
<hr className={styles.divider} /> <div className={styles.splitSection}>
<div>
<span className={styles.sectionTitle} style={{ fontWeight: 600 }}>
OSC Output
</span>
<span className={styles.sectionSubtitle}>Ontime data feedback</span>
</div>
<Switch {...register('enabledOut')} variant='ontime-on-light' />
</div>
<div className={styles.splitSection}> <FormControl isInvalid={!!errors.targetIP} className={styles.splitSection}>
<div> <label htmlFor='targetIP'>
<span className={styles.sectionTitle} style={{ fontWeight: 600 }}> <span className={styles.sectionTitle}>OSC target IP</span>
OSC Output {errors.targetIP ? (
</span> <span className={styles.error}>{errors.targetIP.message}</span>
<span className={styles.sectionSubtitle}>Ontime data feedback</span> ) : (
</div> <span className={styles.sectionSubtitle}>Default 127.0.0.1</span>
<Switch {...register('enabledOut')} variant='ontime-on-light' /> )}
</div> </label>
<Input
id='targetIP'
placeholder='127.0.0.1'
width='140px'
size='sm'
textAlign='right'
variant='ontime-filled-on-light'
{...register('targetIP', {
required: { value: true, message: 'Required field' },
pattern: {
value: isIPAddress,
message: 'Invalid IP address',
},
})}
/>
</FormControl>
<FormControl isInvalid={!!errors.targetIP} className={styles.splitSection}> <FormControl className={styles.splitSection}>
<label htmlFor='targetIP'> <label htmlFor='portOut'>
<span className={styles.sectionTitle}>OSC target IP</span> <span className={styles.sectionTitle}>OSC target Port</span>
{errors.targetIP ? ( {errors.portOut ? (
<span className={styles.error}>{errors.targetIP.message}</span> <span className={styles.error}>{errors.portOut.message}</span>
) : ( ) : (
<span className={styles.sectionSubtitle}>Default 127.0.0.1</span> <span className={styles.sectionSubtitle}>Default 9999</span>
)} )}
</label> </label>
<Input <Input
id='targetIP' id='portOut'
placeholder='127.0.0.1' placeholder='9999'
width='140px' width='75px'
size='sm' size='sm'
textAlign='right' textAlign='right'
variant='ontime-filled-on-light' maxLength={5}
{...register('targetIP', { variant='ontime-filled-on-light'
required: { value: true, message: 'Required field' }, {...register('portOut', {
pattern: { required: { value: true, message: 'Required field' },
value: isIPAddress, max: { value: 65535, message: 'Port in incorrect range (1024 - 65535)' },
message: 'Invalid IP address', min: { value: 1024, message: 'Port in incorrect range (1024 - 65535)' },
}, pattern: {
})} value: isOnlyNumbers,
/> message: 'Value should be numeric',
</FormControl> },
})}
<FormControl className={styles.splitSection}> />
<label htmlFor='portOut'> </FormControl>
<span className={styles.sectionTitle}>OSC target Port</span>
{errors.portOut ? (
<span className={styles.error}>{errors.portOut.message}</span>
) : (
<span className={styles.sectionSubtitle}>Default 9999</span>
)}
</label>
<Input
id='portOut'
placeholder='9999'
width='75px'
size='sm'
textAlign='right'
maxLength={5}
variant='ontime-filled-on-light'
{...register('portOut', {
required: { value: true, message: 'Required field' },
max: { value: 65535, message: 'Port in incorrect range (1024 - 65535)' },
min: { value: 1024, message: 'Port in incorrect range (1024 - 65535)' },
pattern: {
value: isOnlyNumbers,
message: 'Value should be numeric',
},
})}
/>
</FormControl>
</ModalBody>
</form>
<OntimeModalFooter <OntimeModalFooter
formId='oscSettings' formId='oscSettings'
handleRevert={resetForm} handleRevert={resetForm}
@@ -168,6 +162,6 @@ export default function OscSettings() {
isValid={isValid} isValid={isValid}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
/> />
</> </form>
); );
} }
@@ -0,0 +1,150 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import {
Button,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Textarea,
} from '@chakra-ui/react';
import type { EventData } from 'ontime-types';
import { EVENT_DATA, RUNDOWN_TABLE } from '../../../common/api/apiConstants';
import { postNew } from '../../../common/api/ontimeApi';
import useEventData from '../../../common/hooks-query/useEventData';
import { eventDataPlaceholder } from '../../../common/models/EventData';
import { ontimeQueryClient } from '../../../common/queryClient';
import styles from '../Modal.module.scss';
interface QuickStartProps {
onClose: () => void;
isOpen: boolean;
}
export default function QuickStart({ onClose, isOpen }: QuickStartProps) {
const { data, status } = useEventData();
const {
handleSubmit,
register,
reset,
formState: { isSubmitting },
} = useForm({ defaultValues: data });
useEffect(() => {
reset(data);
}, [reset, data]);
const onSubmit = async (data: Partial<EventData>) => {
await postNew(data);
await ontimeQueryClient.invalidateQueries(EVENT_DATA);
await ontimeQueryClient.invalidateQueries(RUNDOWN_TABLE);
};
const onReset = () => reset(eventDataPlaceholder);
const disableButtons = status !== 'success' || isSubmitting;
return (
<Modal
onClose={onClose}
isOpen={isOpen}
closeOnOverlayClick={false}
motionPreset='slideInBottom'
size='xl'
scrollBehavior='inside'
preserveScrollBarGap
variant='ontime'
>
<ModalOverlay />
<ModalContent>
<ModalHeader>Ontime quick start</ModalHeader>
<ModalCloseButton />
<ModalBody className={styles.pad}>
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer}>
<div className={styles.entryRow}>
<label className={styles.sectionTitle}>
Event title
<Input
variant='ontime-filled-on-light'
size='sm'
maxLength={50}
placeholder='Eurovision song contest'
{...register('title')}
/>
</label>
</div>
<div className={styles.entryRow}>
<label className={styles.sectionTitle}>
Public Info
<Textarea
variant='ontime-filled-on-light'
size='sm'
maxLength={150}
placeholder='Shows always start ontime'
{...register('publicInfo')}
/>
</label>
</div>
<div className={styles.entryRow}>
<label className={styles.sectionTitle}>
Public QR Code Url
<Input
variant='ontime-filled-on-light'
size='sm'
placeholder='www.getontime.no'
{...register('publicUrl')}
/>
</label>
</div>
<div className={styles.entryRow}>
<label className={styles.sectionTitle}>
Backstage Info
<Textarea
variant='ontime-filled-on-light'
size='sm'
maxLength={150}
placeholder='Wi-Fi password: 1234'
{...register('backstageInfo')}
/>
</label>
</div>
<div className={styles.entryRow}>
<label className={styles.sectionTitle}>
Backstage QR Code Url
<Input
variant='ontime-filled-on-light'
size='sm'
placeholder='www.ontime.gitbook.io'
{...register('backstageUrl')}
/>
</label>
</div>
<div className={styles.footerNotes}>
Note: Application options will be kept but rundown and event data will be reset <br />
</div>
<ModalFooter className={styles.buttonSection}>
<Button onClick={onReset} isDisabled={disableButtons} variant='ontime-ghost-on-light' size='sm'>
Clear data
</Button>
<Button
type='submit'
isLoading={isSubmitting}
isDisabled={disableButtons}
variant='ontime-filled'
padding='0 2em'
size='sm'
>
New showfile
</Button>
</ModalFooter>
</form>
</ModalBody>
</ModalContent>
</Modal>
);
}
@@ -1,4 +1,4 @@
import { ReactNode, useMemo } from 'react'; import { ComponentType, useMemo } from 'react';
import { Playback, TitleBlock } from 'ontime-types'; import { Playback, TitleBlock } from 'ontime-types';
import { useStore } from 'zustand'; import { useStore } from 'zustand';
@@ -10,22 +10,23 @@ import { useViewOptionsStore } from '../../common/stores/viewOptions';
export type TitleManager = TitleBlock & { showNow: boolean; showNext: boolean }; export type TitleManager = TitleBlock & { showNow: boolean; showNext: boolean };
const withData = (Component: ReactNode) => { const withData = <P extends object>(Component: ComponentType<P>) => {
return (props) => { // eslint-disable-next-line react/display-name -- its ok
return (props: P) => {
// persisted app state // persisted app state
const isMirrored = useViewOptionsStore((state) => state.mirror); const isMirrored = useViewOptionsStore((state) => state.mirror);
// HTTP API data // HTTP API data
const { data: eventsData } = useRundown(); const { data: rundownData } = useRundown();
const { data: genData } = useEventData(); const { data: eventData } = useEventData();
const { data: viewSettings } = useViewSettings(); const { data: viewSettings } = useViewSettings();
const publicEvents = useMemo(() => { const publicEvents = useMemo(() => {
if (Array.isArray(eventsData)) { if (Array.isArray(rundownData)) {
return eventsData.filter((e) => e.type === 'event' && e.title && e.isPublic); return rundownData.filter((e) => e.type === 'event' && e.title && e.isPublic);
} }
return []; return [];
}, [eventsData]); }, [rundownData]);
// websocket data // websocket data
const data = useStore(runtime); const data = useStore(runtime);
@@ -99,12 +100,12 @@ const withData = (Component: ReactNode) => {
publicTitle={publicTitleManager} publicTitle={publicTitleManager}
time={TimeManagerType} time={TimeManagerType}
events={publicEvents} events={publicEvents}
backstageEvents={eventsData} backstageEvents={rundownData}
selectedId={selectedId} selectedId={selectedId}
publicSelectedId={publicSelectedId} publicSelectedId={publicSelectedId}
viewSettings={viewSettings} viewSettings={viewSettings}
nextId={nextId} nextId={nextId}
general={genData} general={eventData}
onAir={onAir} onAir={onAir}
/> />
); );
@@ -21,7 +21,7 @@ interface MinimalTimerProps {
} }
export default function MinimalTimer(props: MinimalTimerProps) { export default function MinimalTimer(props: MinimalTimerProps) {
const { isMirrored, pres, time, viewSettings, general } = props; const { isMirrored, pres, time, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL); const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@@ -129,7 +129,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
const isPlaying = time.playback !== Playback.Pause; const isPlaying = time.playback !== Playback.Pause;
const isNegative = const isNegative =
(time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp; (time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp;
const showEndMessage = (time.current ?? 0) < 0 && general.endMessage && !hideEndMessage; const showEndMessage = (time.current ?? 0) < 0 && viewSettings.endMessage && !hideEndMessage;
const showFinished = const showFinished =
time.finished && !userOptions?.hideOvertime && (time.timerType !== TimerType.Clock || showEndMessage); time.finished && !userOptions?.hideOvertime && (time.timerType !== TimerType.Clock || showEndMessage);
@@ -162,7 +162,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
</div> </div>
)} )}
{showEndMessage ? ( {showEndMessage ? (
<div className='end-message'>{general.endMessage}</div> <div className='end-message'>{viewSettings.endMessage}</div>
) : ( ) : (
<div <div
className={timerClasses} className={timerClasses}
@@ -46,7 +46,7 @@ interface TimerProps {
} }
export default function Timer(props: TimerProps) { export default function Timer(props: TimerProps) {
const { isMirrored, general, pres, title, time, viewSettings } = props; const { isMirrored, pres, title, time, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL); const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
@@ -65,7 +65,7 @@ export default function Timer(props: TimerProps) {
const isNegative = const isNegative =
(time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp; (time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp;
const showEndMessage = (time.current ?? 1) < 0 && general.endMessage; const showEndMessage = (time.current ?? 1) < 0 && viewSettings.endMessage;
const showProgress = time.playback !== Playback.Stop; const showProgress = time.playback !== Playback.Stop;
const showFinished = time.finished && (time.timerType !== TimerType.Clock || showEndMessage); const showFinished = time.finished && (time.timerType !== TimerType.Clock || showEndMessage);
const showClock = time.timerType !== TimerType.Clock; const showClock = time.timerType !== TimerType.Clock;
@@ -95,7 +95,7 @@ export default function Timer(props: TimerProps) {
<div className='timer-container'> <div className='timer-container'>
{showEndMessage ? ( {showEndMessage ? (
<div className='end-message'>{general.endMessage}</div> <div className='end-message'>{viewSettings.endMessage}</div>
) : ( ) : (
<div <div
className={timerClasses} className={timerClasses}
+4 -1
View File
@@ -2,7 +2,7 @@ export const ontimeModal = {
header: { header: {
fontWeight: 400, fontWeight: 400,
letterSpacing: '0.3px', letterSpacing: '0.3px',
padding: '8px 16px', padding: '16px 24px',
fontSize: '20px', fontSize: '20px',
color: '#202020', // $gray-50 color: '#202020', // $gray-50
}, },
@@ -19,6 +19,9 @@ export const ontimeModal = {
closeButton: { closeButton: {
color: '#202020', // $gray-50 color: '#202020', // $gray-50
}, },
footer: {
padding: '8px',
},
}; };
export const ontimeSmallModal = { export const ontimeSmallModal = {
+3
View File
@@ -13,4 +13,7 @@ export const ontimeTab = {
tablist: { tablist: {
borderBottom: '2px solid #ececec', // $gray-100 borderBottom: '2px solid #ececec', // $gray-100
}, },
tabpanel: {
padding: 0,
},
}; };
+5 -8
View File
@@ -23,13 +23,11 @@ export const ontimeInputFilled = {
export const ontimeInputFilledOnLight = { export const ontimeInputFilledOnLight = {
field: { field: {
backgroundColor: 'white', backgroundColor: 'white',
border: '2px solid transparent', border: '2px solid #f6f6f6', // $gray-50
_hover: { _hover: {
backgroundColor: 'white',
border: '2px solid #D2DDFF', // $blue-200 border: '2px solid #D2DDFF', // $blue-200
}, },
_focus: { _focus: {
backgroundColor: 'white',
border: '2px solid #578AF4', // $blue-500 border: '2px solid #578AF4', // $blue-500
}, },
}, },
@@ -42,16 +40,15 @@ export const ontimeTextAreaFilled = {
export const ontimeTextAreaFilledOnLight = { export const ontimeTextAreaFilledOnLight = {
borderRadius: '3px', borderRadius: '3px',
fontWeight: '400', fontWeight: '400',
backgroundColor: '#ececec', // $gray-100 backgroundColor: 'white',
color: '#202020', // $gray-1200 color: '#202020', // $gray-1200
border: '1px solid transparent', border: '2px solid #f6f6f6', // $gray-50
_hover: { _hover: {
backgroundColor: '#cfcfcf', // $gray-300 border: '2px solid #D2DDFF', // $blue-200
}, },
_focus: { _focus: {
backgroundColor: '#cfcfcf', // $gray-300
color: '#101010', color: '#101010',
border: '1px solid #578AF4', // $blue-500 border: '2px solid #578AF4', // $blue-500
}, },
_placeholder: { color: '#9d9d9d' }, // $gray-500 _placeholder: { color: '#9d9d9d' }, // $gray-500
}; };
+1 -1
View File
@@ -88,7 +88,7 @@ const theme = extendTheme({
}, },
variants: { variants: {
'ontime-filled': { ...ontimeTextAreaFilled }, 'ontime-filled': { ...ontimeTextAreaFilled },
'ontime-filled-onlight': { ...ontimeTextAreaFilledOnLight }, 'ontime-filled-on-light': { ...ontimeTextAreaFilledOnLight },
}, },
}, },
Tooltip: { Tooltip: {
@@ -12,7 +12,7 @@ export class DataProvider {
return data; return data;
} }
static async setEventData(newData: EventData) { static async setEventData(newData: Partial<EventData>) {
data.eventData = { ...data.eventData, ...newData }; data.eventData = { ...data.eventData, ...newData };
await this.persist(); await this.persist();
return data.eventData; return data.eventData;
@@ -8,7 +8,6 @@ describe('safeMerge', () => {
publicUrl: 'existing public URL', publicUrl: 'existing public URL',
backstageUrl: 'existing backstageUrl', backstageUrl: 'existing backstageUrl',
backstageInfo: 'existing backstageInfo', backstageInfo: 'existing backstageInfo',
endMessage: 'existing endMessage',
}, },
settings: { settings: {
app: 'ontime', app: 'ontime',
@@ -20,6 +19,7 @@ describe('safeMerge', () => {
}, },
viewSettings: { viewSettings: {
overrideStyles: false, overrideStyles: false,
endMessage: 'existing endMessage',
}, },
aliases: [], aliases: [],
userFields: { userFields: {
@@ -75,7 +75,6 @@ describe('safeMerge', () => {
publicInfo: 'new public info', publicInfo: 'new public info',
backstageUrl: 'existing backstageUrl', backstageUrl: 'existing backstageUrl',
backstageInfo: 'existing backstageInfo', backstageInfo: 'existing backstageInfo',
endMessage: 'existing endMessage',
}); });
}); });
@@ -145,7 +144,6 @@ describe('safeMerge', () => {
publicInfo: '', publicInfo: '',
backstageUrl: '', backstageUrl: '',
backstageInfo: '', backstageInfo: '',
endMessage: '',
}, },
settings: { settings: {
app: 'ontime', app: 'ontime',
@@ -157,6 +155,7 @@ describe('safeMerge', () => {
}, },
viewSettings: { viewSettings: {
overrideStyles: false, overrideStyles: false,
endMessage: '',
}, },
aliases: [], aliases: [],
userFields: { userFields: {
@@ -1,14 +1,14 @@
import { removeUndefined } from '../utils/parserUtils.js'; import { removeUndefined } from '../utils/parserUtils.js';
import { failEmptyObjects } from '../utils/routerUtils.js'; import { failEmptyObjects } from '../utils/routerUtils.js';
import { DataProvider } from '../classes/data-provider/DataProvider.ts'; import { DataProvider } from '../classes/data-provider/DataProvider.js';
// Create controller for GET request to 'event' // Create controller for GET request to 'event'
export const getEvent = async (req, res) => { export const getEventData = async (req, res) => {
res.json(DataProvider.getEventData()); res.json(DataProvider.getEventData());
}; };
// Create controller for POST request to 'event' // Create controller for POST request to 'event'
export const postEvent = async (req, res) => { export const postEventData = async (req, res) => {
if (failEmptyObjects(req.body, res)) { if (failEmptyObjects(req.body, res)) {
return; return;
} }
@@ -1,6 +1,6 @@
import { body, validationResult } from 'express-validator'; import { body, validationResult } from 'express-validator';
export const eventSanitizer = [ export const eventDataSanitizer = [
body('title').optional().isString().trim(), body('title').optional().isString().trim(),
body('publicUrl').optional().isString().trim(), body('publicUrl').optional().isString().trim(),
body('publicInfo').optional().isString().trim(), body('publicInfo').optional().isString().trim(),
@@ -1,7 +1,8 @@
import fs from 'fs'; import fs from 'fs';
import type { EventData } from 'ontime-types';
import { networkInterfaces } from 'os'; import { networkInterfaces } from 'os';
import { generateId } from 'ontime-utils'; import { generateId } from 'ontime-utils';
import { fileHandler } from '../utils/parser.ts'; import { fileHandler } from '../utils/parser.js';
import { DataProvider } from '../classes/data-provider/DataProvider.js'; import { DataProvider } from '../classes/data-provider/DataProvider.js';
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js'; import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
import { mergeObject } from '../utils/parserUtils.js'; import { mergeObject } from '../utils/parserUtils.js';
@@ -10,6 +11,7 @@ import { eventStore } from '../stores/EventStore.js';
import { resolveDbPath } from '../setup.js'; import { resolveDbPath } from '../setup.js';
import { oscIntegration } from '../services/integration-service/OscIntegration.js'; import { oscIntegration } from '../services/integration-service/OscIntegration.js';
import { logger } from '../classes/Logger.js'; import { logger } from '../classes/Logger.js';
import { deleteAllEvents } from '../services/RundownService.js';
// Create controller for GET request to '/ontime/poll' // Create controller for GET request to '/ontime/poll'
// Returns data for current state // Returns data for current state
@@ -56,9 +58,9 @@ const uploadAndParse = async (file, req, res, options) => {
try { try {
const result = await fileHandler(file); const result = await fileHandler(file);
if (result?.error) { if ('error' in result && result.error) {
res.status(400).send({ message: result.message }); res.status(400).send({ message: result.message });
} else if (result.message === 'success') { } else if ('data' in result && result.message === 'success') {
PlaybackService.stop(); PlaybackService.stop();
// explicitly write objects // explicitly write objects
if (typeof result !== 'undefined') { if (typeof result !== 'undefined') {
@@ -241,7 +243,10 @@ export const postViewSettings = async (req, res) => {
} }
try { try {
const newData = { overrideStyles: req.body.overrideStyles }; const newData = {
overrideStyles: req.body.overrideStyles,
endMessage: req.body?.endMessage || '',
};
await DataProvider.setViewSettings(newData); await DataProvider.setViewSettings(newData);
res.status(200).send(newData); res.status(200).send(newData);
} catch (error) { } catch (error) {
@@ -288,3 +293,21 @@ export const dbUpload = async (req, res) => {
const file = req.file.path; const file = req.file.path;
await uploadAndParse(file, req, res, options); await uploadAndParse(file, req, res, options);
}; };
// Create controller for POST request to '/ontime/new'
export const postNew = async (req, res) => {
try {
const newEventData: Omit<EventData, 'endMessage'> = {
title: req.body?.title ?? '',
publicUrl: req.body?.publicUrl ?? '',
publicInfo: req.body?.publicInfo ?? '',
backstageUrl: req.body?.backstageUrl ?? '',
backstageInfo: req.body?.backstageInfo ?? '',
};
const newData = await DataProvider.setEventData(newEventData);
await deleteAllEvents();
res.status(201).send(newData);
} catch (error) {
res.status(400).send(error);
}
};
@@ -6,6 +6,7 @@ import { validateOscSubscription } from '../utils/parserFunctions.js';
*/ */
export const viewValidator = [ export const viewValidator = [
check('overrideStyles').isBoolean().withMessage('overrideStyles value must be boolean'), check('overrideStyles').isBoolean().withMessage('overrideStyles value must be boolean'),
check('endMessage').isString().trim().withMessage('endMessage value must be string'),
(req, res, next) => { (req, res, next) => {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
+1 -1
View File
@@ -8,7 +8,6 @@ export const dbModel: DatabaseModel = {
publicInfo: '', publicInfo: '',
backstageUrl: '', backstageUrl: '',
backstageInfo: '', backstageInfo: '',
endMessage: '',
}, },
settings: { settings: {
app: 'ontime', app: 'ontime',
@@ -20,6 +19,7 @@ export const dbModel: DatabaseModel = {
}, },
viewSettings: { viewSettings: {
overrideStyles: false, overrideStyles: false,
endMessage: '',
}, },
aliases: [], aliases: [],
userFields: { userFields: {
+6 -6
View File
@@ -1,12 +1,12 @@
import express from 'express'; import express from 'express';
// import event controller
import { getEventData, postEventData } from '../controllers/eventDataController.ts';
import { eventDataSanitizer } from '../controllers/eventDataController.validate.ts';
export const router = express.Router(); export const router = express.Router();
// import event controller
import { getEvent, postEvent } from '../controllers/eventDataController.js';
import { eventSanitizer } from '../controllers/eventDataController.validate.js';
// create route between controller and 'GET /event' endpoint // create route between controller and 'GET /event' endpoint
router.get('/', getEvent); router.get('/', getEventData);
// create route between controller and 'POST /event' endpoint // create route between controller and 'POST /event' endpoint
router.post('/', eventSanitizer, postEvent); router.post('/', eventDataSanitizer, postEventData);
+5
View File
@@ -11,6 +11,7 @@ import {
getViewSettings, getViewSettings,
poll, poll,
postAliases, postAliases,
postNew,
postOSC, postOSC,
postSettings, postSettings,
postUserFields, postUserFields,
@@ -24,6 +25,7 @@ import {
validateUserFields, validateUserFields,
viewValidator, viewValidator,
} from '../controllers/ontimeController.validate.js'; } from '../controllers/ontimeController.validate.js';
import { eventDataSanitizer } from '../controllers/eventDataController.validate.js';
export const router = express.Router(); export const router = express.Router();
@@ -68,3 +70,6 @@ router.get('/osc', getOSC);
// create route between controller and '/ontime/osc' endpoint // create route between controller and '/ontime/osc' endpoint
router.post('/osc', validateOSC, postOSC); router.post('/osc', validateOSC, postOSC);
// create route between controller and '/ontime/new' endpoint
router.post('/new', eventDataSanitizer, postNew);
+1 -1
View File
@@ -352,7 +352,7 @@ type ResponseError = { error: true; message: string };
* @param {string} file - reference to file * @param {string} file - reference to file
* @return {object} - parse result message * @return {object} - parse result message
*/ */
export const fileHandler = async (file) => { export const fileHandler = async (file): ResponseOK | ResponseError => {
let res: Partial<ResponseOK | ResponseError> = {}; let res: Partial<ResponseOK | ResponseError> = {};
// check which file type are we dealing with // check which file type are we dealing with
+1 -1
View File
@@ -87,7 +87,6 @@ export const parseEventData = (data, enforce): EventData => {
publicInfo: e.publicInfo || dbModel.eventData.publicInfo, publicInfo: e.publicInfo || dbModel.eventData.publicInfo,
backstageUrl: e.backstageUrl || dbModel.eventData.backstageUrl, backstageUrl: e.backstageUrl || dbModel.eventData.backstageUrl,
backstageInfo: e.backstageInfo || dbModel.eventData.backstageInfo, backstageInfo: e.backstageInfo || dbModel.eventData.backstageInfo,
endMessage: e.endMessage || dbModel.eventData.endMessage,
}; };
} else if (enforce) { } else if (enforce) {
newEventData = { ...dbModel.eventData }; newEventData = { ...dbModel.eventData };
@@ -145,6 +144,7 @@ export const parseViewSettings = (data, enforce): ViewSettings => {
const viewSettings = { const viewSettings = {
overrideStyles: v.overrideStyles ?? dbModel.viewSettings.overrideStyles, overrideStyles: v.overrideStyles ?? dbModel.viewSettings.overrideStyles,
endMessage: v.endMessage ?? dbModel.viewSettings.endMessage,
}; };
// write to db // write to db
+3 -3
View File
@@ -224,8 +224,7 @@
"publicUrl": "www.getontime.no", "publicUrl": "www.getontime.no",
"publicInfo": "WiFi: demoproject \nPassword: ontimeproject", "publicInfo": "WiFi: demoproject \nPassword: ontimeproject",
"backstageUrl": "www.getontime.no", "backstageUrl": "www.getontime.no",
"backstageInfo": "WiFi: demobackstage\nPassword: ontimeproject", "backstageInfo": "WiFi: demobackstage\nPassword: ontimeproject"
"endMessage": ""
}, },
"settings": { "settings": {
"app": "ontime", "app": "ontime",
@@ -235,7 +234,8 @@
"pinCode": "1234" "pinCode": "1234"
}, },
"viewSettings": { "viewSettings": {
"overrideStyles": false "overrideStyles": false,
"endMessage": ""
}, },
"osc": { "osc": {
"port": 8888, "port": 8888,
+3 -3
View File
@@ -98,8 +98,7 @@
"publicUrl": "www.getontime.no", "publicUrl": "www.getontime.no",
"publicInfo": "WiFi: demoproject \nPassword: ontimeproject", "publicInfo": "WiFi: demoproject \nPassword: ontimeproject",
"backstageUrl": "www.getontime.no", "backstageUrl": "www.getontime.no",
"backstageInfo": "WiFi: demobackstage\nPassword: ontimeproject", "backstageInfo": "WiFi: demobackstage\nPassword: ontimeproject"
"endMessage": ""
}, },
"settings": { "settings": {
"app": "ontime", "app": "ontime",
@@ -110,7 +109,8 @@
"timeFormat": "24" "timeFormat": "24"
}, },
"viewSettings": { "viewSettings": {
"overrideStyles": false "overrideStyles": false,
"endMessage": ""
}, },
"aliases": [ "aliases": [
{ {
+3 -3
View File
@@ -33,8 +33,7 @@
"publicUrl": "www.getontime.no", "publicUrl": "www.getontime.no",
"publicInfo": "WiFi: demoproject \nPassword: ontimeproject", "publicInfo": "WiFi: demoproject \nPassword: ontimeproject",
"backstageUrl": "www.getontime.no", "backstageUrl": "www.getontime.no",
"backstageInfo": "WiFi: demobackstage\nPassword: ontimeproject", "backstageInfo": "WiFi: demobackstage\nPassword: ontimeproject"
"endMessage": ""
}, },
"settings": { "settings": {
"app": "ontime", "app": "ontime",
@@ -45,7 +44,8 @@
"timeFormat": "24" "timeFormat": "24"
}, },
"viewSettings": { "viewSettings": {
"overrideStyles": false "overrideStyles": false,
"endMessage": ""
}, },
"aliases": [ "aliases": [
{ {
+3 -3
View File
@@ -98,8 +98,7 @@
"publicUrl": "www.getontime.no", "publicUrl": "www.getontime.no",
"publicInfo": "WiFi: demoproject \nPassword: ontimeproject", "publicInfo": "WiFi: demoproject \nPassword: ontimeproject",
"backstageUrl": "www.getontime.no", "backstageUrl": "www.getontime.no",
"backstageInfo": "WiFi: demobackstage\nPassword: ontimeproject", "backstageInfo": "WiFi: demobackstage\nPassword: ontimeproject"
"endMessage": ""
}, },
"settings": { "settings": {
"app": "ontime", "app": "ontime",
@@ -110,7 +109,8 @@
"timeFormat": "24" "timeFormat": "24"
}, },
"viewSettings": { "viewSettings": {
"overrideStyles": false "overrideStyles": false,
"endMessage": ""
}, },
"aliases": [ "aliases": [
{ {
@@ -4,5 +4,4 @@ export type EventData = {
publicInfo: string; publicInfo: string;
backstageUrl: string; backstageUrl: string;
backstageInfo: string; backstageInfo: string;
endMessage: string;
}; };
@@ -1,3 +1,4 @@
export type ViewSettings = { export type ViewSettings = {
overrideStyles: boolean; overrideStyles: boolean;
endMessage: string;
} }