feat: extra info in backstage

This commit is contained in:
Carlos Valente
2025-07-26 09:46:56 +02:00
committed by Carlos Valente
parent dc3a32d2bc
commit 6b4d96dddd
4 changed files with 107 additions and 13 deletions
@@ -1,4 +1,4 @@
import type { CustomFields } from 'ontime-types';
import type { CustomFields, ProjectData } from 'ontime-types';
import type { SelectOption } from '../select/Select';
@@ -53,6 +53,18 @@ export function makeCustomFieldSelectOptions(customFields: CustomFields, filterI
return options;
}
/**
* Creates data for a select element that displays project custom data
*/
export function makeProjectDataOptions(projectData: ProjectData): SelectOption[] {
return projectData.custom.map((entry, index) => {
return {
value: `${index}-${entry.title}`,
label: entry.title,
};
});
}
type ViewParamsObj = { [key: string]: string | FormDataEntryValue };
/**
+26 -4
View File
@@ -149,10 +149,27 @@
.info {
grid-area: info;
display: flex;
gap: max(1vw, 16px);
align-self: flex-end;
flex-direction: column;
gap: $view-element-gap;
align-self: end;
overflow: hidden;
align-items: end;
}
.info__column {
display: flex;
flex-direction: column;
}
.info-card {
display: flex;
gap: $view-element-gap;
&__label {
font-size: $timer-label-size;
color: var(--label-color-override, $viewer-label-color);
font-weight: 600;
text-transform: uppercase;
}
&__message {
font-size: $base-font-size;
@@ -162,11 +179,16 @@
flex: 1;
}
.qr {
&__qr {
padding: 0.5rem;
background-color: $ui-white;
border-radius: 2px;
}
&__img {
padding: 0.5rem;
border-radius: 2px;
}
}
}
+41 -5
View File
@@ -48,7 +48,7 @@ export default function Backstage({
settings,
}: BackstageProps) {
const { getLocalizedString } = useTranslation();
const { secondarySource } = useBackstageOptions();
const { secondarySource, extraInfo } = useBackstageOptions();
const [blinkClass, setBlinkClass] = useState(false);
const { height: screenHeight } = useViewportSize();
@@ -104,8 +104,8 @@ export default function Backstage({
// gather option data
const defaultFormat = getDefaultFormat(settings?.timeFormat);
const backstageOptions = useMemo(
() => getBackstageOptions(defaultFormat, customFields),
[defaultFormat, customFields],
() => getBackstageOptions(defaultFormat, customFields, general),
[defaultFormat, customFields, general],
);
return (
@@ -180,8 +180,44 @@ export default function Backstage({
{showSchedule && <ScheduleExport selectedId={selectedId} />}
<div className={cx(['info', !showSchedule && 'info--stretch'])}>
{general.url && <QRCode value={general.url} size={qrSize} level='L' className='qr' />}
{general.info && <div className='info__message'>{general.info}</div>}
{extraInfo && <ExtraInfo projectData={general} size={qrSize} source={extraInfo} />}
<div className='info-card'>
{general.url && <QRCode value={general.url} size={qrSize} level='L' className='info-card__qr' />}
{general.info && <div className='info-card__message'>{general.info}</div>}
</div>
</div>
</div>
);
}
interface ExtraInfoProps {
projectData: ProjectData;
size: number;
source: string;
}
function ExtraInfo({ projectData, size, source }: ExtraInfoProps) {
const info = projectData.custom.find((entry, index) => {
const label = `${index}-${entry.title}`;
return label === source;
});
if (!info) {
return null;
}
return (
<div className='info-card'>
{info.url && (
<img
className='info-card__img'
width={size}
src={info.url}
onError={(event) => (event.currentTarget.style.display = 'none')}
/>
)}
<div className='info__column'>
{info.title && <div className='info-card__label'>{info.title}</div>}
{info.value && <div className='info-card__message'>{info.value}</div>}
</div>
</div>
);
@@ -1,15 +1,23 @@
import { useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import { CustomFields, OntimeEvent } from 'ontime-types';
import { CustomFields, OntimeEvent, ProjectData } from 'ontime-types';
import { getTimeOption } from '../../common/components/view-params-editor/common.options';
import { OptionTitle } from '../../common/components/view-params-editor/constants';
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils';
import {
makeOptionsFromCustomFields,
makeProjectDataOptions,
} from '../../common/components/view-params-editor/viewParams.utils';
import { scheduleOptions } from '../common/schedule/schedule.options';
export const getBackstageOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
export const getBackstageOptions = (
timeFormat: string,
customFields: CustomFields,
projectData: ProjectData,
): ViewOption[] => {
const secondaryOptions = makeOptionsFromCustomFields(customFields, [{ value: 'note', label: 'Note' }]);
const projectDataOptions = makeProjectDataOptions(projectData);
return [
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
@@ -28,11 +36,26 @@ export const getBackstageOptions = (timeFormat: string, customFields: CustomFiel
],
},
scheduleOptions,
{
title: OptionTitle.ElementVisibility,
collapsible: true,
options: [
{
id: 'extra-info',
title: 'Extra info',
description: 'Select a project data source to show in the view',
type: 'option',
values: projectDataOptions,
defaultValue: '',
},
],
},
];
};
type BackstageOptions = {
secondarySource: keyof OntimeEvent | null;
extraInfo: string | null;
};
/**
@@ -43,6 +66,7 @@ function getOptionsFromParams(searchParams: URLSearchParams): BackstageOptions {
// we manually make an object that matches the key above
return {
secondarySource: searchParams.get('secondary-src') as keyof OntimeEvent | null,
extraInfo: searchParams.get('extra-info'),
};
}