Fix workflow dropdown hotkey scope (#11518)

The hokey scope was set on focus of the outermost container of the field
inputs. When clicking on a dropdown inside this container, the hokey
scope was set first by the dropdown and then by the focused container.
This led to the previous hotkey scope being overwritten.
The fix consists in moving the hokey scope setter on focus to the inner
container.

Before:


https://github.com/user-attachments/assets/12538c5b-43ab-4b76-a867-7eda6992b1ea


After:


https://github.com/user-attachments/assets/002fedba-010c-41e9-bec6-d5d80cb2dcc5
This commit is contained in:
Raphaël Bosi
2025-04-11 11:07:36 +02:00
committed by GitHub
parent d34ec7b253
commit 637a7f0e64
14 changed files with 133 additions and 90 deletions

View File

@ -1,5 +1,5 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { VariableChipStandalone } from '@/object-record/record-field/form-types/components/VariableChipStandalone';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
@ -84,7 +84,7 @@ export const FormBooleanFieldInput = ({
{label ? <InputLabel>{label}</InputLabel> : null}
<FormFieldInputRowContainer>
<FormFieldInputInputContainer
<FormFieldInputInnerContainer
hasRightElement={isDefined(VariablePicker) && !readonly}
>
{draftValue.type === 'static' ? (
@ -101,7 +101,7 @@ export const FormBooleanFieldInput = ({
onRemove={readonly ? undefined : handleUnlinkVariable}
/>
)}
</FormFieldInputInputContainer>
</FormFieldInputInnerContainer>
{VariablePicker && !readonly ? (
<VariablePicker

View File

@ -1,5 +1,5 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { VariableChipStandalone } from '@/object-record/record-field/form-types/components/VariableChipStandalone';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
@ -32,7 +32,7 @@ import { isDefined } from 'twenty-shared/utils';
import { TEXT_INPUT_STYLE } from 'twenty-ui/theme';
import { Nullable } from 'twenty-ui/utilities';
const StyledInputContainer = styled(FormFieldInputInputContainer)`
const StyledInputContainer = styled(FormFieldInputInnerContainer)`
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr 0px;

View File

@ -1,7 +1,5 @@
import styled from '@emotion/styled';
import { ReactNode } from 'react';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { FormFieldInputHotKeyScope } from '@/object-record/record-field/form-types/constants/FormFieldInputHotKeyScope';
const StyledFormFieldInputContainer = styled.div`
display: flex;
@ -16,27 +14,8 @@ export const FormFieldInputContainer = ({
children: ReactNode;
testId?: string;
}) => {
const {
goBackToPreviousHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope();
const onFocus = () => {
setHotkeyScopeAndMemorizePreviousScope(
FormFieldInputHotKeyScope.FormFieldInput,
);
};
const onBlur = () => {
goBackToPreviousHotkeyScope();
};
return (
<StyledFormFieldInputContainer
data-testid={testId}
onFocus={onFocus}
onBlur={onBlur}
>
<StyledFormFieldInputContainer data-testid={testId}>
{children}
</StyledFormFieldInputContainer>
);

View File

@ -0,0 +1,90 @@
import { FormFieldInputHotKeyScope } from '@/object-record/record-field/form-types/constants/FormFieldInputHotKeyScope';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { forwardRef, HTMLAttributes, Ref } from 'react';
type FormFieldInputInnerContainerProps = {
hasRightElement: boolean;
multiline?: boolean;
readonly?: boolean;
preventSetHotkeyScope?: boolean;
};
const StyledFormFieldInputInnerContainer = styled.div<FormFieldInputInnerContainerProps>`
background-color: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
border-bottom-left-radius: ${({ theme }) => theme.border.radius.sm};
${({ multiline, hasRightElement, theme }) =>
multiline || !hasRightElement
? css`
border-right: auto;
border-bottom-right-radius: ${theme.border.radius.sm};
border-top-right-radius: ${theme.border.radius.sm};
`
: css`
border-right: none;
border-bottom-right-radius: none;
border-top-right-radius: none;
`}
box-sizing: border-box;
display: flex;
overflow: ${({ multiline }) => (multiline ? 'auto' : 'hidden')};
width: 100%;
`;
export const FormFieldInputInnerContainer = forwardRef(
(
{
className,
children,
onFocus,
onBlur,
hasRightElement,
multiline,
readonly,
preventSetHotkeyScope = false,
}: HTMLAttributes<HTMLDivElement> & FormFieldInputInnerContainerProps,
ref: Ref<HTMLDivElement>,
) => {
const {
goBackToPreviousHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope();
const handleFocus = (e: React.FocusEvent<HTMLDivElement>) => {
onFocus?.(e);
if (!preventSetHotkeyScope) {
setHotkeyScopeAndMemorizePreviousScope(
FormFieldInputHotKeyScope.FormFieldInput,
);
}
};
const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
onBlur?.(e);
if (!preventSetHotkeyScope) {
goBackToPreviousHotkeyScope();
}
};
return (
<StyledFormFieldInputInnerContainer
ref={ref}
className={className}
hasRightElement={hasRightElement}
multiline={multiline}
readonly={readonly}
onFocus={handleFocus}
onBlur={handleBlur}
>
{children}
</StyledFormFieldInputInnerContainer>
);
},
);

View File

@ -1,33 +0,0 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
const StyledFormFieldInputInputContainer = styled.div<{
hasRightElement: boolean;
multiline?: boolean;
readonly?: boolean;
}>`
background-color: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
border-bottom-left-radius: ${({ theme }) => theme.border.radius.sm};
${({ multiline, hasRightElement, theme }) =>
multiline || !hasRightElement
? css`
border-right: auto;
border-bottom-right-radius: ${theme.border.radius.sm};
border-top-right-radius: ${theme.border.radius.sm};
`
: css`
border-right: none;
border-bottom-right-radius: none;
border-top-right-radius: none;
`}
box-sizing: border-box;
display: flex;
overflow: ${({ multiline }) => (multiline ? 'auto' : 'hidden')};
width: 100%;
`;
export const FormFieldInputInputContainer = StyledFormFieldInputInputContainer;

View File

@ -1,7 +1,7 @@
import styled from '@emotion/styled';
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { VariableChipStandalone } from '@/object-record/record-field/form-types/components/VariableChipStandalone';
import { FormMultiSelectFieldInputHotKeyScope } from '@/object-record/record-field/form-types/constants/FormMultiSelectFieldInputHotKeyScope';
@ -17,9 +17,9 @@ import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariabl
import { useTheme } from '@emotion/react';
import { useId, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { VisibilityHidden } from 'twenty-ui/accessibility';
import { IconChevronDown } from 'twenty-ui/display';
import { SelectOption } from 'twenty-ui/input';
import { VisibilityHidden } from 'twenty-ui/accessibility';
type FormMultiSelectFieldInputProps = {
label?: string;
@ -185,7 +185,7 @@ export const FormMultiSelectFieldInput = ({
{label ? <InputLabel>{label}</InputLabel> : null}
<FormFieldInputRowContainer>
<FormFieldInputInputContainer
<FormFieldInputInnerContainer
hasRightElement={isDefined(VariablePicker) && !readonly}
>
{draftValue.type === 'static' ? (
@ -231,7 +231,7 @@ export const FormMultiSelectFieldInput = ({
onRemove={readonly ? undefined : handleUnlinkVariable}
/>
)}
</FormFieldInputInputContainer>
</FormFieldInputInnerContainer>
<StyledSelectInputContainer>
{draftValue.type === 'static' &&
draftValue.editingMode === 'edit' && (

View File

@ -1,5 +1,5 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { VariableChipStandalone } from '@/object-record/record-field/form-types/components/VariableChipStandalone';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
@ -108,7 +108,7 @@ export const FormNumberFieldInput = ({
{label ? <InputLabel htmlFor={inputId}>{label}</InputLabel> : null}
<FormFieldInputRowContainer>
<FormFieldInputInputContainer
<FormFieldInputInnerContainer
hasRightElement={isDefined(VariablePicker) && !readonly}
onBlur={onBlur}
>
@ -128,7 +128,7 @@ export const FormNumberFieldInput = ({
onRemove={readonly ? undefined : handleUnlinkVariable}
/>
)}
</FormFieldInputInputContainer>
</FormFieldInputInnerContainer>
{VariablePicker && !readonly ? (
<VariablePicker

View File

@ -1,14 +1,14 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { TextVariableEditor } from '@/object-record/record-field/form-types/components/TextVariableEditor';
import { useTextVariableEditor } from '@/object-record/record-field/form-types/hooks/useTextVariableEditor';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { InputErrorHelper } from '@/ui/input/components/InputErrorHelper';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { useId } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
import { InputErrorHelper } from '@/ui/input/components/InputErrorHelper';
type FormRawJsonFieldInputProps = {
label?: string;
@ -70,13 +70,13 @@ export const FormRawJsonFieldInput = ({
{label ? <InputLabel>{label}</InputLabel> : null}
<FormFieldInputRowContainer multiline>
<FormFieldInputInputContainer
<FormFieldInputInnerContainer
hasRightElement={isDefined(VariablePicker) && !readonly}
multiline
onBlur={onBlur}
>
<TextVariableEditor editor={editor} multiline readonly={readonly} />
</FormFieldInputInputContainer>
</FormFieldInputInnerContainer>
{VariablePicker && !readonly && (
<VariablePicker

View File

@ -1,5 +1,5 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { VariableChipStandalone } from '@/object-record/record-field/form-types/components/VariableChipStandalone';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
@ -134,14 +134,14 @@ export const FormSelectFieldInput = ({
disabled={readonly}
/>
) : (
<FormFieldInputInputContainer
<FormFieldInputInnerContainer
hasRightElement={isDefined(VariablePicker) && !readonly}
>
<VariableChipStandalone
rawVariableName={draftValue.value}
onRemove={readonly ? undefined : handleUnlinkVariable}
/>
</FormFieldInputInputContainer>
</FormFieldInputInnerContainer>
)}
{isDefined(VariablePicker) && !readonly && (

View File

@ -1,6 +1,6 @@
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { FormSingleRecordFieldChip } from '@/object-record/record-field/form-types/components/FormSingleRecordFieldChip';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
@ -20,7 +20,7 @@ import { isDefined, isValidUuid } from 'twenty-shared/utils';
import { IconChevronDown, IconForbid } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
const StyledFormSelectContainer = styled(FormFieldInputInputContainer)`
const StyledFormSelectContainer = styled(FormFieldInputInnerContainer)`
justify-content: space-between;
align-items: center;
padding-right: ${({ theme }) => theme.spacing(1)};
@ -132,6 +132,7 @@ export const FormSingleRecordPicker = ({
<FormFieldInputRowContainer>
<StyledFormSelectContainer
hasRightElement={isDefined(VariablePicker) && !disabled}
preventSetHotkeyScope={true}
>
<FormSingleRecordFieldChip
draftValue={draftValue}

View File

@ -1,5 +1,5 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { TextVariableEditor } from '@/object-record/record-field/form-types/components/TextVariableEditor';
import { useTextVariableEditor } from '@/object-record/record-field/form-types/hooks/useTextVariableEditor';
@ -70,7 +70,7 @@ export const FormTextFieldInput = ({
{label ? <InputLabel>{label}</InputLabel> : null}
<FormFieldInputRowContainer multiline={multiline}>
<FormFieldInputInputContainer
<FormFieldInputInnerContainer
hasRightElement={isDefined(VariablePicker) && !readonly}
multiline={multiline}
onBlur={onBlur}
@ -80,7 +80,7 @@ export const FormTextFieldInput = ({
multiline={multiline}
readonly={readonly}
/>
</FormFieldInputInputContainer>
</FormFieldInputInnerContainer>
{VariablePicker && !readonly ? (
<VariablePicker

View File

@ -1,5 +1,5 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { VariableChipStandalone } from '@/object-record/record-field/form-types/components/VariableChipStandalone';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
@ -94,7 +94,7 @@ export const FormUuidFieldInput = ({
{label ? <InputLabel htmlFor={inputId}>{label}</InputLabel> : null}
<FormFieldInputRowContainer>
<FormFieldInputInputContainer
<FormFieldInputInnerContainer
hasRightElement={isDefined(VariablePicker) && !readonly}
>
{draftValue.type === 'static' ? (
@ -113,7 +113,7 @@ export const FormUuidFieldInput = ({
onRemove={readonly ? undefined : handleUnlinkVariable}
/>
)}
</FormFieldInputInputContainer>
</FormFieldInputInnerContainer>
{VariablePicker && !readonly ? (
<VariablePicker

View File

@ -1,5 +1,5 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { WorkflowFormAction } from '@/workflow/types/Workflow';
@ -200,7 +200,7 @@ export const WorkflowEditActionFormBuilder = ({
onMouseLeave={() => setHoveredField(null)}
>
<FormFieldInputRowContainer>
<FormFieldInputInputContainer
<FormFieldInputInnerContainer
hasRightElement={false}
onClick={() => {
handleFieldClick(field.id);
@ -220,7 +220,7 @@ export const WorkflowEditActionFormBuilder = ({
/>
)}
</StyledFieldContainer>
</FormFieldInputInputContainer>
</FormFieldInputInnerContainer>
</FormFieldInputRowContainer>
{!actionOptions.readonly &&
(isFieldSelected(field.id) || isFieldHovered(field.id)) && (
@ -263,7 +263,7 @@ export const WorkflowEditActionFormBuilder = ({
<StyledRowContainer>
<FormFieldInputContainer>
<FormFieldInputRowContainer>
<FormFieldInputInputContainer
<FormFieldInputInnerContainer
hasRightElement={false}
onClick={() => {
const { label, name } = getDefaultFormFieldSettings(
@ -296,7 +296,7 @@ export const WorkflowEditActionFormBuilder = ({
{t`Add Field`}
</StyledAddFieldButtonContentContainer>
</StyledFieldContainer>
</FormFieldInputInputContainer>
</FormFieldInputInnerContainer>
</FormFieldInputRowContainer>
</FormFieldInputContainer>
</StyledRowContainer>

View File

@ -1,6 +1,8 @@
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/StyledDropdownButtonContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { WorkflowVariablesDropdownFieldItems } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdownFieldItems';
import { WorkflowVariablesDropdownObjectItems } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdownObjectItems';
import { WorkflowVariablesDropdownWorkflowStepItems } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdownWorkflowStepItems';
@ -11,6 +13,7 @@ import { StepOutputSchema } from '@/workflow/workflow-variables/types/StepOutput
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { IconVariablePlus } from 'twenty-ui/display';
@ -43,7 +46,10 @@ export const WorkflowVariablesDropdown = ({
const theme = useTheme();
const dropdownId = `${SEARCH_VARIABLES_DROPDOWN_ID}-${inputId}`;
const { isDropdownOpen, closeDropdown } = useDropdown(dropdownId);
const isDropdownOpen = useRecoilValue(
extractComponentState(isDropdownOpenComponentState, dropdownId),
);
const { closeDropdown } = useDropdownV2();
const availableVariablesInWorkflowStep = useAvailableVariablesInWorkflowStep({
objectNameSingularToSelect,
});
@ -68,7 +74,7 @@ export const WorkflowVariablesDropdown = ({
const handleSubItemSelect = (subItem: string) => {
onVariableSelect(subItem);
setSelectedStep(initialStep);
closeDropdown();
closeDropdown(dropdownId);
};
const handleBack = () => {