refactor: migrate project data

This commit is contained in:
Carlos Valente
2025-07-08 17:11:25 +02:00
committed by Carlos Valente
parent 1fc6f9b3ea
commit 6f34e1006b
38 changed files with 136 additions and 228 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ const SMinimalTimer = withPreset(withData(MinimalTimerView));
const SClock = withPreset(withData(ClockView)); const SClock = withPreset(withData(ClockView));
const SCountdown = withPreset(withData(Countdown)); const SCountdown = withPreset(withData(Countdown));
const SBackstage = withPreset(withData(Backstage)); const SBackstage = withPreset(withData(Backstage));
const SProjectInfo = withPreset(withData(ProjectInfo)); const SProjectInfo = withPreset(ProjectInfo); // NOTE: ProjectInfo does not use the viewWrapper since it has no options
const SLowerThird = withPreset(withData(Lower)); const SLowerThird = withPreset(withData(Lower));
const SStudio = withPreset(withData(StudioClock)); const SStudio = withPreset(withData(StudioClock));
const STimeline = withPreset(withData(Timeline)); const STimeline = withPreset(withData(Timeline));
@@ -8,11 +8,11 @@ import useViewEditor from './useViewEditor';
interface ViewNavigationMenuProps { interface ViewNavigationMenuProps {
isLockable?: boolean; isLockable?: boolean;
supressSettings?: boolean; suppressSettings?: boolean;
} }
export default memo(ViewNavigationMenu); export default memo(ViewNavigationMenu);
function ViewNavigationMenu({ isLockable, supressSettings }: ViewNavigationMenuProps) { function ViewNavigationMenu({ isLockable, suppressSettings }: ViewNavigationMenuProps) {
const [isMenuOpen, menuHandler] = useDisclosure(); const [isMenuOpen, menuHandler] = useDisclosure();
const { showEditFormDrawer, isViewLocked } = useViewEditor({ isLockable }); const { showEditFormDrawer, isViewLocked } = useViewEditor({ isLockable });
@@ -28,7 +28,7 @@ function ViewNavigationMenu({ isLockable, supressSettings }: ViewNavigationMenuP
[ [
'mod + ,', 'mod + ,',
() => { () => {
if (isViewLocked || supressSettings) return; if (isViewLocked || suppressSettings) return;
showEditFormDrawer(); showEditFormDrawer();
}, },
{ preventDefault: true }, { preventDefault: true },
@@ -43,7 +43,7 @@ function ViewNavigationMenu({ isLockable, supressSettings }: ViewNavigationMenuP
<> <>
<FloatingNavigation <FloatingNavigation
toggleMenu={menuHandler.toggle} toggleMenu={menuHandler.toggle}
toggleSettings={supressSettings ? undefined : () => showEditFormDrawer()} toggleSettings={suppressSettings ? undefined : () => showEditFormDrawer()}
/> />
<NavigationMenu isOpen={isMenuOpen} onClose={menuHandler.close} /> <NavigationMenu isOpen={isMenuOpen} onClose={menuHandler.close} />
</> </>
+3 -3
View File
@@ -3,8 +3,8 @@ import { ProjectData } from 'ontime-types';
export const projectDataPlaceholder: ProjectData = { export const projectDataPlaceholder: ProjectData = {
title: '', title: '',
description: '', description: '',
backstageUrl: '', url: '',
backstageInfo: '', info: '',
projectLogo: null, logo: null,
custom: [], custom: [],
}; };
@@ -22,8 +22,8 @@ interface ProjectCreateFromProps {
type ProjectCreateFormValues = { type ProjectCreateFormValues = {
title?: string; title?: string;
description?: string; description?: string;
backstageInfo?: string; info?: string;
backstageUrl?: string; url?: string;
custom?: { title: string; value: string }[]; custom?: { title: string; value: string }[];
}; };
@@ -107,18 +107,12 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
<Input fluid maxLength={100} placeholder='Euro Love, Malmö 2024' {...register('description')} /> <Input fluid maxLength={100} placeholder='Euro Love, Malmö 2024' {...register('description')} />
</label> </label>
<label> <label>
Backstage info Project info
<Textarea <Textarea fluid maxLength={150} placeholder='Wi-Fi password: 1234' resize='vertical' {...register('info')} />
fluid
maxLength={150}
placeholder='Wi-Fi password: 1234'
resize='vertical'
{...register('backstageInfo')}
/>
</label> </label>
<label> <label>
Backstage QR code Url Project QR code URL
<Input fluid placeholder={documentationUrl} {...register('backstageUrl')} /> <Input fluid placeholder={documentationUrl} {...register('url')} />
</label> </label>
<Panel.Section> <Panel.Section>
<Panel.ListItem> <Panel.ListItem>
@@ -61,16 +61,16 @@ export default function ProjectData() {
validateLogo(file); validateLogo(file);
const response = await uploadProjectLogo(file); const response = await uploadProjectLogo(file);
setValue('projectLogo', response.data.logoFilename, { setValue('logo', response.data.logoFilename, {
shouldDirty: true, shouldDirty: true,
}); });
} catch (error) { } catch (error) {
const message = maybeAxiosError(error); const message = maybeAxiosError(error);
setError('projectLogo', { message }); setError('logo', { message });
} }
}; };
const { ref, ...projectLogoRest } = register('projectLogo'); const { ref, ...projectLogoRest } = register('logo');
const uploadInputRef = useRef<HTMLInputElement | null>(null); const uploadInputRef = useRef<HTMLInputElement | null>(null);
@@ -81,7 +81,7 @@ export default function ProjectData() {
const handleDeleteLogo = (e: React.MouseEvent<HTMLButtonElement>) => { const handleDeleteLogo = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setValue('projectLogo', null, { setValue('logo', null, {
shouldDirty: true, shouldDirty: true,
}); });
}; };
@@ -149,12 +149,12 @@ export default function ProjectData() {
onChange={handleUploadProjectLogo} onChange={handleUploadProjectLogo}
/> />
<Panel.Card className={style.uploadLogoCard}> <Panel.Card className={style.uploadLogoCard}>
{watch('projectLogo') ? ( {watch('logo') ? (
<> <>
<img src={`${projectLogoPath}/${watch('projectLogo')}`} /> <img src={`${projectLogoPath}/${watch('logo')}`} />
<Button <Button
variant='subtle-destructive' variant='subtle-destructive'
disabled={isSubmitting || !watch('projectLogo')} disabled={isSubmitting || !watch('logo')}
onClick={handleDeleteLogo} onClick={handleDeleteLogo}
> >
<IoTrash /> <IoTrash />
@@ -167,7 +167,7 @@ export default function ProjectData() {
Upload logo Upload logo
</Button> </Button>
)} )}
{errors?.projectLogo?.message && <Panel.Error>{errors.projectLogo.message}</Panel.Error>} {errors?.logo?.message && <Panel.Error>{errors.logo.message}</Panel.Error>}
</Panel.Card> </Panel.Card>
</label> </label>
</Panel.Section> </Panel.Section>
@@ -177,18 +177,18 @@ export default function ProjectData() {
<Input fluid maxLength={100} placeholder='Euro Love, Malmö 2024' {...register('description')} /> <Input fluid maxLength={100} placeholder='Euro Love, Malmö 2024' {...register('description')} />
</label> </label>
<label> <label>
Backstage info Project info
<Textarea <Textarea
fluid fluid
maxLength={150} maxLength={150}
placeholder='Wi-Fi password: 1234' placeholder='Wi-Fi password: 1234'
resize='vertical' resize='vertical'
{...register('backstageInfo')} {...register('info')}
/> />
</label> </label>
<label> <label>
Backstage QR code URL Project QR code URL
<Input fluid placeholder={documentationUrl} {...register('backstageUrl')} /> <Input fluid placeholder={documentationUrl} {...register('url')} />
</label> </label>
<Panel.Section style={{ marginTop: 0 }}> <Panel.Section style={{ marginTop: 0 }}>
<Panel.ListItem> <Panel.ListItem>
@@ -20,7 +20,7 @@ function MessageControlExport() {
<ProtectRoute permission='editor'> <ProtectRoute permission='editor'>
<div className={style.messages} data-testid='panel-messages-control'> <div className={style.messages} data-testid='panel-messages-control'>
{!isExtracted && <Corner onClick={(event) => handleLinks('messagecontrol', event)} />} {!isExtracted && <Corner onClick={(event) => handleLinks('messagecontrol', event)} />}
{isExtracted && <ViewNavigationMenu supressSettings />} {isExtracted && <ViewNavigationMenu suppressSettings />}
<div className={classes}> <div className={classes}>
<ErrorBoundary> <ErrorBoundary>
@@ -18,7 +18,7 @@ function TimerControlExport() {
<ProtectRoute permission='editor'> <ProtectRoute permission='editor'>
<div className={style.playback} data-testid='panel-timer-control'> <div className={style.playback} data-testid='panel-timer-control'>
{!isExtracted && <Corner onClick={(event) => handleLinks('timercontrol', event)} />} {!isExtracted && <Corner onClick={(event) => handleLinks('timercontrol', event)} />}
{isExtracted && <ViewNavigationMenu supressSettings />} {isExtracted && <ViewNavigationMenu suppressSettings />}
<div className={style.content}> <div className={style.content}>
<ErrorBoundary> <ErrorBoundary>
@@ -36,7 +36,7 @@ function RundownExport() {
data-testid='panel-rundown' data-testid='panel-rundown'
> >
<FinderPlacement /> <FinderPlacement />
<ViewNavigationMenu supressSettings /> <ViewNavigationMenu suppressSettings />
<div className={style.content}> <div className={style.content}>
<ErrorBoundary> <ErrorBoundary>
<ContextMenu> <ContextMenu>
@@ -125,7 +125,7 @@ export default function Clock(props: ClockProps) {
}} }}
data-testid='clock-view' data-testid='clock-view'
> >
{general?.projectLogo && <ViewLogo name={general.projectLogo} className='logo' />} {general?.logo && <ViewLogo name={general.logo} className='logo' />}
<ViewParamsEditor viewOptions={clockOptions} /> <ViewParamsEditor viewOptions={clockOptions} />
<SuperscriptTime <SuperscriptTime
time={clock} time={clock}
@@ -160,7 +160,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
}} }}
data-testid='minimal-timer' data-testid='minimal-timer'
> >
{general?.projectLogo && <ViewLogo name={general.projectLogo} className='logo' />} {general?.logo && <ViewLogo name={general.logo} className='logo' />}
<ViewParamsEditor viewOptions={MINIMAL_TIMER_OPTIONS} /> <ViewParamsEditor viewOptions={MINIMAL_TIMER_OPTIONS} />
{showEndMessage ? ( {showEndMessage ? (
<div className='end-message'>{viewSettings.endMessage}</div> <div className='end-message'>{viewSettings.endMessage}</div>
+2 -2
View File
@@ -25,6 +25,6 @@ export const langDe: TranslationObject = {
'timeline.followedby': 'Gefolgt von', 'timeline.followedby': 'Gefolgt von',
'project.title': 'Titel', 'project.title': 'Titel',
'project.description': 'Beschreibung', 'project.description': 'Beschreibung',
'project.backstage_info': 'Backstage-Informationen', 'project.info': 'Projektinfo',
'project.backstage_url': 'Backstage-URL', 'project.url': 'Projekt-URL',
}; };
+2 -2
View File
@@ -23,8 +23,8 @@ export const langEn = {
'timeline.followedby': 'Followed by', 'timeline.followedby': 'Followed by',
'project.title': 'Title', 'project.title': 'Title',
'project.description': 'Description', 'project.description': 'Description',
'project.backstage_info': 'Backstage Info', 'project.info': 'Project Info',
'project.backstage_url': 'Backstage URL', 'project.url': 'Project URL',
}; };
export type TranslationObject = Record<keyof typeof langEn, string>; export type TranslationObject = Record<keyof typeof langEn, string>;
+2 -2
View File
@@ -25,6 +25,6 @@ export const langEs: TranslationObject = {
'timeline.followedby': 'Seguido por', 'timeline.followedby': 'Seguido por',
'project.title': 'Título', 'project.title': 'Título',
'project.description': 'Descripción', 'project.description': 'Descripción',
'project.backstage_info': 'Información de backstage', 'project.info': 'Información del proyecto',
'project.backstage_url': 'URL de backstage', 'project.url': 'URL del proyecto',
}; };
+2 -3
View File
@@ -25,7 +25,6 @@ export const langFr: TranslationObject = {
'timeline.followedby': 'Suivi de', 'timeline.followedby': 'Suivi de',
'project.title': 'Titre', 'project.title': 'Titre',
'project.description': 'Description', 'project.description': 'Description',
'project.backstage_info': 'Informations des coulisses', 'project.info': 'Informations du projet',
'project.backstage_url': 'URL des coulisses', 'project.url': 'URL du projet',
}; };
+2 -2
View File
@@ -25,6 +25,6 @@ export const langIt: TranslationObject = {
'timeline.followedby': 'Seguito da', 'timeline.followedby': 'Seguito da',
'project.title': 'Titolo', 'project.title': 'Titolo',
'project.description': 'Descrizione', 'project.description': 'Descrizione',
'project.backstage_info': 'Informazioni di backstage', 'project.info': 'Informazioni sul progetto',
'project.backstage_url': 'URL di backstage', 'project.url': 'URL del progetto',
}; };
@@ -103,7 +103,7 @@ export default function Backstage(props: BackstageProps) {
<div className={`backstage ${isMirrored ? 'mirror' : ''}`} data-testid='backstage-view'> <div className={`backstage ${isMirrored ? 'mirror' : ''}`} data-testid='backstage-view'>
<ViewParamsEditor viewOptions={backstageOptions} /> <ViewParamsEditor viewOptions={backstageOptions} />
<div className='project-header'> <div className='project-header'>
{general?.projectLogo && <ViewLogo name={general.projectLogo} className='logo' />} {general?.logo && <ViewLogo name={general.logo} className='logo' />}
<div className='title'>{general.title}</div> <div className='title'>{general.title}</div>
<div className='clock-container'> <div className='clock-container'>
<div className='label'>{getLocalizedString('common.time_now')}</div> <div className='label'>{getLocalizedString('common.time_now')}</div>
@@ -171,8 +171,8 @@ export default function Backstage(props: BackstageProps) {
{showSchedule && <ScheduleExport selectedId={selectedId} />} {showSchedule && <ScheduleExport selectedId={selectedId} />}
<div className={cx(['info', !showSchedule && 'info--stretch'])}> <div className={cx(['info', !showSchedule && 'info--stretch'])}>
{general.backstageUrl && <QRCode value={general.backstageUrl} size={qrSize} level='L' className='qr' />} {general.url && <QRCode value={general.url} size={qrSize} level='L' className='qr' />}
{general.backstageInfo && <div className='info__message'>{general.backstageInfo}</div>} {general.info && <div className='info__message'>{general.info}</div>}
</div> </div>
</div> </div>
); );
@@ -72,7 +72,7 @@ export default function Countdown({
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'> <div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
<ViewParamsEditor viewOptions={countdownOptions} /> <ViewParamsEditor viewOptions={countdownOptions} />
<div className='project-header'> <div className='project-header'>
{general?.projectLogo && <ViewLogo name={general.projectLogo} className='logo' />} {general?.logo && <ViewLogo name={general.logo} className='logo' />}
<div className='title'>{general.title}</div> <div className='title'>{general.title}</div>
<div className='clock-container'> <div className='clock-container'>
<div className='label'>{getLocalizedString('common.time_now')}</div> <div className='label'>{getLocalizedString('common.time_now')}</div>
@@ -38,7 +38,7 @@ describe('makeTable()', () => {
const headerData = { const headerData = {
title: 'test title', title: 'test title',
description: 'test description', description: 'test description',
projectLogo: 'test logo', logo: 'test logo',
}; };
const tableData = [ const tableData = [
{ {
@@ -41,7 +41,10 @@
white-space: break-spaces; white-space: break-spaces;
} }
a.info__value { .link.info__value {
display: flex;
gap: 0.5rem;
align-items: center;
color: $action-text-color; color: $action-text-color;
&:hover { &:hover {
@@ -1,37 +1,30 @@
import { ProjectData } from 'ontime-types'; import { Fragment } from 'react/jsx-runtime';
import { IoOpenOutline } from 'react-icons/io5';
import Empty from '../../common/components/state/Empty'; import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu';
import EmptyPage from '../../common/components/state/EmptyPage'; import EmptyPage from '../../common/components/state/EmptyPage';
import ViewLogo from '../../common/components/view-logo/ViewLogo'; import ViewLogo from '../../common/components/view-logo/ViewLogo';
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor'; import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
import { useWindowTitle } from '../../common/hooks/useWindowTitle'; import { useWindowTitle } from '../../common/hooks/useWindowTitle';
import useProjectData from '../../common/hooks-query/useProjectData';
import { useViewOptionsStore } from '../../common/stores/viewOptions';
import { useTranslation } from '../../translation/TranslationProvider'; import { useTranslation } from '../../translation/TranslationProvider';
import BackstageInfo from './backstage-info/BackstageInfo';
import CustomInfo from './custom-info/CustomInfo';
import { projectInfoOptions } from './projectInfo.options';
import './ProjectInfo.scss'; import './ProjectInfo.scss';
interface ProjectInfoProps { export default function ProjectInfo() {
general: ProjectData; // persisted app state
isMirrored: boolean; const isMirrored = useViewOptionsStore((state) => state.mirror);
} const { data, status } = useProjectData();
export default function ProjectInfo(props: ProjectInfoProps) {
const { general, isMirrored } = props;
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
useWindowTitle('Project info'); useWindowTitle('Project info');
if (!general) { if (status === 'pending' || !data) {
return <Empty text={getLocalizedString('common.no_data')} />;
}
if (!general) {
return ( return (
<> <>
<ViewParamsEditor viewOptions={projectInfoOptions} /> <ViewNavigationMenu isLockable suppressSettings />
<ViewParamsEditor viewOptions={[]} />
<EmptyPage text={getLocalizedString('common.no_data')} />; <EmptyPage text={getLocalizedString('common.no_data')} />;
</> </>
); );
@@ -41,13 +34,12 @@ export default function ProjectInfo(props: ProjectInfoProps) {
* Check if there is data to show at all * Check if there is data to show at all
* We need a special check for the project fields which can be an empty array * We need a special check for the project fields which can be an empty array
*/ */
const isEmpty = Object.values(general).every( const isEmpty = Object.values(data).every((value) => !value || (value && Array.isArray(value) && value.length === 0));
(value) => !value || (value && Array.isArray(value) && value.length === 0),
);
if (isEmpty) { if (isEmpty) {
return ( return (
<> <>
<ViewParamsEditor viewOptions={projectInfoOptions} /> <ViewNavigationMenu isLockable suppressSettings />
<ViewParamsEditor viewOptions={[]} />
<EmptyPage text={getLocalizedString('common.no_data')} />; <EmptyPage text={getLocalizedString('common.no_data')} />;
</> </>
); );
@@ -55,23 +47,47 @@ export default function ProjectInfo(props: ProjectInfoProps) {
return ( return (
<div className={`project ${isMirrored ? 'mirror' : ''}`} data-testid='project-view'> <div className={`project ${isMirrored ? 'mirror' : ''}`} data-testid='project-view'>
<ViewParamsEditor viewOptions={projectInfoOptions} /> <ViewNavigationMenu isLockable suppressSettings />
{general.projectLogo && <ViewLogo name={general.projectLogo} className='logo' />} <ViewParamsEditor viewOptions={[]} />
{data.logo && <ViewLogo name={data.logo} className='logo' />}
<div className='info'> <div className='info'>
{general.title && ( {data.title && (
<> <>
<div className='info__label'>{getLocalizedString('project.title')}</div> <div className='info__label'>{getLocalizedString('project.title')}</div>
<div className='info__value'>{general.title}</div> <div className='info__value'>{data.title}</div>
</> </>
)} )}
{general.description && ( {data.description && (
<> <>
<div className='info__label'>{getLocalizedString('project.description')}</div> <div className='info__label'>{getLocalizedString('project.description')}</div>
<div className='info__value'>{general.description}</div> <div className='info__value'>{data.description}</div>
</> </>
)} )}
<BackstageInfo general={general} /> {data.info && (
<CustomInfo general={general} /> <>
<div className='info__label'>{getLocalizedString('project.info')}</div>
<div className='info__value'>{data.info}</div>
</>
)}
{data.url && (
<>
<div className='info__label'>{getLocalizedString('project.url')}</div>
<a href={data.url} target='_blank' rel='noreferrer' className='info__value link'>
{data.url} <IoOpenOutline style={{ fontSize: '1em' }} />
</a>
</>
)}
{data.custom.map((info, idx) => {
if (!info.title || !info.value) {
return null;
}
return (
<Fragment key={`${info.title}-${idx}`}>
<div className='info__label'>{info.title}</div>
<div className='info__value'>{info.value}</div>
</Fragment>
);
})}
</div> </div>
</div> </div>
); );
@@ -1,40 +0,0 @@
import { useSearchParams } from 'react-router-dom';
import { ProjectData } from 'ontime-types';
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
import { useTranslation } from '../../../translation/TranslationProvider';
interface BackstageInfoProps {
general: ProjectData;
}
export default function BackstageInfo(props: BackstageInfoProps) {
const { general } = props;
const [searchParams] = useSearchParams();
const { getLocalizedString } = useTranslation();
const showBackstage = isStringBoolean(searchParams.get('showBackstage'));
if (!showBackstage) {
return null;
}
return (
<>
{general.backstageInfo && (
<>
<div className='info__label'>{getLocalizedString('project.backstage_info')}</div>
<div className='info__value'>{general.backstageInfo}</div>
</>
)}
{general.backstageUrl && (
<>
<div className='info__label'>{getLocalizedString('project.backstage_url')}</div>
<a href={general.backstageUrl} target='_blank' rel='noreferrer' className='info__value'>
{general.backstageUrl}
</a>
</>
)}
</>
);
}
@@ -1,36 +0,0 @@
import { Fragment } from 'react';
import { useSearchParams } from 'react-router-dom';
import { ProjectData } from 'ontime-types';
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
interface CustomInfoProps {
general: ProjectData;
}
export default function CustomInfo(props: CustomInfoProps) {
const { general } = props;
const [searchParams] = useSearchParams();
const showCustom = isStringBoolean(searchParams.get('showCustom'));
if (!showCustom || general.custom === undefined || general.custom.length === 0) {
return null;
}
return (
<>
{general.custom.map((info, idx) => {
if (!info.title || !info.value) {
return null;
}
return (
<Fragment key={`${info.title}-${idx}`}>
<div className='info__label'>{info.title}</div>
<div className='info__value'>{info.value}</div>
</Fragment>
);
})}
</>
);
}
@@ -1,25 +0,0 @@
import { OptionTitle } from '../../common/components/view-params-editor/constants';
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
export const projectInfoOptions: ViewOption[] = [
{
title: OptionTitle.BehaviourOptions,
collapsible: true,
options: [
{
id: 'showBackstage',
title: 'Show backstage Data',
description: 'Whether to show fields related to the backstage views',
type: 'boolean',
defaultValue: false,
},
{
id: 'showCustom',
title: 'Show Custom Data',
description: 'Whether to show fields related to the custom data',
type: 'boolean',
defaultValue: false,
},
],
},
];
+1 -1
View File
@@ -50,7 +50,7 @@ export default function Studio({
<ViewParamsEditor viewOptions={studioOptions} /> <ViewParamsEditor viewOptions={studioOptions} />
<div className='project-header'> <div className='project-header'>
{general?.projectLogo && <ViewLogo name={general.projectLogo} className='logo' />} {general?.logo && <ViewLogo name={general.logo} className='logo' />}
<div className='title'>{general.title}</div> <div className='title'>{general.title}</div>
</div> </div>
@@ -75,7 +75,7 @@ export default function TimelinePage(props: TimelinePageProps) {
<div className='timeline' data-testid='timeline-view'> <div className='timeline' data-testid='timeline-view'>
<ViewParamsEditor viewOptions={progressOptions} /> <ViewParamsEditor viewOptions={progressOptions} />
<div className='project-header'> <div className='project-header'>
{general?.projectLogo && <ViewLogo name={general.projectLogo} className='logo' />} {general?.logo && <ViewLogo name={general.logo} className='logo' />}
{general.title} {general.title}
<div className='clock-container'> <div className='clock-container'>
<div className='label'>{getLocalizedString('common.time_now')}</div> <div className='label'>{getLocalizedString('common.time_now')}</div>
+1 -1
View File
@@ -131,7 +131,7 @@ export default function Timer(props: TimerProps) {
className={cx(['stage-timer', isMirrored && 'mirror', showFinished && 'stage-timer--finished'])} className={cx(['stage-timer', isMirrored && 'mirror', showFinished && 'stage-timer--finished'])}
data-testid='timer-view' data-testid='timer-view'
> >
{general?.projectLogo && <ViewLogo name={general.projectLogo} className='logo' />} {general?.logo && <ViewLogo name={general.logo} className='logo' />}
<ViewParamsEditor viewOptions={timerOptions} /> <ViewParamsEditor viewOptions={timerOptions} />
+3 -3
View File
@@ -53,9 +53,9 @@ export async function createProjectFile(req: Request, res: Response<{ filename:
project: { project: {
title: req.body?.title ?? '', title: req.body?.title ?? '',
description: req.body?.description ?? '', description: req.body?.description ?? '',
backstageUrl: req.body?.backstageUrl ?? '', url: req.body?.url ?? '',
backstageInfo: req.body?.backstageInfo ?? '', info: req.body?.info ?? '',
projectLogo: req.body?.projectLogo ?? null, logo: req.body?.logo ?? null,
custom: req.body?.custom ?? [], custom: req.body?.custom ?? [],
}, },
}); });
+3 -4
View File
@@ -11,10 +11,9 @@ export const validateNewProject = [
body('filename').optional().isString().trim(), body('filename').optional().isString().trim(),
body('title').optional().isString().trim(), body('title').optional().isString().trim(),
body('description').optional().isString().trim(), body('description').optional().isString().trim(),
body('backstageUrl').optional().isString().trim(), body('url').optional().isString().trim(),
body('backstageInfo').optional().isString().trim(), body('info').optional().isString().trim(),
body('projectLogo').optional().isString().trim(), body('logo').optional().isString().trim(),
body('endMessage').optional().isString().trim(),
body('custom').optional().isArray(), body('custom').optional().isArray(),
requestValidationFunction, requestValidationFunction,
@@ -17,9 +17,9 @@ export function parseProjectData(data: Partial<DatabaseModel>, emitError?: Error
return { return {
title: data.project.title ?? dbModel.project.title, title: data.project.title ?? dbModel.project.title,
description: data.project.description ?? dbModel.project.description, description: data.project.description ?? dbModel.project.description,
backstageUrl: data.project.backstageUrl ?? dbModel.project.backstageUrl, url: data.project.url ?? dbModel.project.url,
backstageInfo: data.project.backstageInfo ?? dbModel.project.backstageInfo, info: data.project.info ?? dbModel.project.info,
projectLogo: data.project.projectLogo ?? dbModel.project.projectLogo, logo: data.project.logo ?? dbModel.project.logo,
custom: data.project.custom ?? dbModel.project.custom, custom: data.project.custom ?? dbModel.project.custom,
}; };
} }
@@ -21,10 +21,9 @@ router.post('/', projectSanitiser, async (req: Request, res: Response<ProjectDat
const newData: Partial<ProjectData> = removeUndefined({ const newData: Partial<ProjectData> = removeUndefined({
title: req.body?.title, title: req.body?.title,
description: req.body?.description, description: req.body?.description,
backstageUrl: req.body?.backstageUrl, url: req.body?.url,
backstageInfo: req.body?.backstageInfo, info: req.body?.info,
endMessage: req.body?.endMessage, logo: req.body?.logo,
projectLogo: req.body?.projectLogo,
custom: req.body?.custom, custom: req.body?.custom,
}); });
@@ -5,10 +5,9 @@ export const projectSanitiser = [
body().notEmpty().withMessage('No object found in request'), body().notEmpty().withMessage('No object found in request'),
body('title').optional().isString().trim(), body('title').optional().isString().trim(),
body('description').optional().isString().trim(), body('description').optional().isString().trim(),
body('backstageUrl').optional().isString().trim(), body('url').optional().isString().trim(),
body('backstageInfo').optional().isString().trim(), body('info').optional().isString().trim(),
body('endMessage').optional().isString().trim(), body('logo').optional({ nullable: true }).isString().trim(), //this is not the logo itself but then name of the logo
body('projectLogo').optional({ nullable: true }).isString().trim(), //this is not the logo itself but then name of the logo
body('custom').optional().isArray(), body('custom').optional().isArray(),
body('custom.*.title').optional().isString().trim().notEmpty(), body('custom.*.title').optional().isString().trim().notEmpty(),
body('custom.*.value').optional().isString().trim().notEmpty(), body('custom.*.value').optional().isString().trim().notEmpty(),
@@ -42,7 +42,7 @@ describe('safeMerge', () => {
const mergedData = safeMerge(demoDb, { const mergedData = safeMerge(demoDb, {
project: { project: {
title: 'new title', title: 'new title',
backstageInfo: 'new backstage info', info: 'new backstage info',
custom: [ custom: [
{ {
title: 'new custom title', title: 'new custom title',
@@ -55,9 +55,9 @@ describe('safeMerge', () => {
expect(mergedData.project).toStrictEqual({ expect(mergedData.project).toStrictEqual({
title: 'new title', title: 'new title',
description: 'Turin 2022', description: 'Turin 2022',
backstageUrl: 'www.github.com/cpvalente/ontime', url: 'www.github.com/cpvalente/ontime',
backstageInfo: 'new backstage info', info: 'new backstage info',
projectLogo: null, logo: null,
custom: [ custom: [
{ {
title: 'new custom title', title: 'new custom title',
+3 -3
View File
@@ -17,9 +17,9 @@ export const dbModel: DatabaseModel = {
project: { project: {
title: '', title: '',
description: '', description: '',
backstageUrl: '', url: '',
backstageInfo: '', info: '',
projectLogo: null, logo: null,
custom: [], custom: [],
}, },
settings: { settings: {
+3 -3
View File
@@ -504,9 +504,9 @@ export const demoDb: DatabaseModel = {
project: { project: {
title: 'Eurovision Song Contest', title: 'Eurovision Song Contest',
description: 'Turin 2022', description: 'Turin 2022',
backstageUrl: 'www.github.com/cpvalente/ontime', url: 'www.github.com/cpvalente/ontime',
backstageInfo: 'Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal', info: 'Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal',
projectLogo: null, logo: null,
custom: [], custom: [],
}, },
settings: { settings: {
@@ -322,8 +322,8 @@ export async function editCurrentProjectData(newData: Partial<ProjectData>) {
const updatedProjectData = await getDataProvider().setProjectData(newData); const updatedProjectData = await getDataProvider().setProjectData(newData);
// Delete the old logo if the logo has been removed // Delete the old logo if the logo has been removed
if (!updatedProjectData.projectLogo && currentProjectData.projectLogo) { if (!updatedProjectData.logo && currentProjectData.logo) {
const filePath = join(publicDir.logoDir, currentProjectData.projectLogo); const filePath = join(publicDir.logoDir, currentProjectData.logo);
deleteFile(filePath).catch((_error) => { deleteFile(filePath).catch((_error) => {
/** we do not handle this error */ /** we do not handle this error */
+3 -3
View File
@@ -452,9 +452,9 @@
"project": { "project": {
"title": "Eurovision Song Contest", "title": "Eurovision Song Contest",
"description": "Turin 2022", "description": "Turin 2022",
"backstageUrl": "www.github.com/cpvalente/ontime", "url": "www.github.com/cpvalente/ontime",
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal", "info": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal",
"projectLogo": null, "logo": null,
"custom": [] "custom": []
}, },
"settings": { "settings": {
+3 -3
View File
@@ -469,9 +469,9 @@
"project": { "project": {
"title": "Eurovision Song Contest", "title": "Eurovision Song Contest",
"description": "Turin 2022", "description": "Turin 2022",
"backstageUrl": "www.github.com/cpvalente/ontime", "url": "www.github.com/cpvalente/ontime",
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal", "info": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal",
"projectLogo": null, "logo": null,
"custom": [] "custom": []
}, },
"settings": { "settings": {
@@ -1,8 +1,8 @@
export type ProjectData = { export type ProjectData = {
title: string; title: string;
description: string; description: string;
backstageUrl: string; url: string;
backstageInfo: string; info: string;
projectLogo: string | null; logo: string | null;
custom: { title: string; value: string }[]; custom: { title: string; value: string }[];
}; };