import type { CustomFields, ProjectData } from 'ontime-types'; import type { SelectOption } from '../select/Select'; import type { MultiselectOption, 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: SelectOption[] = [], filterImageType = true, ): SelectOption[] { const options: SelectOption[] = [...additionalOptions]; // Add custom fields first for (const [key, value] of Object.entries(customFields)) { if (filterImageType && value.type === 'image') { continue; } options.push({ value: `custom-${key}`, label: `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): MultiselectOption[] { const options: MultiselectOption[] = []; // Add custom fields first for (const [key, value] of Object.entries(customFields)) { if (filterImageType && value.type === 'image') { continue; } options.push({ value: key, label: value.label, colour: value.colour, }); } return options; } /** * Creates data for a select element that displays project custom data */ export function makeProjectDataOptions( projectData: ProjectData, additionalOptions: SelectOption[] = [], ): SelectOption[] { const generatedOptions = projectData.custom.map((entry, index) => { return { value: `${index}-${entry.title}`, label: entry.title, }; }); return [...additionalOptions, ...generatedOptions]; } 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; colorFields: Set; booleanFields: Set; isPersistedField: Set; persistedValues: Record; }; /** * Utility collects metadata about fields from view options * - where are the default values * - which fields are colours * - which fields are persisted */ function collectFieldMetadata(paramFields: ViewOption[]): FieldMetadata { const metadata: FieldMetadata = { defaultValues: {}, colorFields: new Set(), booleanFields: 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); } else if (option.type === 'boolean') { metadata.booleanFields.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(); 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) => { // some field types need extra processing const processedValue = (() => { if (metadata.colorFields.has(id)) { return sanitiseColour(v); } if (metadata.booleanFields.has(id)) { return v === 'on' ? 'true' : 'false'; } return v; })(); if (metadata.isPersistedField.has(id) || metadata.defaultValues[id] !== processedValue) { addUniqueParam(id, processedValue); } }); } }); return newSearchParams; } /** * Extracts search params that are not managed by the view params editor * @param currentParams - The current URL search params * @param paramFields - The view options that define the managed parameters * @returns A new URLSearchParams object with preserved params */ export function getPreservedSearchParams(currentParams: URLSearchParams, paramFields: ViewOption[]) { const managedParamIds = new Set(); paramFields.forEach((section) => { section.options.forEach((option) => { managedParamIds.add(option.id); }); }); const preserved = new URLSearchParams(); currentParams.forEach((value, key) => { if (!managedParamIds.has(key)) { preserved.append(key, value); } }); return preserved; }