mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-10 00:29:41 +00:00
Compare commits
4 Commits
new-db
...
v3.16.1-alpha
| Author | SHA1 | Date | |
|---|---|---|---|
| d89309a120 | |||
| 0dcea4f2d7 | |||
| 696c016c90 | |||
| eed6373dbf |
@@ -25,4 +25,4 @@ export const AutoTextArea = (props: TextareaProps & { inputref: RefObject<unknow
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -8,4 +8,5 @@ export const projectDataPlaceholder: ProjectData = {
|
|||||||
backstageUrl: '',
|
backstageUrl: '',
|
||||||
backstageInfo: '',
|
backstageInfo: '',
|
||||||
projectLogo: null,
|
projectLogo: null,
|
||||||
|
custom: [],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ declare module '@tanstack/react-table' {
|
|||||||
options: {
|
options: {
|
||||||
showDelayedTimes: boolean;
|
showDelayedTimes: boolean;
|
||||||
hideTableSeconds: boolean;
|
hideTableSeconds: boolean;
|
||||||
|
allowEdits: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
|
import { IoTrash } from 'react-icons/io5';
|
||||||
import { Button, Input, Textarea } from '@chakra-ui/react';
|
import { Button, Input, Textarea } from '@chakra-ui/react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ type ProjectCreateFormValues = {
|
|||||||
publicUrl?: string;
|
publicUrl?: string;
|
||||||
backstageInfo?: string;
|
backstageInfo?: string;
|
||||||
backstageUrl?: string;
|
backstageUrl?: string;
|
||||||
|
custom?: { title: string; value: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
||||||
@@ -34,6 +36,7 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
|||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
register,
|
||||||
|
control,
|
||||||
formState: { isSubmitting, isValid },
|
formState: { isSubmitting, isValid },
|
||||||
setFocus,
|
setFocus,
|
||||||
} = useForm<ProjectCreateFormValues>({
|
} = useForm<ProjectCreateFormValues>({
|
||||||
@@ -44,6 +47,11 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: 'custom',
|
||||||
|
});
|
||||||
|
|
||||||
// set focus to first field
|
// set focus to first field
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFocus('title');
|
setFocus('title');
|
||||||
@@ -59,6 +67,7 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
|||||||
...values,
|
...values,
|
||||||
filename,
|
filename,
|
||||||
});
|
});
|
||||||
|
|
||||||
await queryClient.invalidateQueries({ queryKey: PROJECT_LIST });
|
await queryClient.invalidateQueries({ queryKey: PROJECT_LIST });
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -66,6 +75,10 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddCustom = () => {
|
||||||
|
append({ title: '', value: '' });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel.Section
|
<Panel.Section
|
||||||
as='form'
|
as='form'
|
||||||
@@ -151,6 +164,42 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
|||||||
{...register('backstageUrl')}
|
{...register('backstageUrl')}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<Panel.Section>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field title='Custom data' description='Add custom data for your project' />
|
||||||
|
<Button variant='ontime-subtle' onClick={handleAddCustom}>
|
||||||
|
+
|
||||||
|
</Button>
|
||||||
|
</Panel.ListItem>
|
||||||
|
{fields.map((field, idx) => (
|
||||||
|
<div key={field.id} className={style.customDataItem}>
|
||||||
|
<Panel.Paragraph>{idx + 1}.</Panel.Paragraph>
|
||||||
|
<label>
|
||||||
|
Title
|
||||||
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
placeholder={field.title}
|
||||||
|
autoComplete='off'
|
||||||
|
{...register(`custom.${idx}.title` as const)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Value
|
||||||
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
placeholder={field.value}
|
||||||
|
autoComplete='off'
|
||||||
|
{...register(`custom.${idx}.value` as const)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<Button variant='ontime-ghosted' onClick={() => remove(idx)}>
|
||||||
|
<IoTrash />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Panel.Section>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ChangeEvent, useEffect, useRef } from 'react';
|
import { ChangeEvent, useEffect, useRef } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
import { IoDownloadOutline, IoTrash } from 'react-icons/io5';
|
import { IoAdd, IoDownloadOutline, IoTrash } from 'react-icons/io5';
|
||||||
import { Button, Input, Textarea } from '@chakra-ui/react';
|
import { Button, Input, Textarea } from '@chakra-ui/react';
|
||||||
import { type ProjectData } from 'ontime-types';
|
import { type ProjectData } from 'ontime-types';
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ export default function ProjectData() {
|
|||||||
formState: { isSubmitting, isValid, isDirty, errors },
|
formState: { isSubmitting, isValid, isDirty, errors },
|
||||||
setError,
|
setError,
|
||||||
watch,
|
watch,
|
||||||
|
control,
|
||||||
setValue,
|
setValue,
|
||||||
} = useForm({
|
} = useForm({
|
||||||
defaultValues: data,
|
defaultValues: data,
|
||||||
@@ -32,6 +33,12 @@ export default function ProjectData() {
|
|||||||
resetOptions: {
|
resetOptions: {
|
||||||
keepDirtyValues: true,
|
keepDirtyValues: true,
|
||||||
},
|
},
|
||||||
|
mode: 'onChange',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: 'custom',
|
||||||
});
|
});
|
||||||
|
|
||||||
// reset form values if data changes
|
// reset form values if data changes
|
||||||
@@ -77,6 +84,10 @@ export default function ProjectData() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddCustom = () => {
|
||||||
|
append({ title: '', value: '' });
|
||||||
|
};
|
||||||
|
|
||||||
const onSubmit = async (formData: ProjectData) => {
|
const onSubmit = async (formData: ProjectData) => {
|
||||||
try {
|
try {
|
||||||
await postProjectData(formData);
|
await postProjectData(formData);
|
||||||
@@ -231,6 +242,69 @@ export default function ProjectData() {
|
|||||||
{...register('backstageUrl')}
|
{...register('backstageUrl')}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<Panel.Section style={{ marginTop: 0 }}>
|
||||||
|
<Panel.ListItem>
|
||||||
|
<Panel.Field title='Custom data' description='' />
|
||||||
|
<Button leftIcon={<IoAdd />} size='sm' variant='ontime-subtle' onClick={handleAddCustom}>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</Panel.ListItem>
|
||||||
|
{fields.length > 0 &&
|
||||||
|
fields.map((field, idx) => {
|
||||||
|
const rowErrors = errors.custom?.[idx] as
|
||||||
|
| {
|
||||||
|
title?: { message?: string };
|
||||||
|
value?: { message?: string };
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
return (
|
||||||
|
<div key={field.id} className={style.customDataItem}>
|
||||||
|
<div>
|
||||||
|
<div className={style.titleRow}>
|
||||||
|
<label>
|
||||||
|
Title
|
||||||
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
defaultValue={field.title}
|
||||||
|
placeholder='Title of your custom data'
|
||||||
|
autoComplete='off'
|
||||||
|
{...register(`custom.${idx}.title`, {
|
||||||
|
required: { value: true, message: 'Field cannot be empty' },
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
variant='ontime-subtle'
|
||||||
|
color='#FA5656' // $red-500
|
||||||
|
onClick={() => remove(idx)}
|
||||||
|
leftIcon={<IoTrash />}
|
||||||
|
>
|
||||||
|
Delete Entry
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{rowErrors?.title?.message && <Panel.Error>{rowErrors.title.message}</Panel.Error>}
|
||||||
|
</div>
|
||||||
|
<label>
|
||||||
|
Value
|
||||||
|
<Textarea
|
||||||
|
variant='ontime-filled'
|
||||||
|
resize='none'
|
||||||
|
size='sm'
|
||||||
|
defaultValue={field.value}
|
||||||
|
autoComplete='off'
|
||||||
|
placeholder='Text of your custom data'
|
||||||
|
{...register(`custom.${idx}.value`, {
|
||||||
|
required: { value: true, message: 'Field cannot be empty' },
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{rowErrors?.value?.message && <Panel.Error>{rowErrors.value.message}</Panel.Error>}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Panel.Section>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
</Panel.Card>
|
</Panel.Card>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
|
|||||||
@@ -57,3 +57,18 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.customDataItem {
|
||||||
|
display: contents;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.titleRow{
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: end;
|
||||||
|
|
||||||
|
label {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,6 +45,10 @@
|
|||||||
@include ellipsis-overflow;
|
@include ellipsis-overflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.offset {
|
||||||
|
color: $muted-gray;
|
||||||
|
}
|
||||||
|
|
||||||
.ahead {
|
.ahead {
|
||||||
color: $playback-ahead;
|
color: $playback-ahead;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { memo, PropsWithChildren, ReactNode, useMemo } from 'react';
|
import { memo, PropsWithChildren, ReactNode, useMemo } from 'react';
|
||||||
|
import { Playback } from 'ontime-types';
|
||||||
import { millisToString } from 'ontime-utils';
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||||
@@ -163,10 +164,10 @@ function ProgressOverview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function RuntimeOverview() {
|
function RuntimeOverview() {
|
||||||
const { clock, offset } = useRuntimePlaybackOverview();
|
const { clock, offset, playback } = useRuntimePlaybackOverview();
|
||||||
|
|
||||||
const offsetText = getOffsetText(offset);
|
const offsetText = getOffsetText(offset);
|
||||||
const offsetClasses = offset === null ? undefined : offset <= 0 ? style.behind : style.ahead;
|
const offsetClasses = cx([style.offset, playback !== Playback.Stop && (offset < 0 ? style.behind : style.ahead)]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ describe('makeTable()', () => {
|
|||||||
[
|
[
|
||||||
"00:00:00",
|
"00:00:00",
|
||||||
"00:00:00",
|
"00:00:00",
|
||||||
"...",
|
"",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useTableNav } from '@table-nav/react';
|
import { useTableNav } from '@table-nav/react';
|
||||||
import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||||
import { isOntimeEvent, MaybeString, OntimeEvent, OntimeRundown, OntimeRundownEntry, TimeField } from 'ontime-types';
|
import { isOntimeEvent, MaybeString, OntimeEvent, OntimeRundown, OntimeRundownEntry, TimeField } from 'ontime-types';
|
||||||
@@ -25,6 +26,9 @@ export default function CuesheetTable(props: CuesheetTableProps) {
|
|||||||
const { data, columns, showModal } = props;
|
const { data, columns, showModal } = props;
|
||||||
|
|
||||||
const { updateEvent, updateTimer } = useEventAction();
|
const { updateEvent, updateTimer } = useEventAction();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const blockEdits = searchParams.get('locked') ?? false;
|
||||||
|
|
||||||
const { followSelected, showDelayedTimes, hideTableSeconds } = useCuesheetOptions();
|
const { followSelected, showDelayedTimes, hideTableSeconds } = useCuesheetOptions();
|
||||||
const { columnVisibility, columnOrder, columnSizing, resetColumnOrder, setColumnVisibility, setColumnSizing } =
|
const { columnVisibility, columnOrder, columnSizing, resetColumnOrder, setColumnVisibility, setColumnSizing } =
|
||||||
useColumnManager(columns);
|
useColumnManager(columns);
|
||||||
@@ -77,6 +81,7 @@ export default function CuesheetTable(props: CuesheetTableProps) {
|
|||||||
options: {
|
options: {
|
||||||
showDelayedTimes,
|
showDelayedTimes,
|
||||||
hideTableSeconds,
|
hideTableSeconds,
|
||||||
|
allowEdits: !blockEdits,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+3
-1
@@ -6,12 +6,13 @@ import useReactiveTextInput from '../../../../common/components/input/text-input
|
|||||||
interface MultiLineCellProps {
|
interface MultiLineCellProps {
|
||||||
initialValue: string;
|
initialValue: string;
|
||||||
handleUpdate: (newValue: string) => void;
|
handleUpdate: (newValue: string) => void;
|
||||||
|
allowEdits?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(MultiLineCell);
|
export default memo(MultiLineCell);
|
||||||
|
|
||||||
function MultiLineCell(props: MultiLineCellProps) {
|
function MultiLineCell(props: MultiLineCellProps) {
|
||||||
const { initialValue, handleUpdate } = props;
|
const { initialValue, handleUpdate, allowEdits } = props;
|
||||||
const ref = useRef<HTMLInputElement | null>(null);
|
const ref = useRef<HTMLInputElement | null>(null);
|
||||||
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
|
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ function MultiLineCell(props: MultiLineCellProps) {
|
|||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
|
isDisabled={!allowEdits}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-2
@@ -1,17 +1,18 @@
|
|||||||
import { forwardRef, memo, useCallback, useImperativeHandle, useRef } from 'react';
|
import { forwardRef, memo, useCallback, useImperativeHandle, useRef } from 'react';
|
||||||
import { Input } from '@chakra-ui/react';
|
import { Input, Text } from '@chakra-ui/react';
|
||||||
|
|
||||||
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
||||||
|
|
||||||
interface SingleLineCellProps {
|
interface SingleLineCellProps {
|
||||||
initialValue: string;
|
initialValue: string;
|
||||||
allowSubmitSameValue?: boolean;
|
allowSubmitSameValue?: boolean;
|
||||||
|
allowEdits?: boolean;
|
||||||
handleUpdate: (newValue: string) => void;
|
handleUpdate: (newValue: string) => void;
|
||||||
handleCancelUpdate?: () => void;
|
handleCancelUpdate?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SingleLineCell = forwardRef((props: SingleLineCellProps, inputRef) => {
|
const SingleLineCell = forwardRef((props: SingleLineCellProps, inputRef) => {
|
||||||
const { initialValue, allowSubmitSameValue, handleUpdate, handleCancelUpdate } = props;
|
const { initialValue, allowSubmitSameValue, handleUpdate, handleCancelUpdate, allowEdits } = props;
|
||||||
const ref = useRef<HTMLInputElement | null>(null);
|
const ref = useRef<HTMLInputElement | null>(null);
|
||||||
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
|
const submitCallback = useCallback((newValue: string) => handleUpdate(newValue), [handleUpdate]);
|
||||||
|
|
||||||
@@ -38,6 +39,14 @@ const SingleLineCell = forwardRef((props: SingleLineCellProps, inputRef) => {
|
|||||||
};
|
};
|
||||||
}, [ref]);
|
}, [ref]);
|
||||||
|
|
||||||
|
if (allowEdits === false) {
|
||||||
|
return (
|
||||||
|
<Text ref={ref} size='sm' variant='ontime-transparent' padding={0} fontSize='md'>
|
||||||
|
{initialValue}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ interface TimeInputDurationProps {
|
|||||||
initialValue: number;
|
initialValue: number;
|
||||||
lockedValue: boolean;
|
lockedValue: boolean;
|
||||||
delayed?: boolean;
|
delayed?: boolean;
|
||||||
|
allowEdits?: boolean;
|
||||||
onSubmit: (value: string) => void;
|
onSubmit: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ interface ParentFocusableInput extends HTMLInputElement {
|
|||||||
export default memo(TimeInputDuration);
|
export default memo(TimeInputDuration);
|
||||||
|
|
||||||
function TimeInputDuration(props: PropsWithChildren<TimeInputDurationProps>) {
|
function TimeInputDuration(props: PropsWithChildren<TimeInputDurationProps>) {
|
||||||
const { initialValue, lockedValue, delayed, onSubmit, children } = props;
|
const { initialValue, lockedValue, delayed, onSubmit, children, allowEdits } = props;
|
||||||
|
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [value, setValue] = useState(initialValue);
|
const [value, setValue] = useState(initialValue);
|
||||||
@@ -86,7 +87,7 @@ function TimeInputDuration(props: PropsWithChildren<TimeInputDurationProps>) {
|
|||||||
|
|
||||||
const timeString = millisToString(value);
|
const timeString = millisToString(value);
|
||||||
|
|
||||||
return isEditing ? (
|
return isEditing && allowEdits ? (
|
||||||
<SingleLineCell
|
<SingleLineCell
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
initialValue={timeString}
|
initialValue={timeString}
|
||||||
|
|||||||
+41
-6
@@ -32,7 +32,13 @@ function MakeStart({ getValue, row, table }: CellContext<OntimeRundownEntry, unk
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TimeInput initialValue={startTime} onSubmit={update} lockedValue={isStartLocked} delayed={delayValue !== 0}>
|
<TimeInput
|
||||||
|
initialValue={startTime}
|
||||||
|
onSubmit={update}
|
||||||
|
lockedValue={isStartLocked}
|
||||||
|
delayed={delayValue !== 0}
|
||||||
|
allowEdits={table.options.meta?.options.allowEdits}
|
||||||
|
>
|
||||||
{formattedTime}
|
{formattedTime}
|
||||||
<DelayIndicator delayValue={delayValue} tooltipPrefix={millisToString(startTime)} />
|
<DelayIndicator delayValue={delayValue} tooltipPrefix={millisToString(startTime)} />
|
||||||
</TimeInput>
|
</TimeInput>
|
||||||
@@ -60,7 +66,13 @@ function MakeEnd({ getValue, row, table }: CellContext<OntimeRundownEntry, unkno
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TimeInput initialValue={endTime} onSubmit={update} lockedValue={isEndLocked} delayed={delayValue !== 0}>
|
<TimeInput
|
||||||
|
initialValue={endTime}
|
||||||
|
onSubmit={update}
|
||||||
|
lockedValue={isEndLocked}
|
||||||
|
delayed={delayValue !== 0}
|
||||||
|
allowEdits={table.options.meta?.options.allowEdits}
|
||||||
|
>
|
||||||
{formattedTime}
|
{formattedTime}
|
||||||
<DelayIndicator delayValue={delayValue} tooltipPrefix={millisToString(endTime)} />
|
<DelayIndicator delayValue={delayValue} tooltipPrefix={millisToString(endTime)} />
|
||||||
</TimeInput>
|
</TimeInput>
|
||||||
@@ -81,7 +93,12 @@ function MakeDuration({ getValue, row, table }: CellContext<OntimeRundownEntry,
|
|||||||
const formattedDuration = formatDuration(duration, false);
|
const formattedDuration = formatDuration(duration, false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TimeInput initialValue={duration} onSubmit={update} lockedValue={isDurationLocked}>
|
<TimeInput
|
||||||
|
initialValue={duration}
|
||||||
|
onSubmit={update}
|
||||||
|
lockedValue={isDurationLocked}
|
||||||
|
allowEdits={table.options.meta?.options.allowEdits}
|
||||||
|
>
|
||||||
{formattedDuration}
|
{formattedDuration}
|
||||||
</TimeInput>
|
</TimeInput>
|
||||||
);
|
);
|
||||||
@@ -103,7 +120,13 @@ function MakeMultiLineField({ row, column, table }: CellContext<OntimeRundownEnt
|
|||||||
|
|
||||||
const initialValue = event[column.id as keyof OntimeRundownEntry] ?? '';
|
const initialValue = event[column.id as keyof OntimeRundownEntry] ?? '';
|
||||||
|
|
||||||
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />;
|
return (
|
||||||
|
<MultiLineCell
|
||||||
|
initialValue={initialValue}
|
||||||
|
handleUpdate={update}
|
||||||
|
allowEdits={table.options.meta?.options.allowEdits}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LazyImage({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
function LazyImage({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||||
@@ -140,7 +163,13 @@ function MakeSingleLineField({ row, column, table }: CellContext<OntimeRundownEn
|
|||||||
|
|
||||||
const initialValue = event[column.id as keyof OntimeRundownEntry] ?? '';
|
const initialValue = event[column.id as keyof OntimeRundownEntry] ?? '';
|
||||||
|
|
||||||
return <SingleLineCell initialValue={initialValue} handleUpdate={update} />;
|
return (
|
||||||
|
<SingleLineCell
|
||||||
|
initialValue={initialValue}
|
||||||
|
handleUpdate={update}
|
||||||
|
allowEdits={table.options.meta?.options.allowEdits}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||||
@@ -158,7 +187,13 @@ function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initialValue = event.custom[column.id] ?? '';
|
const initialValue = event.custom[column.id] ?? '';
|
||||||
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />;
|
return (
|
||||||
|
<MultiLineCell
|
||||||
|
initialValue={initialValue}
|
||||||
|
handleUpdate={update}
|
||||||
|
allowEdits={table.options.meta?.options.allowEdits}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<OntimeRundownEntry>[] {
|
export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<OntimeRundownEntry>[] {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type CsvHeaderKey = OntimeEntryCommonKeys | keyof CustomFields;
|
|||||||
|
|
||||||
export const parseField = (field: CsvHeaderKey, data: unknown): string => {
|
export const parseField = (field: CsvHeaderKey, data: unknown): string => {
|
||||||
if (field === 'timeStart' || field === 'timeEnd' || field === 'duration') {
|
if (field === 'timeStart' || field === 'timeEnd' || field === 'duration') {
|
||||||
return millisToString(data as MaybeNumber);
|
return millisToString(data as MaybeNumber, { fallback: '' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field === 'isPublic' || field === 'skip') {
|
if (field === 'isPublic' || field === 'skip') {
|
||||||
|
|||||||
@@ -25,7 +25,9 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
width: min(calc(100vw - 4rem), 800px);
|
width: min(calc(100vw - 4rem), 960px);
|
||||||
|
|
||||||
|
padding-bottom: 10vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info__label {
|
.info__label {
|
||||||
@@ -33,11 +35,15 @@
|
|||||||
color: var(--label-color-override, $viewer-label-color);
|
color: var(--label-color-override, $viewer-label-color);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin-top: $view-element-gap;
|
margin-top: $view-element-gap;
|
||||||
white-space: pre;
|
}
|
||||||
|
|
||||||
|
.info__value {
|
||||||
|
white-space: break-spaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.info__value {
|
a.info__value {
|
||||||
color: $action-text-color;
|
color: $action-text-color;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $ontime-color;
|
color: $ontime-color;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
|||||||
import { useTranslation } from '../../translation/TranslationProvider';
|
import { useTranslation } from '../../translation/TranslationProvider';
|
||||||
|
|
||||||
import BackstageInfo from './backstage-info/BackstageInfo';
|
import BackstageInfo from './backstage-info/BackstageInfo';
|
||||||
|
import CustomInfo from './custom-info/CustomInfo';
|
||||||
import PublicInfo from './public-info/PublicInfo';
|
import PublicInfo from './public-info/PublicInfo';
|
||||||
import { projectInfoOptions } from './projectInfo.options';
|
import { projectInfoOptions } from './projectInfo.options';
|
||||||
|
|
||||||
@@ -66,6 +67,7 @@ export default function ProjectInfo(props: ProjectInfoProps) {
|
|||||||
)}
|
)}
|
||||||
<BackstageInfo general={general} />
|
<BackstageInfo general={general} />
|
||||||
<PublicInfo general={general} />
|
<PublicInfo general={general} />
|
||||||
|
<CustomInfo general={general} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,14 +9,21 @@ export const projectInfoOptions: ViewOption[] = [
|
|||||||
{
|
{
|
||||||
id: 'showBackstage',
|
id: 'showBackstage',
|
||||||
title: 'Show backstage Data',
|
title: 'Show backstage Data',
|
||||||
description: 'Weather to show fields related to the backstage views',
|
description: 'Whether to show fields related to the backstage views',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'showPublic',
|
id: 'showPublic',
|
||||||
title: 'Show Public Data',
|
title: 'Show Public Data',
|
||||||
description: 'Weather to show fields related to the public views',
|
description: 'Whether to show fields related to the public views',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'showCustom',
|
||||||
|
title: 'Show Custom Data',
|
||||||
|
description: 'Whether to show fields related to the custom data',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export async function createProjectFile(req: Request, res: Response<{ filename:
|
|||||||
backstageUrl: req.body?.backstageUrl ?? '',
|
backstageUrl: req.body?.backstageUrl ?? '',
|
||||||
backstageInfo: req.body?.backstageInfo ?? '',
|
backstageInfo: req.body?.backstageInfo ?? '',
|
||||||
projectLogo: req.body?.projectLogo ?? null,
|
projectLogo: req.body?.projectLogo ?? null,
|
||||||
|
custom: req.body?.custom ?? [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -203,7 +204,7 @@ export async function loadProject(req: Request, res: Response<MessageResponse |
|
|||||||
/**
|
/**
|
||||||
* Loads the demo project
|
* Loads the demo project
|
||||||
*/
|
*/
|
||||||
export async function loadDemo(req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
export async function loadDemo(_req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
||||||
try {
|
try {
|
||||||
const projectName = await projectService.loadDemoProject();
|
const projectName = await projectService.loadDemoProject();
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const validateNewProject = [
|
|||||||
body('backstageInfo').optional().isString().trim(),
|
body('backstageInfo').optional().isString().trim(),
|
||||||
body('projectLogo').optional().isString().trim(),
|
body('projectLogo').optional().isString().trim(),
|
||||||
body('endMessage').optional().isString().trim(),
|
body('endMessage').optional().isString().trim(),
|
||||||
|
body('custom').optional().isArray(),
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import type { Request, Response } from 'express';
|
|||||||
|
|
||||||
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 { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
|
||||||
import { editCurrentProjectData } from '../../services/project-service/ProjectService.js';
|
import { editCurrentProjectData } from '../../services/project-service/ProjectService.js';
|
||||||
|
import * as projectDao from './project.dao.js';
|
||||||
|
|
||||||
export function getProjectData(_req: Request, res: Response<ProjectData>) {
|
export function getProjectData(_req: Request, res: Response<ProjectData>) {
|
||||||
res.json(getDataProvider().getProjectData());
|
res.json(projectDao.getProjectData());
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function postProjectData(req: Request, res: Response<ProjectData | ErrorResponse>) {
|
export async function postProjectData(req: Request, res: Response<ProjectData | ErrorResponse>) {
|
||||||
@@ -27,6 +27,7 @@ export async function postProjectData(req: Request, res: Response<ProjectData |
|
|||||||
backstageInfo: req.body?.backstageInfo,
|
backstageInfo: req.body?.backstageInfo,
|
||||||
endMessage: req.body?.endMessage,
|
endMessage: req.body?.endMessage,
|
||||||
projectLogo: req.body?.projectLogo,
|
projectLogo: req.body?.projectLogo,
|
||||||
|
custom: req.body?.custom,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedData = await editCurrentProjectData(newData);
|
const updatedData = await editCurrentProjectData(newData);
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ProjectData } from 'ontime-types';
|
||||||
|
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a copy of the stored project data
|
||||||
|
*/
|
||||||
|
export function getProjectData(): ProjectData {
|
||||||
|
return structuredClone(getDataProvider().getProjectData());
|
||||||
|
}
|
||||||
@@ -10,6 +10,9 @@ export const projectSanitiser = [
|
|||||||
body('backstageInfo').optional().isString().trim(),
|
body('backstageInfo').optional().isString().trim(),
|
||||||
body('endMessage').optional().isString().trim(),
|
body('endMessage').optional().isString().trim(),
|
||||||
body('projectLogo').optional({ nullable: true }).isString().trim(),
|
body('projectLogo').optional({ nullable: true }).isString().trim(),
|
||||||
|
body('custom').optional().isArray(),
|
||||||
|
body('custom.*.title').optional().isString().trim().notEmpty(),
|
||||||
|
body('custom.*.value').optional().isString().trim().notEmpty(),
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ function getData(): Readonly<DatabaseModel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setProjectData(newData: Partial<ProjectData>): ReadonlyPromise<ProjectData> {
|
async function setProjectData(newData: Partial<ProjectData>): ReadonlyPromise<ProjectData> {
|
||||||
db.data.project = { ...db.data.project, ...newData };
|
db.data.project = { ...structuredClone(db.data.project), ...structuredClone(newData) }; // Performing deep copy as we're updating / merging data
|
||||||
await persist();
|
await persist();
|
||||||
return db.data.project;
|
return db.data.project;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,24 +4,27 @@ import { DatabaseModel } from 'ontime-types';
|
|||||||
* Merges a partial ontime project into a given ontime project
|
* Merges a partial ontime project into a given ontime project
|
||||||
*/
|
*/
|
||||||
export function safeMerge(existing: DatabaseModel, newData: Partial<DatabaseModel>): DatabaseModel {
|
export function safeMerge(existing: DatabaseModel, newData: Partial<DatabaseModel>): DatabaseModel {
|
||||||
|
const deepExisting = structuredClone(existing);
|
||||||
|
const deepNewData = structuredClone(newData);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
rundown = existing.rundown,
|
rundown = deepExisting.rundown,
|
||||||
project = {},
|
project = {},
|
||||||
settings = {},
|
settings = {},
|
||||||
viewSettings = {},
|
viewSettings = {},
|
||||||
urlPresets = existing.urlPresets,
|
urlPresets = deepExisting.urlPresets,
|
||||||
customFields = existing.customFields,
|
customFields = deepExisting.customFields,
|
||||||
automation = existing.automation,
|
automation = deepExisting.automation,
|
||||||
} = newData;
|
} = deepNewData;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...existing,
|
...deepExisting,
|
||||||
rundown,
|
rundown,
|
||||||
project: { ...existing.project, ...project },
|
project: { ...deepExisting.project, ...project },
|
||||||
settings: { ...existing.settings, ...settings },
|
settings: { ...deepExisting.settings, ...settings },
|
||||||
viewSettings: { ...existing.viewSettings, ...viewSettings },
|
viewSettings: { ...deepExisting.viewSettings, ...viewSettings },
|
||||||
urlPresets: urlPresets ?? existing.urlPresets,
|
urlPresets: urlPresets ?? deepExisting.urlPresets,
|
||||||
customFields: customFields ?? existing.customFields,
|
customFields: customFields ?? deepExisting.customFields,
|
||||||
automation: { ...existing.automation, ...automation },
|
automation: { ...deepExisting.automation, ...automation },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ describe('safeMerge', () => {
|
|||||||
publicInfo: 'existing backstageInfo',
|
publicInfo: 'existing backstageInfo',
|
||||||
backstageInfo: 'existing backstageInfo',
|
backstageInfo: 'existing backstageInfo',
|
||||||
projectLogo: null,
|
projectLogo: null,
|
||||||
|
custom: [
|
||||||
|
{
|
||||||
|
title: 'existing custom title',
|
||||||
|
value: 'existing custom value',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
app: 'ontime',
|
app: 'ontime',
|
||||||
@@ -62,6 +68,12 @@ describe('safeMerge', () => {
|
|||||||
project: {
|
project: {
|
||||||
title: 'new title',
|
title: 'new title',
|
||||||
publicInfo: 'new public info',
|
publicInfo: 'new public info',
|
||||||
|
custom: [
|
||||||
|
{
|
||||||
|
title: 'new custom title',
|
||||||
|
value: 'new custom value',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// @ts-expect-error -- just testing
|
// @ts-expect-error -- just testing
|
||||||
@@ -74,6 +86,12 @@ describe('safeMerge', () => {
|
|||||||
backstageUrl: 'existing backstageUrl',
|
backstageUrl: 'existing backstageUrl',
|
||||||
backstageInfo: 'existing backstageInfo',
|
backstageInfo: 'existing backstageInfo',
|
||||||
projectLogo: null,
|
projectLogo: null,
|
||||||
|
custom: [
|
||||||
|
{
|
||||||
|
title: 'new custom title',
|
||||||
|
value: 'new custom value',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -107,6 +125,7 @@ describe('safeMerge', () => {
|
|||||||
backstageUrl: '',
|
backstageUrl: '',
|
||||||
backstageInfo: '',
|
backstageInfo: '',
|
||||||
projectLogo: null,
|
projectLogo: null,
|
||||||
|
custom: [],
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
app: 'ontime',
|
app: 'ontime',
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const dbModel: DatabaseModel = {
|
|||||||
backstageUrl: '',
|
backstageUrl: '',
|
||||||
backstageInfo: '',
|
backstageInfo: '',
|
||||||
projectLogo: null,
|
projectLogo: null,
|
||||||
|
custom: [],
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
app: 'ontime',
|
app: 'ontime',
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ export const demoDb: DatabaseModel = {
|
|||||||
backstageUrl: 'www.github.com/cpvalente/ontime',
|
backstageUrl: 'www.github.com/cpvalente/ontime',
|
||||||
backstageInfo: 'Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal',
|
backstageInfo: 'Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal',
|
||||||
projectLogo: null,
|
projectLogo: null,
|
||||||
|
custom: [],
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
app: 'ontime',
|
app: 'ontime',
|
||||||
|
|||||||
@@ -314,17 +314,13 @@ export async function patchCurrentProject(data: Partial<DatabaseModel>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the title of a project
|
* Patches the current project data
|
||||||
* it handles invalidating the necessary data
|
* Handles deleting the local logo if the logo has been removed
|
||||||
*/
|
*/
|
||||||
export async function editCurrentProjectData(newData: Partial<ProjectData>) {
|
export async function editCurrentProjectData(newData: Partial<ProjectData>) {
|
||||||
const currentProjectData = getDataProvider().getProjectData();
|
const currentProjectData = getDataProvider().getProjectData();
|
||||||
const updatedProjectData = await getDataProvider().setProjectData(newData);
|
const updatedProjectData = await getDataProvider().setProjectData(newData);
|
||||||
|
|
||||||
if (currentProjectData.title !== updatedProjectData.title) {
|
|
||||||
// something
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.projectLogo && currentProjectData.projectLogo) {
|
||||||
const filePath = join(publicDir.logoDir, currentProjectData.projectLogo);
|
const filePath = join(publicDir.logoDir, currentProjectData.projectLogo);
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ describe('parseProject()', () => {
|
|||||||
publicInfo: 'publicInfo',
|
publicInfo: 'publicInfo',
|
||||||
backstageUrl: 'backstageUrl',
|
backstageUrl: 'backstageUrl',
|
||||||
backstageInfo: 'backstageInfo',
|
backstageInfo: 'backstageInfo',
|
||||||
|
custom: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errorEmitter,
|
errorEmitter,
|
||||||
@@ -77,6 +78,7 @@ describe('parseProject()', () => {
|
|||||||
backstageUrl: 'backstageUrl',
|
backstageUrl: 'backstageUrl',
|
||||||
backstageInfo: 'backstageInfo',
|
backstageInfo: 'backstageInfo',
|
||||||
projectLogo: null,
|
projectLogo: null,
|
||||||
|
custom: [],
|
||||||
});
|
});
|
||||||
expect(errorEmitter).not.toHaveBeenCalled();
|
expect(errorEmitter).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export function parseProject(data: Partial<DatabaseModel>, emitError?: ErrorEmit
|
|||||||
backstageUrl: data.project.backstageUrl ?? dbModel.project.backstageUrl,
|
backstageUrl: data.project.backstageUrl ?? dbModel.project.backstageUrl,
|
||||||
backstageInfo: data.project.backstageInfo ?? dbModel.project.backstageInfo,
|
backstageInfo: data.project.backstageInfo ?? dbModel.project.backstageInfo,
|
||||||
projectLogo: data.project.projectLogo ?? dbModel.project.projectLogo,
|
projectLogo: data.project.projectLogo ?? dbModel.project.projectLogo,
|
||||||
|
custom: data.project.custom ?? dbModel.project.custom,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -410,7 +410,8 @@
|
|||||||
"publicInfo": "Rehearsal Schedule - Turin 2022",
|
"publicInfo": "Rehearsal Schedule - Turin 2022",
|
||||||
"backstageUrl": "www.github.com/cpvalente/ontime",
|
"backstageUrl": "www.github.com/cpvalente/ontime",
|
||||||
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal",
|
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal",
|
||||||
"projectLogo": null
|
"projectLogo": null,
|
||||||
|
"custom": []
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"app": "ontime",
|
"app": "ontime",
|
||||||
@@ -455,4 +456,4 @@
|
|||||||
"label": "artist"
|
"label": "artist"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+3
-2
@@ -410,7 +410,8 @@
|
|||||||
"publicInfo": "Rehearsal Schedule - Turin 2022",
|
"publicInfo": "Rehearsal Schedule - Turin 2022",
|
||||||
"backstageUrl": "www.github.com/cpvalente/ontime",
|
"backstageUrl": "www.github.com/cpvalente/ontime",
|
||||||
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal",
|
"backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal",
|
||||||
"projectLogo": null
|
"projectLogo": null,
|
||||||
|
"custom": []
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"app": "ontime",
|
"app": "ontime",
|
||||||
@@ -455,4 +456,4 @@
|
|||||||
"label": "artist"
|
"label": "artist"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ export type ProjectData = {
|
|||||||
backstageUrl: string;
|
backstageUrl: string;
|
||||||
backstageInfo: string;
|
backstageInfo: string;
|
||||||
projectLogo: string | null;
|
projectLogo: string | null;
|
||||||
|
custom: { title: string; value: string }[];
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user