From 9f9ee0bb58100a60261d683c68628c359af2e911 Mon Sep 17 00:00:00 2001 From: kattoczko <50657366+kattoczko@users.noreply.github.com> Date: Mon, 21 Aug 2023 21:52:59 +0100 Subject: [PATCH] feat: create clear button for ControlledCustomAutocomplete (#1222) Co-authored-by: Ali BARIN --- .../CustomOptions.tsx | 37 +++--- .../ControlledCustomAutocomplete/index.tsx | 117 ++++++++++++------ .../ControlledCustomAutocomplete/style.ts | 6 +- packages/web/src/components/Slate/Element.tsx | 2 +- packages/web/src/components/Slate/utils.ts | 76 ++++++++---- 5 files changed, 148 insertions(+), 90 deletions(-) diff --git a/packages/web/src/components/ControlledCustomAutocomplete/CustomOptions.tsx b/packages/web/src/components/ControlledCustomAutocomplete/CustomOptions.tsx index 4f76e2ae..e878a629 100644 --- a/packages/web/src/components/ControlledCustomAutocomplete/CustomOptions.tsx +++ b/packages/web/src/components/ControlledCustomAutocomplete/CustomOptions.tsx @@ -20,7 +20,7 @@ interface CustomOptionsProps { onTabChange: (tabIndex: 0 | 1) => void; label?: string; initialTabIndex?: 0 | 1; -}; +} const CustomOptions = (props: CustomOptionsProps) => { const { @@ -34,17 +34,23 @@ const CustomOptions = (props: CustomOptionsProps) => { label, initialTabIndex, } = props; - const [activeTabIndex, setActiveTabIndex] = React.useState(undefined); - React.useEffect(function applyInitialActiveTabIndex() { - setActiveTabIndex((currentActiveTabIndex) => { - if (currentActiveTabIndex === undefined) { - return initialTabIndex; - } + const [activeTabIndex, setActiveTabIndex] = React.useState< + number | undefined + >(undefined); - return currentActiveTabIndex; - }); - }, [initialTabIndex]); + React.useEffect( + function applyInitialActiveTabIndex() { + setActiveTabIndex((currentActiveTabIndex) => { + if (currentActiveTabIndex === undefined) { + return initialTabIndex; + } + + return currentActiveTabIndex; + }); + }, + [initialTabIndex] + ); return ( { - + - + ); }; - export default CustomOptions; diff --git a/packages/web/src/components/ControlledCustomAutocomplete/index.tsx b/packages/web/src/components/ControlledCustomAutocomplete/index.tsx index 6a5a75c8..42a352f9 100644 --- a/packages/web/src/components/ControlledCustomAutocomplete/index.tsx +++ b/packages/web/src/components/ControlledCustomAutocomplete/index.tsx @@ -1,15 +1,17 @@ import * as React from 'react'; import { useController, useFormContext } from 'react-hook-form'; +import { IconButton } from '@mui/material'; import FormHelperText from '@mui/material/FormHelperText'; import { AutocompleteProps } from '@mui/material/Autocomplete'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import ClearIcon from '@mui/icons-material/Clear'; import type { IFieldDropdownOption } from '@automatisch/types'; -import { FakeDropdownButton } from './style'; +import { ActionButtonsWrapper } from './style'; import ClickAwayListener from '@mui/base/ClickAwayListener'; import InputLabel from '@mui/material/InputLabel'; import { createEditor } from 'slate'; -import { Editable, ReactEditor,} from 'slate-react'; +import { Editable, ReactEditor } from 'slate-react'; import Slate from 'components/Slate'; import Element from 'components/Slate/Element'; @@ -23,7 +25,11 @@ import { overrideEditorValue, focusEditor, } from 'components/Slate/utils'; -import { FakeInput, InputLabelWrapper, ChildrenWrapper, } from 'components/PowerInput/style'; +import { + FakeInput, + InputLabelWrapper, + ChildrenWrapper, +} from 'components/PowerInput/style'; import { VariableElement } from 'components/Slate/types'; import CustomOptions from './CustomOptions'; import { processStepWithExecutions } from 'components/PowerInput/data'; @@ -75,9 +81,11 @@ function ControlledCustomAutocomplete( onChange: controllerOnChange, onBlur: controllerOnBlur, } = field; - const [, forceUpdate] = React.useReducer(x => x + 1, 0); + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); const [isInitialValueSet, setInitialValue] = React.useState(false); - const [isSingleChoice, setSingleChoice] = React.useState(undefined); + const [isSingleChoice, setSingleChoice] = React.useState( + undefined + ); const priorStepsWithExecutions = React.useContext(StepExecutionsContext); const editorRef = React.useRef(null); const renderElement = React.useCallback( @@ -104,12 +112,12 @@ function ControlledCustomAutocomplete( const promoteValue = () => { const serializedValue = serialize(editor.children); controllerOnChange(serializedValue); - } + }; const resizeObserver = React.useMemo(function syncCustomOptionsPosition() { return new ResizeObserver(() => { forceUpdate(); - }) + }); }, []); React.useEffect(() => { @@ -121,24 +129,37 @@ function ControlledCustomAutocomplete( } }, dependsOnValues); - React.useEffect(function updateInitialValue() { - const hasOptions = options.length; - const isOptionsLoaded = loading === false; - if (!isInitialValueSet && hasOptions && isOptionsLoaded) { - setInitialValue(true); + React.useEffect( + function updateInitialValue() { + const hasOptions = options.length; + const isOptionsLoaded = loading === false; + if (!isInitialValueSet && hasOptions && isOptionsLoaded) { + setInitialValue(true); - const option: IFieldDropdownOption | undefined = options.find((option) => option.value === value); + const option: IFieldDropdownOption | undefined = options.find( + (option) => option.value === value + ); - if (option) { - overrideEditorValue(editor, { option, focus: false }); - setSingleChoice(true); - } else if (value) { - setSingleChoice(false); + if (option) { + overrideEditorValue(editor, { option, focus: false }); + setSingleChoice(true); + } else if (value) { + setSingleChoice(false); + } } - } - }, [isInitialValueSet, options, loading]); + }, + [isInitialValueSet, options, loading] + ); - const hideSuggestionsOnShift = (event: React.KeyboardEvent) => { + React.useEffect(() => { + if (!showVariableSuggestions && value !== serialize(editor.children)) { + promoteValue(); + } + }, [showVariableSuggestions]); + + const hideSuggestionsOnShift = ( + event: React.KeyboardEvent + ) => { if (event.code === 'Tab') { setShowVariableSuggestions(false); } @@ -170,21 +191,26 @@ function ControlledCustomAutocomplete( (event: React.MouseEvent, option: IFieldDropdownOption) => { event.stopPropagation(); overrideEditorValue(editor, { option, focus: false }); - setShowVariableSuggestions(false); - - promoteValue(); + setSingleChoice(true); }, [stepsWithVariables] ); + const handleClearButtonClick = (event: React.MouseEvent) => { + event.stopPropagation(); + resetEditor(editor); + promoteValue(); + setSingleChoice(undefined); + }; + const reset = (tabIndex: 0 | 1) => { const isOptions = tabIndex === 0; setSingleChoice(isOptions); resetEditor(editor, { focus: true }); - } + }; return ( { - promoteValue(); - - setShowVariableSuggestions(false); - }} + onClickAway={() => setShowVariableSuggestions(false)} > {/* ref-able single child for ClickAwayListener */} @@ -232,14 +254,27 @@ function ControlledCustomAutocomplete( }} /> - - - + + {isSingleChoice && serialize(editor.children) && ( + + + + )} + + + + {/* ghost placer for the variables popover */}
theme.spacing(1)}; + right: 0; top: 50%; transform: translateY(-50%); `; diff --git a/packages/web/src/components/Slate/Element.tsx b/packages/web/src/components/Slate/Element.tsx index 5139a5a8..ede69372 100644 --- a/packages/web/src/components/Slate/Element.tsx +++ b/packages/web/src/components/Slate/Element.tsx @@ -9,4 +9,4 @@ export default function Element(props: any) { default: return

{children}

; } -}; +} diff --git a/packages/web/src/components/Slate/utils.ts b/packages/web/src/components/Slate/utils.ts index 58a11d7b..d6f0d7e9 100644 --- a/packages/web/src/components/Slate/utils.ts +++ b/packages/web/src/components/Slate/utils.ts @@ -3,7 +3,13 @@ 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'; +import type { + CustomEditor, + CustomElement, + CustomText, + ParagraphElement, + VariableElement, +} from './types'; type StepWithVariables = { id: string; @@ -13,7 +19,7 @@ type StepWithVariables = { sampleValue: string; value: string; }[]; -} +}; type StepsWithVariables = StepWithVariables[]; @@ -26,10 +32,7 @@ function isCustomText(value: any): value is CustomText { return false; } -function getStepPosition( - id: string, - stepsWithVariables: StepsWithVariables -) { +function getStepPosition(id: string, stepsWithVariables: StepsWithVariables) { const stepIndex = stepsWithVariables.findIndex((stepWithVariables) => { return stepWithVariables.id === id; }); @@ -48,29 +51,36 @@ function getVariableStepId(variable: string) { return stepId; } -function getVariableSampleValue(variable: string, stepsWithVariables: StepsWithVariables) { +function getVariableSampleValue( + variable: string, + stepsWithVariables: StepsWithVariables +) { const variableStepId = getVariableStepId(variable); - const stepWithVariables = stepsWithVariables.find(({ id }: { id: string }) => id === variableStepId); + 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); + const variableData = stepWithVariables.output.find( + ({ value }) => variableName === value + ); if (!variableData) return null; return variableData.sampleValue; } -function getVariableDetails(variable: string, stepsWithVariables: StepsWithVariables) { +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}.` - ); + const label = variableName.replace(`step.${stepId}.`, `step${stepPosition}.`); return { sampleValue, @@ -114,7 +124,10 @@ export const deserialize = ( type: 'paragraph', children: nodes.map((node) => { if (node.match(variableRegExp)) { - const variableDetails = getVariableDetails(node, stepsWithVariables); + const variableDetails = getVariableDetails( + node, + stepsWithVariables + ); return { type: 'variable', @@ -199,7 +212,10 @@ export const insertVariable = ( variableData: Record, stepsWithVariables: StepsWithVariables ) => { - const variableDetails = getVariableDetails(`{{${variableData.value}}}`, stepsWithVariables); + const variableDetails = getVariableDetails( + `{{${variableData.value}}}`, + stepsWithVariables + ); const variable: VariableElement = { type: 'variable', @@ -217,15 +233,18 @@ export const insertVariable = ( export const focusEditor = (editor: CustomEditor) => { ReactEditor.focus(editor); editor.move(); -} +}; -export const resetEditor = (editor: CustomEditor, options?: { focus: boolean }) => { +export const resetEditor = ( + editor: CustomEditor, + options?: { focus: boolean } +) => { const focus = options?.focus || false; editor.removeNodes({ at: { anchor: editor.start([]), - focus: editor.end([]) + focus: editor.end([]), }, }); @@ -235,9 +254,12 @@ export const resetEditor = (editor: CustomEditor, options?: { focus: boolean }) if (focus) { focusEditor(editor); } -} +}; -export const overrideEditorValue = (editor: CustomEditor, options: { option: IFieldDropdownOption, focus: boolean }) => { +export const overrideEditorValue = ( + editor: CustomEditor, + options: { option: IFieldDropdownOption; focus: boolean } +) => { const { option, focus } = options; const variable: ParagraphElement = { @@ -245,8 +267,8 @@ export const overrideEditorValue = (editor: CustomEditor, options: { option: IFi children: [ { value: option.value as string, - text: option.label as string - } + text: option.label as string, + }, ], }; @@ -254,7 +276,7 @@ export const overrideEditorValue = (editor: CustomEditor, options: { option: IFi editor.removeNodes({ at: { anchor: editor.start([]), - focus: editor.end([]) + focus: editor.end([]), }, }); @@ -270,9 +292,9 @@ export const createTextNode = (text: string): ParagraphElement => ({ type: 'paragraph', children: [ { - text - } - ] + text, + }, + ], }); export const customizeEditor = (editor: CustomEditor): CustomEditor => {