mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 17:35:05 +10:00
refactor: add detailed docs for placeholder string parsing functions
This commit is contained in:
@@ -51,6 +51,22 @@ const CHECKBOX_VALIDATION_RULE_BY_ALIAS: Record<string, string> = {
|
|||||||
atMost: 'Select at most',
|
atMost: 'Select at most',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Split a string on a delimiter, treating `\` as an escape for the next character.
|
||||||
|
Delimiters preceded by `\` are kept in the output instead of splitting (e.g. `\,`, `\=`, `\|`).
|
||||||
|
|
||||||
|
With delimiter ',' (top-level placeholder parts):
|
||||||
|
'radio, r1, options=Card/Check|Bank Transfer, selected=Bank Transfer'
|
||||||
|
-> ['radio', ' r1', ' options=Card/Check|Bank Transfer', ' selected=Bank Transfer']
|
||||||
|
|
||||||
|
With delimiter '=' (split one field metadata token into key + value):
|
||||||
|
'options=Card/Check|Bank Transfer'
|
||||||
|
-> ['options', 'Card/Check|Bank Transfer']
|
||||||
|
|
||||||
|
With delimiter '|' (split option list inside 'options='):
|
||||||
|
'Card/Check|Bank Transfer'
|
||||||
|
-> ['Card/Check', 'Bank Transfer']
|
||||||
|
*/
|
||||||
const splitPlaceholderToken = (value: string, delimiter: string): string[] => {
|
const splitPlaceholderToken = (value: string, delimiter: string): string[] => {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
let currentPart = '';
|
let currentPart = '';
|
||||||
@@ -79,6 +95,14 @@ const splitPlaceholderToken = (value: string, delimiter: string): string[] => {
|
|||||||
return parts;
|
return parts;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Split a metadata token (e.g. 'required=true') into [key, value].
|
||||||
|
Splits on the first unescaped '='; returns null if there is no key or value.
|
||||||
|
|
||||||
|
E.g.
|
||||||
|
'options=Card/Check|Bank Transfer' -> ['options', 'Card/Check|Bank Transfer']
|
||||||
|
'required=true' -> ['required', 'true']
|
||||||
|
*/
|
||||||
const splitPlaceholderKeyValue = (value: string): [string, string] | null => {
|
const splitPlaceholderKeyValue = (value: string): [string, string] | null => {
|
||||||
const [key, ...valueParts] = splitPlaceholderToken(value, '=');
|
const [key, ...valueParts] = splitPlaceholderToken(value, '=');
|
||||||
|
|
||||||
@@ -97,19 +121,33 @@ const normalizePlaceholderSelectionValue = (value: string): string => {
|
|||||||
return unescapePlaceholderValue(value).replace(/\s+/g, ' ').trim();
|
return unescapePlaceholderValue(value).replace(/\s+/g, ' ').trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Split an options string into individual choices.
|
||||||
|
Splits on unescaped '|', then unescapes, trims, and drops empty entries.
|
||||||
|
|
||||||
|
E.g.
|
||||||
|
'Card/Check|Bank Transfer' -> ['Card/Check', 'Bank Transfer']
|
||||||
|
'Card\\|Check|Bank Transfer' -> ['Card|Check', 'Bank Transfer']
|
||||||
|
*/
|
||||||
const parsePlaceholderOptions = (value: string): string[] => {
|
const parsePlaceholderOptions = (value: string): string[] => {
|
||||||
return splitPlaceholderToken(value, '|')
|
return splitPlaceholderToken(value, '|')
|
||||||
.map((option) => normalizePlaceholderSelectionValue(option))
|
.map((option) => normalizePlaceholderSelectionValue(option))
|
||||||
.filter((option) => option.length > 0);
|
.filter((option) => option.length > 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Split a placeholder string into top-level parts (field type, recipient, metadata).
|
||||||
|
Splits on unescaped commas, then trims whitespace.
|
||||||
|
|
||||||
|
E.g.
|
||||||
|
'SIGNATURE, r1, required=true'
|
||||||
|
-> ['SIGNATURE', 'r1', 'required=true']
|
||||||
|
*/
|
||||||
export const parsePlaceholderData = (value: string): string[] => {
|
export const parsePlaceholderData = (value: string): string[] => {
|
||||||
return splitPlaceholderToken(value, ',').map((token) => token.trim());
|
return splitPlaceholderToken(value, ',').map((token) => token.trim());
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseRawFieldMetaFromPlaceholder = (
|
export const parseRawFieldMetaFromPlaceholder = (fieldMetaData: string[]): Record<string, string> => {
|
||||||
fieldMetaData: string[],
|
|
||||||
): Record<string, string> => {
|
|
||||||
const rawFieldMeta: Record<string, string> = {};
|
const rawFieldMeta: Record<string, string> = {};
|
||||||
|
|
||||||
for (const fieldMeta of fieldMetaData) {
|
for (const fieldMeta of fieldMetaData) {
|
||||||
@@ -199,10 +237,7 @@ const parseSelectionFieldOptions = (
|
|||||||
return parsedOptions;
|
return parsedOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyRadioFieldOptions = (
|
const applyRadioFieldOptions = (parsedFieldMeta: Record<string, unknown>, rawFieldMeta: Record<string, string>) => {
|
||||||
parsedFieldMeta: Record<string, unknown>,
|
|
||||||
rawFieldMeta: Record<string, string>,
|
|
||||||
) => {
|
|
||||||
const options = parseSelectionFieldOptions(rawFieldMeta, FieldType.RADIO);
|
const options = parseSelectionFieldOptions(rawFieldMeta, FieldType.RADIO);
|
||||||
const defaultValue = getDefaultFieldMetaValue(rawFieldMeta);
|
const defaultValue = getDefaultFieldMetaValue(rawFieldMeta);
|
||||||
|
|
||||||
@@ -216,9 +251,7 @@ const applyRadioFieldOptions = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedOptionIndex = defaultValue
|
const selectedOptionIndex = defaultValue ? options.findIndex((option) => option === defaultValue) : -1;
|
||||||
? options.findIndex((option) => option === defaultValue)
|
|
||||||
: -1;
|
|
||||||
|
|
||||||
if (defaultValue && selectedOptionIndex === -1) {
|
if (defaultValue && selectedOptionIndex === -1) {
|
||||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||||
@@ -233,10 +266,7 @@ const applyRadioFieldOptions = (
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyCheckboxFieldOptions = (
|
const applyCheckboxFieldOptions = (parsedFieldMeta: Record<string, unknown>, rawFieldMeta: Record<string, string>) => {
|
||||||
parsedFieldMeta: Record<string, unknown>,
|
|
||||||
rawFieldMeta: Record<string, string>,
|
|
||||||
) => {
|
|
||||||
const options = parseSelectionFieldOptions(rawFieldMeta, FieldType.CHECKBOX);
|
const options = parseSelectionFieldOptions(rawFieldMeta, FieldType.CHECKBOX);
|
||||||
const checkedValues = getCheckedFieldMetaValues(rawFieldMeta);
|
const checkedValues = getCheckedFieldMetaValues(rawFieldMeta);
|
||||||
|
|
||||||
@@ -250,18 +280,15 @@ const applyCheckboxFieldOptions = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const unmatchedCheckedValues = checkedValues.filter(
|
const unmatchedCheckedValues = checkedValues.filter((checkedValue) => !options.includes(checkedValue));
|
||||||
(checkedValue) => !options.includes(checkedValue),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (unmatchedCheckedValues.length > 0) {
|
if (unmatchedCheckedValues.length > 0) {
|
||||||
const unmatchedCheckedValue = unmatchedCheckedValues[0];
|
const unmatchedCheckedValue = unmatchedCheckedValues[0];
|
||||||
|
|
||||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||||
message: [
|
message: [`Checkbox placeholder checked value "${unmatchedCheckedValue}"`, 'must match one of the options'].join(
|
||||||
`Checkbox placeholder checked value "${unmatchedCheckedValue}"`,
|
' ',
|
||||||
'must match one of the options',
|
),
|
||||||
].join(' '),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,10 +299,7 @@ const applyCheckboxFieldOptions = (
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyDropdownFieldOptions = (
|
const applyDropdownFieldOptions = (parsedFieldMeta: Record<string, unknown>, rawFieldMeta: Record<string, string>) => {
|
||||||
parsedFieldMeta: Record<string, unknown>,
|
|
||||||
rawFieldMeta: Record<string, string>,
|
|
||||||
) => {
|
|
||||||
const options = parseSelectionFieldOptions(rawFieldMeta, FieldType.DROPDOWN);
|
const options = parseSelectionFieldOptions(rawFieldMeta, FieldType.DROPDOWN);
|
||||||
const defaultValue = getDefaultFieldMetaValue(rawFieldMeta);
|
const defaultValue = getDefaultFieldMetaValue(rawFieldMeta);
|
||||||
|
|
||||||
@@ -304,20 +328,12 @@ const applyDropdownFieldOptions = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSelectionFieldType = (fieldType: FieldType): boolean => {
|
|
||||||
return (
|
|
||||||
fieldType === FieldType.CHECKBOX ||
|
|
||||||
fieldType === FieldType.RADIO ||
|
|
||||||
fieldType === FieldType.DROPDOWN
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const shouldSkipGenericFieldMetaParsing = (property: string, fieldType: FieldType): boolean => {
|
const shouldSkipGenericFieldMetaParsing = (property: string, fieldType: FieldType): boolean => {
|
||||||
if (property === 'options' || property === 'default' || property === 'selected') {
|
if (property === 'options' || property === 'default' || property === 'selected') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSelectionFieldType(fieldType)) {
|
if (fieldType !== FieldType.CHECKBOX && fieldType !== FieldType.RADIO && fieldType !== FieldType.DROPDOWN) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user