import { defaults, isFunction, isNil, isString } from 'lodash'; import React from 'react'; import type { IMapPair } from './MapPair'; import { MapPair } from './MapPair'; import type { IPipeline } from '../../domain'; import type { IFormInputProps, IValidator, IValidatorResult } from '../../presentation'; import { createFakeReactSyntheticEvent } from '../../presentation'; import './MapEditor.less'; export interface IMapEditorInputProps extends IFormInputProps { addButtonLabel?: string; allowEmptyValues?: boolean; hiddenKeys?: string[]; keyLabel?: string; label?: string; labelsLeft?: boolean; valueLabel?: string; valueCanContainSpel?: boolean; pipeline?: IPipeline; } export interface IMapEditorModel { [key: string]: string; } const duplicateKeyPattern = /^__MapEditorDuplicateKey__\d+__/; // Convert the controlled component value (an object) to a list of IMapPairs function objectToTuples(model: IMapEditorModel, errors: IMapEditorModel): IMapPair[] { model = model || {}; errors = errors || {}; return Object.keys(model).map((key) => { const keyWithoutMagicString = key.split(duplicateKeyPattern).pop(); return { key: keyWithoutMagicString, value: model[key], error: errors[key] }; }); } // Convert a list of IMapPairs to an object with keys and values // Prepend any duplicate keys with a magic string function tuplesToObject(pairs: IMapPair[]): IMapEditorModel { return pairs.reduce((acc, pair, idx) => { // Cannot have duplicate keys in an object, so prepend a magic string to the key const key = isNil(acc[pair.key]) ? pair.key : `__MapEditorDuplicateKey__${idx}__${pair.key}`; return { ...acc, [key]: pair.value }; }, {} as IMapEditorModel); } function mapEditorValidator(options?: { allowEmptyValues: boolean }): IValidator { const opts = defaults({}, options, { allowEmptyValues: false }); return function (values: IMapEditorModel): IValidatorResult { const errors = Object.keys(values || {}).reduce((acc, key) => { if (!key) { return { ...acc, [key]: 'Empty key' }; } else if (duplicateKeyPattern.exec(key)) { return { ...acc, [key]: 'Duplicate key' }; } else if (!opts.allowEmptyValues && !values[key]) { return { ...acc, [key]: 'Empty value' }; } return acc; }, {}) as any; return Object.keys(errors).length ? errors : null; }; } export function MapEditorInput({ addButtonLabel = 'Add Field', allowEmptyValues = false, hiddenKeys = [], keyLabel = 'Key', label, labelsLeft = false, name, onChange, value, valueLabel = 'Value', valueCanContainSpel = false, validation, pipeline, }: IMapEditorInputProps) { const rowProps = { keyLabel, valueLabel, labelsLeft }; const validator = React.useRef(mapEditorValidator({ allowEmptyValues })); const columnCount = labelsLeft ? 5 : 3; const tableClass = label ? '' : 'no-border-top'; const isParameterized = isString(value); const backingModel = !isString(value) ? objectToTuples(value, (validation && validation.messageNode) || {}) : null; // Register/unregister validator, if a validation prop was supplied React.useEffect(() => { if (validation && isFunction(validation.addValidator)) { validation.addValidator((validator.current as any) as IValidator); } return () => { if (validation && isFunction(validation.removeValidator)) { validation.removeValidator((validator.current as any) as IValidator); } }; }, []); const handleChanged = () => { onChange(createFakeReactSyntheticEvent({ name, value: tuplesToObject(backingModel) })); }; const handlePairChanged = (newPair: IMapPair, index: number) => { backingModel[index] = newPair; handleChanged(); }; const handleDeletePair = (index: number) => { backingModel.splice(index, 1); handleChanged(); }; const handleAddPair = () => { backingModel.push({ key: '', value: '' }); handleChanged(); }; return (
{label && (
{label}
)} {isParameterized && } {!isParameterized && ( {!labelsLeft && ( )} {backingModel .filter((p) => !hiddenKeys.includes(p.key)) .map((pair, index) => ( handlePairChanged(x, index)} onDelete={() => handleDeletePair(index)} pair={pair} valueCanContainSpel={valueCanContainSpel} pipeline={pipeline} /> ))}
{keyLabel} {valueLabel}
)}
); }