feat: introduce CustomAutocomplete with variables
This commit is contained in:
280
packages/web/src/components/Slate/utils.ts
Normal file
280
packages/web/src/components/Slate/utils.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import { Text, Descendant } from 'slate';
|
||||
import { withHistory } from 'slate-history';
|
||||
import { ReactEditor, withReact } from 'slate-react';
|
||||
import { IFieldDropdownOption } from '@automatisch/types';
|
||||
|
||||
import type { CustomEditor, CustomElement, CustomText, ParagraphElement, VariableElement } from './types';
|
||||
|
||||
type StepWithVariables = {
|
||||
id: string;
|
||||
name: string;
|
||||
output: {
|
||||
label: string;
|
||||
sampleValue: string;
|
||||
value: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
type StepsWithVariables = StepWithVariables[];
|
||||
|
||||
function isCustomText(value: any): value is CustomText {
|
||||
const isText = Text.isText(value);
|
||||
const hasValueProperty = 'value' in value;
|
||||
|
||||
if (isText && hasValueProperty) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getStepPosition(
|
||||
id: string,
|
||||
stepsWithVariables: StepsWithVariables
|
||||
) {
|
||||
const stepIndex = stepsWithVariables.findIndex((stepWithVariables) => {
|
||||
return stepWithVariables.id === id;
|
||||
});
|
||||
|
||||
return stepIndex + 1;
|
||||
}
|
||||
|
||||
function getVariableName(variable: string) {
|
||||
return variable.replace(/{{|}}/g, '');
|
||||
}
|
||||
|
||||
function getVariableStepId(variable: string) {
|
||||
const nameWithoutCurlies = getVariableName(variable);
|
||||
const stepId = nameWithoutCurlies.match(stepIdRegExp)?.[1] || '';
|
||||
|
||||
return stepId;
|
||||
}
|
||||
|
||||
function getVariableSampleValue(variable: string, stepsWithVariables: StepsWithVariables) {
|
||||
const variableStepId = getVariableStepId(variable);
|
||||
const stepWithVariables = stepsWithVariables.find(({ id }: { id: string }) => id === variableStepId);
|
||||
|
||||
if (!stepWithVariables) return null;
|
||||
|
||||
const variableName = getVariableName(variable);
|
||||
const variableData = stepWithVariables.output.find(({ value }) => variableName === value);
|
||||
|
||||
if (!variableData) return null;
|
||||
|
||||
return variableData.sampleValue;
|
||||
}
|
||||
|
||||
function getVariableDetails(variable: string, stepsWithVariables: StepsWithVariables) {
|
||||
const variableName = getVariableName(variable);
|
||||
const stepId = getVariableStepId(variableName);
|
||||
const stepPosition = getStepPosition(stepId, stepsWithVariables);
|
||||
const sampleValue = getVariableSampleValue(variable, stepsWithVariables);
|
||||
const label = variableName.replace(
|
||||
`step.${stepId}.`,
|
||||
`step${stepPosition}.`
|
||||
);
|
||||
|
||||
return {
|
||||
sampleValue,
|
||||
label,
|
||||
};
|
||||
}
|
||||
|
||||
const variableRegExp = /({{.*?}})/;
|
||||
const stepIdRegExp = /^step.([\da-zA-Z-]*)/;
|
||||
|
||||
export const deserialize = (
|
||||
value: string,
|
||||
options: readonly IFieldDropdownOption[],
|
||||
stepsWithVariables: StepsWithVariables
|
||||
): Descendant[] => {
|
||||
if (!value)
|
||||
return [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ text: '' }],
|
||||
},
|
||||
];
|
||||
|
||||
const selectedNativeOption = options.find((option) => value === option.value);
|
||||
|
||||
if (selectedNativeOption) {
|
||||
return [
|
||||
{
|
||||
type: 'paragraph',
|
||||
value: selectedNativeOption.value as string,
|
||||
children: [{ text: selectedNativeOption.label }],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return value.split('\n').map((line) => {
|
||||
const nodes = line.split(variableRegExp);
|
||||
|
||||
if (nodes.length > 1) {
|
||||
return {
|
||||
type: 'paragraph',
|
||||
children: nodes.map((node) => {
|
||||
if (node.match(variableRegExp)) {
|
||||
const variableDetails = getVariableDetails(node, stepsWithVariables);
|
||||
|
||||
return {
|
||||
type: 'variable',
|
||||
name: variableDetails.label,
|
||||
sampleValue: variableDetails.sampleValue,
|
||||
value: node,
|
||||
children: [{ text: '' }],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
text: node,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'paragraph',
|
||||
children: [{ text: line }],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const serialize = (value: Descendant[]): string => {
|
||||
const serializedNodes = value.map((node) => serializeNode(node));
|
||||
|
||||
const hasSingleNode = value.length === 1;
|
||||
|
||||
/**
|
||||
* return single serialize node alone so that we don't stringify.
|
||||
* booleans stay booleans, numbers stay number
|
||||
*/
|
||||
if (hasSingleNode) {
|
||||
return serializedNodes[0];
|
||||
}
|
||||
|
||||
const serializedValue = serializedNodes.join('\n');
|
||||
return serializedValue;
|
||||
};
|
||||
|
||||
const serializeNode = (node: CustomElement | Descendant): string => {
|
||||
if (isCustomText(node)) return node.value;
|
||||
|
||||
if (Text.isText(node)) {
|
||||
return node.text;
|
||||
}
|
||||
|
||||
if (node.type === 'variable') {
|
||||
return node.value as string;
|
||||
}
|
||||
|
||||
const hasSingleChild = node.children.length === 1;
|
||||
|
||||
/**
|
||||
* serialize alone so that we don't stringify.
|
||||
* booleans stay booleans, numbers stay number
|
||||
*/
|
||||
if (hasSingleChild) {
|
||||
return serializeNode(node.children[0]);
|
||||
}
|
||||
|
||||
return node.children.map((n) => serializeNode(n)).join('');
|
||||
};
|
||||
|
||||
export const withVariables = (editor: CustomEditor) => {
|
||||
const { isInline, isVoid } = editor;
|
||||
|
||||
editor.isInline = (element: CustomElement) => {
|
||||
return element.type === 'variable' ? true : isInline(element);
|
||||
};
|
||||
|
||||
editor.isVoid = (element: CustomElement) => {
|
||||
return element.type === 'variable' ? true : isVoid(element);
|
||||
};
|
||||
|
||||
return editor;
|
||||
};
|
||||
|
||||
export const insertVariable = (
|
||||
editor: CustomEditor,
|
||||
variableData: Record<string, unknown>,
|
||||
stepsWithVariables: StepsWithVariables
|
||||
) => {
|
||||
const variableDetails = getVariableDetails(`{{${variableData.value}}}`, stepsWithVariables);
|
||||
|
||||
const variable: VariableElement = {
|
||||
type: 'variable',
|
||||
name: variableDetails.label,
|
||||
sampleValue: variableDetails.sampleValue,
|
||||
value: `{{${variableData.value}}}`,
|
||||
children: [{ text: '' }],
|
||||
};
|
||||
|
||||
editor.insertNodes(variable, { select: false });
|
||||
|
||||
focusEditor(editor);
|
||||
};
|
||||
|
||||
export const focusEditor = (editor: CustomEditor) => {
|
||||
ReactEditor.focus(editor);
|
||||
editor.move();
|
||||
}
|
||||
|
||||
export const resetEditor = (editor: CustomEditor, options?: { focus: boolean }) => {
|
||||
const focus = options?.focus || false;
|
||||
|
||||
editor.removeNodes({
|
||||
at: {
|
||||
anchor: editor.start([]),
|
||||
focus: editor.end([])
|
||||
},
|
||||
});
|
||||
|
||||
// `editor.normalize({ force: true })` doesn't add an empty node in the editor
|
||||
editor.insertNode(createTextNode(''));
|
||||
|
||||
if (focus) {
|
||||
focusEditor(editor);
|
||||
}
|
||||
}
|
||||
|
||||
export const overrideEditorValue = (editor: CustomEditor, options: { option: IFieldDropdownOption, focus: boolean }) => {
|
||||
const { option, focus } = options;
|
||||
|
||||
const variable: ParagraphElement = {
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
value: option.value as string,
|
||||
text: option.label as string
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
editor.withoutNormalizing(() => {
|
||||
editor.removeNodes({
|
||||
at: {
|
||||
anchor: editor.start([]),
|
||||
focus: editor.end([])
|
||||
},
|
||||
});
|
||||
|
||||
editor.insertNode(variable);
|
||||
|
||||
if (focus) {
|
||||
focusEditor(editor);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const createTextNode = (text: string): ParagraphElement => ({
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export const customizeEditor = (editor: CustomEditor): CustomEditor => {
|
||||
return withVariables(withReact(withHistory(editor)));
|
||||
};
|
Reference in New Issue
Block a user