feat: create clear button for ControlledCustomAutocomplete (#1222)
Co-authored-by: Ali BARIN <ali.barin53@gmail.com>
This commit is contained in:
@@ -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<number | undefined>(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 (
|
||||
<Popper
|
||||
@@ -75,22 +81,15 @@ const CustomOptions = (props: CustomOptionsProps) => {
|
||||
</Tabs>
|
||||
|
||||
<TabPanel value={activeTabIndex ?? 0} index={0}>
|
||||
<Options
|
||||
data={options}
|
||||
onOptionClick={onOptionClick}
|
||||
/>
|
||||
<Options data={options} onOptionClick={onOptionClick} />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={activeTabIndex ?? 0} index={1}>
|
||||
<Suggestions
|
||||
data={data}
|
||||
onSuggestionClick={onSuggestionClick}
|
||||
/>
|
||||
<Suggestions data={data} onSuggestionClick={onSuggestionClick} />
|
||||
</TabPanel>
|
||||
</Paper>
|
||||
</Popper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default CustomOptions;
|
||||
|
@@ -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<boolean | undefined>(undefined);
|
||||
const [isSingleChoice, setSingleChoice] = React.useState<boolean | undefined>(
|
||||
undefined
|
||||
);
|
||||
const priorStepsWithExecutions = React.useContext(StepExecutionsContext);
|
||||
const editorRef = React.useRef<HTMLDivElement | null>(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<HTMLInputElement>) => {
|
||||
React.useEffect(() => {
|
||||
if (!showVariableSuggestions && value !== serialize(editor.children)) {
|
||||
promoteValue();
|
||||
}
|
||||
}, [showVariableSuggestions]);
|
||||
|
||||
const hideSuggestionsOnShift = (
|
||||
event: React.KeyboardEvent<HTMLInputElement>
|
||||
) => {
|
||||
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 (
|
||||
<Slate
|
||||
@@ -193,11 +219,7 @@ function ControlledCustomAutocomplete(
|
||||
>
|
||||
<ClickAwayListener
|
||||
mouseEvent="onMouseDown"
|
||||
onClickAway={() => {
|
||||
promoteValue();
|
||||
|
||||
setShowVariableSuggestions(false);
|
||||
}}
|
||||
onClickAway={() => setShowVariableSuggestions(false)}
|
||||
>
|
||||
{/* ref-able single child for ClickAwayListener */}
|
||||
<ChildrenWrapper style={{ width: '100%' }} data-test="power-input">
|
||||
@@ -232,14 +254,27 @@ function ControlledCustomAutocomplete(
|
||||
}}
|
||||
/>
|
||||
|
||||
<FakeDropdownButton
|
||||
disabled={disabled}
|
||||
edge="end"
|
||||
size="small"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ArrowDropDownIcon />
|
||||
</FakeDropdownButton>
|
||||
<ActionButtonsWrapper direction="row" mr={1.5}>
|
||||
{isSingleChoice && serialize(editor.children) && (
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
edge="end"
|
||||
size="small"
|
||||
tabIndex={-1}
|
||||
onClick={handleClearButtonClick}
|
||||
>
|
||||
<ClearIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
edge="end"
|
||||
size="small"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ArrowDropDownIcon />
|
||||
</IconButton>
|
||||
</ActionButtonsWrapper>
|
||||
</FakeInput>
|
||||
{/* ghost placer for the variables popover */}
|
||||
<div
|
||||
@@ -247,14 +282,16 @@ function ControlledCustomAutocomplete(
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 16,
|
||||
left: 16
|
||||
left: 16,
|
||||
}}
|
||||
/>
|
||||
|
||||
<CustomOptions
|
||||
label={label}
|
||||
open={showVariableSuggestions}
|
||||
initialTabIndex={isSingleChoice === undefined ? undefined : (isSingleChoice ? 0 : 1)}
|
||||
initialTabIndex={
|
||||
isSingleChoice === undefined ? undefined : isSingleChoice ? 0 : 1
|
||||
}
|
||||
anchorEl={editorRef.current}
|
||||
data={stepsWithVariables}
|
||||
options={options}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import MuiIconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import MuiTabs from '@mui/material/Tabs';
|
||||
|
||||
export const FakeDropdownButton = styled(MuiIconButton)`
|
||||
export const ActionButtonsWrapper = styled(Stack)`
|
||||
position: absolute;
|
||||
right: ${({ theme }) => theme.spacing(1)};
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
`;
|
||||
|
@@ -9,4 +9,4 @@ export default function Element(props: any) {
|
||||
default:
|
||||
return <p {...attributes}>{children}</p>;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -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<string, unknown>,
|
||||
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 => {
|
||||
|
Reference in New Issue
Block a user