mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-20 06:29:07 +00:00
refactor: improve handling of multi-fields
This commit is contained in:
committed by
Carlos Valente
parent
b2b115c329
commit
5c86914cda
@@ -18,19 +18,21 @@ import {
|
|||||||
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
||||||
|
|
||||||
import InlineColourPicker from './InlineColourPicker';
|
import InlineColourPicker from './InlineColourPicker';
|
||||||
import { ParamField } from './types';
|
import { ParamField } from './viewParams.types';
|
||||||
|
|
||||||
interface EditFormInputProps {
|
interface ParamInputProps {
|
||||||
paramField: ParamField;
|
paramField: ParamField;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ParamInput(props: EditFormInputProps) {
|
export default function ParamInput({ paramField }: ParamInputProps) {
|
||||||
const { paramField } = props;
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { id, type, defaultValue } = paramField;
|
const { id, type, defaultValue } = paramField;
|
||||||
|
|
||||||
if (type === 'persist') {
|
if (type === 'persist') {
|
||||||
return null;
|
if (!paramField.values || !paramField.values.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return <input hidden name={id} readOnly value={paramField.values.join(',')} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'option') {
|
if (type === 'option') {
|
||||||
|
|||||||
@@ -15,74 +15,12 @@ import {
|
|||||||
import useViewSettings from '../../hooks-query/useViewSettings';
|
import useViewSettings from '../../hooks-query/useViewSettings';
|
||||||
import Info from '../info/Info';
|
import Info from '../info/Info';
|
||||||
|
|
||||||
import { ViewOption } from './types';
|
import { ViewOption } from './viewParams.types';
|
||||||
|
import { getURLSearchParamsFromObj } from './viewParams.utils';
|
||||||
import ViewParamsSection from './ViewParamsSection';
|
import ViewParamsSection from './ViewParamsSection';
|
||||||
|
|
||||||
import style from './ViewParamsEditor.module.scss';
|
import style from './ViewParamsEditor.module.scss';
|
||||||
|
|
||||||
type ViewParamsObj = { [key: string]: string | FormDataEntryValue };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility remove the # character from a hex string
|
|
||||||
*/
|
|
||||||
function sanitiseColour(colour: string) {
|
|
||||||
if (colour.startsWith('#')) {
|
|
||||||
return colour.substring(1);
|
|
||||||
}
|
|
||||||
return colour;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes a new URLSearchParams object from the given params object
|
|
||||||
*/
|
|
||||||
const getURLSearchParamsFromObj = (paramsObj: ViewParamsObj, paramFields: ViewOption[]) => {
|
|
||||||
const newSearchParams = new URLSearchParams();
|
|
||||||
|
|
||||||
// Convert paramFields to an object that contains default values
|
|
||||||
const defaultValues: Record<string, string> = {};
|
|
||||||
paramFields.forEach((section) => {
|
|
||||||
section.options.forEach((option) => {
|
|
||||||
defaultValues[option.id] = String(option.defaultValue);
|
|
||||||
|
|
||||||
// extract persisted values
|
|
||||||
if ('type' in option && option.type === 'persist') {
|
|
||||||
newSearchParams.set(option.id, option.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// compare which values are different from the default values
|
|
||||||
Object.entries(paramsObj).forEach(([id, value]) => {
|
|
||||||
if (typeof value === 'string' && value.length) {
|
|
||||||
// we dont know which values contain colours
|
|
||||||
// unfortunately this means we run all the strings through the sanitation
|
|
||||||
const valueWithoutHash = sanitiseColour(value);
|
|
||||||
if (defaultValues[id] !== valueWithoutHash) {
|
|
||||||
handleValueString(id, valueWithoutHash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Utility function contains logic to add a value into the searchParams object */
|
|
||||||
function handleValueString(id: string, value: string) {
|
|
||||||
const maybeMultipleValues = value.split(',');
|
|
||||||
|
|
||||||
// we need to check if the value contains comma separated list, for the case of the multi-select data
|
|
||||||
if (Array.isArray(maybeMultipleValues) && maybeMultipleValues.length > 1) {
|
|
||||||
const added = new Set();
|
|
||||||
maybeMultipleValues.forEach((v) => {
|
|
||||||
if (!added.has(v)) {
|
|
||||||
added.add(v);
|
|
||||||
newSearchParams.append(id, v);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
newSearchParams.set(id, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newSearchParams;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface EditFormDrawerProps {
|
interface EditFormDrawerProps {
|
||||||
viewOptions: ViewOption[];
|
viewOptions: ViewOption[];
|
||||||
}
|
}
|
||||||
@@ -94,6 +32,7 @@ export default function ViewParamsEditor({ viewOptions }: EditFormDrawerProps) {
|
|||||||
|
|
||||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||||
|
|
||||||
|
// handle opening the drawer
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isEditing = searchParams.get('edit');
|
const isEditing = searchParams.get('edit');
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { useLocalStorage } from '@mantine/hooks';
|
|||||||
|
|
||||||
import { cx } from '../../utils/styleUtils';
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
|
import { OptionTitle } from './constants';
|
||||||
import ParamInput from './ParamInput';
|
import ParamInput from './ParamInput';
|
||||||
import { type ParamField } from './types';
|
import { type ParamField } from './viewParams.types';
|
||||||
|
|
||||||
import style from './ViewParamsSection.module.scss';
|
import style from './ViewParamsSection.module.scss';
|
||||||
|
|
||||||
@@ -14,9 +15,7 @@ interface ViewParamsSectionProps {
|
|||||||
options: ParamField[];
|
options: ParamField[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ViewParamsSection(props: ViewParamsSectionProps) {
|
export default function ViewParamsSection({ title, collapsible, options }: ViewParamsSectionProps) {
|
||||||
const { title, collapsible, options } = props;
|
|
||||||
|
|
||||||
const [collapsed, setCollapsed] = useLocalStorage({ key: `params-${title}`, defaultValue: false });
|
const [collapsed, setCollapsed] = useLocalStorage({ key: `params-${title}`, defaultValue: false });
|
||||||
|
|
||||||
const handleCollapse = () => {
|
const handleCollapse = () => {
|
||||||
@@ -27,28 +26,52 @@ export default function ViewParamsSection(props: ViewParamsSectionProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={style.section}>
|
<section className={style.section}>
|
||||||
<div className={cx([style.sectionHeader, collapsible && style.collapsible])} onClick={handleCollapse}>
|
{title === OptionTitle.Hidden ? (
|
||||||
{title}
|
<HiddenContents options={options} />
|
||||||
{collapsible && <IoChevronDown className={cx([collapsed ? style.closed : style.open])} />}
|
) : (
|
||||||
</div>
|
|
||||||
|
|
||||||
{!collapsed && (
|
|
||||||
<>
|
<>
|
||||||
{options.map((option) => {
|
<div className={cx([style.sectionHeader, collapsible && style.collapsible])} onClick={handleCollapse}>
|
||||||
if (option.type === 'persist') {
|
{title}
|
||||||
return null;
|
{collapsible && <IoChevronDown className={cx([collapsed ? style.closed : style.open])} />}
|
||||||
}
|
</div>
|
||||||
|
<SectionContents options={options} collapsed={collapsed} />
|
||||||
return (
|
|
||||||
<label key={option.title} className={style.label}>
|
|
||||||
<span className={style.title}>{option.title}</span>
|
|
||||||
<span className={style.description}>{option.description}</span>
|
|
||||||
<ParamInput paramField={option} />
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SectionContentsProps {
|
||||||
|
options: ParamField[];
|
||||||
|
collapsed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SectionContents({ options, collapsed }: SectionContentsProps) {
|
||||||
|
if (collapsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{options.map((option) => {
|
||||||
|
return (
|
||||||
|
<label key={option.title} className={style.label}>
|
||||||
|
<span className={style.title}>{option.title}</span>
|
||||||
|
<span className={style.description}>{option.description}</span>
|
||||||
|
<ParamInput paramField={option} />
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HiddenContents({ options }: { options: ParamField[] }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{options.map((option, index) => {
|
||||||
|
return <ParamInput key={option.title + index} paramField={option} />;
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
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',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+204
@@ -0,0 +1,204 @@
|
|||||||
|
import { CustomFields } from 'ontime-types';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { OptionTitle } from '../constants';
|
||||||
|
import type { ViewOption } from '../viewParams.types';
|
||||||
|
import { getURLSearchParamsFromObj, makeOptionsFromCustomFields } from '../viewParams.utils';
|
||||||
|
|
||||||
|
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',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getURLSearchParamsFromObj', () => {
|
||||||
|
// Mock view options for testing
|
||||||
|
const mockViewOptions: ViewOption[] = [
|
||||||
|
{
|
||||||
|
title: OptionTitle.DataSources,
|
||||||
|
collapsible: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
id: 'color',
|
||||||
|
title: 'Color',
|
||||||
|
description: 'The color value',
|
||||||
|
type: 'colour',
|
||||||
|
defaultValue: 'ff0000',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'persist-field',
|
||||||
|
title: 'Persistent Field',
|
||||||
|
description: 'A field that persists',
|
||||||
|
type: 'persist',
|
||||||
|
values: ['persisted-value'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'multi-select',
|
||||||
|
title: 'Multi Select',
|
||||||
|
description: 'A multi-select field',
|
||||||
|
type: 'option',
|
||||||
|
values: { value1: 'Value 1', value2: 'Value 2' },
|
||||||
|
defaultValue: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
it('should handle empty params object', () => {
|
||||||
|
const params = {};
|
||||||
|
const result = getURLSearchParamsFromObj(params, mockViewOptions);
|
||||||
|
|
||||||
|
// Should only include persisted values
|
||||||
|
expect(result.get('persist-field')).toBe('persisted-value');
|
||||||
|
expect(result.get('color')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include values that match defaults', () => {
|
||||||
|
const params = {
|
||||||
|
color: 'ff0000', // same as the default
|
||||||
|
'other-param': 'value',
|
||||||
|
};
|
||||||
|
const result = getURLSearchParamsFromObj(params, mockViewOptions);
|
||||||
|
|
||||||
|
expect(result.get('color')).toBeNull();
|
||||||
|
expect(result.get('other-param')).toBe('value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sanitize color values with #', () => {
|
||||||
|
const params = {
|
||||||
|
color: '#00ff00', // different from default and includes #
|
||||||
|
};
|
||||||
|
const result = getURLSearchParamsFromObj(params, mockViewOptions);
|
||||||
|
|
||||||
|
expect(result.get('color')).toBe('00ff00');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multi-select values', () => {
|
||||||
|
const params = {
|
||||||
|
'multi-select': 'value1,value2,value3',
|
||||||
|
};
|
||||||
|
const result = getURLSearchParamsFromObj(params, mockViewOptions);
|
||||||
|
|
||||||
|
// Should have multiple entries for the same key
|
||||||
|
const values = result.getAll('multi-select');
|
||||||
|
expect(values).toEqual(['value1', 'value2', 'value3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include empty string values', () => {
|
||||||
|
const params = {
|
||||||
|
'empty-param': '',
|
||||||
|
};
|
||||||
|
const result = getURLSearchParamsFromObj(params, mockViewOptions);
|
||||||
|
expect(result.get('empty-param')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow multiple values for persisted fields', () => {
|
||||||
|
const mockOptionsWithMultiPersist: ViewOption[] = [
|
||||||
|
{
|
||||||
|
title: OptionTitle.DataSources,
|
||||||
|
collapsible: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
id: 'sub',
|
||||||
|
title: 'Event subscription',
|
||||||
|
description: 'The events to follow',
|
||||||
|
values: [],
|
||||||
|
type: 'persist',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
sub: 'value1,value2,value3',
|
||||||
|
};
|
||||||
|
const result = getURLSearchParamsFromObj(params, mockOptionsWithMultiPersist);
|
||||||
|
const values = result.getAll('sub');
|
||||||
|
|
||||||
|
expect(values).toStrictEqual(['value1', 'value2', 'value3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow adding multiple values for any field', () => {
|
||||||
|
const params = {
|
||||||
|
'regular-field': 'value1,value2,value3',
|
||||||
|
};
|
||||||
|
const result = getURLSearchParamsFromObj(params, mockViewOptions);
|
||||||
|
|
||||||
|
const values = result.getAll('regular-field');
|
||||||
|
expect(values).toHaveLength(3);
|
||||||
|
expect(values).toEqual(['value1', 'value2', 'value3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deduplicate repeated key-value pairs', () => {
|
||||||
|
const params = {
|
||||||
|
multiValue: 'value1,value1,value2,value2,value3',
|
||||||
|
repeatedField: 'same,same,same',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getURLSearchParamsFromObj(params, mockViewOptions);
|
||||||
|
|
||||||
|
// Check multiValue field has unique values
|
||||||
|
expect(result.getAll('multiValue')).toEqual(['value1', 'value2', 'value3']);
|
||||||
|
|
||||||
|
// Check repeatedField has only one instance of the value
|
||||||
|
expect(result.getAll('repeatedField')).toEqual(['same']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deduplicate persisted values while maintaining order', () => {
|
||||||
|
const mockOptionsWithDuplicates: ViewOption[] = [
|
||||||
|
{
|
||||||
|
title: OptionTitle.StyleOverride,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
id: 'sub',
|
||||||
|
title: 'Subscription',
|
||||||
|
description: 'Persisted subscription values',
|
||||||
|
type: 'persist',
|
||||||
|
values: ['value1', 'value1', 'value2', 'value2', 'value3', 'value1'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = getURLSearchParamsFromObj({}, mockOptionsWithDuplicates);
|
||||||
|
|
||||||
|
// Should only include unique values while maintaining order
|
||||||
|
expect(result.getAll('sub')).toEqual(['value1', 'value2', 'value3']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { ParamField } from './viewParams.types';
|
||||||
|
|
||||||
|
export const getTimeOption = (timeFormat: string): ParamField => {
|
||||||
|
const placeholder = `${timeFormat} (default)`;
|
||||||
|
return {
|
||||||
|
id: 'timeformat',
|
||||||
|
title: 'Time format string, taken from the Application Settings',
|
||||||
|
description: 'Format for auxiliar time fields (not the running), eg. HH:mm:ss or hh:mm:ss a, see docs for help',
|
||||||
|
type: 'string',
|
||||||
|
placeholder,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hideTimerSeconds: ParamField = {
|
||||||
|
id: 'hideTimerSeconds',
|
||||||
|
title: 'Hide seconds in timer',
|
||||||
|
description: 'Whether to hide seconds in the running timer',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const showLeadingZeros: ParamField = {
|
||||||
|
id: 'showLeadingZeros',
|
||||||
|
title: 'Show leading zeros in timer',
|
||||||
|
description: 'Whether to show leading zeros in the running timer',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
};
|
||||||
@@ -1,61 +1,6 @@
|
|||||||
import { CustomFields } from 'ontime-types';
|
/**
|
||||||
|
* Gathers possible titles for view options
|
||||||
import type { MultiselectOptions, ParamField } from './types';
|
*/
|
||||||
|
|
||||||
export const makeOptionsFromCustomFields = (
|
|
||||||
customFields: CustomFields,
|
|
||||||
additionalOptions: Readonly<Record<string, string>> = {},
|
|
||||||
filterImageType = true,
|
|
||||||
) => {
|
|
||||||
const options = { ...additionalOptions };
|
|
||||||
for (const [key, value] of Object.entries(customFields)) {
|
|
||||||
if (filterImageType && value.type === 'image') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
options[`custom-${key}`] = `Custom: ${value.label}`;
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function makeCustomFieldSelectOptions(customFields: CustomFields, filterImageType = true): MultiselectOptions {
|
|
||||||
const options: MultiselectOptions = {};
|
|
||||||
for (const [key, value] of Object.entries(customFields)) {
|
|
||||||
if (filterImageType && value.type === 'image') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
options[key] = { value: key, label: value.label, colour: value.colour };
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getTimeOption = (timeFormat: string): ParamField => {
|
|
||||||
const placeholder = `${timeFormat} (default)`;
|
|
||||||
return {
|
|
||||||
id: 'timeformat',
|
|
||||||
title: 'Time format string, taken from the Application Settings',
|
|
||||||
description: 'Format for auxiliar time fields (not the running), eg. HH:mm:ss or hh:mm:ss a, see docs for help',
|
|
||||||
type: 'string',
|
|
||||||
placeholder,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hideTimerSeconds: ParamField = {
|
|
||||||
id: 'hideTimerSeconds',
|
|
||||||
title: 'Hide seconds in timer',
|
|
||||||
description: 'Whether to hide seconds in the running timer',
|
|
||||||
type: 'boolean',
|
|
||||||
defaultValue: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const showLeadingZeros: ParamField = {
|
|
||||||
id: 'showLeadingZeros',
|
|
||||||
title: 'Show leading zeros in timer',
|
|
||||||
description: 'Whether to show leading zeros in the running timer',
|
|
||||||
type: 'boolean',
|
|
||||||
defaultValue: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum OptionTitle {
|
export enum OptionTitle {
|
||||||
ClockOptions = 'Clock Options',
|
ClockOptions = 'Clock Options',
|
||||||
TimerOptions = 'Timer Options',
|
TimerOptions = 'Timer Options',
|
||||||
@@ -65,4 +10,7 @@ export enum OptionTitle {
|
|||||||
StyleOverride = 'View style override',
|
StyleOverride = 'View style override',
|
||||||
Animation = 'View animation',
|
Animation = 'View animation',
|
||||||
Schedule = 'Schedule options',
|
Schedule = 'Schedule options',
|
||||||
|
|
||||||
|
/** rendered as hidden inputs */
|
||||||
|
Hidden = 'Hidden options',
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ type StringField = { type: 'string'; defaultValue?: string; prefix?: string; pla
|
|||||||
type NumberField = { type: 'number'; defaultValue?: number; prefix?: string; placeholder?: string };
|
type NumberField = { type: 'number'; defaultValue?: number; prefix?: string; placeholder?: string };
|
||||||
type BooleanField = { type: 'boolean'; defaultValue: boolean };
|
type BooleanField = { type: 'boolean'; defaultValue: boolean };
|
||||||
type ColourField = { type: 'colour'; defaultValue: string; placeholder?: string };
|
type ColourField = { type: 'colour'; defaultValue: string; placeholder?: string };
|
||||||
type PersistedField = { type: 'persist'; defaultValue?: string; value: string };
|
type PersistedField = { type: 'persist'; defaultValue?: string[]; values: string[] };
|
||||||
|
|
||||||
export type ParamField = BaseField &
|
export type ParamField = BaseField &
|
||||||
(OptionsField | MultiOptionsField | StringField | NumberField | BooleanField | ColourField | PersistedField);
|
(OptionsField | MultiOptionsField | StringField | NumberField | BooleanField | ColourField | PersistedField);
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import type { CustomFields } from 'ontime-types';
|
||||||
|
|
||||||
|
import type { MultiselectOptions, ViewOption } from './viewParams.types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a list of custom fields for a select
|
||||||
|
* Filters out image type custom fields
|
||||||
|
*/
|
||||||
|
export function makeOptionsFromCustomFields(
|
||||||
|
customFields: CustomFields,
|
||||||
|
additionalOptions: Readonly<Record<string, string>> = {},
|
||||||
|
filterImageType = true,
|
||||||
|
): Record<string, string> {
|
||||||
|
const options = { ...additionalOptions };
|
||||||
|
for (const [key, value] of Object.entries(customFields)) {
|
||||||
|
if (filterImageType && value.type === 'image') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
options[`custom-${key}`] = `Custom: ${value.label}`;
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates data for a multiselect component from custom fields
|
||||||
|
* Filters out image type custom fields
|
||||||
|
*/
|
||||||
|
export function makeCustomFieldSelectOptions(customFields: CustomFields, filterImageType = true): MultiselectOptions {
|
||||||
|
const options: MultiselectOptions = {};
|
||||||
|
for (const [key, value] of Object.entries(customFields)) {
|
||||||
|
if (filterImageType && value.type === 'image') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
options[key] = { value: key, label: value.label, colour: value.colour };
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ViewParamsObj = { [key: string]: string | FormDataEntryValue };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility remove the # character from a hex string
|
||||||
|
*/
|
||||||
|
function sanitiseColour(colour: string) {
|
||||||
|
if (colour.startsWith('#')) {
|
||||||
|
return colour.substring(1);
|
||||||
|
}
|
||||||
|
return colour;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldMetadata = {
|
||||||
|
defaultValues: Record<string, string>;
|
||||||
|
colorFields: Set<string>;
|
||||||
|
isPersistedField: Set<string>;
|
||||||
|
persistedValues: Record<string, string[]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility collects metadata about fields from view options
|
||||||
|
*/
|
||||||
|
function collectFieldMetadata(paramFields: ViewOption[]): FieldMetadata {
|
||||||
|
const metadata: FieldMetadata = {
|
||||||
|
defaultValues: {},
|
||||||
|
colorFields: new Set(),
|
||||||
|
isPersistedField: new Set(),
|
||||||
|
persistedValues: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
paramFields.forEach((section) => {
|
||||||
|
section.options.forEach((option) => {
|
||||||
|
if (option.type === 'persist') {
|
||||||
|
metadata.isPersistedField.add(option.id);
|
||||||
|
if (option.values) {
|
||||||
|
metadata.persistedValues[option.id] = option.values;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
metadata.defaultValues[option.id] = String(option.defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.type === 'colour') {
|
||||||
|
metadata.colorFields.add(option.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a new URLSearchParams object from the given params object
|
||||||
|
* @param paramsObj - The object containing parameters to be converted
|
||||||
|
* @param paramFields - The view options that define the parameters
|
||||||
|
* @returns A new URLSearchParams object with the parameters
|
||||||
|
*/
|
||||||
|
export function getURLSearchParamsFromObj(paramsObj: ViewParamsObj, paramFields: ViewOption[]) {
|
||||||
|
const newSearchParams = new URLSearchParams();
|
||||||
|
const addedPairs = new Set<string>();
|
||||||
|
const metadata = collectFieldMetadata(paramFields);
|
||||||
|
|
||||||
|
// Utility function to safely add params without duplicates
|
||||||
|
const addUniqueParam = (id: string, value: string) => {
|
||||||
|
const pair = `${id}:${value}`;
|
||||||
|
if (!addedPairs.has(pair)) {
|
||||||
|
addedPairs.add(pair);
|
||||||
|
newSearchParams.append(id, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// First add all persisted values
|
||||||
|
Object.entries(metadata.persistedValues).forEach(([id, values]) => {
|
||||||
|
values.forEach((value) => {
|
||||||
|
if (value) {
|
||||||
|
addUniqueParam(id, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then process user-provided values
|
||||||
|
Object.entries(paramsObj).forEach(([id, value]) => {
|
||||||
|
if (typeof value === 'string' && value.length) {
|
||||||
|
// For persisted fields, clear existing values before adding new ones
|
||||||
|
if (metadata.isPersistedField.has(id)) {
|
||||||
|
// Clear tracking of previous values for this field
|
||||||
|
Array.from(addedPairs).forEach((pair) => {
|
||||||
|
if (pair.startsWith(`${id}:`)) {
|
||||||
|
addedPairs.delete(pair);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
newSearchParams.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process and add new values
|
||||||
|
value.split(',').forEach((v) => {
|
||||||
|
const processedValue = metadata.colorFields.has(id) ? sanitiseColour(v) : v;
|
||||||
|
if (metadata.isPersistedField.has(id) || metadata.defaultValues[id] !== processedValue) {
|
||||||
|
addUniqueParam(id, processedValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return newSearchParams;
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { CustomFields } from 'ontime-types';
|
import { CustomFields } 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 {
|
import {
|
||||||
getTimeOption,
|
|
||||||
makeCustomFieldSelectOptions,
|
makeCustomFieldSelectOptions,
|
||||||
makeOptionsFromCustomFields,
|
makeOptionsFromCustomFields,
|
||||||
OptionTitle,
|
} from '../../common/components/view-params-editor/viewParams.utils';
|
||||||
} from '../../common/components/view-params-editor/constants';
|
|
||||||
import { ViewOption } from '../../common/components/view-params-editor/types';
|
|
||||||
|
|
||||||
export const getOperatorOptions = (customFields: CustomFields, timeFormat: string): ViewOption[] => {
|
export const getOperatorOptions = (customFields: CustomFields, timeFormat: string): ViewOption[] => {
|
||||||
const fieldOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', note: 'Note' });
|
const fieldOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', note: 'Note' });
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getTimeOption, OptionTitle } from '../../../common/components/view-params-editor/constants';
|
import { getTimeOption } from '../../../common/components/view-params-editor/common.options';
|
||||||
import { ViewOption } from '../../../common/components/view-params-editor/types';
|
import { OptionTitle } from '../../../common/components/view-params-editor/constants';
|
||||||
|
import { ViewOption } from '../../../common/components/view-params-editor/viewParams.types';
|
||||||
|
|
||||||
export const getClockOptions = (timeFormat: string): ViewOption[] => [
|
export const getClockOptions = (timeFormat: string): ViewOption[] => [
|
||||||
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
|
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { useMemo } from 'react';
|
|||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { CustomFields } from 'ontime-types';
|
import { CustomFields } from 'ontime-types';
|
||||||
|
|
||||||
import { makeOptionsFromCustomFields, OptionTitle } from '../../../common/components/view-params-editor/constants';
|
import { OptionTitle } from '../../../common/components/view-params-editor/constants';
|
||||||
import { ViewOption } from '../../../common/components/view-params-editor/types';
|
import { ViewOption } from '../../../common/components/view-params-editor/viewParams.types';
|
||||||
|
import { makeOptionsFromCustomFields } from '../../../common/components/view-params-editor/viewParams.utils';
|
||||||
import safeParseNumber from '../../../common/utils/safeParseNumber';
|
import safeParseNumber from '../../../common/utils/safeParseNumber';
|
||||||
|
|
||||||
export const getLowerThirdOptions = (customFields: CustomFields): ViewOption[] => {
|
export const getLowerThirdOptions = (customFields: CustomFields): ViewOption[] => {
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import {
|
import { hideTimerSeconds, showLeadingZeros } from '../../../common/components/view-params-editor/common.options';
|
||||||
hideTimerSeconds,
|
import { OptionTitle } from '../../../common/components/view-params-editor/constants';
|
||||||
OptionTitle,
|
import { ViewOption } from '../../../common/components/view-params-editor/viewParams.types';
|
||||||
showLeadingZeros,
|
|
||||||
} from '../../../common/components/view-params-editor/constants';
|
|
||||||
import { ViewOption } from '../../../common/components/view-params-editor/types';
|
|
||||||
|
|
||||||
export const MINIMAL_TIMER_OPTIONS: ViewOption[] = [
|
export const MINIMAL_TIMER_OPTIONS: ViewOption[] = [
|
||||||
{ title: OptionTitle.TimerOptions, collapsible: true, options: [hideTimerSeconds, showLeadingZeros] },
|
{ title: OptionTitle.TimerOptions, collapsible: true, options: [hideTimerSeconds, showLeadingZeros] },
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getTimeOption, hideTimerSeconds, OptionTitle } from '../../../common/components/view-params-editor/constants';
|
import { getTimeOption, hideTimerSeconds } from '../../../common/components/view-params-editor/common.options';
|
||||||
import type { ViewOption } from '../../../common/components/view-params-editor/types';
|
import { OptionTitle } from '../../../common/components/view-params-editor/constants';
|
||||||
|
import type { ViewOption } from '../../../common/components/view-params-editor/viewParams.types';
|
||||||
|
|
||||||
export const getStudioClockOptions = (timeFormat: string): ViewOption[] => [
|
export const getStudioClockOptions = (timeFormat: string): ViewOption[] => [
|
||||||
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
|
{ title: OptionTitle.ClockOptions, collapsible: true, options: [getTimeOption(timeFormat)] },
|
||||||
|
|||||||
@@ -2,12 +2,10 @@ import { useMemo } from 'react';
|
|||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { CustomFields, OntimeEvent } from 'ontime-types';
|
import { CustomFields, OntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
import {
|
import { getTimeOption } from '../../common/components/view-params-editor/common.options';
|
||||||
getTimeOption,
|
import { OptionTitle } from '../../common/components/view-params-editor/constants';
|
||||||
makeOptionsFromCustomFields,
|
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
|
||||||
OptionTitle,
|
import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils';
|
||||||
} from '../../common/components/view-params-editor/constants';
|
|
||||||
import { ViewOption } from '../../common/components/view-params-editor/types';
|
|
||||||
import { scheduleOptions } from '../common/schedule/schedule.options';
|
import { scheduleOptions } from '../common/schedule/schedule.options';
|
||||||
|
|
||||||
export const getBackstageOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
|
export const getBackstageOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
|||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { OptionTitle } from '../../../common/components/view-params-editor/constants';
|
import { OptionTitle } from '../../../common/components/view-params-editor/constants';
|
||||||
import { ViewOption } from '../../../common/components/view-params-editor/types';
|
import { ViewOption } from '../../../common/components/view-params-editor/viewParams.types';
|
||||||
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
import { isStringBoolean } from '../../../features/viewers/common/viewUtils';
|
||||||
|
|
||||||
export const scheduleOptions: ViewOption = {
|
export const scheduleOptions: ViewOption = {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export default function Countdown({
|
|||||||
|
|
||||||
// gather option data
|
// gather option data
|
||||||
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
const defaultFormat = getDefaultFormat(settings?.timeFormat);
|
||||||
const countdownOptions = getCountdownOptions(defaultFormat, customFields);
|
const countdownOptions = getCountdownOptions(defaultFormat, customFields, subscriptions);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
|
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
|
||||||
|
|||||||
@@ -2,15 +2,17 @@ import { useMemo } from 'react';
|
|||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { CustomFields, EntryId, OntimeEvent } from 'ontime-types';
|
import { CustomFields, EntryId, OntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
import {
|
import { getTimeOption } from '../../common/components/view-params-editor/common.options';
|
||||||
getTimeOption,
|
import { OptionTitle } from '../../common/components/view-params-editor/constants';
|
||||||
makeOptionsFromCustomFields,
|
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
|
||||||
OptionTitle,
|
import { makeOptionsFromCustomFields } from '../../common/components/view-params-editor/viewParams.utils';
|
||||||
} from '../../common/components/view-params-editor/constants';
|
|
||||||
import { ViewOption } from '../../common/components/view-params-editor/types';
|
|
||||||
import { isStringBoolean } from '../../features/viewers/common/viewUtils';
|
import { isStringBoolean } from '../../features/viewers/common/viewUtils';
|
||||||
|
|
||||||
export const getCountdownOptions = (timeFormat: string, customFields: CustomFields): ViewOption[] => {
|
export const getCountdownOptions = (
|
||||||
|
timeFormat: string,
|
||||||
|
customFields: CustomFields,
|
||||||
|
persistedSubscriptions: EntryId[],
|
||||||
|
): ViewOption[] => {
|
||||||
const secondaryOptions = makeOptionsFromCustomFields(customFields, { note: 'Note' });
|
const secondaryOptions = makeOptionsFromCustomFields(customFields, { note: 'Note' });
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -20,15 +22,6 @@ export const getCountdownOptions = (timeFormat: string, customFields: CustomFiel
|
|||||||
collapsible: true,
|
collapsible: true,
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
id: 'sub',
|
|
||||||
title: 'Event subscription',
|
|
||||||
description: 'The events to follow',
|
|
||||||
value: '',
|
|
||||||
type: 'persist',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// TODO: adding a secondary source is removing the subscriptions
|
|
||||||
// this seems to be a bug with persist assuming that the property has a single entry
|
|
||||||
id: 'secondary-src',
|
id: 'secondary-src',
|
||||||
title: 'Event secondary text',
|
title: 'Event secondary text',
|
||||||
description: 'Select the data source for auxiliary text shown in the card',
|
description: 'Select the data source for auxiliary text shown in the card',
|
||||||
@@ -51,6 +44,18 @@ export const getCountdownOptions = (timeFormat: string, customFields: CustomFiel
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: OptionTitle.Hidden,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
id: 'sub',
|
||||||
|
title: 'Event subscription',
|
||||||
|
description: 'The events to follow',
|
||||||
|
values: persistedSubscriptions,
|
||||||
|
type: 'persist',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
|||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { OptionTitle } from '../../common/components/view-params-editor/constants';
|
import { OptionTitle } from '../../common/components/view-params-editor/constants';
|
||||||
import { ViewOption } from '../../common/components/view-params-editor/types';
|
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
|
||||||
import { isStringBoolean } from '../../features/viewers/common/viewUtils';
|
import { isStringBoolean } from '../../features/viewers/common/viewUtils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { OptionTitle } from '../../common/components/view-params-editor/constants';
|
import { OptionTitle } from '../../common/components/view-params-editor/constants';
|
||||||
import { ViewOption } from '../../common/components/view-params-editor/types';
|
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
|
||||||
|
|
||||||
export const projectInfoOptions: ViewOption[] = [
|
export const projectInfoOptions: ViewOption[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getTimeOption, OptionTitle } from '../../common/components/view-params-editor/constants';
|
import { getTimeOption } from '../../common/components/view-params-editor/common.options';
|
||||||
import { ViewOption } from '../../common/components/view-params-editor/types';
|
import { OptionTitle } from '../../common/components/view-params-editor/constants';
|
||||||
|
import { ViewOption } from '../../common/components/view-params-editor/viewParams.types';
|
||||||
|
|
||||||
export const getTimelineOptions = (timeFormat: string): ViewOption[] => {
|
export const getTimelineOptions = (timeFormat: string): ViewOption[] => {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import { validateTimerType } from 'ontime-utils';
|
|||||||
import {
|
import {
|
||||||
getTimeOption,
|
getTimeOption,
|
||||||
hideTimerSeconds,
|
hideTimerSeconds,
|
||||||
makeOptionsFromCustomFields,
|
|
||||||
OptionTitle,
|
|
||||||
showLeadingZeros,
|
showLeadingZeros,
|
||||||
} from '../../common/components/view-params-editor/constants';
|
} from '../../common/components/view-params-editor/common.options';
|
||||||
import { ViewOption } from '../../common/components/view-params-editor/types';
|
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 { isStringBoolean } from '../../features/viewers/common/viewUtils';
|
import { isStringBoolean } from '../../features/viewers/common/viewUtils';
|
||||||
|
|
||||||
// manually match the properties of TimerType excluding the None
|
// manually match the properties of TimerType excluding the None
|
||||||
|
|||||||
Reference in New Issue
Block a user