Replace hotkey scopes by focus stack (Part 4 - Inputs) (#12933)

# Replace hotkey scopes by focus stack (Part 4 - Inputs)

This PR is the 4th part of a refactoring aiming to deprecate the hotkey
scopes api in favor of the new focus stack api which is more robust.
Part 1: https://github.com/twentyhq/twenty/pull/12673
Part 2: https://github.com/twentyhq/twenty/pull/12798
Part 3: https://github.com/twentyhq/twenty/pull/12910

In this part, I refactored all inputs in the app so that each input has
a unique id which can be used to track the focused element.
This commit is contained in:
Raphaël Bosi
2025-07-07 15:42:12 +02:00
committed by GitHub
parent 0199d5f72a
commit c6e5bab4e9
140 changed files with 1178 additions and 1004 deletions

View File

@ -64,15 +64,15 @@ const TextInput: React.FC<TextInputProps> = ({
placeholder,
icon,
}) => {
const inputId = useId();
const instanceId = useId();
return (
<StyledContainer fullWidth={fullWidth}>
{label && <StyledLabel htmlFor={inputId}>{label}</StyledLabel>}
{label && <StyledLabel htmlFor={instanceId}>{label}</StyledLabel>}
<StyledInputContainer>
{icon && <StyledIcon>{icon}</StyledIcon>}
<StyledInput
id={inputId}
id={instanceId}
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}

View File

@ -10,7 +10,7 @@ import { FieldContext } from '@/object-record/record-field/contexts/FieldContext
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { Chip, ChipAccent, ChipSize, ChipVariant } from 'twenty-ui/components';
import { IconCalendarEvent } from 'twenty-ui/display';
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
@ -20,6 +20,8 @@ type CalendarEventDetailsProps = {
calendarEvent: CalendarEvent;
};
const INPUT_ID_PREFIX = 'calendar-event-details';
const StyledContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
align-items: flex-start;
@ -111,10 +113,14 @@ export const CalendarEventDetails = ({
>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputId(calendarEvent.id, fieldName),
instanceId: getRecordFieldInputInstanceId({
recordId: calendarEvent.id,
fieldName,
prefix: INPUT_ID_PREFIX,
}),
}}
>
<RecordInlineCell readonly />
<RecordInlineCell instanceIdPrefix={INPUT_ID_PREFIX} />
</RecordFieldComponentInstanceContext.Provider>
</FieldContext.Provider>
</StyledPropertyBox>

View File

@ -34,8 +34,7 @@ import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecor
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
import { isInlineCellInEditModeScopedState } from '@/object-record/record-inline-cell/states/isInlineCellInEditModeScopedState';
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
import { getRecordTitleCellId } from '@/object-record/record-title-cell/utils/getRecordTitleCellId';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
@ -364,11 +363,11 @@ export const ActivityRichTextEditor = ({
objectRecordId: activityId,
});
const recordTitleCellId = getRecordTitleCellId(
activityId,
labelIdentifierFieldMetadataItem?.id,
RecordTitleCellContainerType.ShowPage,
);
const recordTitleCellId = getRecordFieldInputInstanceId({
recordId: activityId,
fieldName: labelIdentifierFieldMetadataItem?.id,
prefix: 'activity-rich-text-editor',
});
const handleBlockEditorFocus = useRecoilCallback(
({ snapshot }) =>

View File

@ -165,6 +165,7 @@ export const AttachmentRow = ({
<AttachmentIcon attachmentType={attachment.type} />
{isEditing ? (
<TextInput
instanceId={`attachment-${attachment.id}-name`}
value={attachmentFileName}
onChange={handleOnChange}
onBlur={handleOnBlur}

View File

@ -1,8 +1,8 @@
import { Form } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { TextInput } from '@/ui/input/components/TextInput';
import { Controller, useFormContext } from 'react-hook-form';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { Form } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { Controller, useFormContext } from 'react-hook-form';
import { isDefined } from 'twenty-shared/utils';
const StyledFullWidthMotionDiv = styled(motion.div)`
@ -41,6 +41,7 @@ export const SignInUpEmailField = ({
}) => (
<StyledInputContainer>
<TextInput
instanceId="sign-in-up-email"
autoFocus
value={value}
placeholder="Email"

View File

@ -46,6 +46,7 @@ export const SignInUpPasswordField = ({
}) => (
<StyledInputContainer>
<TextInput
instanceId="sign-in-up-password"
autoFocus
value={value}
type="password"

View File

@ -40,6 +40,7 @@ export const ObjectFilterDropdownNumberInput = () => {
return (
<DropdownMenuItemsContainer>
<DropdownMenuInput
instanceId="object-filter-dropdown-number-input"
ref={handleInputRef}
value={objectFilterDropdownFilterValue}
autoFocus

View File

@ -40,6 +40,7 @@ export const ObjectFilterDropdownTextInput = () => {
return (
<DropdownMenuItemsContainer>
<DropdownMenuInput
instanceId="object-filter-dropdown-text-input"
ref={handleInputRef}
value={objectFilterDropdownFilterValue}
autoFocus

View File

@ -1,6 +1,7 @@
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardCardBodyContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBodyContainer';
import { StopPropagationContainer } from '@/object-record/record-board/record-board-card/components/StopPropagationContainer';
import { RECORD_BOARD_CARD_INPUT_ID_PREFIX } from '@/object-record/record-board/record-board-card/constants/RecordBoardCardInputIdPrefix';
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
import { RecordBoardFieldDefinition } from '@/object-record/record-board/types/RecordBoardFieldDefinition';
import {
@ -13,7 +14,7 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { useContext } from 'react';
export const RecordBoardCardBody = ({
@ -73,14 +74,16 @@ export const RecordBoardCardBody = ({
>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputId(
instanceId: getRecordFieldInputInstanceId({
recordId,
fieldDefinition.metadata.fieldName,
'record-board-card',
),
fieldName: fieldDefinition.metadata.fieldName,
prefix: RECORD_BOARD_CARD_INPUT_ID_PREFIX,
}),
}}
>
<RecordInlineCell />
<RecordInlineCell
instanceIdPrefix={RECORD_BOARD_CARD_INPUT_ID_PREFIX}
/>
</RecordFieldComponentInstanceContext.Provider>
</FieldContext.Provider>
</StopPropagationContainer>

View File

@ -0,0 +1 @@
export const RECORD_BOARD_CARD_INPUT_ID_PREFIX = 'record-board-card';

View File

@ -29,7 +29,7 @@ export const FormBooleanFieldInput = ({
readonly,
VariablePicker,
}: FormBooleanFieldInputProps) => {
const inputId = useId();
const instanceId = useId();
const [draftValue, setDraftValue] = useState<
| {
@ -105,7 +105,7 @@ export const FormBooleanFieldInput = ({
{VariablePicker && !readonly ? (
<VariablePicker
inputId={inputId}
instanceId={instanceId}
onVariableSelect={handleVariableTagInsert}
/>
) : null}

View File

@ -89,7 +89,7 @@ export const FormDateTimeFieldInput = ({
isDateTimeInput: !dateOnly,
});
const inputId = useId();
const instanceId = useId();
const [draftValue, setDraftValue] = useState<DraftValue>(
isStandaloneVariableString(defaultValue)
@ -340,7 +340,7 @@ export const FormDateTimeFieldInput = ({
{VariablePicker && !readonly ? (
<VariablePicker
inputId={inputId}
instanceId={instanceId}
onVariableSelect={handleVariableTagInsert}
/>
) : null}

View File

@ -82,7 +82,7 @@ export const FormMultiSelectFieldInput = ({
placeholder,
testId,
}: FormMultiSelectFieldInputProps) => {
const inputId = useId();
const instanceId = useId();
const theme = useTheme();
const hotkeyScope =
@ -268,7 +268,7 @@ export const FormMultiSelectFieldInput = ({
{VariablePicker && !readonly && (
<VariablePicker
inputId={inputId}
instanceId={instanceId}
onVariableSelect={handleVariableTagInsert}
/>
)}

View File

@ -45,7 +45,7 @@ export const FormNumberFieldInput = ({
readonly,
error: errorFromProps,
}: FormNumberFieldInputProps) => {
const inputId = useId();
const instanceId = useId();
const [errorMessage, setErrorMessage] = useState<string | undefined>(
undefined,
);
@ -117,7 +117,7 @@ export const FormNumberFieldInput = ({
return (
<FormFieldInputContainer>
{label ? <InputLabel htmlFor={inputId}>{label}</InputLabel> : null}
{label ? <InputLabel htmlFor={instanceId}>{label}</InputLabel> : null}
<FormFieldInputRowContainer>
<FormFieldInputInnerContainer
@ -126,7 +126,7 @@ export const FormNumberFieldInput = ({
>
{draftValue.type === 'static' ? (
<StyledInput
inputId={inputId}
instanceId={instanceId}
placeholder={
isDefined(placeholder) && !isEmpty(placeholder)
? placeholder
@ -148,7 +148,7 @@ export const FormNumberFieldInput = ({
{VariablePicker && !readonly ? (
<VariablePicker
inputId={inputId}
instanceId={instanceId}
onVariableSelect={handleVariableTagInsert}
/>
) : null}

View File

@ -31,7 +31,7 @@ export const FormRawJsonFieldInput = ({
readonly,
VariablePicker,
}: FormRawJsonFieldInputProps) => {
const inputId = useId();
const instanceId = useId();
const editor = useTextVariableEditor({
placeholder: placeholder ?? 'Enter a JSON object',
@ -80,7 +80,7 @@ export const FormRawJsonFieldInput = ({
{VariablePicker && !readonly && (
<VariablePicker
inputId={inputId}
instanceId={instanceId}
multiline
onVariableSelect={handleVariableTagInsert}
/>

View File

@ -6,16 +6,16 @@ import { VariablePickerComponent } from '@/object-record/record-field/form-types
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { Select } from '@/ui/input/components/Select';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import { useTheme } from '@emotion/react';
import { useId, useState } from 'react';
import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils';
import { SelectOption } from 'twenty-ui/input';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useTheme } from '@emotion/react';
import { IconCircleOff } from 'twenty-ui/display';
import { SelectOption } from 'twenty-ui/input';
type FormSelectFieldInputProps = {
label?: string;
@ -36,7 +36,7 @@ export const FormSelectFieldInput = ({
}: FormSelectFieldInputProps) => {
const theme = useTheme();
const inputId = useId();
const instanceId = useId();
const hotkeyScope = InlineCellHotkeyScope.InlineCell;
@ -135,7 +135,7 @@ export const FormSelectFieldInput = ({
<FormFieldInputRowContainer>
{draftValue.type === 'static' ? (
<Select
dropdownId={`${inputId}-select-display`}
dropdownId={`${instanceId}-select-display`}
options={options}
value={selectedOption?.value}
onChange={onSelect}
@ -160,7 +160,7 @@ export const FormSelectFieldInput = ({
{isDefined(VariablePicker) && !readonly && (
<VariablePicker
inputId={inputId}
instanceId={instanceId}
onVariableSelect={handleVariableTagInsert}
/>
)}

View File

@ -204,7 +204,7 @@ export const FormSingleRecordPicker = ({
)}
{isDefined(VariablePicker) && !disabled && (
<VariablePicker
inputId={variablesDropdownId}
instanceId={variablesDropdownId}
disabled={disabled}
onVariableSelect={handleVariableTagInsert}
objectNameSingularToSelect={objectNameSingular}

View File

@ -36,7 +36,7 @@ export const FormTextFieldInput = ({
readonly,
VariablePicker,
}: FormTextFieldInputProps) => {
const inputId = useId();
const instanceId = useId();
const editor = useTextVariableEditor({
placeholder: placeholder ?? 'Enter text',
@ -84,7 +84,7 @@ export const FormTextFieldInput = ({
{VariablePicker && !readonly ? (
<VariablePicker
inputId={inputId}
instanceId={instanceId}
multiline={multiline}
onVariableSelect={handleVariableTagInsert}
/>

View File

@ -31,7 +31,7 @@ export const FormUuidFieldInput = ({
readonly,
VariablePicker,
}: FormUuidFieldInputProps) => {
const inputId = useId();
const instanceId = useId();
const [draftValue, setDraftValue] = useState<
| {
@ -91,7 +91,7 @@ export const FormUuidFieldInput = ({
return (
<FormFieldInputContainer>
{label ? <InputLabel htmlFor={inputId}>{label}</InputLabel> : null}
{label ? <InputLabel htmlFor={instanceId}>{label}</InputLabel> : null}
<FormFieldInputRowContainer>
<FormFieldInputInnerContainer
@ -99,7 +99,7 @@ export const FormUuidFieldInput = ({
>
{draftValue.type === 'static' ? (
<StyledInput
inputId={inputId}
instanceId={instanceId}
placeholder={placeholder ?? 'Enter a UUID'}
value={draftValue.value}
copyButton={false}
@ -117,7 +117,7 @@ export const FormUuidFieldInput = ({
{VariablePicker && !readonly ? (
<VariablePicker
inputId={inputId}
instanceId={instanceId}
onVariableSelect={handleVariableTagInsert}
/>
) : null}

View File

@ -1,5 +1,5 @@
export type VariablePickerComponent = React.FC<{
inputId: string;
instanceId: string;
disabled?: boolean;
multiline?: boolean;
onVariableSelect: (variableName: string) => void;

View File

@ -15,11 +15,11 @@ import {
} from '@/object-record/record-field/types/FieldMetadata';
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
import { getFieldInputInstanceId } from '@/object-record/record-field/utils/getFieldInputInstanceId';
import { INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY } from '@/object-record/record-inline-cell/constants/InlineCellHotkeyScopeMemoizeKey';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
@ -41,9 +41,11 @@ export const useOpenFieldInputEditMode = () => {
({
fieldDefinition,
recordId,
prefix,
}: {
fieldDefinition: FieldDefinition<FieldMetadata>;
recordId: string;
prefix?: string;
}) => {
if (
isFieldRelationFromManyObjects(fieldDefinition) &&
@ -75,9 +77,10 @@ export const useOpenFieldInputEditMode = () => {
});
openActivityTargetCellEditMode({
recordPickerInstanceId: getFieldInputInstanceId({
recordPickerInstanceId: getRecordFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
prefix,
}),
activityTargetObjectRecords,
});
@ -87,7 +90,8 @@ export const useOpenFieldInputEditMode = () => {
if (isFieldRelationToOneObject(fieldDefinition)) {
openRelationToOneFieldInput({
fieldName: fieldDefinition.metadata.fieldName,
recordId: recordId,
recordId,
prefix,
});
return;
@ -103,22 +107,25 @@ export const useOpenFieldInputEditMode = () => {
fieldName: fieldDefinition.metadata.fieldName,
objectNameSingular:
fieldDefinition.metadata.relationObjectMetadataNameSingular,
recordId: recordId,
recordId,
prefix,
});
return;
}
}
pushFocusItemToFocusStack({
focusId: getFieldInputInstanceId({
focusId: getRecordFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
prefix,
}),
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: getFieldInputInstanceId({
instanceId: getRecordFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
prefix,
}),
},
hotkeyScope: {
@ -142,14 +149,17 @@ export const useOpenFieldInputEditMode = () => {
const closeFieldInput = ({
fieldDefinition,
recordId,
prefix,
}: {
fieldDefinition: FieldDefinition<FieldMetadata>;
recordId: string;
prefix?: string;
}) => {
removeFocusItemFromFocusStackById({
focusId: getFieldInputInstanceId({
focusId: getRecordFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
prefix,
}),
});
};

View File

@ -2,11 +2,13 @@ import { useAddressField } from '@/object-record/record-field/meta-types/hooks/u
import { FieldAddressDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
import { AddressInput } from '@/ui/field/input/components/AddressInput';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from '@/object-record/record-field/types/FieldInputEvent';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { usePersistField } from '../../../hooks/usePersistField';
export type AddressFieldInputProps = {
@ -70,8 +72,13 @@ export const AddressFieldInput = ({
setDraftValue(convertToAddress(newAddress));
};
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
return (
<AddressInput
instanceId={instanceId}
value={convertToAddress(draftValue)}
onClickOutside={handleClickOutside}
onEnter={handleEnter}

View File

@ -6,11 +6,13 @@ import { CurrencyInput } from '@/ui/field/input/components/CurrencyInput';
import { useCurrencyField } from '../../hooks/useCurrencyField';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from '@/object-record/record-field/types/FieldInputEvent';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
type CurrencyFieldInputProps = {
@ -31,6 +33,10 @@ export const CurrencyFieldInput = ({
const { draftValue, persistCurrencyField, setDraftValue, defaultValue } =
useCurrencyField();
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
const defaultCurrencyCodeWithoutSQLQuotes = (
defaultValue as FieldCurrencyValue
).currencyCode.replace(/'/g, '') as CurrencyCode;
@ -114,6 +120,7 @@ export const CurrencyFieldInput = ({
return (
<CurrencyInput
instanceId={instanceId}
value={draftValue?.amount?.toString() ?? ''}
currencyCode={currencyCode}
autoFocus

View File

@ -1,8 +1,10 @@
import { useDateField } from '@/object-record/record-field/meta-types/hooks/useDateField';
import { DateInput } from '@/ui/field/input/components/DateInput';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { isDefined } from 'twenty-shared/utils';
import { Nullable } from 'twenty-ui/utilities';
import { usePersistField } from '../../../hooks/usePersistField';
@ -26,6 +28,10 @@ export const DateFieldInput = ({
}: DateFieldInputProps) => {
const { fieldValue, setDraftValue } = useDateField();
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
const persistField = usePersistField();
const persistDate = (newDate: Nullable<Date>) => {
@ -73,6 +79,7 @@ export const DateFieldInput = ({
return (
<DateInput
instanceId={instanceId}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}

View File

@ -2,8 +2,10 @@ import { DateInput } from '@/ui/field/input/components/DateInput';
import { FieldInputEvent } from '@/object-record/record-field/meta-types/input/components/NumberFieldInput';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { Nullable } from 'twenty-ui/utilities';
import { usePersistField } from '../../../hooks/usePersistField';
import { useDateTimeField } from '../../hooks/useDateTimeField';
@ -25,6 +27,10 @@ export const DateTimeFieldInput = ({
}: DateTimeFieldInputProps) => {
const { fieldValue, setDraftValue } = useDateTimeField();
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
const persistField = usePersistField();
const persistDate = (newDate: Nullable<Date>) => {
@ -68,6 +74,7 @@ export const DateTimeFieldInput = ({
return (
<DateInput
instanceId={instanceId}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}

View File

@ -5,11 +5,13 @@ import { DoubleTextInput } from '@/ui/field/input/components/DoubleTextInput';
import { FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/FirstNamePlaceholder';
import { LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/LastNamePlaceholder';
import { isDoubleTextFieldEmpty } from '@/object-record/record-field/meta-types/input/utils/isDoubleTextFieldEmpty';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from '@/object-record/record-field/types/FieldInputEvent';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
type FullNameFieldInputProps = {
onClickOutside?: FieldInputClickOutsideEvent;
@ -78,8 +80,13 @@ export const FullNameFieldInput = ({
setDraftValue(getRequiredDraftValueFromDoubleText(newDoubleText));
};
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
return (
<DoubleTextInput
instanceId={instanceId}
firstValue={draftValue?.firstName ?? ''}
secondValue={draftValue?.lastName ?? ''}
firstValuePlaceholder={

View File

@ -3,8 +3,8 @@ import styled from '@emotion/styled';
import { forwardRef, InputHTMLAttributes, ReactNode, useRef } from 'react';
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
import { TEXT_INPUT_STYLE } from 'twenty-ui/theme';
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
const StyledInput = styled.input<{
withRightComponent?: boolean;
@ -84,6 +84,7 @@ export type MultiItemBaseInputProps = Omit<HTMLInputProps, 'onChange'> & {
hasError?: boolean;
hasItem: boolean;
onChange: (value: string) => void;
instanceId: string;
};
export const MultiItemBaseInput = forwardRef<
@ -108,6 +109,7 @@ export const MultiItemBaseInput = forwardRef<
error = '',
hasError = false,
hasItem,
instanceId,
},
ref,
) => {
@ -115,6 +117,7 @@ export const MultiItemBaseInput = forwardRef<
const combinedRef = useCombinedRefs(ref, inputRef);
useRegisterInputEvents({
focusId: instanceId,
inputRef,
inputValue: value,
onEnter,

View File

@ -6,13 +6,15 @@ import {
MultiItemBaseInput,
MultiItemBaseInputProps,
} from '@/object-record/record-field/meta-types/input/components/MultiItemBaseInput';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { PhoneRecord } from '@/object-record/record-field/types/FieldMetadata';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { IconCheck, IconPlus } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
@ -79,7 +81,17 @@ export const MultiItemFieldInput = <T,>({
listenerId: hotkeyScope,
});
useScopedHotkeys(Key.Escape, handleDropdownClose, hotkeyScope);
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
useHotkeysOnFocusedElement({
focusId: instanceId,
keys: [Key.Escape],
callback: handleDropdownClose,
scope: hotkeyScope,
dependencies: [handleDropdownClose],
});
const [isInputDisplayed, setIsInputDisplayed] = useState(false);
const [inputValue, setInputValue] = useState('');
@ -204,6 +216,7 @@ export const MultiItemFieldInput = <T,>({
)}
{isInputDisplayed || !items.length ? (
<MultiItemBaseInput
instanceId={instanceId}
autoFocus
placeholder={placeholder}
value={inputValue}

View File

@ -1,7 +1,8 @@
import { useMultiSelectField } from '@/object-record/record-field/meta-types/hooks/useMultiSelectField';
import { SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-field/meta-types/input/constants/SelectFieldInputSelectableListComponentInstanceId';
import { getFieldInputInstanceId } from '@/object-record/record-field/utils/getFieldInputInstanceId';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { MultiSelectInput } from '@/ui/field/input/components/MultiSelectInput';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
type MultiSelectFieldInputProps = {
onCancel?: () => void;
@ -10,18 +11,18 @@ type MultiSelectFieldInputProps = {
export const MultiSelectFieldInput = ({
onCancel,
}: MultiSelectFieldInputProps) => {
const { persistField, fieldDefinition, fieldValues, recordId } =
useMultiSelectField();
const { persistField, fieldDefinition, fieldValues } = useMultiSelectField();
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
return (
<MultiSelectInput
selectableListComponentInstanceId={
SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID
}
focusId={getFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
})}
focusId={instanceId}
options={fieldDefinition.metadata.options}
onCancel={onCancel}
onOptionSelected={persistField}

View File

@ -1,8 +1,10 @@
import { TextInput } from '@/ui/field/input/components/TextInput';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { FieldInputContainer } from '@/ui/field/input/components/FieldInputContainer';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useNumberField } from '../../hooks/useNumberField';
export type FieldInputEvent = (persist: () => void) => void;
@ -25,6 +27,10 @@ export const NumberFieldInput = ({
const { fieldDefinition, draftValue, setDraftValue, persistNumberField } =
useNumberField();
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
const handleEnter = (newText: string) => {
onEnter?.(() => persistNumberField(newText));
};
@ -55,6 +61,7 @@ export const NumberFieldInput = ({
return (
<FieldInputContainer>
<TextInput
instanceId={instanceId}
placeholder={fieldDefinition.metadata.placeHolder}
autoFocus
value={draftValue?.toString() ?? ''}

View File

@ -1,13 +1,15 @@
import styled from '@emotion/styled';
import { isWorkflowRunJsonField } from '@/object-record/record-field/meta-types/utils/isWorkflowRunJsonField';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from '@/object-record/record-field/types/FieldInputEvent';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useLingui } from '@lingui/react/macro';
import { useRef, useState } from 'react';
import { Key } from 'ts-key-enum';
@ -70,6 +72,9 @@ export const RawJsonFieldInput = ({
const hotkeyScope = DEFAULT_CELL_SCOPE.scope;
const containerRef = useRef<HTMLDivElement>(null);
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
const [isEditing, setIsEditing] = useState(false);
@ -104,32 +109,35 @@ export const RawJsonFieldInput = ({
listenerId: hotkeyScope,
});
useScopedHotkeys(
[Key.Escape],
() => {
useHotkeysOnFocusedElement({
keys: [Key.Escape],
callback: () => {
handleEscape(draftValue ?? '');
},
hotkeyScope,
[handleEscape, draftValue],
);
scope: hotkeyScope,
focusId: instanceId,
dependencies: [handleEscape, draftValue],
});
useScopedHotkeys(
'tab',
() => {
useHotkeysOnFocusedElement({
keys: ['tab'],
callback: () => {
handleTab(draftValue ?? '');
},
hotkeyScope,
[handleTab, draftValue],
);
scope: hotkeyScope,
focusId: instanceId,
dependencies: [handleTab, draftValue],
});
useScopedHotkeys(
'shift+tab',
() => {
useHotkeysOnFocusedElement({
keys: ['shift+tab'],
callback: () => {
handleShiftTab(draftValue ?? '');
},
hotkeyScope,
[handleShiftTab, draftValue],
);
scope: hotkeyScope,
focusId: instanceId,
dependencies: [handleShiftTab, draftValue],
});
const showEditingButton = !isWorkflowRunJsonField({
objectMetadataNameSingular:

View File

@ -10,14 +10,15 @@ import { FieldContext } from '@/object-record/record-field/contexts/FieldContext
import { useRelationField } from '@/object-record/record-field/meta-types/hooks/useRelationField';
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { getFieldInputInstanceId } from '@/object-record/record-field/utils/getFieldInputInstanceId';
import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker';
import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRecoilCallback } from 'recoil';
@ -31,10 +32,9 @@ export const RelationFromManyFieldInput = ({
onSubmit,
}: RelationFromManyFieldInputProps) => {
const { fieldDefinition, recordId } = useContext(FieldContext);
const recordPickerInstanceId = getFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
});
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
const { updateRelation } = useUpdateRelationFromManyFieldInput();
const fieldName = fieldDefinition.metadata.fieldName;
@ -94,7 +94,7 @@ export const RelationFromManyFieldInput = ({
const multipleRecordPickerPickableMorphItemsCallbackState =
useRecoilComponentCallbackStateV2(
multipleRecordPickerPickableMorphItemsComponentState,
recordPickerInstanceId,
instanceId,
);
const { performSearch: multipleRecordPickerPerformSearch } =
useMultipleRecordPickerPerformSearch();
@ -123,7 +123,7 @@ export const RelationFromManyFieldInput = ({
set(multipleRecordPickerPickableMorphItemsCallbackState, newMorphItems);
multipleRecordPickerPerformSearch({
multipleRecordPickerInstanceId: recordPickerInstanceId,
multipleRecordPickerInstanceId: instanceId,
forceSearchFilter: searchInput,
forceSearchableObjectMetadataItems: [relationObjectMetadataItem],
forcePickableMorphItems: newMorphItems,
@ -132,7 +132,7 @@ export const RelationFromManyFieldInput = ({
[
createNewRecordAndOpenRightDrawer,
relationObjectMetadataItem,
recordPickerInstanceId,
instanceId,
multipleRecordPickerPickableMorphItemsCallbackState,
multipleRecordPickerPerformSearch,
],
@ -142,15 +142,15 @@ export const RelationFromManyFieldInput = ({
return (
<MultipleRecordPicker
focusId={recordPickerInstanceId}
componentInstanceId={recordPickerInstanceId}
focusId={instanceId}
componentInstanceId={instanceId}
onSubmit={handleSubmit}
onChange={(morphItem) => {
if (isRelationFromActivityTargets) {
updateActivityTargetFromCell({
morphItem,
activityTargetWithTargetRecords: activityTargetObjectRecords,
recordPickerInstanceId,
recordPickerInstanceId: instanceId,
});
} else {
updateRelation(morphItem);

View File

@ -3,14 +3,15 @@ import { useRelationField } from '../../hooks/useRelationField';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { getFieldInputInstanceId } from '@/object-record/record-field/utils/getFieldInputInstanceId';
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { isDefined } from 'twenty-shared/utils';
@ -29,10 +30,9 @@ export const RelationToOneFieldInput = ({
const persistField = usePersistField();
const recordPickerInstanceId = getFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
});
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
const handleRecordSelected = (
selectedRecord: SingleRecordPickerRecord | null | undefined,
@ -67,7 +67,7 @@ export const RelationToOneFieldInput = ({
const setSingleRecordPickerSelectedId = useSetRecoilComponentStateV2(
singleRecordPickerSelectedIdComponentState,
recordPickerInstanceId,
instanceId,
);
const handleCreateNew = async (searchInput?: string) => {
@ -84,8 +84,8 @@ export const RelationToOneFieldInput = ({
return (
<SingleRecordPicker
focusId={recordPickerInstanceId}
componentInstanceId={recordPickerInstanceId}
focusId={instanceId}
componentInstanceId={instanceId}
EmptyIcon={IconForbid}
emptyLabel={'No ' + fieldDefinition.label}
onCancel={onCancel}
@ -94,7 +94,7 @@ export const RelationToOneFieldInput = ({
objectNameSingular={
fieldDefinition.metadata.relationObjectMetadataNameSingular
}
recordPickerInstanceId={recordPickerInstanceId}
recordPickerInstanceId={instanceId}
layoutDirection={
layoutDirection === 'downward'
? 'search-bar-on-top'

View File

@ -3,14 +3,16 @@ import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableE
import { useRichTextCommandMenu } from '@/command-menu/hooks/useRichTextCommandMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from '@/object-record/record-field/types/FieldInputEvent';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { lazy, Suspense, useRef } from 'react';
import { Suspense, lazy, useRef } from 'react';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { IconLayoutSidebarLeftCollapse } from 'twenty-ui/display';
import { FloatingIconButton } from 'twenty-ui/input';
@ -72,6 +74,9 @@ export const RichTextFieldInput = ({
} & RichTextFieldInputProps) => {
const { editRichText } = useRichTextCommandMenu();
const containerRef = useRef<HTMLDivElement>(null);
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
onClickOutside?.(() => {}, event);
@ -82,6 +87,7 @@ export const RichTextFieldInput = ({
};
useRegisterInputEvents({
focusId: instanceId,
inputRef: containerRef,
inputValue: null,
onClickOutside: handleClickOutside,

View File

@ -1,12 +1,13 @@
import { useClearField } from '@/object-record/record-field/hooks/useClearField';
import { useSelectField } from '@/object-record/record-field/meta-types/hooks/useSelectField';
import { SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-field/meta-types/input/constants/SelectFieldInputSelectableListComponentInstanceId';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { getFieldInputInstanceId } from '@/object-record/record-field/utils/getFieldInputInstanceId';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { SelectInput } from '@/ui/field/input/components/SelectInput';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useState } from 'react';
import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils';
@ -21,8 +22,11 @@ export const SelectFieldInput = ({
onSubmit,
onCancel,
}: SelectFieldInputProps) => {
const { persistField, fieldDefinition, fieldValue, recordId } =
useSelectField();
const { persistField, fieldDefinition, fieldValue } = useSelectField();
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
const [filteredOptions, setFilteredOptions] = useState<SelectOption[]>([]);
@ -46,15 +50,16 @@ export const SelectFieldInput = ({
resetSelectedItem();
};
useScopedHotkeys(
Key.Escape,
() => {
useHotkeysOnFocusedElement({
keys: [Key.Escape],
callback: () => {
onCancel?.();
resetSelectedItem();
},
DEFAULT_CELL_SCOPE.scope,
[onCancel, resetSelectedItem],
);
scope: DEFAULT_CELL_SCOPE.scope,
focusId: instanceId,
dependencies: [onCancel, resetSelectedItem],
});
const optionIds = [
`No ${fieldDefinition.label}`,
@ -67,10 +72,7 @@ export const SelectFieldInput = ({
SELECT_FIELD_INPUT_SELECTABLE_LIST_COMPONENT_INSTANCE_ID
}
selectableItemIdArray={optionIds}
focusId={getFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
})}
focusId={instanceId}
onEnter={(itemId) => {
const option = filteredOptions.find(
(option) => option.value === itemId,

View File

@ -3,12 +3,14 @@ import { TextAreaInput } from '@/ui/field/input/components/TextAreaInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useTextField } from '../../hooks/useTextField';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
} from '@/object-record/record-field/types/FieldInputEvent';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { FieldInputContainer } from '@/ui/field/input/components/FieldInputContainer';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { turnIntoUndefinedIfWhitespacesOnly } from '~/utils/string/turnIntoUndefinedIfWhitespacesOnly';
export type TextFieldInputProps = {
@ -28,6 +30,10 @@ export const TextFieldInput = ({
}: TextFieldInputProps) => {
const { fieldDefinition, draftValue, setDraftValue } = useTextField();
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
const persistField = usePersistField();
const handleEnter = (newText: string) => {
onEnter?.(() => persistField(newText.trim()));
@ -59,6 +65,7 @@ export const TextFieldInput = ({
return (
<FieldInputContainer>
<TextAreaInput
instanceId={instanceId}
placeholder={fieldDefinition.metadata.placeHolder}
autoFocus
value={draftValue ?? ''}

View File

@ -1,18 +1,20 @@
import { Decorator, Meta, StoryObj } from '@storybook/react';
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
import { useEffect } from 'react';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useAddressField } from '@/object-record/record-field/meta-types/hooks/useAddressField';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { FieldAddressDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import {
AddressInput,
AddressInputProps,
} from '@/ui/field/input/components/AddressInput';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useEffect } from 'react';
import { FieldMetadataType } from '~/generated-metadata/graphql';
const AddressValueSetterEffect = ({
@ -43,21 +45,30 @@ const AddressInputWithContext = ({
onTab,
onShiftTab,
}: AddressInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const instanceId = getRecordFieldInputInstanceId({
recordId: recordId ?? '',
fieldName: 'Address',
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
});
useEffect(() => {
setHotKeyScope(DEFAULT_CELL_SCOPE.scope);
}, [setHotKeyScope]);
pushFocusItemToFocusStack({
focusId: instanceId,
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: instanceId,
},
hotkeyScope: DEFAULT_CELL_SCOPE,
});
}, [instanceId, pushFocusItemToFocusStack]);
return (
<div>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputId(
recordId ?? '',
'Address',
'record-table-cell',
),
instanceId: instanceId,
}}
>
<FieldContext.Provider
@ -80,6 +91,7 @@ const AddressInputWithContext = ({
>
<AddressValueSetterEffect value={value} />
<AddressInput
instanceId={instanceId}
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
@ -116,7 +128,16 @@ const meta: Meta = {
title: 'UI/Data/Field/Input/AddressFieldInput',
component: AddressInputWithContext,
args: {
value: 'text',
value: {
addressStreet1: 'Address 1',
addressStreet2: null,
addressCity: null,
addressState: null,
addressPostcode: null,
addressCountry: null,
addressLat: null,
addressLng: null,
},
onEnter: enterJestFn,
onEscape: escapeJestfn,
onClickOutside: clickOutsideJestFn,
@ -148,7 +169,10 @@ export const Enter: Story = {
expect(enterJestFn).toHaveBeenCalledTimes(0);
await canvas.findByText('Address 1');
const addressInput = await canvas.findByDisplayValue('Address 1');
await userEvent.click(addressInput);
await userEvent.keyboard('{enter}');
await waitFor(() => {

View File

@ -6,9 +6,11 @@ import { useEffect } from 'react';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useArrayField } from '@/object-record/record-field/meta-types/hooks/useArrayField';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { ArrayFieldInput } from '../ArrayFieldInput';
@ -55,20 +57,33 @@ const ArrayInputWithContext = ({
onCancel,
onClickOutside,
}: ArrayInputWithContextProps) => {
const setHotkeyScope = useSetHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const instanceId = getRecordFieldInputInstanceId({
recordId,
fieldName: 'tags',
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
});
useEffect(() => {
setHotkeyScope(DEFAULT_CELL_SCOPE.scope);
}, [setHotkeyScope]);
pushFocusItemToFocusStack({
focusId: instanceId,
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: instanceId,
},
hotkeyScope: DEFAULT_CELL_SCOPE,
});
}, [pushFocusItemToFocusStack, instanceId]);
return (
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputId(
instanceId: getRecordFieldInputInstanceId({
recordId,
'tags',
'record-table-cell',
),
fieldName: 'tags',
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
}),
}}
>
<FieldContext.Provider

View File

@ -8,7 +8,8 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import {
BooleanFieldInput,
BooleanFieldInputProps,
@ -43,11 +44,11 @@ const BooleanFieldInputWithContext = ({
return (
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputId(
recordId ?? '',
'Boolean',
'record-table-cell',
),
instanceId: getRecordFieldInputInstanceId({
recordId: recordId ?? '',
fieldName: 'Boolean',
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
}),
}}
>
<FieldContext.Provider

View File

@ -2,13 +2,15 @@ import { Meta, StoryObj } from '@storybook/react';
import { expect, fn, userEvent, within } from '@storybook/test';
import { useEffect } from 'react';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect';
import { useDateTimeField } from '../../../hooks/useDateTimeField';
import {
@ -52,7 +54,7 @@ const DateFieldValueGater = ({
type DateFieldInputWithContextProps = DateTimeFieldInputProps & {
value: Date;
recordId?: string;
recordId: string;
};
const DateFieldInputWithContext = ({
@ -62,20 +64,28 @@ const DateFieldInputWithContext = ({
onEnter,
onClickOutside,
}: DateFieldInputWithContextProps) => {
const setHotkeyScope = useSetHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const instanceId = getRecordFieldInputInstanceId({
recordId,
fieldName: 'Date',
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
});
useEffect(() => {
setHotkeyScope(DEFAULT_CELL_SCOPE.scope);
}, [setHotkeyScope]);
pushFocusItemToFocusStack({
focusId: instanceId,
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: instanceId,
},
hotkeyScope: DEFAULT_CELL_SCOPE,
});
}, [pushFocusItemToFocusStack, instanceId]);
return (
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputId(
recordId ?? '',
'Date',
'record-table-cell',
),
instanceId: instanceId,
}}
>
<FieldContext.Provider
@ -91,7 +101,7 @@ const DateFieldInputWithContext = ({
objectMetadataNameSingular: 'person',
},
},
recordId: '123',
recordId,
isLabelIdentifier: false,
isReadOnly: false,
}}

View File

@ -8,9 +8,11 @@ import { FieldContext } from '@/object-record/record-field/contexts/FieldContext
import { useEmailsField } from '@/object-record/record-field/meta-types/hooks/useEmailsField';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { FieldEmailsValue } from '@/object-record/record-field/types/FieldMetadata';
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { EmailsFieldInput } from '../EmailsFieldInput';
@ -57,20 +59,28 @@ const EmailInputWithContext = ({
onCancel,
onClickOutside,
}: EmailInputWithContextProps) => {
const setHotkeyScope = useSetHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const instanceId = getRecordFieldInputInstanceId({
recordId,
fieldName: 'emails',
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
});
useEffect(() => {
setHotkeyScope(DEFAULT_CELL_SCOPE.scope);
}, [setHotkeyScope]);
pushFocusItemToFocusStack({
focusId: instanceId,
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: instanceId,
},
hotkeyScope: DEFAULT_CELL_SCOPE,
});
}, [pushFocusItemToFocusStack, instanceId]);
return (
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputId(
recordId,
'emails',
'record-table-cell',
),
instanceId: instanceId,
}}
>
<FieldContext.Provider

View File

@ -5,9 +5,11 @@ import { useEffect } from 'react';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { LinksFieldInput } from '../LinksFieldInput';
@ -38,7 +40,7 @@ type LinksInputWithContextProps = {
primaryLinkLabel: string | null;
secondaryLinks: Array<{ url: string | null; label: string | null }> | null;
};
recordId?: string;
recordId: string;
onCancel?: () => void;
onClickOutside?: (event: MouseEvent | TouchEvent) => void;
};
@ -67,21 +69,29 @@ const LinksInputWithContext = ({
onCancel,
onClickOutside,
}: LinksInputWithContextProps) => {
const setHotkeyScope = useSetHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const instanceId = getRecordFieldInputInstanceId({
recordId,
fieldName: 'Links',
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
});
useEffect(() => {
setHotkeyScope(DEFAULT_CELL_SCOPE.scope);
}, [setHotkeyScope]);
pushFocusItemToFocusStack({
focusId: instanceId,
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: instanceId,
},
hotkeyScope: DEFAULT_CELL_SCOPE,
});
}, [pushFocusItemToFocusStack, instanceId]);
return (
<div>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputId(
recordId ?? '',
'Links',
'record-table-cell',
),
instanceId: instanceId,
}}
>
<FieldContext.Provider
@ -97,7 +107,7 @@ const LinksInputWithContext = ({
objectMetadataNameSingular: 'company',
},
},
recordId: recordId ?? '123',
recordId,
isLabelIdentifier: false,
isReadOnly: false,
useUpdateRecord: () => [updateRecord, { loading: false }],
@ -131,6 +141,7 @@ const meta: Meta = {
primaryLinkLabel: null,
secondaryLinks: null,
},
recordId: '123',
onCancel: cancelJestFn,
onClickOutside: clickOutsideJestFn,
},

View File

@ -2,14 +2,16 @@ import { Decorator, Meta, StoryObj } from '@storybook/react';
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
import { useEffect, useState } from 'react';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { useNumberField } from '../../../hooks/useNumberField';
@ -27,7 +29,7 @@ const NumberFieldValueSetterEffect = ({ value }: { value: number }) => {
type NumberFieldInputWithContextProps = NumberFieldInputProps & {
value: number;
recordId?: string;
recordId: string;
};
const NumberFieldInputWithContext = ({
@ -39,25 +41,38 @@ const NumberFieldInputWithContext = ({
onTab,
onShiftTab,
}: NumberFieldInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const [isReady, setIsReady] = useState(false);
const instanceId = getRecordFieldInputInstanceId({
recordId,
fieldName: 'Number',
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
});
useEffect(() => {
if (!isReady) {
setHotKeyScope(DEFAULT_CELL_SCOPE.scope);
pushFocusItemToFocusStack({
focusId: instanceId,
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: instanceId,
},
hotkeyScope: DEFAULT_CELL_SCOPE,
});
setIsReady(true);
}
}, [isReady, setHotKeyScope]);
}, [isReady, pushFocusItemToFocusStack, instanceId]);
return (
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputId(
recordId ?? '',
'Number',
'record-table-cell',
),
instanceId: getRecordFieldInputInstanceId({
recordId,
fieldName: 'Number',
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
}),
}}
>
<FieldContext.Provider

View File

@ -9,9 +9,11 @@ import { usePhonesField } from '@/object-record/record-field/meta-types/hooks/us
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata';
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { PhonesFieldInput } from '../PhonesFieldInput';
@ -58,20 +60,28 @@ const PhoneInputWithContext = ({
onCancel,
onClickOutside,
}: PhoneInputWithContextProps) => {
const setHotkeyScope = useSetHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const instanceId = getRecordFieldInputInstanceId({
recordId,
fieldName: 'phones',
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
});
useEffect(() => {
setHotkeyScope(DEFAULT_CELL_SCOPE.scope);
}, [setHotkeyScope]);
pushFocusItemToFocusStack({
focusId: instanceId,
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: instanceId,
},
hotkeyScope: DEFAULT_CELL_SCOPE,
});
}, [pushFocusItemToFocusStack, instanceId]);
return (
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputId(
recordId,
'phones',
'record-table-cell',
),
instanceId: instanceId,
}}
>
<FieldContext.Provider

View File

@ -2,12 +2,14 @@ import { Decorator, Meta, StoryObj } from '@storybook/react';
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
import { useEffect } from 'react';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldRatingValue } from '../../../../types/FieldMetadata';
import { useRatingField } from '../../../hooks/useRatingField';
@ -29,7 +31,7 @@ const RatingFieldValueSetterEffect = ({
type RatingFieldInputWithContextProps = RatingFieldInputProps & {
value: FieldRatingValue;
recordId?: string;
recordId: string;
};
const RatingFieldInputWithContext = ({
@ -37,20 +39,29 @@ const RatingFieldInputWithContext = ({
value,
onSubmit,
}: RatingFieldInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const instanceId = getRecordFieldInputInstanceId({
recordId,
fieldName: 'Rating',
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
});
useEffect(() => {
setHotKeyScope(DEFAULT_CELL_SCOPE.scope);
}, [setHotKeyScope]);
pushFocusItemToFocusStack({
focusId: instanceId,
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: instanceId,
},
hotkeyScope: DEFAULT_CELL_SCOPE,
});
}, [pushFocusItemToFocusStack, instanceId]);
return (
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputId(
recordId ?? '',
'Rating',
'record-table-cell',
),
instanceId: instanceId,
}}
>
<FieldContext.Provider

View File

@ -5,7 +5,7 @@ import { useSetRecoilState } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { RelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
@ -19,9 +19,9 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { getFieldInputInstanceId } from '@/object-record/record-field/utils/getFieldInputInstanceId';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from '~/generated-metadata/graphql';
@ -40,7 +40,7 @@ const RelationWorkspaceSetterEffect = () => {
};
const RelationManyFieldInputWithContext = () => {
const setHotKeyScope = useSetHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const fieldDefinition = useMemo(
() => ({
@ -72,7 +72,14 @@ const RelationManyFieldInputWithContext = () => {
useEffect(() => {
setRecordStoreFieldValue([]);
setHotKeyScope(DropdownHotkeyScope.Dropdown);
pushFocusItemToFocusStack({
focusId: 'relation-from-many-field-input',
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: 'relation-from-many-field-input',
},
hotkeyScope: DEFAULT_CELL_SCOPE,
});
openFieldInput({
fieldDefinition,
recordId: 'recordId',
@ -80,7 +87,7 @@ const RelationManyFieldInputWithContext = () => {
}, [
fieldDefinition,
openFieldInput,
setHotKeyScope,
pushFocusItemToFocusStack,
setRecordStoreFieldValue,
]);
@ -88,10 +95,7 @@ const RelationManyFieldInputWithContext = () => {
<div>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getFieldInputInstanceId({
recordId: 'recordId',
fieldName: 'people',
}),
instanceId: 'relation-from-many-field-input',
}}
>
<FieldContext.Provider

View File

@ -18,8 +18,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState';
import { getFieldInputInstanceId } from '@/object-record/record-field/utils/getFieldInputInstanceId';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
@ -68,24 +67,13 @@ const RelationToOneFieldInputWithContext = ({
useEffect(() => {
pushFocusItemToFocusStack({
focusId: getFieldInputInstanceId({
recordId: '123',
fieldName: 'Relation',
}),
focusId: 'relation-to-one-field-input',
component: {
type: FocusComponentType.DROPDOWN,
instanceId: getFieldInputInstanceId({
recordId: '123',
fieldName: 'Relation',
}),
instanceId: 'relation-to-one-field-input',
},
hotkeyScope: {
scope: DropdownHotkeyScope.Dropdown,
},
memoizeKey: getFieldInputInstanceId({
recordId: '123',
fieldName: 'Relation',
}),
hotkeyScope: DEFAULT_CELL_SCOPE,
memoizeKey: 'relation-to-one-field-input',
});
}, [pushFocusItemToFocusStack]);

View File

@ -4,8 +4,11 @@ import { useEffect } from 'react';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { Decorator, Meta, StoryObj } from '@storybook/react';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
@ -35,16 +38,29 @@ const RichTextFieldInputWithContext = ({
onClickOutside,
onEscape,
}: RichTextFieldInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const instanceId = getRecordFieldInputInstanceId({
recordId: targetableObjectId,
fieldName: 'richText',
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
});
useEffect(() => {
setHotKeyScope(DEFAULT_CELL_SCOPE.scope);
}, [setHotKeyScope]);
pushFocusItemToFocusStack({
focusId: instanceId,
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: instanceId,
},
hotkeyScope: DEFAULT_CELL_SCOPE,
});
}, [pushFocusItemToFocusStack, instanceId]);
return (
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: 'record-field-component-instance-id',
instanceId: instanceId,
}}
>
<FieldContext.Provider

View File

@ -1,18 +1,20 @@
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
import { useEffect, useState } from 'react';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { Decorator, Meta, StoryObj } from '@storybook/react';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { useTextField } from '../../../hooks/useTextField';
import { TextFieldInput, TextFieldInputProps } from '../TextFieldInput';
const TextFieldValueSetterEffect = ({ value }: { value: string }) => {
const { setFieldValue } = useTextField();
@ -25,7 +27,7 @@ const TextFieldValueSetterEffect = ({ value }: { value: string }) => {
type TextFieldInputWithContextProps = TextFieldInputProps & {
value: string;
recordId?: string;
recordId: string;
};
const TextFieldInputWithContext = ({
@ -37,26 +39,39 @@ const TextFieldInputWithContext = ({
onTab,
onShiftTab,
}: TextFieldInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const [isReady, setIsReady] = useState(false);
const instanceId = getRecordFieldInputInstanceId({
recordId,
fieldName: 'Text',
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
});
useEffect(() => {
if (!isReady) {
setHotKeyScope(DEFAULT_CELL_SCOPE.scope);
pushFocusItemToFocusStack({
focusId: instanceId,
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: instanceId,
},
hotkeyScope: DEFAULT_CELL_SCOPE,
});
setIsReady(true);
}
}, [isReady, setHotKeyScope]);
}, [isReady, pushFocusItemToFocusStack, instanceId]);
return (
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: 'record-field-component-instance-id',
instanceId: instanceId,
}}
>
<FieldContext.Provider
value={{
recordId: recordId ?? '123',
recordId,
fieldDefinition: {
fieldMetadataId: 'text',
label: 'Text',

View File

@ -3,7 +3,6 @@ import {
FieldRelationFromManyValue,
FieldRelationValue,
} from '@/object-record/record-field/types/FieldMetadata';
import { getFieldInputInstanceId } from '@/object-record/record-field/utils/getFieldInputInstanceId';
import { useMultipleRecordPickerOpen } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerOpen';
import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
@ -11,6 +10,7 @@ import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
@ -28,14 +28,17 @@ export const useOpenRelationFromManyFieldInput = () => {
fieldName,
objectNameSingular,
recordId,
prefix,
}: {
fieldName: string;
objectNameSingular: string;
recordId: string;
prefix?: string;
}) => {
const recordPickerInstanceId = getFieldInputInstanceId({
const recordPickerInstanceId = getRecordFieldInputInstanceId({
recordId,
fieldName,
prefix,
});
const fieldValue = snapshot

View File

@ -2,10 +2,10 @@ import {
FieldRelationToOneValue,
FieldRelationValue,
} from '@/object-record/record-field/types/FieldMetadata';
import { getFieldInputInstanceId } from '@/object-record/record-field/utils/getFieldInputInstanceId';
import { useSingleRecordPickerOpen } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerOpen';
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
@ -18,10 +18,19 @@ export const useOpenRelationToOneFieldInput = () => {
const openRelationToOneFieldInput = useRecoilCallback(
({ set, snapshot }) =>
({ fieldName, recordId }: { fieldName: string; recordId: string }) => {
const recordPickerInstanceId = getFieldInputInstanceId({
({
fieldName,
recordId,
prefix,
}: {
fieldName: string;
recordId: string;
prefix?: string;
}) => {
const recordPickerInstanceId = getRecordFieldInputInstanceId({
recordId,
fieldName,
prefix,
});
const fieldValue = snapshot
.getLoadable<FieldRelationValue<FieldRelationToOneValue>>(

View File

@ -1,6 +1,6 @@
import { Key } from 'ts-key-enum';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isDefined } from 'twenty-shared/utils';
@ -13,6 +13,7 @@ export const useRegisterInputEvents = <T>({
onTab,
onShiftTab,
onClickOutside,
focusId,
hotkeyScope,
}: {
inputRef: React.RefObject<any>;
@ -23,6 +24,7 @@ export const useRegisterInputEvents = <T>({
onTab?: (inputValue: T) => void;
onShiftTab?: (inputValue: T) => void;
onClickOutside?: (event: MouseEvent | TouchEvent, inputValue: T) => void;
focusId: string;
hotkeyScope: string;
}) => {
useListenClickOutside({
@ -34,39 +36,43 @@ export const useRegisterInputEvents = <T>({
listenerId: hotkeyScope,
});
useScopedHotkeys(
'enter',
() => {
useHotkeysOnFocusedElement({
keys: [Key.Enter],
callback: () => {
onEnter?.(inputValue);
},
hotkeyScope,
[onEnter, inputValue],
);
focusId,
scope: hotkeyScope,
dependencies: [onEnter, inputValue],
});
useScopedHotkeys(
[Key.Escape],
() => {
useHotkeysOnFocusedElement({
keys: [Key.Escape],
callback: () => {
onEscape?.(inputValue);
},
hotkeyScope,
[onEscape, inputValue],
);
focusId,
scope: hotkeyScope,
dependencies: [onEscape, inputValue],
});
useScopedHotkeys(
'tab',
() => {
useHotkeysOnFocusedElement({
keys: [Key.Tab],
callback: () => {
onTab?.(inputValue);
},
hotkeyScope,
[onTab, inputValue],
);
focusId,
scope: hotkeyScope,
dependencies: [onTab, inputValue],
});
useScopedHotkeys(
'shift+tab',
() => {
useHotkeysOnFocusedElement({
keys: [`${Key.Shift}+${Key.Tab}`],
callback: () => {
onShiftTab?.(inputValue);
},
hotkeyScope,
[onShiftTab, inputValue],
);
focusId,
scope: hotkeyScope,
dependencies: [onShiftTab, inputValue],
});
};

View File

@ -1,9 +0,0 @@
export const getFieldInputInstanceId = ({
recordId,
fieldName,
}: {
recordId: string;
fieldName: string;
}) => {
return `${recordId}-${fieldName}`;
};

View File

@ -28,11 +28,14 @@ import {
} from './RecordInlineCellContext';
type RecordInlineCellProps = {
readonly?: boolean;
loading?: boolean;
instanceIdPrefix?: string;
};
export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
export const RecordInlineCell = ({
loading,
instanceIdPrefix,
}: RecordInlineCellProps) => {
const {
fieldDefinition,
recordId,
@ -47,13 +50,28 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
const onOpenEditMode = onOpenEditModeFromContext
? onOpenEditModeFromContext
: () => openFieldInput({ fieldDefinition, recordId });
: () =>
openFieldInput({
fieldDefinition,
recordId,
prefix: instanceIdPrefix,
});
const onCloseEditMode = useCallback(() => {
onCloseEditModeFromContext
? onCloseEditModeFromContext()
: closeFieldInput({ fieldDefinition, recordId });
}, [onCloseEditModeFromContext, closeFieldInput, fieldDefinition, recordId]);
: closeFieldInput({
fieldDefinition,
recordId,
prefix: instanceIdPrefix,
});
}, [
onCloseEditModeFromContext,
closeFieldInput,
fieldDefinition,
recordId,
instanceIdPrefix,
]);
const buttonIcon = useGetButtonIcon();

View File

@ -5,7 +5,7 @@ import { useContext } from 'react';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
import { RecordInlineCellValue } from '@/object-record/record-inline-cell/components/RecordInlineCellValue';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata';
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
@ -123,10 +123,10 @@ export const RecordInlineCellContainer = () => {
};
const theme = useTheme();
const labelId = `label-${getRecordFieldInputId(
const labelId = `label-${getRecordFieldInputInstanceId({
recordId,
fieldDefinition?.metadata?.fieldName,
)}`;
fieldName: fieldDefinition?.metadata?.fieldName,
})}`;
return (
<StyledInlineCellBaseContainer

View File

@ -14,7 +14,6 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRef } from 'react';
import { useRecoilCallback } from 'recoil';
@ -41,8 +40,6 @@ export const MultipleRecordPicker = ({
componentInstanceId,
focusId,
}: MultipleRecordPickerProps) => {
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
const selectableListComponentInstanceId =
getMultipleRecordPickerSelectableListId(componentInstanceId);
@ -79,7 +76,6 @@ export const MultipleRecordPicker = ({
const handleSubmit = () => {
onSubmit?.();
goBackToPreviousHotkeyScope();
resetSelectedItem();
resetState();
};

View File

@ -18,7 +18,7 @@ import { useRecordShowContainerActions } from '@/object-record/record-show/hooks
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { RecordDetailDuplicatesSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailDuplicatesSection';
import { RecordDetailRelationSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSection';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
import { useIsInRightDrawerOrThrow } from '@/ui/layout/right-drawer/contexts/RightDrawerContext';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -28,6 +28,8 @@ type FieldsCardProps = {
objectRecordId: string;
};
const INPUT_ID_PREFIX = 'fields-card';
export const FieldsCard = ({
objectNameSingular,
objectRecordId,
@ -139,13 +141,13 @@ export const FieldsCard = ({
}}
>
<ActivityTargetsInlineCell
componentInstanceId={getRecordFieldInputId(
objectRecordId,
fieldMetadataItem.name,
isInRightDrawer
componentInstanceId={getRecordFieldInputInstanceId({
recordId: objectRecordId,
fieldName: fieldMetadataItem.name,
prefix: isInRightDrawer
? 'right-drawer-fields-card'
: 'fields-card',
)}
})}
activityObjectNameSingular={
objectNameSingular as
| CoreObjectNameSingular.Note
@ -185,14 +187,17 @@ export const FieldsCard = ({
>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputId(
objectRecordId,
fieldMetadataItem.name,
'fields-card',
),
instanceId: getRecordFieldInputInstanceId({
recordId: objectRecordId,
fieldName: fieldMetadataItem.name,
prefix: INPUT_ID_PREFIX,
}),
}}
>
<RecordInlineCell loading={recordLoading} />
<RecordInlineCell
loading={recordLoading}
instanceIdPrefix={INPUT_ID_PREFIX}
/>
</RecordFieldComponentInstanceContext.Provider>
</FieldContext.Provider>
))}

View File

@ -29,7 +29,7 @@ import { RecordDetailRecordsListItem } from '@/object-record/record-show/record-
import { getRecordFieldCardRelationPickerDropdownId } from '@/object-record/record-show/utils/getRecordFieldCardRelationPickerDropdownId';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
@ -320,14 +320,14 @@ export const RecordDetailRelationRecordsListItem = ({
>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputId(
relationRecord.id,
fieldMetadataItem.name,
'record-detail',
),
instanceId: getRecordFieldInputInstanceId({
recordId: relationRecord.id,
fieldName: fieldMetadataItem.name,
prefix: 'record-detail',
}),
}}
>
<RecordInlineCell />
<RecordInlineCell instanceIdPrefix="record-detail" />
</RecordFieldComponentInstanceContext.Provider>
</FieldContext.Provider>
),

View File

@ -0,0 +1 @@
export const RECORD_TABLE_CELL_INPUT_ID_PREFIX = 'record-table-cell';

View File

@ -1,5 +1,6 @@
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { useBuildRecordInputFromFilters } from '@/object-record/record-table/hooks/useBuildRecordInputFromFilters';
@ -10,6 +11,7 @@ import { canOpenObjectInSidePanel } from '@/object-record/utils/canOpenObjectInS
import { AppPath } from '@/types/AppPath';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { useNavigateApp } from '~/hooks/useNavigateApp';
@ -58,11 +60,16 @@ export const useCreateNewIndexRecord = ({
isNewRecord: true,
});
openRecordTitleCell({
recordId,
fieldMetadataId: objectMetadataItem.labelIdentifierFieldMetadataId,
containerType: RecordTitleCellContainerType.ShowPage,
});
const labelIdentifierFieldMetadataItem =
getLabelIdentifierFieldMetadataItem(objectMetadataItem);
if (isDefined(labelIdentifierFieldMetadataItem)) {
openRecordTitleCell({
recordId,
fieldName: labelIdentifierFieldMetadataItem.name,
containerType: RecordTitleCellContainerType.ShowPage,
});
}
} else {
navigate(AppPath.RecordShowPage, {
objectNameSingular: objectMetadataItem.nameSingular,
@ -74,8 +81,7 @@ export const useCreateNewIndexRecord = ({
buildRecordInputFromFilters,
createOneRecord,
navigate,
objectMetadataItem.labelIdentifierFieldMetadataId,
objectMetadataItem.nameSingular,
objectMetadataItem,
openRecordInCommandMenu,
openRecordTitleCell,
],

View File

@ -1,11 +1,12 @@
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableCellFieldContextGeneric } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextGeneric';
import { RecordTableCellFieldContextLabelIdentifier } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextLabelIdentifier';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { ReactNode, useContext } from 'react';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@ -24,11 +25,11 @@ export const RecordTableCellFieldContextWrapper = ({
return null;
}
const instanceId = getRecordFieldInputId(
const instanceId = getRecordFieldInputInstanceId({
recordId,
columnDefinition.metadata.fieldName,
'record-table-cell',
);
fieldName: columnDefinition.metadata.fieldName,
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
});
const isLabelIdentifier = isLabelIdentifierField({
fieldMetadataItem: {

View File

@ -7,6 +7,7 @@ import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldVal
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/FocusClickOutsideListenerId';
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
import { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/useLeaveTableFocus';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
@ -20,7 +21,7 @@ import { viewableRecordNameSingularState } from '@/object-record/record-right-dr
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
import { recordTableCellEditModePositionComponentState } from '@/object-record/record-table/states/recordTableCellEditModePositionComponentState';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useOpenRecordFromIndexView } from '@/object-record/record-index/hooks/useOpenRecordFromIndexView';
@ -166,6 +167,7 @@ export const useOpenRecordTableCellV2 = (recordTableId: string) => {
openFieldInput({
fieldDefinition,
recordId,
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
});
setCurrentTableCellInEditModePosition(cellPosition);
@ -174,11 +176,11 @@ export const useOpenRecordTableCellV2 = (recordTableId: string) => {
value: initialValue,
recordId,
fieldDefinition,
fieldComponentInstanceId: getRecordFieldInputId(
fieldComponentInstanceId: getRecordFieldInputInstanceId({
recordId,
fieldDefinition.metadata.fieldName,
'record-table-cell',
),
fieldName: fieldDefinition.metadata.fieldName,
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
}),
});
toggleClickOutside(false);

View File

@ -18,7 +18,7 @@ import { RecordTitleCellFieldDisplay } from '@/object-record/record-title-cell/c
import { RecordTitleCellFieldInput } from '@/object-record/record-title-cell/components/RecordTitleCellFieldInput';
import { useRecordTitleCell } from '@/object-record/record-title-cell/hooks/useRecordTitleCell';
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
import { getRecordTitleCellId } from '@/object-record/record-title-cell/utils/getRecordTitleCellId';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
type RecordTitleCellProps = {
loading?: boolean;
@ -37,54 +37,46 @@ export const RecordTitleCell = ({
const { closeRecordTitleCell } = useRecordTitleCell();
const handleEnter: FieldInputEvent = (persistField) => {
const closeCell = () => {
closeRecordTitleCell({
recordId,
fieldMetadataId: fieldDefinition?.fieldMetadataId,
fieldName: fieldDefinition.metadata.fieldName,
containerType,
});
};
const handleEnter: FieldInputEvent = (persistField) => {
closeCell();
persistField();
};
const handleEscape: FieldInputEvent = (persistField) => {
closeRecordTitleCell({
recordId,
fieldMetadataId: fieldDefinition?.fieldMetadataId,
containerType,
});
persistField();
const handleEscape = () => {
closeCell();
};
const handleTab: FieldInputEvent = (persistField) => {
closeRecordTitleCell({
recordId,
fieldMetadataId: fieldDefinition?.fieldMetadataId,
containerType,
});
closeCell();
persistField();
};
const handleShiftTab: FieldInputEvent = (persistField) => {
closeRecordTitleCell({
recordId,
fieldMetadataId: fieldDefinition?.fieldMetadataId,
containerType,
});
closeCell();
persistField();
};
const handleClickOutside: FieldInputClickOutsideEvent = (persistField) => {
closeRecordTitleCell({
recordId,
fieldMetadataId: fieldDefinition?.fieldMetadataId,
containerType,
});
closeCell();
persistField();
};
const recordTitleCellContextValue: RecordTitleCellContextProps = {
editModeContent: (
<RecordTitleCellFieldInput
instanceId={getRecordFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
prefix: containerType,
})}
onEnter={handleEnter}
onEscape={handleEscape}
onTab={handleTab}
@ -93,7 +85,9 @@ export const RecordTitleCell = ({
sizeVariant={sizeVariant}
/>
),
displayModeContent: <RecordTitleCellFieldDisplay />,
displayModeContent: (
<RecordTitleCellFieldDisplay containerType={containerType} />
),
editModeContentOnly: isFieldInputOnly,
loading: loading,
isReadOnly,
@ -103,11 +97,11 @@ export const RecordTitleCell = ({
return (
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordTitleCellId(
instanceId: getRecordFieldInputInstanceId({
recordId,
fieldDefinition?.fieldMetadataId,
containerType,
),
fieldName: fieldDefinition.metadata.fieldName,
prefix: containerType,
}),
}}
>
<FieldFocusContextProvider>

View File

@ -3,9 +3,14 @@ import { isFieldFullName } from '@/object-record/record-field/types/guards/isFie
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
import { RecordTitleCellSingleTextDisplayMode } from '@/object-record/record-title-cell/components/RecordTitleCellTextFieldDisplay';
import { RecordTitleFullNameFieldDisplay } from '@/object-record/record-title-cell/components/RecordTitleFullNameFieldDisplay';
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
import { useContext } from 'react';
export const RecordTitleCellFieldDisplay = () => {
export const RecordTitleCellFieldDisplay = ({
containerType,
}: {
containerType: RecordTitleCellContainerType;
}) => {
const { fieldDefinition } = useContext(FieldContext);
if (!isFieldText(fieldDefinition) && !isFieldFullName(fieldDefinition)) {
@ -15,9 +20,9 @@ export const RecordTitleCellFieldDisplay = () => {
return (
<>
{isFieldText(fieldDefinition) ? (
<RecordTitleCellSingleTextDisplayMode />
<RecordTitleCellSingleTextDisplayMode containerType={containerType} />
) : isFieldFullName(fieldDefinition) ? (
<RecordTitleFullNameFieldDisplay />
<RecordTitleFullNameFieldDisplay containerType={containerType} />
) : null}
</>
);

View File

@ -9,6 +9,7 @@ import { RecordTitleFullNameFieldInput } from '@/object-record/record-title-cell
import { TitleInputHotkeyScope } from '@/ui/input/types/TitleInputHotkeyScope';
type RecordTitleCellFieldInputProps = {
instanceId: string;
onClickOutside?: (
persist: () => void,
event: MouseEvent | TouchEvent,
@ -21,6 +22,7 @@ type RecordTitleCellFieldInputProps = {
};
export const RecordTitleCellFieldInput = ({
instanceId,
sizeVariant,
onEnter,
onEscape,
@ -38,6 +40,7 @@ export const RecordTitleCellFieldInput = ({
<>
{isFieldText(fieldDefinition) ? (
<RecordTitleCellTextFieldInput
instanceId={instanceId}
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}

View File

@ -1,7 +1,7 @@
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { RecordTitleCellContext } from '@/object-record/record-title-cell/components/RecordTitleCellContext';
import { useRecordTitleCell } from '@/object-record/record-title-cell/hooks/useRecordTitleCell';
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
import { Theme, withTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useContext } from 'react';
@ -30,7 +30,11 @@ const StyledEmptyText = withTheme(styled.div<{ theme: Theme }>`
color: ${({ theme }) => theme.font.color.tertiary};
`);
export const RecordTitleCellSingleTextDisplayMode = () => {
export const RecordTitleCellSingleTextDisplayMode = ({
containerType,
}: {
containerType: RecordTitleCellContainerType;
}) => {
const { recordId, fieldDefinition } = useContext(FieldContext);
const recordValue = useRecoilValue(recordStoreFamilyState(recordId));
@ -40,14 +44,12 @@ export const RecordTitleCellSingleTextDisplayMode = () => {
const { openRecordTitleCell } = useRecordTitleCell();
const { containerType } = useContext(RecordTitleCellContext);
return (
<StyledDiv
onClick={() => {
openRecordTitleCell({
recordId,
fieldMetadataId: fieldDefinition.fieldMetadataId,
fieldName: fieldDefinition.metadata.fieldName,
containerType,
});
}}

View File

@ -11,6 +11,7 @@ import { isDefined } from 'twenty-shared/utils';
import { turnIntoUndefinedIfWhitespacesOnly } from '~/utils/string/turnIntoUndefinedIfWhitespacesOnly';
type RecordTitleCellTextFieldInputProps = {
instanceId: string;
onClickOutside?: FieldInputClickOutsideEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
@ -21,6 +22,7 @@ type RecordTitleCellTextFieldInputProps = {
};
export const RecordTitleCellTextFieldInput = ({
instanceId,
sizeVariant,
onEnter,
onEscape,
@ -40,6 +42,7 @@ export const RecordTitleCellTextFieldInput = ({
const persistField = usePersistField();
useRegisterInputEvents<string>({
focusId: instanceId,
inputRef: wrapperRef,
inputValue: draftValue ?? '',
onEnter: (inputValue) => {

View File

@ -2,13 +2,15 @@ import styled from '@emotion/styled';
import { ClipboardEvent, useEffect, useRef, useState } from 'react';
import { Key } from 'ts-key-enum';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { FieldDoubleText } from '@/object-record/record-field/types/FieldDoubleText';
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { isDefined } from 'twenty-shared/utils';
import { splitFullName } from '~/utils/format/spiltFullName';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
import { isDefined } from 'twenty-shared/utils';
const StyledContainer = styled.div`
display: flex;
@ -82,33 +84,39 @@ export const RecordTitleDoubleTextInput = ({
const [focusPosition, setFocusPosition] = useState<'left' | 'right'>('left');
useScopedHotkeys(
Key.Enter,
() => {
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
useHotkeysOnFocusedElement({
keys: [Key.Enter],
callback: () => {
onEnter({
firstValue: firstInternalValue,
secondValue: secondInternalValue,
});
},
hotkeyScope,
[onEnter, firstInternalValue, secondInternalValue],
);
scope: hotkeyScope,
focusId: instanceId,
dependencies: [onEnter, firstInternalValue, secondInternalValue],
});
useScopedHotkeys(
[Key.Escape],
() => {
useHotkeysOnFocusedElement({
keys: [Key.Escape],
callback: () => {
onEscape({
firstValue: firstInternalValue,
secondValue: secondInternalValue,
});
},
hotkeyScope,
[onEscape, firstInternalValue, secondInternalValue],
);
scope: hotkeyScope,
focusId: instanceId,
dependencies: [onEscape, firstInternalValue, secondInternalValue],
});
useScopedHotkeys(
'tab',
() => {
useHotkeysOnFocusedElement({
keys: ['tab'],
callback: () => {
if (focusPosition === 'left') {
setFocusPosition('right');
secondValueInputRef.current?.focus();
@ -119,13 +127,19 @@ export const RecordTitleDoubleTextInput = ({
});
}
},
hotkeyScope,
[onTab, firstInternalValue, secondInternalValue, focusPosition],
);
scope: hotkeyScope,
focusId: instanceId,
dependencies: [
onTab,
firstInternalValue,
secondInternalValue,
focusPosition,
],
});
useScopedHotkeys(
'shift+tab',
() => {
useHotkeysOnFocusedElement({
keys: ['shift+tab'],
callback: () => {
if (focusPosition === 'right') {
setFocusPosition('left');
firstValueInputRef.current?.focus();
@ -136,9 +150,15 @@ export const RecordTitleDoubleTextInput = ({
});
}
},
hotkeyScope,
[onShiftTab, firstInternalValue, secondInternalValue, focusPosition],
);
scope: hotkeyScope,
focusId: instanceId,
dependencies: [
onShiftTab,
firstInternalValue,
secondInternalValue,
focusPosition,
],
});
useListenClickOutside({
refs: [containerRef],

View File

@ -2,8 +2,10 @@ import { FieldContext } from '@/object-record/record-field/contexts/FieldContext
import { useFullNameFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useFullNameFieldDisplay';
import { INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY } from '@/object-record/record-inline-cell/constants/InlineCellHotkeyScopeMemoizeKey';
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { TitleInputHotkeyScope } from '@/ui/input/types/TitleInputHotkeyScope';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { Theme, withTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
@ -33,8 +35,12 @@ const StyledEmptyText = withTheme(styled.div<{ theme: Theme }>`
color: ${({ theme }) => theme.font.color.tertiary};
`);
export const RecordTitleFullNameFieldDisplay = () => {
const { fieldDefinition } = useContext(FieldContext);
export const RecordTitleFullNameFieldDisplay = ({
containerType,
}: {
containerType: string;
}) => {
const { recordId, fieldDefinition } = useContext(FieldContext);
const { openInlineCell } = useInlineCell();
@ -45,14 +51,29 @@ export const RecordTitleFullNameFieldDisplay = () => {
.join(' ')
.trim();
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const recordTitleCellId = getRecordFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
prefix: containerType,
});
return (
<StyledDiv
onClick={() => {
setHotkeyScopeAndMemorizePreviousScope({
scope: TitleInputHotkeyScope.TitleInput,
pushFocusItemToFocusStack({
focusId: recordTitleCellId,
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: recordTitleCellId,
},
hotkeyScope: {
scope: TitleInputHotkeyScope.TitleInput,
},
memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
});
openInlineCell();
}}
>

View File

@ -3,7 +3,7 @@ import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/s
import { INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY } from '@/object-record/record-inline-cell/constants/InlineCellHotkeyScopeMemoizeKey';
import { isInlineCellInEditModeScopedState } from '@/object-record/record-inline-cell/states/isInlineCellInEditModeScopedState';
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
import { getRecordTitleCellId } from '@/object-record/record-title-cell/utils/getRecordTitleCellId';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { TitleInputHotkeyScope } from '@/ui/input/types/TitleInputHotkeyScope';
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
@ -25,26 +25,30 @@ export const useRecordTitleCell = () => {
({ set }) =>
({
recordId,
fieldMetadataId,
fieldName,
containerType,
}: {
recordId: string;
fieldMetadataId: string;
fieldName: string;
containerType: RecordTitleCellContainerType;
}) => {
set(
isInlineCellInEditModeScopedState(
getRecordTitleCellId(recordId, fieldMetadataId, containerType),
getRecordFieldInputInstanceId({
recordId,
fieldName,
prefix: containerType,
}),
),
false,
);
removeFocusItemFromFocusStackById({
focusId: getRecordTitleCellId(
focusId: getRecordFieldInputInstanceId({
recordId,
fieldMetadataId,
containerType,
),
fieldName,
prefix: containerType,
}),
});
goBackToPreviousDropdownFocusId();
@ -58,47 +62,47 @@ export const useRecordTitleCell = () => {
({ set, snapshot }) =>
({
recordId,
fieldMetadataId,
fieldName,
containerType,
customEditHotkeyScopeForField,
}: {
recordId: string;
fieldMetadataId: string;
fieldName: string;
containerType: RecordTitleCellContainerType;
customEditHotkeyScopeForField?: HotkeyScope;
}) => {
if (isDefined(customEditHotkeyScopeForField)) {
pushFocusItemToFocusStack({
focusId: getRecordTitleCellId(
focusId: getRecordFieldInputInstanceId({
recordId,
fieldMetadataId,
containerType,
),
fieldName,
prefix: containerType,
}),
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: getRecordTitleCellId(
instanceId: getRecordFieldInputInstanceId({
recordId,
fieldMetadataId,
containerType,
),
fieldName,
prefix: containerType,
}),
},
hotkeyScope: customEditHotkeyScopeForField,
memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
});
} else {
pushFocusItemToFocusStack({
focusId: getRecordTitleCellId(
focusId: getRecordFieldInputInstanceId({
recordId,
fieldMetadataId,
containerType,
),
fieldName,
prefix: containerType,
}),
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: getRecordTitleCellId(
instanceId: getRecordFieldInputInstanceId({
recordId,
fieldMetadataId,
containerType,
),
fieldName,
prefix: containerType,
}),
},
hotkeyScope: {
scope: TitleInputHotkeyScope.TitleInput,
@ -107,11 +111,11 @@ export const useRecordTitleCell = () => {
});
}
const recordTitleCellId = getRecordTitleCellId(
const recordTitleCellId = getRecordFieldInputInstanceId({
recordId,
fieldMetadataId,
containerType,
);
fieldName,
prefix: containerType,
});
set(isInlineCellInEditModeScopedState(recordTitleCellId), true);
const recordIndexFieldDefinitions = snapshot
@ -119,7 +123,7 @@ export const useRecordTitleCell = () => {
.getValue();
const fieldDefinition = recordIndexFieldDefinitions.find(
(field) => field.fieldMetadataId === fieldMetadataId,
(field) => field.metadata.fieldName === fieldName,
);
if (!fieldDefinition) {

View File

@ -1,9 +0,0 @@
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
export const getRecordTitleCellId = (
recordId: string,
fieldMetadataId: string,
containerType: RecordTitleCellContainerType,
) => {
return `${recordId}-${fieldMetadataId}-${containerType}`;
};

View File

@ -1,9 +1,14 @@
import { isDefined } from 'twenty-shared/utils';
export const getRecordFieldInputId = (
recordId: string,
fieldName?: string,
prefix?: string,
): string => {
export const getRecordFieldInputInstanceId = ({
recordId,
fieldName,
prefix,
}: {
recordId: string;
fieldName?: string;
prefix?: string;
}): string => {
if (isDefined(prefix)) {
return `${prefix}-${recordId}-${fieldName}`;
}

View File

@ -95,6 +95,7 @@ export const SettingsAccountsBlocklistInput = ({
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<TextInput
instanceId="settings-accounts-blocklist-input"
placeholder="eddy@gmail.com, @apple.com"
value={value}
onChange={onChange}

View File

@ -44,6 +44,7 @@ export const SetttingsAccountsImapConnectionForm = ({
defaultValue={defaultValues?.handle}
render={({ field, fieldState }) => (
<TextInput
instanceId="email-address-imap-connection-form"
label={t`Email Address`}
placeholder={t`john.doe@example.com`}
value={field.value}
@ -58,6 +59,7 @@ export const SetttingsAccountsImapConnectionForm = ({
defaultValue={defaultValues?.host}
render={({ field, fieldState }) => (
<TextInput
instanceId="host-imap-connection-form"
label={t`IMAP Server`}
placeholder={t`imap.example.com`}
value={field.value}
@ -72,6 +74,7 @@ export const SetttingsAccountsImapConnectionForm = ({
defaultValue={defaultValues?.port ?? 993}
render={({ field, fieldState }) => (
<TextInput
instanceId="port-imap-connection-form"
label={t`IMAP Port`}
type="number"
placeholder={t`993`}
@ -104,6 +107,7 @@ export const SetttingsAccountsImapConnectionForm = ({
defaultValue={defaultValues?.password}
render={({ field, fieldState }) => (
<TextInput
instanceId="password-imap-connection-form"
label={t`Password`}
placeholder={t`••••••••`}
type="password"

View File

@ -156,6 +156,7 @@ export const SettingsAdminGeneral = () => {
<StyledContainer>
<TextInput
instanceId="admin-user-lookup"
value={userIdentifier}
onChange={setUserIdentifier}
onInputEnter={handleSearch}

View File

@ -60,6 +60,8 @@ export const ConfigVariableDatabaseInput = ({
onChange(newValues);
};
const jsonArrayTextAreaId = `${label}-json-array`;
switch (type) {
case ConfigVariableType.BOOLEAN:
return (
@ -134,6 +136,7 @@ export const ConfigVariableDatabaseInput = ({
/>
) : (
<TextArea
textAreaId={jsonArrayTextAreaId}
label={label}
value={
Array.isArray(value)

View File

@ -18,6 +18,7 @@ export const ConfigVariableSearchInput = ({
}: ConfigVariableSearchInputProps) => {
return (
<StyledSearchInput
instanceId="config-variable-search"
placeholder={t`Search config variables`}
value={value}
onChange={onChange}

View File

@ -1,8 +1,8 @@
import { TextInput } from '@/ui/input/components/TextInput';
import styled from '@emotion/styled';
import { castAsNumberOrNull } from '~/utils/cast-as-number-or-null';
import { IconButton } from 'twenty-ui/input';
import { IconMinus, IconPlus } from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
import { castAsNumberOrNull } from '~/utils/cast-as-number-or-null';
type SettingsCounterProps = {
value: number;
@ -77,6 +77,7 @@ export const SettingsCounter = ({
disabled={disabled}
/>
<StyledTextInput
instanceId="settings-counter-input"
name="counter"
fullWidth
value={value.toString()}

View File

@ -29,6 +29,8 @@ export const SettingsDataModelFieldDescriptionForm = ({
const { control } =
useFormContext<SettingsDataModelFieldDescriptionFormValues>();
const descriptionTextAreaId = `${fieldMetadataItem?.id}-description`;
return (
<Controller
name="description"
@ -36,6 +38,7 @@ export const SettingsDataModelFieldDescriptionForm = ({
defaultValue={fieldMetadataItem?.description}
render={({ field: { onChange, value } }) => (
<TextArea
textAreaId={descriptionTextAreaId}
placeholder={t`Write a description`}
minRows={4}
value={value ?? undefined}

View File

@ -14,7 +14,6 @@ import { TextInput } from '@/ui/input/components/TextInput';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
import {
AppTooltip,
IconInfoCircle,
@ -22,6 +21,7 @@ import {
TooltipDelay,
} from 'twenty-ui/display';
import { Card } from 'twenty-ui/layout';
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
export const settingsDataModelFieldIconLabelFormSchema = (
existingOtherLabels: string[] = [],
@ -94,6 +94,9 @@ export const SettingsDataModelFieldIconLabelForm = ({
const { t } = useLingui();
const labelTextInputId = `${fieldMetadataItem?.id}-label`;
const nameTextInputId = `${fieldMetadataItem?.id}-name`;
const isLabelSyncedWithName =
watch('isLabelSyncedWithName') ??
(isDefined(fieldMetadataItem)
@ -133,6 +136,7 @@ export const SettingsDataModelFieldIconLabelForm = ({
defaultValue={fieldMetadataItem?.label}
render={({ field: { onChange, value } }) => (
<TextInput
instanceId={labelTextInputId}
placeholder={t`Employees`}
value={value}
onChange={(value) => {
@ -167,6 +171,7 @@ export const SettingsDataModelFieldIconLabelForm = ({
render={({ field: { onChange, value } }) => (
<>
<TextInput
instanceId={nameTextInputId}
label={t`API Name`}
placeholder={t`employees`}
value={value}

View File

@ -108,6 +108,7 @@ export const SettingsObjectNewFieldSelector = ({
{' '}
<Section>
<StyledSearchInput
instanceId="new-field-type-search"
LeftIcon={IconSearch}
placeholder={t`Search a type`}
value={searchQuery}

View File

@ -117,6 +117,7 @@ export const SettingsDataModelFieldDateForm = ({
defaultValue={initialCustomUnicodeDateFormat}
render={({ field: { onChange, value } }) => (
<StyledTextInput
instanceId="custom-date-format-input"
placeholder={t`Format e.g. d-MMM-y (qqq''yy)`}
value={value}
onChange={(value) => onChange(value)}

View File

@ -187,6 +187,7 @@ export const SettingsDataModelFieldRelationForm = ({
defaultValue={initialRelationFieldMetadataItem.label}
render={({ field: { onChange, value } }) => (
<TextInput
instanceId="relation-field-label"
disabled={disableFieldEdition}
placeholder={t`Field name`}
value={value}

View File

@ -99,6 +99,7 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
/>
<AdvancedSettingsWrapper animationDimension="width" hideDot>
<StyledOptionInput
instanceId={`select-option-value-${option.id}`}
value={option.value}
onChange={(input) =>
onChange({
@ -133,6 +134,7 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
}
/>
<StyledOptionInput
instanceId={`select-option-label-${option.id}`}
value={option.label}
onChange={(label) => {
const optionNameHasBeenEdited = !(

View File

@ -123,6 +123,10 @@ export const SettingsDataModelObjectAboutForm = ({
});
};
const descriptionTextAreaId = `${objectMetadataItem?.id}-description`;
const labelSingularTextInputId = `${objectMetadataItem?.id}-label-singular`;
const labelPluralTextInputId = `${objectMetadataItem?.id}-label-plural`;
return (
<>
<StyledInputsContainer>
@ -150,6 +154,7 @@ export const SettingsDataModelObjectAboutForm = ({
defaultValue={objectMetadataItem?.labelSingular ?? ''}
render={({ field: { onChange, value }, formState: { errors } }) => (
<TextInput
instanceId={labelSingularTextInputId}
// TODO we should discuss on how to notify user about form validation schema issue, from now just displaying red borders
noErrorHelper={true}
error={errors.labelSingular?.message}
@ -181,6 +186,7 @@ export const SettingsDataModelObjectAboutForm = ({
defaultValue={objectMetadataItem?.labelPlural ?? ''}
render={({ field: { onChange, value }, formState: { errors } }) => (
<TextInput
instanceId={labelPluralTextInputId}
// TODO we should discuss on how to notify user about form validation schema issue, from now just displaying red borders
noErrorHelper={true}
error={errors.labelPlural?.message}
@ -210,6 +216,7 @@ export const SettingsDataModelObjectAboutForm = ({
control={control}
render={({ field: { onChange, value } }) => (
<TextArea
textAreaId={descriptionTextAreaId}
placeholder={t`Write a description`}
minRows={4}
value={value ?? undefined}
@ -264,6 +271,7 @@ export const SettingsDataModelObjectAboutForm = ({
}) => (
<>
<TextInput
instanceId={`${objectMetadataItem?.id}-${fieldName}`}
label={label}
placeholder={placeholder}
value={value}

View File

@ -28,7 +28,7 @@ export const ApiKeyInput = ({ apiKey }: ApiKeyInputProps) => {
return (
<StyledContainer>
<StyledLinkContainer>
<TextInput value={apiKey} fullWidth />
<TextInput instanceId="api-key-display" value={apiKey} fullWidth />
</StyledLinkContainer>
<Button
Icon={IconCopy}

View File

@ -56,9 +56,12 @@ export const ApiKeyNameInput = ({
return debouncedUpdate.cancel;
}, [debouncedUpdate, apiKeyName]);
const nameTextInputId = `${apiKeyId}-name`;
return (
<StyledComboInputContainer>
<TextInput
instanceId={nameTextInputId}
placeholder="E.g. backoffice integration"
onChange={onNameUpdate}
fullWidth

View File

@ -115,6 +115,10 @@ export const SettingsDevelopersWebhookForm = ({
{ label: 'Deleted', value: 'deleted', Icon: IconTrash },
];
const descriptionTextAreaId = `${webhookId}-description`;
const targetUrlTextInputId = `${webhookId}-target-url`;
const secretTextInputId = `${webhookId}-secret`;
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<FormProvider {...formConfig}>
@ -156,6 +160,7 @@ export const SettingsDevelopersWebhookForm = ({
}) => {
return (
<TextInput
instanceId={targetUrlTextInputId}
placeholder={t`https://example.com/webhook`}
value={value}
onChange={onChange}
@ -177,6 +182,7 @@ export const SettingsDevelopersWebhookForm = ({
control={formConfig.control}
render={({ field: { onChange, value } }) => (
<TextArea
textAreaId={descriptionTextAreaId}
placeholder={t`Write a description`}
minRows={4}
value={value || ''}
@ -242,6 +248,7 @@ export const SettingsDevelopersWebhookForm = ({
control={formConfig.control}
render={({ field: { onChange, value } }) => (
<TextInput
instanceId={secretTextInputId}
placeholder={t`Secret (optional)`}
value={value || ''}
onChange={onChange}

View File

@ -1,5 +1,5 @@
import { Controller, useFormContext } from 'react-hook-form';
import styled from '@emotion/styled';
import { Controller, useFormContext } from 'react-hook-form';
import { z } from 'zod';
import { TextInput } from '@/ui/input/components/TextInput';
@ -129,6 +129,7 @@ export const SettingsIntegrationDatabaseConnectionForm = ({
render={({ field: { onChange, value } }) => {
return (
<TextInput
instanceId={`${databaseKey}-${name}`}
autoComplete="new-password" // Disable autocomplete
label={label}
value={value}

View File

@ -117,6 +117,7 @@ export const PlaygroundSetupForm = () => {
control={control}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<TextInput
instanceId="playground-api-key"
label={t`API Key`}
placeholder="Enter your API key"
value={value}

View File

@ -8,6 +8,7 @@ export const EmailField = () => {
return (
<TextInput
instanceId={`user-email-${currentUser?.id}`}
value={currentUser?.email}
disabled
fullWidth

View File

@ -1,6 +1,6 @@
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useEffect, useState } from 'react';
import styled from '@emotion/styled';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useDebouncedCallback } from 'use-debounce';
@ -109,9 +109,13 @@ export const NameFields = ({
currentWorkspaceMember,
]);
const firstNameTextInputId = `${currentWorkspaceMember?.id}-first-name`;
const lastNameTextInputId = `${currentWorkspaceMember?.id}-last-name`;
return (
<StyledComboInputContainer>
<TextInput
instanceId={firstNameTextInputId}
label={t`First Name`}
value={firstName}
onChange={setFirstName}
@ -119,6 +123,7 @@ export const NameFields = ({
fullWidth
/>
<TextInput
instanceId={lastNameTextInputId}
label={t`Last Name`}
value={lastName}
onChange={setLastName}

View File

@ -210,6 +210,7 @@ export const SettingsRoleAssignment = ({
/>
<StyledSearchContainer>
<StyledSearchInput
instanceId="role-assignment-member-search"
value={searchFilter}
onChange={handleSearchChange}
placeholder={t`Search an assigned team member...`}

View File

@ -125,6 +125,7 @@ export const SettingsRolePermissionsObjectLevelObjectPicker = ({
<Section>
<StyledSearchContainer>
<StyledSearchInput
instanceId="role-permissions-object-search"
value={searchFilter}
onChange={handleSearchChange}
placeholder={t`Search an object`}

View File

@ -42,6 +42,9 @@ export const SettingsRoleSettings = ({
const { openModal } = useModal();
const descriptionTextAreaId = `${roleId}-description`;
const nameTextInputId = `${roleId}-name`;
return (
<>
<Section>
@ -60,6 +63,7 @@ export const SettingsRoleSettings = ({
/>
</StyledInputContainer>
<TextInput
instanceId={nameTextInputId}
value={settingsDraftRole.label}
fullWidth
onChange={(value: string) => {
@ -73,6 +77,7 @@ export const SettingsRoleSettings = ({
/>
</StyledInputsContainer>
<TextArea
textAreaId={descriptionTextAreaId}
minRows={4}
placeholder={t`Write a description`}
value={settingsDraftRole.description || ''}

View File

@ -39,6 +39,7 @@ export const SettingsRoleLabelContainer = ({
return (
<StyledHeaderTitle>
<TitleInput
instanceId="role-label-input"
disabled={!settingsDraftRole.isEditable}
sizeVariant="md"
value={settingsDraftRole.label}

View File

@ -7,12 +7,12 @@ import { SettingsSSOSAMLForm } from '@/settings/security/components/SSO/Settings
import { SettingSecurityNewSSOIdentityFormValues } from '@/settings/security/types/SSOIdentityProvider';
import { TextInput } from '@/ui/input/components/TextInput';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { ReactElement, useMemo } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { IdentityProviderType } from '~/generated/graphql';
import { t } from '@lingui/core/macro';
import { H2Title, IconComponent, IconKey } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { IdentityProviderType } from '~/generated/graphql';
const StyledInputsContainer = styled.div`
display: grid;
@ -88,6 +88,7 @@ export const SettingsSSOIdentitiesProvidersForm = () => {
control={control}
render={({ field: { onChange, value } }) => (
<TextInput
instanceId="sso-identity-provider-name"
autoComplete="off"
label={t`Name`}
value={value}

View File

@ -54,6 +54,7 @@ export const SettingsSSOOIDCForm = () => {
<StyledContainer>
<StyledLinkContainer>
<TextInput
instanceId="sso-oidc-authorized-uri"
readOnly={true}
label={t`Authorized URI`}
value={authorizedUrl}
@ -81,6 +82,7 @@ export const SettingsSSOOIDCForm = () => {
<StyledContainer>
<StyledLinkContainer>
<TextInput
instanceId="sso-oidc-redirection-uri"
readOnly={true}
label={t`Redirection URI`}
value={redirectionUrl}
@ -118,6 +120,7 @@ export const SettingsSSOOIDCForm = () => {
control={control}
render={({ field: { onChange, value } }) => (
<TextInput
instanceId="sso-oidc-client-id"
autoComplete="off"
label={t`Client ID`}
value={value}
@ -132,6 +135,7 @@ export const SettingsSSOOIDCForm = () => {
control={control}
render={({ field: { onChange, value } }) => (
<TextInput
instanceId="sso-oidc-client-secret"
autoComplete="off"
type="password"
label={t`Client Secret`}
@ -147,6 +151,7 @@ export const SettingsSSOOIDCForm = () => {
control={control}
render={({ field: { onChange, value } }) => (
<TextInput
instanceId="sso-oidc-issuer"
autoComplete="off"
label={t`Issuer URI`}
value={value}

View File

@ -169,6 +169,7 @@ export const SettingsSSOSAMLForm = () => {
<StyledContainer>
<StyledLinkContainer>
<TextInput
instanceId="sso-saml-acs-url"
disabled={true}
label="ACS Url"
value={acsUrl}
@ -196,6 +197,7 @@ export const SettingsSSOSAMLForm = () => {
<StyledContainer>
<StyledLinkContainer>
<TextInput
instanceId="sso-saml-entity-id"
disabled={true}
label="Entity ID"
value={entityID}

View File

@ -18,11 +18,15 @@ export const SettingsServerlessFunctionNewForm = ({
formValues: ServerlessFunctionNewFormValues;
onChange: (key: string) => (value: string) => void;
}) => {
const descriptionTextAreaId = `${formValues.name}-description`;
const nameTextInputId = `${formValues.name}-name`;
return (
<Section>
<H2Title title="About" description="Name and set your function" />
<StyledInputsContainer>
<TextInput
instanceId={nameTextInputId}
placeholder="Name"
fullWidth
autoFocusOnMount
@ -30,6 +34,7 @@ export const SettingsServerlessFunctionNewForm = ({
onChange={onChange('name')}
/>
<TextArea
textAreaId={descriptionTextAreaId}
placeholder="Description"
minRows={4}
value={formValues.description}

Some files were not shown because too many files have changed in this diff Show More