mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 15:09:10 +00:00
refactor: migrate project data
This commit is contained in:
committed by
Carlos Valente
parent
1fc6f9b3ea
commit
6f34e1006b
@@ -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,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>
|
||||||
|
|||||||
@@ -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',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|
||||||
|
|||||||
@@ -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 ?? [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
Vendored
+3
-3
@@ -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 }[];
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user