mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-11 02:13:48 +00:00
feat: add custom field type of image
This commit is contained in:
committed by
Carlos Valente
parent
d0b4c6a279
commit
570ff59cfc
@@ -0,0 +1,45 @@
|
||||
import { CustomFields } from 'ontime-types';
|
||||
|
||||
import { makeOptionsFromCustomFields } from '../constants';
|
||||
|
||||
describe('makeOptionsFromCustomFields', () => {
|
||||
const testCustomFields: CustomFields = {
|
||||
field1: { label: 'Field 1', colour: 'red', type: 'string' },
|
||||
field2: { label: 'Field 2', colour: 'blue', type: 'string' },
|
||||
};
|
||||
|
||||
it('creates a record of keys for the given custom fields', () => {
|
||||
const result = makeOptionsFromCustomFields(testCustomFields);
|
||||
expect(result).toStrictEqual({
|
||||
'custom-field1': 'Custom: Field 1',
|
||||
'custom-field2': 'Custom: Field 2',
|
||||
});
|
||||
});
|
||||
|
||||
it('appends additional data', () => {
|
||||
const additionalData = {
|
||||
test1: 'test1',
|
||||
test2: 'test2',
|
||||
};
|
||||
const result = makeOptionsFromCustomFields(testCustomFields, additionalData);
|
||||
expect(result).toStrictEqual({
|
||||
'custom-field1': 'Custom: Field 1',
|
||||
'custom-field2': 'Custom: Field 2',
|
||||
test1: 'test1',
|
||||
test2: 'test2',
|
||||
});
|
||||
});
|
||||
|
||||
it('filtersImageTypes', () => {
|
||||
const customFieldsWIthImage: CustomFields = {
|
||||
...testCustomFields,
|
||||
field3: { label: 'Field 3', colour: 'green', type: 'image' },
|
||||
};
|
||||
|
||||
const result = makeOptionsFromCustomFields(customFieldsWIthImage);
|
||||
expect(result).toStrictEqual({
|
||||
'custom-field1': 'Custom: Field 1',
|
||||
'custom-field2': 'Custom: Field 2',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,11 +5,17 @@ import type { ParamField } from './types';
|
||||
export const makeOptionsFromCustomFields = (
|
||||
customFields: CustomFields,
|
||||
additionalOptions: Record<string, string> = {},
|
||||
filterImageType = true,
|
||||
) => {
|
||||
return Object.entries(customFields).reduce((options, [key, value]) => {
|
||||
const options = structuredClone(additionalOptions);
|
||||
for (const [key, value] of Object.entries(customFields)) {
|
||||
if (filterImageType && value.type === 'image') {
|
||||
continue;
|
||||
}
|
||||
|
||||
options[`custom-${key}`] = `Custom: ${value.label}`;
|
||||
return options;
|
||||
}, additionalOptions);
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
export const getTimeOption = (timeFormat: string): ParamField => {
|
||||
|
||||
+12
-7
@@ -6,6 +6,7 @@ import { CustomField, CustomFieldLabel } from 'ontime-types';
|
||||
|
||||
import CopyTag from '../../../../../common/components/copy-tag/CopyTag';
|
||||
import Swatch from '../../../../../common/components/input/colour-input/Swatch';
|
||||
import Tag from '../../../../../common/components/tag/Tag';
|
||||
import * as Panel from '../../../panel-utils/PanelUtils';
|
||||
|
||||
import CustomFieldForm from './CustomFieldForm';
|
||||
@@ -13,19 +14,20 @@ import CustomFieldForm from './CustomFieldForm';
|
||||
import style from '../FeatureSettings.module.scss';
|
||||
|
||||
interface CustomFieldEntryProps {
|
||||
field: string;
|
||||
colour: string;
|
||||
label: string;
|
||||
fieldKey: string;
|
||||
type: 'string' | 'image';
|
||||
onEdit: (label: CustomFieldLabel, patch: CustomField) => Promise<void>;
|
||||
onDelete: (label: CustomFieldLabel) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function CustomFieldEntry(props: CustomFieldEntryProps) {
|
||||
const { colour, label, onEdit, onDelete, field } = props;
|
||||
const { colour, label, fieldKey, type, onEdit, onDelete } = props;
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const handleEdit = async (patch: CustomField) => {
|
||||
await onEdit(field, patch);
|
||||
await onEdit(fieldKey, patch);
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
@@ -38,7 +40,7 @@ export default function CustomFieldEntry(props: CustomFieldEntryProps) {
|
||||
onSubmit={handleEdit}
|
||||
initialColour={colour}
|
||||
initialLabel={label}
|
||||
initialKey={field}
|
||||
initialKey={fieldKey}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -50,10 +52,13 @@ export default function CustomFieldEntry(props: CustomFieldEntryProps) {
|
||||
<td>
|
||||
<Swatch color={colour} />
|
||||
</td>
|
||||
<td>
|
||||
<Tag>{type}</Tag>
|
||||
</td>
|
||||
<td className={style.halfWidth}>{label}</td>
|
||||
<td className={style.fullWidth}>
|
||||
<CopyTag label='Copy key to use in integrations' copyValue={field}>
|
||||
{field}
|
||||
<CopyTag label='Copy key to use in integrations' copyValue={fieldKey}>
|
||||
{fieldKey}
|
||||
</CopyTag>
|
||||
</td>
|
||||
<Panel.InlineElements relation='inner' as='td'>
|
||||
@@ -71,7 +76,7 @@ export default function CustomFieldEntry(props: CustomFieldEntryProps) {
|
||||
color='#FA5656' // $red-500
|
||||
icon={<IoTrash />}
|
||||
aria-label='Delete entry'
|
||||
onClick={() => onDelete(field)}
|
||||
onClick={() => onDelete(fieldKey)}
|
||||
/>
|
||||
</Panel.InlineElements>
|
||||
</tr>
|
||||
|
||||
+31
-9
@@ -1,10 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Button, Input } from '@chakra-ui/react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { Button, Input, Radio, RadioGroup } from '@chakra-ui/react';
|
||||
import { CustomField } from 'ontime-types';
|
||||
import { customFieldLabelToKey, isAlphanumericWithSpace } from 'ontime-utils';
|
||||
|
||||
import { maybeAxiosError } from '../../../../../common/api/utils';
|
||||
import Info from '../../../../../common/components/info/Info';
|
||||
import SwatchSelect from '../../../../../common/components/input/colour-input/SwatchSelect';
|
||||
import useCustomFields from '../../../../../common/hooks-query/useCustomFields';
|
||||
import { preventEscape } from '../../../../../common/utils/keyEvent';
|
||||
@@ -20,6 +21,8 @@ interface CustomFieldsFormProps {
|
||||
initialKey?: string;
|
||||
}
|
||||
|
||||
type CustomFieldFormData = CustomField & { key: string };
|
||||
|
||||
export default function CustomFieldForm(props: CustomFieldsFormProps) {
|
||||
const { onSubmit, onCancel, initialColour, initialLabel, initialKey } = props;
|
||||
const { data } = useCustomFields();
|
||||
@@ -28,6 +31,7 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
|
||||
const [_, setColour] = useState(initialColour || '');
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
register,
|
||||
setFocus,
|
||||
@@ -35,17 +39,17 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
|
||||
setValue,
|
||||
getValues,
|
||||
formState: { errors, isSubmitting, isValid, isDirty },
|
||||
} = useForm({
|
||||
defaultValues: { label: initialLabel || '', colour: initialColour || '', key: initialKey || '' },
|
||||
} = useForm<CustomFieldFormData>({
|
||||
defaultValues: { type: 'string', label: initialLabel || '', colour: initialColour || '' },
|
||||
resetOptions: {
|
||||
keepDirtyValues: true,
|
||||
},
|
||||
});
|
||||
|
||||
const setupSubmit = async (values: { label: string; colour: string }) => {
|
||||
const { label, colour } = values;
|
||||
const setupSubmit = async (values: CustomFieldFormData) => {
|
||||
const { type, label, colour } = values;
|
||||
const newField: CustomField = {
|
||||
type: 'string', // type is not user definable yet
|
||||
type,
|
||||
colour,
|
||||
label,
|
||||
};
|
||||
@@ -77,6 +81,26 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
|
||||
className={style.fieldForm}
|
||||
onKeyDown={(event) => preventEscape(event, onCancel)}
|
||||
>
|
||||
<Info>
|
||||
Please note that images can quickly deteriorate your app's performance.
|
||||
<br />
|
||||
Prefer using small, and compressed images.
|
||||
</Info>
|
||||
<div>
|
||||
<Panel.Description>Type</Panel.Description>
|
||||
<Controller
|
||||
name='type'
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<RadioGroup {...field} size='sm' isDisabled={isEditMode} variant='ontime'>
|
||||
<Panel.InlineElements relation='component'>
|
||||
<Radio value='string'>Text</Radio>
|
||||
<Radio value='image'>Image</Radio>
|
||||
</Panel.InlineElements>
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.twoCols}>
|
||||
<div>
|
||||
<Panel.Description>Label (only alphanumeric characters are allowed)</Panel.Description>
|
||||
@@ -105,12 +129,10 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
|
||||
<Input {...register('key')} disabled size='sm' variant='ontime-filled' autoComplete='off' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Panel.Description>Colour</Panel.Description>
|
||||
<SwatchSelect name='colour' value={colour} handleChange={(_field, value) => handleSelectColour(value)} />
|
||||
</div>
|
||||
|
||||
{errors.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
||||
<Panel.InlineElements relation='inner' align='end'>
|
||||
<Button size='sm' variant='ontime-ghosted' onClick={onCancel}>
|
||||
|
||||
+4
-2
@@ -74,19 +74,21 @@ export default function CustomFields() {
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Colour</th>
|
||||
<th>Type</th>
|
||||
<th>Name</th>
|
||||
<th>Key (used in Integrations)</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.entries(data).map(([key, { colour, label }]) => {
|
||||
{Object.entries(data).map(([key, { colour, label, type }]) => {
|
||||
return (
|
||||
<CustomFieldEntry
|
||||
key={key}
|
||||
field={key}
|
||||
fieldKey={key}
|
||||
colour={colour}
|
||||
label={label}
|
||||
type={type}
|
||||
onEdit={handleEditField}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
|
||||
@@ -78,3 +78,9 @@
|
||||
font-size: 1.25em;
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
|
||||
.customImage {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 72px;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,11 @@ import useCustomFields from '../../../common/hooks-query/useCustomFields';
|
||||
import { getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||
import * as Editor from '../../editors/editor-utils/EditorUtils';
|
||||
|
||||
import EventEditorImage from './composite/EventEditorImage';
|
||||
import EventEditorTimes from './composite/EventEditorTimes';
|
||||
import EventEditorTitles from './composite/EventEditorTitles';
|
||||
import EventTextArea from './composite/EventTextArea';
|
||||
import EventTextInput from './composite/EventTextInput';
|
||||
import EventEditorEmpty from './EventEditorEmpty';
|
||||
|
||||
import style from './EventEditor.module.scss';
|
||||
@@ -87,6 +89,7 @@ export default function EventEditor(props: EventEditorProps) {
|
||||
</Button>
|
||||
)}
|
||||
</Editor.Title>
|
||||
|
||||
{Object.keys(customFields).map((fieldKey) => {
|
||||
const key = `${event.id}-${fieldKey}`;
|
||||
const fieldName = `custom-${fieldKey}`;
|
||||
@@ -94,17 +97,41 @@ export default function EventEditor(props: EventEditorProps) {
|
||||
const { backgroundColor, color } = getAccessibleColour(customFields[fieldKey].colour);
|
||||
const labelText = customFields[fieldKey].label;
|
||||
|
||||
return (
|
||||
<EventTextArea
|
||||
key={key}
|
||||
field={fieldName}
|
||||
label={labelText}
|
||||
initialValue={initialValue}
|
||||
submitHandler={handleSubmit}
|
||||
className={style.decorated}
|
||||
style={{ '--decorator-bg': backgroundColor, '--decorator-color': color } as CSSProperties}
|
||||
/>
|
||||
);
|
||||
if (customFields[fieldKey].type === 'string') {
|
||||
return (
|
||||
<EventTextArea
|
||||
key={key}
|
||||
field={fieldName}
|
||||
label={labelText}
|
||||
initialValue={initialValue}
|
||||
submitHandler={handleSubmit}
|
||||
className={style.decorated}
|
||||
style={{ '--decorator-bg': backgroundColor, '--decorator-color': color } as CSSProperties}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (customFields[fieldKey].type === 'image') {
|
||||
return (
|
||||
<div key={key} className={style.customImage}>
|
||||
<EventTextInput
|
||||
key={key}
|
||||
field={fieldName}
|
||||
label={labelText}
|
||||
initialValue={initialValue}
|
||||
placeholder='Paste image URL'
|
||||
submitHandler={handleSubmit}
|
||||
className={style.decorated}
|
||||
maxLength={255}
|
||||
style={{ '--decorator-bg': backgroundColor, '--decorator-color': color } as CSSProperties}
|
||||
/>
|
||||
<EventEditorImage src={initialValue} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// we should have exhausted all types by now
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
.imageContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: $gray-1250;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
.imageOverlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import style from './EventEditorImage.module.scss';
|
||||
|
||||
interface EventEditorImageProps {
|
||||
src: string;
|
||||
}
|
||||
|
||||
export default function EventEditorImage(props: EventEditorImageProps) {
|
||||
const { src } = props;
|
||||
|
||||
return (
|
||||
<div className={style.imageContainer}>
|
||||
<img loading='lazy' src={src} />
|
||||
<div className={style.imageOverlay} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,11 +9,12 @@ interface EventTextInputProps extends InputProps {
|
||||
field: EditorUpdateFields;
|
||||
label: string;
|
||||
initialValue: string;
|
||||
placeholder?: string;
|
||||
submitHandler: (field: EditorUpdateFields, value: string) => void;
|
||||
}
|
||||
|
||||
export default function EventTextInput(props: EventTextInputProps) {
|
||||
const { field, label, initialValue, submitHandler, maxLength } = props;
|
||||
const { className, field, label, initialValue, style: givenStyles, submitHandler, maxLength, placeholder } = props;
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
||||
|
||||
@@ -23,7 +24,9 @@ export default function EventTextInput(props: EventTextInputProps) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Editor.Label htmlFor={field}>{label}</Editor.Label>
|
||||
<Editor.Label className={className} htmlFor={field} style={givenStyles}>
|
||||
{label}
|
||||
</Editor.Label>
|
||||
<Input
|
||||
id={field}
|
||||
ref={ref}
|
||||
@@ -32,6 +35,7 @@ export default function EventTextInput(props: EventTextInputProps) {
|
||||
data-testid='input-textfield'
|
||||
value={value}
|
||||
maxLength={maxLength || 100}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
|
||||
@@ -12,6 +12,7 @@ $white-60: rgba(255, 255, 255, 0.60);
|
||||
$white-90: rgba(255, 255, 255, 0.90);
|
||||
|
||||
$black-10: rgba(0, 0, 0, 0.10);
|
||||
$black-60: rgba(0, 0, 0, 0.60);
|
||||
|
||||
$gray-50: #f6f6f6;
|
||||
$gray-100: #ececec;
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
.imageCell {
|
||||
position: relative;
|
||||
min-height: 2rem;
|
||||
|
||||
&:hover {
|
||||
.overlay {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
background-color: $black-60;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.image {
|
||||
background-color: $gray-1350;
|
||||
min-height: 2rem;
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { memo } from 'react';
|
||||
import { Input } from '@chakra-ui/react';
|
||||
|
||||
import style from './EditableImage.module.scss';
|
||||
|
||||
interface EditableImageProps {
|
||||
initialValue: string;
|
||||
updateValue: (newValue: string) => void;
|
||||
}
|
||||
|
||||
export default memo(EditableImage);
|
||||
|
||||
function EditableImage(props: EditableImageProps) {
|
||||
const { initialValue, updateValue } = props;
|
||||
|
||||
const handleUpdate = (newValue: string) => {
|
||||
if (newValue === initialValue) {
|
||||
return;
|
||||
}
|
||||
if (newValue !== '' && !newValue.startsWith('http')) {
|
||||
return;
|
||||
}
|
||||
updateValue(newValue);
|
||||
};
|
||||
|
||||
if (!initialValue) {
|
||||
return (
|
||||
<Input
|
||||
size='sm'
|
||||
variant='ontime-transparent'
|
||||
padding={0}
|
||||
fontSize='md'
|
||||
placeholder='Paste image URL'
|
||||
onBlur={(event) => handleUpdate(event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
handleUpdate(event.currentTarget.value);
|
||||
}
|
||||
}}
|
||||
defaultValue={initialValue}
|
||||
spellCheck={false}
|
||||
autoComplete='off'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={style.imageCell}>
|
||||
<div className={style.overlay}>
|
||||
<button onClick={() => handleUpdate('')}>Delete</button>
|
||||
</div>
|
||||
<img loading='lazy' src={initialValue} className={style.image} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+21
-3
@@ -6,6 +6,7 @@ import { millisToString, removeSeconds } from 'ontime-utils';
|
||||
import DelayIndicator from '../../../../common/components/delay-indicator/DelayIndicator';
|
||||
import { formatDuration } from '../../../../common/utils/time';
|
||||
|
||||
import EditableImage from './EditableImage';
|
||||
import MultiLineCell from './MultiLineCell';
|
||||
import SingleLineCell from './SingleLineCell';
|
||||
import TimeInput from './TimeInput';
|
||||
@@ -105,6 +106,24 @@ function MakeMultiLineField({ row, column, table }: CellContext<OntimeRundownEnt
|
||||
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />;
|
||||
}
|
||||
|
||||
function LazyImage({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||
const update = useCallback(
|
||||
(newValue: string) => {
|
||||
table.options.meta?.handleUpdate(row.index, column.id, newValue, true);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- we skip table.options.meta since the reference seems unstable
|
||||
[column.id, row.index],
|
||||
);
|
||||
|
||||
const event = row.original;
|
||||
if (!isOntimeEvent(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const initialValue = event.custom[column.id];
|
||||
return <EditableImage initialValue={initialValue} updateValue={update} />;
|
||||
}
|
||||
|
||||
function MakeSingleLineField({ row, column, table }: CellContext<OntimeRundownEntry, unknown>) {
|
||||
const update = useCallback(
|
||||
(newValue: string) => {
|
||||
@@ -139,7 +158,6 @@ function MakeCustomField({ row, column, table }: CellContext<OntimeRundownEntry,
|
||||
}
|
||||
|
||||
const initialValue = event.custom[column.id] ?? '';
|
||||
|
||||
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />;
|
||||
}
|
||||
|
||||
@@ -148,8 +166,8 @@ export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<Ontim
|
||||
accessorKey: key,
|
||||
id: key,
|
||||
header: customFields[key].label,
|
||||
meta: { colour: customFields[key].colour },
|
||||
cell: MakeCustomField,
|
||||
meta: { colour: customFields[key].colour, type: customFields[key].type },
|
||||
cell: customFields[key].type === 'string' ? MakeCustomField : LazyImage,
|
||||
size: 250,
|
||||
minSize: 75,
|
||||
}));
|
||||
|
||||
@@ -11,7 +11,7 @@ export const validateCustomField = [
|
||||
.custom((value) => {
|
||||
return isAlphanumericWithSpace(value);
|
||||
}),
|
||||
body('type').exists().isString().trim(),
|
||||
body('type').exists().isIn(['string', 'image']),
|
||||
body('colour').exists().isString().trim(),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -23,8 +23,14 @@ export const validateCustomField = [
|
||||
|
||||
export const validateEditCustomField = [
|
||||
param('label').exists().isString().trim(),
|
||||
body('label').exists().isString().trim(),
|
||||
body('type').exists().isString().trim(),
|
||||
body('label')
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.custom((value) => {
|
||||
return isAlphanumericWithSpace(value);
|
||||
}),
|
||||
body('type').exists().isIn(['string', 'image']),
|
||||
body('colour').exists().isString().trim(),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
@@ -156,7 +156,7 @@ describe('parseCustomFields()', () => {
|
||||
});
|
||||
|
||||
describe('sanitiseCustomFields()', () => {
|
||||
it('returns an empty array if not an array', () => {
|
||||
it('returns an empty object the type is incorrect', () => {
|
||||
expect(sanitiseCustomFields({})).toEqual({});
|
||||
});
|
||||
|
||||
@@ -170,16 +170,16 @@ describe('sanitiseCustomFields()', () => {
|
||||
expect(sanitationResult).toStrictEqual(customFields);
|
||||
});
|
||||
|
||||
it('type is forced to be string', () => {
|
||||
const customFields: CustomFields = {
|
||||
// @ts-expect-error intentional bad data
|
||||
test: { label: 'test', type: 'another', colour: 'red' },
|
||||
};
|
||||
const expectedCustomFields: CustomFields = {
|
||||
test: { label: 'test', type: 'string', colour: 'red' },
|
||||
};
|
||||
const sanitationResult = sanitiseCustomFields(customFields);
|
||||
expect(sanitationResult).toStrictEqual(expectedCustomFields);
|
||||
it('type should be one of (image | string)', () => {
|
||||
const testTypes = sanitiseCustomFields({
|
||||
test1: { label: 'test1', type: 'another', colour: 'red' },
|
||||
test2: { label: 'test2', type: 'image', colour: 'red' },
|
||||
test3: { label: 'test3', type: 'string', colour: 'red' },
|
||||
});
|
||||
expect(testTypes).toMatchObject({
|
||||
test2: { label: 'test2', type: 'image', colour: 'red' },
|
||||
test3: { label: 'test3', type: 'string', colour: 'red' },
|
||||
});
|
||||
});
|
||||
|
||||
it('colour must be a string', () => {
|
||||
|
||||
@@ -217,15 +217,15 @@ export function sanitiseCustomFields(data: object): CustomFields {
|
||||
}
|
||||
|
||||
const keyFromLabel = customFieldLabelToKey(field.label);
|
||||
//Test label and key cohesion, but allow old lowercased keys to stay
|
||||
//TODO: the `toLocaleLowerCase` part here is to conserve keys from old projects and could be removed at some point (okt. 2024)
|
||||
// Test label and key cohesion, but allow old lowercased keys to stay
|
||||
// TODO: the `toLocaleLowerCase` part here is to conserve keys from old projects and could be removed at some point (okt. 2024)
|
||||
const key = originalKey.toLocaleLowerCase() === keyFromLabel.toLocaleLowerCase() ? originalKey : keyFromLabel;
|
||||
if (key in newCustomFields) {
|
||||
continue;
|
||||
}
|
||||
|
||||
newCustomFields[key] = {
|
||||
type: 'string',
|
||||
type: field.type,
|
||||
colour: field.colour,
|
||||
label: field.label,
|
||||
};
|
||||
@@ -238,7 +238,9 @@ export function sanitiseCustomFields(data: object): CustomFields {
|
||||
'label' in data &&
|
||||
data.label !== '' &&
|
||||
'colour' in data &&
|
||||
typeof data.colour === 'string'
|
||||
typeof data.colour === 'string' &&
|
||||
'type' in data &&
|
||||
(data.type === 'string' || data.type === 'image')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export type CustomFieldLabel = string;
|
||||
|
||||
export type CustomField = {
|
||||
type: 'string';
|
||||
type: 'string' | 'image';
|
||||
colour: string;
|
||||
label: CustomFieldLabel;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user