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, placeholder,
icon, icon,
}) => { }) => {
const inputId = useId(); const instanceId = useId();
return ( return (
<StyledContainer fullWidth={fullWidth}> <StyledContainer fullWidth={fullWidth}>
{label && <StyledLabel htmlFor={inputId}>{label}</StyledLabel>} {label && <StyledLabel htmlFor={instanceId}>{label}</StyledLabel>}
<StyledInputContainer> <StyledInputContainer>
{icon && <StyledIcon>{icon}</StyledIcon>} {icon && <StyledIcon>{icon}</StyledIcon>}
<StyledInput <StyledInput
id={inputId} id={instanceId}
type="text" type="text"
value={value} value={value}
onChange={(e) => onChange(e.target.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 { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox'; 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 { Chip, ChipAccent, ChipSize, ChipVariant } from 'twenty-ui/components';
import { IconCalendarEvent } from 'twenty-ui/display'; import { IconCalendarEvent } from 'twenty-ui/display';
import { mapArrayToObject } from '~/utils/array/mapArrayToObject'; import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
@ -20,6 +20,8 @@ type CalendarEventDetailsProps = {
calendarEvent: CalendarEvent; calendarEvent: CalendarEvent;
}; };
const INPUT_ID_PREFIX = 'calendar-event-details';
const StyledContainer = styled.div` const StyledContainer = styled.div`
background: ${({ theme }) => theme.background.secondary}; background: ${({ theme }) => theme.background.secondary};
align-items: flex-start; align-items: flex-start;
@ -111,10 +113,14 @@ export const CalendarEventDetails = ({
> >
<RecordFieldComponentInstanceContext.Provider <RecordFieldComponentInstanceContext.Provider
value={{ 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> </RecordFieldComponentInstanceContext.Provider>
</FieldContext.Provider> </FieldContext.Provider>
</StyledPropertyBox> </StyledPropertyBox>

View File

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

View File

@ -165,6 +165,7 @@ export const AttachmentRow = ({
<AttachmentIcon attachmentType={attachment.type} /> <AttachmentIcon attachmentType={attachment.type} />
{isEditing ? ( {isEditing ? (
<TextInput <TextInput
instanceId={`attachment-${attachment.id}-name`}
value={attachmentFileName} value={attachmentFileName}
onChange={handleOnChange} onChange={handleOnChange}
onBlur={handleOnBlur} 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 { TextInput } from '@/ui/input/components/TextInput';
import { Controller, useFormContext } from 'react-hook-form';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { motion } from 'framer-motion'; 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'; import { isDefined } from 'twenty-shared/utils';
const StyledFullWidthMotionDiv = styled(motion.div)` const StyledFullWidthMotionDiv = styled(motion.div)`
@ -41,6 +41,7 @@ export const SignInUpEmailField = ({
}) => ( }) => (
<StyledInputContainer> <StyledInputContainer>
<TextInput <TextInput
instanceId="sign-in-up-email"
autoFocus autoFocus
value={value} value={value}
placeholder="Email" placeholder="Email"

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardCardBodyContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBodyContainer'; 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 { 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 { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
import { RecordBoardFieldDefinition } from '@/object-record/record-board/types/RecordBoardFieldDefinition'; import { RecordBoardFieldDefinition } from '@/object-record/record-board/types/RecordBoardFieldDefinition';
import { import {
@ -13,7 +14,7 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon'; import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly'; import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; 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'; import { useContext } from 'react';
export const RecordBoardCardBody = ({ export const RecordBoardCardBody = ({
@ -73,14 +74,16 @@ export const RecordBoardCardBody = ({
> >
<RecordFieldComponentInstanceContext.Provider <RecordFieldComponentInstanceContext.Provider
value={{ value={{
instanceId: getRecordFieldInputId( instanceId: getRecordFieldInputInstanceId({
recordId, recordId,
fieldDefinition.metadata.fieldName, fieldName: fieldDefinition.metadata.fieldName,
'record-board-card', prefix: RECORD_BOARD_CARD_INPUT_ID_PREFIX,
), }),
}} }}
> >
<RecordInlineCell /> <RecordInlineCell
instanceIdPrefix={RECORD_BOARD_CARD_INPUT_ID_PREFIX}
/>
</RecordFieldComponentInstanceContext.Provider> </RecordFieldComponentInstanceContext.Provider>
</FieldContext.Provider> </FieldContext.Provider>
</StopPropagationContainer> </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, readonly,
VariablePicker, VariablePicker,
}: FormBooleanFieldInputProps) => { }: FormBooleanFieldInputProps) => {
const inputId = useId(); const instanceId = useId();
const [draftValue, setDraftValue] = useState< const [draftValue, setDraftValue] = useState<
| { | {
@ -105,7 +105,7 @@ export const FormBooleanFieldInput = ({
{VariablePicker && !readonly ? ( {VariablePicker && !readonly ? (
<VariablePicker <VariablePicker
inputId={inputId} instanceId={instanceId}
onVariableSelect={handleVariableTagInsert} onVariableSelect={handleVariableTagInsert}
/> />
) : null} ) : null}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,11 +15,11 @@ import {
} from '@/object-record/record-field/types/FieldMetadata'; } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects'; import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject'; 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 { INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY } from '@/object-record/record-inline-cell/constants/InlineCellHotkeyScopeMemoizeKey';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; 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 { 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 { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById'; import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
@ -41,9 +41,11 @@ export const useOpenFieldInputEditMode = () => {
({ ({
fieldDefinition, fieldDefinition,
recordId, recordId,
prefix,
}: { }: {
fieldDefinition: FieldDefinition<FieldMetadata>; fieldDefinition: FieldDefinition<FieldMetadata>;
recordId: string; recordId: string;
prefix?: string;
}) => { }) => {
if ( if (
isFieldRelationFromManyObjects(fieldDefinition) && isFieldRelationFromManyObjects(fieldDefinition) &&
@ -75,9 +77,10 @@ export const useOpenFieldInputEditMode = () => {
}); });
openActivityTargetCellEditMode({ openActivityTargetCellEditMode({
recordPickerInstanceId: getFieldInputInstanceId({ recordPickerInstanceId: getRecordFieldInputInstanceId({
recordId, recordId,
fieldName: fieldDefinition.metadata.fieldName, fieldName: fieldDefinition.metadata.fieldName,
prefix,
}), }),
activityTargetObjectRecords, activityTargetObjectRecords,
}); });
@ -87,7 +90,8 @@ export const useOpenFieldInputEditMode = () => {
if (isFieldRelationToOneObject(fieldDefinition)) { if (isFieldRelationToOneObject(fieldDefinition)) {
openRelationToOneFieldInput({ openRelationToOneFieldInput({
fieldName: fieldDefinition.metadata.fieldName, fieldName: fieldDefinition.metadata.fieldName,
recordId: recordId, recordId,
prefix,
}); });
return; return;
@ -103,22 +107,25 @@ export const useOpenFieldInputEditMode = () => {
fieldName: fieldDefinition.metadata.fieldName, fieldName: fieldDefinition.metadata.fieldName,
objectNameSingular: objectNameSingular:
fieldDefinition.metadata.relationObjectMetadataNameSingular, fieldDefinition.metadata.relationObjectMetadataNameSingular,
recordId: recordId, recordId,
prefix,
}); });
return; return;
} }
} }
pushFocusItemToFocusStack({ pushFocusItemToFocusStack({
focusId: getFieldInputInstanceId({ focusId: getRecordFieldInputInstanceId({
recordId, recordId,
fieldName: fieldDefinition.metadata.fieldName, fieldName: fieldDefinition.metadata.fieldName,
prefix,
}), }),
component: { component: {
type: FocusComponentType.OPENED_FIELD_INPUT, type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: getFieldInputInstanceId({ instanceId: getRecordFieldInputInstanceId({
recordId, recordId,
fieldName: fieldDefinition.metadata.fieldName, fieldName: fieldDefinition.metadata.fieldName,
prefix,
}), }),
}, },
hotkeyScope: { hotkeyScope: {
@ -142,14 +149,17 @@ export const useOpenFieldInputEditMode = () => {
const closeFieldInput = ({ const closeFieldInput = ({
fieldDefinition, fieldDefinition,
recordId, recordId,
prefix,
}: { }: {
fieldDefinition: FieldDefinition<FieldMetadata>; fieldDefinition: FieldDefinition<FieldMetadata>;
recordId: string; recordId: string;
prefix?: string;
}) => { }) => {
removeFocusItemFromFocusStackById({ removeFocusItemFromFocusStackById({
focusId: getFieldInputInstanceId({ focusId: getRecordFieldInputInstanceId({
recordId, recordId,
fieldName: fieldDefinition.metadata.fieldName, 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 { FieldAddressDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
import { AddressInput } from '@/ui/field/input/components/AddressInput'; import { AddressInput } from '@/ui/field/input/components/AddressInput';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { import {
FieldInputClickOutsideEvent, FieldInputClickOutsideEvent,
FieldInputEvent, FieldInputEvent,
} from '@/object-record/record-field/types/FieldInputEvent'; } from '@/object-record/record-field/types/FieldInputEvent';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; 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'; import { usePersistField } from '../../../hooks/usePersistField';
export type AddressFieldInputProps = { export type AddressFieldInputProps = {
@ -70,8 +72,13 @@ export const AddressFieldInput = ({
setDraftValue(convertToAddress(newAddress)); setDraftValue(convertToAddress(newAddress));
}; };
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
return ( return (
<AddressInput <AddressInput
instanceId={instanceId}
value={convertToAddress(draftValue)} value={convertToAddress(draftValue)}
onClickOutside={handleClickOutside} onClickOutside={handleClickOutside}
onEnter={handleEnter} onEnter={handleEnter}

View File

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

View File

@ -1,8 +1,10 @@
import { useDateField } from '@/object-record/record-field/meta-types/hooks/useDateField'; import { useDateField } from '@/object-record/record-field/meta-types/hooks/useDateField';
import { DateInput } from '@/ui/field/input/components/DateInput'; 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 { FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; 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 { isDefined } from 'twenty-shared/utils';
import { Nullable } from 'twenty-ui/utilities'; import { Nullable } from 'twenty-ui/utilities';
import { usePersistField } from '../../../hooks/usePersistField'; import { usePersistField } from '../../../hooks/usePersistField';
@ -26,6 +28,10 @@ export const DateFieldInput = ({
}: DateFieldInputProps) => { }: DateFieldInputProps) => {
const { fieldValue, setDraftValue } = useDateField(); const { fieldValue, setDraftValue } = useDateField();
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
const persistField = usePersistField(); const persistField = usePersistField();
const persistDate = (newDate: Nullable<Date>) => { const persistDate = (newDate: Nullable<Date>) => {
@ -73,6 +79,7 @@ export const DateFieldInput = ({
return ( return (
<DateInput <DateInput
instanceId={instanceId}
onClickOutside={handleClickOutside} onClickOutside={handleClickOutside}
onEnter={handleEnter} onEnter={handleEnter}
onEscape={handleEscape} 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 { 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 { FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; 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 { Nullable } from 'twenty-ui/utilities';
import { usePersistField } from '../../../hooks/usePersistField'; import { usePersistField } from '../../../hooks/usePersistField';
import { useDateTimeField } from '../../hooks/useDateTimeField'; import { useDateTimeField } from '../../hooks/useDateTimeField';
@ -25,6 +27,10 @@ export const DateTimeFieldInput = ({
}: DateTimeFieldInputProps) => { }: DateTimeFieldInputProps) => {
const { fieldValue, setDraftValue } = useDateTimeField(); const { fieldValue, setDraftValue } = useDateTimeField();
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
const persistField = usePersistField(); const persistField = usePersistField();
const persistDate = (newDate: Nullable<Date>) => { const persistDate = (newDate: Nullable<Date>) => {
@ -68,6 +74,7 @@ export const DateTimeFieldInput = ({
return ( return (
<DateInput <DateInput
instanceId={instanceId}
onClickOutside={handleClickOutside} onClickOutside={handleClickOutside}
onEnter={handleEnter} onEnter={handleEnter}
onEscape={handleEscape} 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 { 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 { 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 { isDoubleTextFieldEmpty } from '@/object-record/record-field/meta-types/input/utils/isDoubleTextFieldEmpty';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { import {
FieldInputClickOutsideEvent, FieldInputClickOutsideEvent,
FieldInputEvent, FieldInputEvent,
} from '@/object-record/record-field/types/FieldInputEvent'; } from '@/object-record/record-field/types/FieldInputEvent';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; 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 = { type FullNameFieldInputProps = {
onClickOutside?: FieldInputClickOutsideEvent; onClickOutside?: FieldInputClickOutsideEvent;
@ -78,8 +80,13 @@ export const FullNameFieldInput = ({
setDraftValue(getRequiredDraftValueFromDoubleText(newDoubleText)); setDraftValue(getRequiredDraftValueFromDoubleText(newDoubleText));
}; };
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
return ( return (
<DoubleTextInput <DoubleTextInput
instanceId={instanceId}
firstValue={draftValue?.firstName ?? ''} firstValue={draftValue?.firstName ?? ''}
secondValue={draftValue?.lastName ?? ''} secondValue={draftValue?.lastName ?? ''}
firstValuePlaceholder={ firstValuePlaceholder={

View File

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

View File

@ -6,13 +6,15 @@ import {
MultiItemBaseInput, MultiItemBaseInput,
MultiItemBaseInputProps, MultiItemBaseInputProps,
} from '@/object-record/record-field/meta-types/input/components/MultiItemBaseInput'; } 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 { FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { PhoneRecord } from '@/object-record/record-field/types/FieldMetadata'; import { PhoneRecord } from '@/object-record/record-field/types/FieldMetadata';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; 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 { 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 { IconCheck, IconPlus } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input'; import { LightIconButton } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation'; import { MenuItem } from 'twenty-ui/navigation';
@ -79,7 +81,17 @@ export const MultiItemFieldInput = <T,>({
listenerId: hotkeyScope, 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 [isInputDisplayed, setIsInputDisplayed] = useState(false);
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
@ -204,6 +216,7 @@ export const MultiItemFieldInput = <T,>({
)} )}
{isInputDisplayed || !items.length ? ( {isInputDisplayed || !items.length ? (
<MultiItemBaseInput <MultiItemBaseInput
instanceId={instanceId}
autoFocus autoFocus
placeholder={placeholder} placeholder={placeholder}
value={inputValue} value={inputValue}

View File

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

View File

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

View File

@ -1,13 +1,15 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { isWorkflowRunJsonField } from '@/object-record/record-field/meta-types/utils/isWorkflowRunJsonField'; import { isWorkflowRunJsonField } from '@/object-record/record-field/meta-types/utils/isWorkflowRunJsonField';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { import {
FieldInputClickOutsideEvent, FieldInputClickOutsideEvent,
FieldInputEvent, FieldInputEvent,
} from '@/object-record/record-field/types/FieldInputEvent'; } from '@/object-record/record-field/types/FieldInputEvent';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; 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 { 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 { useLingui } from '@lingui/react/macro';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
@ -70,6 +72,9 @@ export const RawJsonFieldInput = ({
const hotkeyScope = DEFAULT_CELL_SCOPE.scope; const hotkeyScope = DEFAULT_CELL_SCOPE.scope;
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
@ -104,32 +109,35 @@ export const RawJsonFieldInput = ({
listenerId: hotkeyScope, listenerId: hotkeyScope,
}); });
useScopedHotkeys( useHotkeysOnFocusedElement({
[Key.Escape], keys: [Key.Escape],
() => { callback: () => {
handleEscape(draftValue ?? ''); handleEscape(draftValue ?? '');
}, },
hotkeyScope, scope: hotkeyScope,
[handleEscape, draftValue], focusId: instanceId,
); dependencies: [handleEscape, draftValue],
});
useScopedHotkeys( useHotkeysOnFocusedElement({
'tab', keys: ['tab'],
() => { callback: () => {
handleTab(draftValue ?? ''); handleTab(draftValue ?? '');
}, },
hotkeyScope, scope: hotkeyScope,
[handleTab, draftValue], focusId: instanceId,
); dependencies: [handleTab, draftValue],
});
useScopedHotkeys( useHotkeysOnFocusedElement({
'shift+tab', keys: ['shift+tab'],
() => { callback: () => {
handleShiftTab(draftValue ?? ''); handleShiftTab(draftValue ?? '');
}, },
hotkeyScope, scope: hotkeyScope,
[handleShiftTab, draftValue], focusId: instanceId,
); dependencies: [handleShiftTab, draftValue],
});
const showEditingButton = !isWorkflowRunJsonField({ const showEditingButton = !isWorkflowRunJsonField({
objectMetadataNameSingular: 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 { useRelationField } from '@/object-record/record-field/meta-types/hooks/useRelationField';
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer'; 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 { 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 { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; 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 { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker';
import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch'; 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 { 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 { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
@ -31,10 +32,9 @@ export const RelationFromManyFieldInput = ({
onSubmit, onSubmit,
}: RelationFromManyFieldInputProps) => { }: RelationFromManyFieldInputProps) => {
const { fieldDefinition, recordId } = useContext(FieldContext); const { fieldDefinition, recordId } = useContext(FieldContext);
const recordPickerInstanceId = getFieldInputInstanceId({ const instanceId = useAvailableComponentInstanceIdOrThrow(
recordId, RecordFieldComponentInstanceContext,
fieldName: fieldDefinition.metadata.fieldName, );
});
const { updateRelation } = useUpdateRelationFromManyFieldInput(); const { updateRelation } = useUpdateRelationFromManyFieldInput();
const fieldName = fieldDefinition.metadata.fieldName; const fieldName = fieldDefinition.metadata.fieldName;
@ -94,7 +94,7 @@ export const RelationFromManyFieldInput = ({
const multipleRecordPickerPickableMorphItemsCallbackState = const multipleRecordPickerPickableMorphItemsCallbackState =
useRecoilComponentCallbackStateV2( useRecoilComponentCallbackStateV2(
multipleRecordPickerPickableMorphItemsComponentState, multipleRecordPickerPickableMorphItemsComponentState,
recordPickerInstanceId, instanceId,
); );
const { performSearch: multipleRecordPickerPerformSearch } = const { performSearch: multipleRecordPickerPerformSearch } =
useMultipleRecordPickerPerformSearch(); useMultipleRecordPickerPerformSearch();
@ -123,7 +123,7 @@ export const RelationFromManyFieldInput = ({
set(multipleRecordPickerPickableMorphItemsCallbackState, newMorphItems); set(multipleRecordPickerPickableMorphItemsCallbackState, newMorphItems);
multipleRecordPickerPerformSearch({ multipleRecordPickerPerformSearch({
multipleRecordPickerInstanceId: recordPickerInstanceId, multipleRecordPickerInstanceId: instanceId,
forceSearchFilter: searchInput, forceSearchFilter: searchInput,
forceSearchableObjectMetadataItems: [relationObjectMetadataItem], forceSearchableObjectMetadataItems: [relationObjectMetadataItem],
forcePickableMorphItems: newMorphItems, forcePickableMorphItems: newMorphItems,
@ -132,7 +132,7 @@ export const RelationFromManyFieldInput = ({
[ [
createNewRecordAndOpenRightDrawer, createNewRecordAndOpenRightDrawer,
relationObjectMetadataItem, relationObjectMetadataItem,
recordPickerInstanceId, instanceId,
multipleRecordPickerPickableMorphItemsCallbackState, multipleRecordPickerPickableMorphItemsCallbackState,
multipleRecordPickerPerformSearch, multipleRecordPickerPerformSearch,
], ],
@ -142,15 +142,15 @@ export const RelationFromManyFieldInput = ({
return ( return (
<MultipleRecordPicker <MultipleRecordPicker
focusId={recordPickerInstanceId} focusId={instanceId}
componentInstanceId={recordPickerInstanceId} componentInstanceId={instanceId}
onSubmit={handleSubmit} onSubmit={handleSubmit}
onChange={(morphItem) => { onChange={(morphItem) => {
if (isRelationFromActivityTargets) { if (isRelationFromActivityTargets) {
updateActivityTargetFromCell({ updateActivityTargetFromCell({
morphItem, morphItem,
activityTargetWithTargetRecords: activityTargetObjectRecords, activityTargetWithTargetRecords: activityTargetObjectRecords,
recordPickerInstanceId, recordPickerInstanceId: instanceId,
}); });
} else { } else {
updateRelation(morphItem); updateRelation(morphItem);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,9 +5,11 @@ import { useEffect } from 'react';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField'; import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; 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 { 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 { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing'; import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import { LinksFieldInput } from '../LinksFieldInput'; import { LinksFieldInput } from '../LinksFieldInput';
@ -38,7 +40,7 @@ type LinksInputWithContextProps = {
primaryLinkLabel: string | null; primaryLinkLabel: string | null;
secondaryLinks: Array<{ url: string | null; label: string | null }> | null; secondaryLinks: Array<{ url: string | null; label: string | null }> | null;
}; };
recordId?: string; recordId: string;
onCancel?: () => void; onCancel?: () => void;
onClickOutside?: (event: MouseEvent | TouchEvent) => void; onClickOutside?: (event: MouseEvent | TouchEvent) => void;
}; };
@ -67,21 +69,29 @@ const LinksInputWithContext = ({
onCancel, onCancel,
onClickOutside, onClickOutside,
}: LinksInputWithContextProps) => { }: LinksInputWithContextProps) => {
const setHotkeyScope = useSetHotkeyScope(); const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const instanceId = getRecordFieldInputInstanceId({
recordId,
fieldName: 'Links',
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
});
useEffect(() => { useEffect(() => {
setHotkeyScope(DEFAULT_CELL_SCOPE.scope); pushFocusItemToFocusStack({
}, [setHotkeyScope]); focusId: instanceId,
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: instanceId,
},
hotkeyScope: DEFAULT_CELL_SCOPE,
});
}, [pushFocusItemToFocusStack, instanceId]);
return ( return (
<div> <div>
<RecordFieldComponentInstanceContext.Provider <RecordFieldComponentInstanceContext.Provider
value={{ value={{
instanceId: getRecordFieldInputId( instanceId: instanceId,
recordId ?? '',
'Links',
'record-table-cell',
),
}} }}
> >
<FieldContext.Provider <FieldContext.Provider
@ -97,7 +107,7 @@ const LinksInputWithContext = ({
objectMetadataNameSingular: 'company', objectMetadataNameSingular: 'company',
}, },
}, },
recordId: recordId ?? '123', recordId,
isLabelIdentifier: false, isLabelIdentifier: false,
isReadOnly: false, isReadOnly: false,
useUpdateRecord: () => [updateRecord, { loading: false }], useUpdateRecord: () => [updateRecord, { loading: false }],
@ -131,6 +141,7 @@ const meta: Meta = {
primaryLinkLabel: null, primaryLinkLabel: null,
secondaryLinks: null, secondaryLinks: null,
}, },
recordId: '123',
onCancel: cancelJestFn, onCancel: cancelJestFn,
onClickOutside: clickOutsideJestFn, 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 { expect, fn, userEvent, waitFor, within } from '@storybook/test';
import { useEffect, useState } from 'react'; 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 { FieldMetadataType } from '~/generated-metadata/graphql';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; 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 { 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 { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { useNumberField } from '../../../hooks/useNumberField'; import { useNumberField } from '../../../hooks/useNumberField';
@ -27,7 +29,7 @@ const NumberFieldValueSetterEffect = ({ value }: { value: number }) => {
type NumberFieldInputWithContextProps = NumberFieldInputProps & { type NumberFieldInputWithContextProps = NumberFieldInputProps & {
value: number; value: number;
recordId?: string; recordId: string;
}; };
const NumberFieldInputWithContext = ({ const NumberFieldInputWithContext = ({
@ -39,25 +41,38 @@ const NumberFieldInputWithContext = ({
onTab, onTab,
onShiftTab, onShiftTab,
}: NumberFieldInputWithContextProps) => { }: NumberFieldInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope(); const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const [isReady, setIsReady] = useState(false); const [isReady, setIsReady] = useState(false);
const instanceId = getRecordFieldInputInstanceId({
recordId,
fieldName: 'Number',
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
});
useEffect(() => { useEffect(() => {
if (!isReady) { if (!isReady) {
setHotKeyScope(DEFAULT_CELL_SCOPE.scope); pushFocusItemToFocusStack({
focusId: instanceId,
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: instanceId,
},
hotkeyScope: DEFAULT_CELL_SCOPE,
});
setIsReady(true); setIsReady(true);
} }
}, [isReady, setHotKeyScope]); }, [isReady, pushFocusItemToFocusStack, instanceId]);
return ( return (
<RecordFieldComponentInstanceContext.Provider <RecordFieldComponentInstanceContext.Provider
value={{ value={{
instanceId: getRecordFieldInputId( instanceId: getRecordFieldInputInstanceId({
recordId ?? '', recordId,
'Number', fieldName: 'Number',
'record-table-cell', prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
), }),
}} }}
> >
<FieldContext.Provider <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 { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { FieldInputClickOutsideEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata'; 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 { 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 { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import { PhonesFieldInput } from '../PhonesFieldInput'; import { PhonesFieldInput } from '../PhonesFieldInput';
@ -58,20 +60,28 @@ const PhoneInputWithContext = ({
onCancel, onCancel,
onClickOutside, onClickOutside,
}: PhoneInputWithContextProps) => { }: PhoneInputWithContextProps) => {
const setHotkeyScope = useSetHotkeyScope(); const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const instanceId = getRecordFieldInputInstanceId({
recordId,
fieldName: 'phones',
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
});
useEffect(() => { useEffect(() => {
setHotkeyScope(DEFAULT_CELL_SCOPE.scope); pushFocusItemToFocusStack({
}, [setHotkeyScope]); focusId: instanceId,
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: instanceId,
},
hotkeyScope: DEFAULT_CELL_SCOPE,
});
}, [pushFocusItemToFocusStack, instanceId]);
return ( return (
<RecordFieldComponentInstanceContext.Provider <RecordFieldComponentInstanceContext.Provider
value={{ value={{
instanceId: getRecordFieldInputId( instanceId: instanceId,
recordId,
'phones',
'record-table-cell',
),
}} }}
> >
<FieldContext.Provider <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 { expect, fn, userEvent, waitFor, within } from '@storybook/test';
import { useEffect } from 'react'; 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 { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; 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 { 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 { FieldMetadataType } from 'twenty-shared/types';
import { FieldRatingValue } from '../../../../types/FieldMetadata'; import { FieldRatingValue } from '../../../../types/FieldMetadata';
import { useRatingField } from '../../../hooks/useRatingField'; import { useRatingField } from '../../../hooks/useRatingField';
@ -29,7 +31,7 @@ const RatingFieldValueSetterEffect = ({
type RatingFieldInputWithContextProps = RatingFieldInputProps & { type RatingFieldInputWithContextProps = RatingFieldInputProps & {
value: FieldRatingValue; value: FieldRatingValue;
recordId?: string; recordId: string;
}; };
const RatingFieldInputWithContext = ({ const RatingFieldInputWithContext = ({
@ -37,20 +39,29 @@ const RatingFieldInputWithContext = ({
value, value,
onSubmit, onSubmit,
}: RatingFieldInputWithContextProps) => { }: RatingFieldInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope(); const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const instanceId = getRecordFieldInputInstanceId({
recordId,
fieldName: 'Rating',
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
});
useEffect(() => { useEffect(() => {
setHotKeyScope(DEFAULT_CELL_SCOPE.scope); pushFocusItemToFocusStack({
}, [setHotKeyScope]); focusId: instanceId,
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: instanceId,
},
hotkeyScope: DEFAULT_CELL_SCOPE,
});
}, [pushFocusItemToFocusStack, instanceId]);
return ( return (
<RecordFieldComponentInstanceContext.Provider <RecordFieldComponentInstanceContext.Provider
value={{ value={{
instanceId: getRecordFieldInputId( instanceId: instanceId,
recordId ?? '',
'Rating',
'record-table-cell',
),
}} }}
> >
<FieldContext.Provider <FieldContext.Provider

View File

@ -5,7 +5,7 @@ import { useSetRecoilState } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { RelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput'; 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 { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; 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 { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode'; import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; 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 { 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 { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from '~/generated-metadata/graphql'; import { RelationType } from '~/generated-metadata/graphql';
@ -40,7 +40,7 @@ const RelationWorkspaceSetterEffect = () => {
}; };
const RelationManyFieldInputWithContext = () => { const RelationManyFieldInputWithContext = () => {
const setHotKeyScope = useSetHotkeyScope(); const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const fieldDefinition = useMemo( const fieldDefinition = useMemo(
() => ({ () => ({
@ -72,7 +72,14 @@ const RelationManyFieldInputWithContext = () => {
useEffect(() => { useEffect(() => {
setRecordStoreFieldValue([]); 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({ openFieldInput({
fieldDefinition, fieldDefinition,
recordId: 'recordId', recordId: 'recordId',
@ -80,7 +87,7 @@ const RelationManyFieldInputWithContext = () => {
}, [ }, [
fieldDefinition, fieldDefinition,
openFieldInput, openFieldInput,
setHotKeyScope, pushFocusItemToFocusStack,
setRecordStoreFieldValue, setRecordStoreFieldValue,
]); ]);
@ -88,10 +95,7 @@ const RelationManyFieldInputWithContext = () => {
<div> <div>
<RecordFieldComponentInstanceContext.Provider <RecordFieldComponentInstanceContext.Provider
value={{ value={{
instanceId: getFieldInputInstanceId({ instanceId: 'relation-from-many-field-input',
recordId: 'recordId',
fieldName: 'people',
}),
}} }}
> >
<FieldContext.Provider <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 { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState'; import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState';
import { getFieldInputInstanceId } from '@/object-record/record-field/utils/getFieldInputInstanceId'; import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
@ -68,24 +67,13 @@ const RelationToOneFieldInputWithContext = ({
useEffect(() => { useEffect(() => {
pushFocusItemToFocusStack({ pushFocusItemToFocusStack({
focusId: getFieldInputInstanceId({ focusId: 'relation-to-one-field-input',
recordId: '123',
fieldName: 'Relation',
}),
component: { component: {
type: FocusComponentType.DROPDOWN, type: FocusComponentType.DROPDOWN,
instanceId: getFieldInputInstanceId({ instanceId: 'relation-to-one-field-input',
recordId: '123',
fieldName: 'Relation',
}),
}, },
hotkeyScope: { hotkeyScope: DEFAULT_CELL_SCOPE,
scope: DropdownHotkeyScope.Dropdown, memoizeKey: 'relation-to-one-field-input',
},
memoizeKey: getFieldInputInstanceId({
recordId: '123',
fieldName: 'Relation',
}),
}); });
}, [pushFocusItemToFocusStack]); }, [pushFocusItemToFocusStack]);

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { Key } from 'ts-key-enum'; 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 { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
@ -13,6 +13,7 @@ export const useRegisterInputEvents = <T>({
onTab, onTab,
onShiftTab, onShiftTab,
onClickOutside, onClickOutside,
focusId,
hotkeyScope, hotkeyScope,
}: { }: {
inputRef: React.RefObject<any>; inputRef: React.RefObject<any>;
@ -23,6 +24,7 @@ export const useRegisterInputEvents = <T>({
onTab?: (inputValue: T) => void; onTab?: (inputValue: T) => void;
onShiftTab?: (inputValue: T) => void; onShiftTab?: (inputValue: T) => void;
onClickOutside?: (event: MouseEvent | TouchEvent, inputValue: T) => void; onClickOutside?: (event: MouseEvent | TouchEvent, inputValue: T) => void;
focusId: string;
hotkeyScope: string; hotkeyScope: string;
}) => { }) => {
useListenClickOutside({ useListenClickOutside({
@ -34,39 +36,43 @@ export const useRegisterInputEvents = <T>({
listenerId: hotkeyScope, listenerId: hotkeyScope,
}); });
useScopedHotkeys( useHotkeysOnFocusedElement({
'enter', keys: [Key.Enter],
() => { callback: () => {
onEnter?.(inputValue); onEnter?.(inputValue);
}, },
hotkeyScope, focusId,
[onEnter, inputValue], scope: hotkeyScope,
); dependencies: [onEnter, inputValue],
});
useScopedHotkeys( useHotkeysOnFocusedElement({
[Key.Escape], keys: [Key.Escape],
() => { callback: () => {
onEscape?.(inputValue); onEscape?.(inputValue);
}, },
hotkeyScope, focusId,
[onEscape, inputValue], scope: hotkeyScope,
); dependencies: [onEscape, inputValue],
});
useScopedHotkeys( useHotkeysOnFocusedElement({
'tab', keys: [Key.Tab],
() => { callback: () => {
onTab?.(inputValue); onTab?.(inputValue);
}, },
hotkeyScope, focusId,
[onTab, inputValue], scope: hotkeyScope,
); dependencies: [onTab, inputValue],
});
useScopedHotkeys( useHotkeysOnFocusedElement({
'shift+tab', keys: [`${Key.Shift}+${Key.Tab}`],
() => { callback: () => {
onShiftTab?.(inputValue); onShiftTab?.(inputValue);
}, },
hotkeyScope, focusId,
[onShiftTab, inputValue], 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'; } from './RecordInlineCellContext';
type RecordInlineCellProps = { type RecordInlineCellProps = {
readonly?: boolean;
loading?: boolean; loading?: boolean;
instanceIdPrefix?: string;
}; };
export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => { export const RecordInlineCell = ({
loading,
instanceIdPrefix,
}: RecordInlineCellProps) => {
const { const {
fieldDefinition, fieldDefinition,
recordId, recordId,
@ -47,13 +50,28 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
const onOpenEditMode = onOpenEditModeFromContext const onOpenEditMode = onOpenEditModeFromContext
? onOpenEditModeFromContext ? onOpenEditModeFromContext
: () => openFieldInput({ fieldDefinition, recordId }); : () =>
openFieldInput({
fieldDefinition,
recordId,
prefix: instanceIdPrefix,
});
const onCloseEditMode = useCallback(() => { const onCloseEditMode = useCallback(() => {
onCloseEditModeFromContext onCloseEditModeFromContext
? onCloseEditModeFromContext() ? onCloseEditModeFromContext()
: closeFieldInput({ fieldDefinition, recordId }); : closeFieldInput({
}, [onCloseEditModeFromContext, closeFieldInput, fieldDefinition, recordId]); fieldDefinition,
recordId,
prefix: instanceIdPrefix,
});
}, [
onCloseEditModeFromContext,
closeFieldInput,
fieldDefinition,
recordId,
instanceIdPrefix,
]);
const buttonIcon = useGetButtonIcon(); const buttonIcon = useGetButtonIcon();

View File

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

View File

@ -14,7 +14,6 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope'; import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; 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 { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRef } from 'react'; import { useRef } from 'react';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
@ -41,8 +40,6 @@ export const MultipleRecordPicker = ({
componentInstanceId, componentInstanceId,
focusId, focusId,
}: MultipleRecordPickerProps) => { }: MultipleRecordPickerProps) => {
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
const selectableListComponentInstanceId = const selectableListComponentInstanceId =
getMultipleRecordPickerSelectableListId(componentInstanceId); getMultipleRecordPickerSelectableListId(componentInstanceId);
@ -79,7 +76,6 @@ export const MultipleRecordPicker = ({
const handleSubmit = () => { const handleSubmit = () => {
onSubmit?.(); onSubmit?.();
goBackToPreviousHotkeyScope();
resetSelectedItem(); resetSelectedItem();
resetState(); 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 { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { RecordDetailDuplicatesSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailDuplicatesSection'; 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 { 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 { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
import { useIsInRightDrawerOrThrow } from '@/ui/layout/right-drawer/contexts/RightDrawerContext'; import { useIsInRightDrawerOrThrow } from '@/ui/layout/right-drawer/contexts/RightDrawerContext';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -28,6 +28,8 @@ type FieldsCardProps = {
objectRecordId: string; objectRecordId: string;
}; };
const INPUT_ID_PREFIX = 'fields-card';
export const FieldsCard = ({ export const FieldsCard = ({
objectNameSingular, objectNameSingular,
objectRecordId, objectRecordId,
@ -139,13 +141,13 @@ export const FieldsCard = ({
}} }}
> >
<ActivityTargetsInlineCell <ActivityTargetsInlineCell
componentInstanceId={getRecordFieldInputId( componentInstanceId={getRecordFieldInputInstanceId({
objectRecordId, recordId: objectRecordId,
fieldMetadataItem.name, fieldName: fieldMetadataItem.name,
isInRightDrawer prefix: isInRightDrawer
? 'right-drawer-fields-card' ? 'right-drawer-fields-card'
: 'fields-card', : 'fields-card',
)} })}
activityObjectNameSingular={ activityObjectNameSingular={
objectNameSingular as objectNameSingular as
| CoreObjectNameSingular.Note | CoreObjectNameSingular.Note
@ -185,14 +187,17 @@ export const FieldsCard = ({
> >
<RecordFieldComponentInstanceContext.Provider <RecordFieldComponentInstanceContext.Provider
value={{ value={{
instanceId: getRecordFieldInputId( instanceId: getRecordFieldInputInstanceId({
objectRecordId, recordId: objectRecordId,
fieldMetadataItem.name, fieldName: fieldMetadataItem.name,
'fields-card', prefix: INPUT_ID_PREFIX,
), }),
}} }}
> >
<RecordInlineCell loading={recordLoading} /> <RecordInlineCell
loading={recordLoading}
instanceIdPrefix={INPUT_ID_PREFIX}
/>
</RecordFieldComponentInstanceContext.Provider> </RecordFieldComponentInstanceContext.Provider>
</FieldContext.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 { getRecordFieldCardRelationPickerDropdownId } from '@/object-record/record-show/utils/getRecordFieldCardRelationPickerDropdownId';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName'; 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 { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
@ -320,14 +320,14 @@ export const RecordDetailRelationRecordsListItem = ({
> >
<RecordFieldComponentInstanceContext.Provider <RecordFieldComponentInstanceContext.Provider
value={{ value={{
instanceId: getRecordFieldInputId( instanceId: getRecordFieldInputInstanceId({
relationRecord.id, recordId: relationRecord.id,
fieldMetadataItem.name, fieldName: fieldMetadataItem.name,
'record-detail', prefix: 'record-detail',
), }),
}} }}
> >
<RecordInlineCell /> <RecordInlineCell instanceIdPrefix="record-detail" />
</RecordFieldComponentInstanceContext.Provider> </RecordFieldComponentInstanceContext.Provider>
</FieldContext.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 { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState'; import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { useBuildRecordInputFromFilters } from '@/object-record/record-table/hooks/useBuildRecordInputFromFilters'; 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 { AppPath } from '@/types/AppPath';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType'; import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { useNavigateApp } from '~/hooks/useNavigateApp'; import { useNavigateApp } from '~/hooks/useNavigateApp';
@ -58,11 +60,16 @@ export const useCreateNewIndexRecord = ({
isNewRecord: true, isNewRecord: true,
}); });
openRecordTitleCell({ const labelIdentifierFieldMetadataItem =
recordId, getLabelIdentifierFieldMetadataItem(objectMetadataItem);
fieldMetadataId: objectMetadataItem.labelIdentifierFieldMetadataId,
containerType: RecordTitleCellContainerType.ShowPage, if (isDefined(labelIdentifierFieldMetadataItem)) {
}); openRecordTitleCell({
recordId,
fieldName: labelIdentifierFieldMetadataItem.name,
containerType: RecordTitleCellContainerType.ShowPage,
});
}
} else { } else {
navigate(AppPath.RecordShowPage, { navigate(AppPath.RecordShowPage, {
objectNameSingular: objectMetadataItem.nameSingular, objectNameSingular: objectMetadataItem.nameSingular,
@ -74,8 +81,7 @@ export const useCreateNewIndexRecord = ({
buildRecordInputFromFilters, buildRecordInputFromFilters,
createOneRecord, createOneRecord,
navigate, navigate,
objectMetadataItem.labelIdentifierFieldMetadataId, objectMetadataItem,
objectMetadataItem.nameSingular,
openRecordInCommandMenu, openRecordInCommandMenu,
openRecordTitleCell, openRecordTitleCell,
], ],

View File

@ -1,11 +1,12 @@
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; 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 { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableCellFieldContextGeneric } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextGeneric'; 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 { 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 { ReactNode, useContext } from 'react';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@ -24,11 +25,11 @@ export const RecordTableCellFieldContextWrapper = ({
return null; return null;
} }
const instanceId = getRecordFieldInputId( const instanceId = getRecordFieldInputInstanceId({
recordId, recordId,
columnDefinition.metadata.fieldName, fieldName: columnDefinition.metadata.fieldName,
'record-table-cell', prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
); });
const isLabelIdentifier = isLabelIdentifierField({ const isLabelIdentifier = isLabelIdentifierField({
fieldMetadataItem: { 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 { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/FocusClickOutsideListenerId'; 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 { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/useLeaveTableFocus';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition'; import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect'; 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 { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
import { recordTableCellEditModePositionComponentState } from '@/object-record/record-table/states/recordTableCellEditModePositionComponentState'; import { recordTableCellEditModePositionComponentState } from '@/object-record/record-table/states/recordTableCellEditModePositionComponentState';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField'; 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 { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useOpenRecordFromIndexView } from '@/object-record/record-index/hooks/useOpenRecordFromIndexView'; import { useOpenRecordFromIndexView } from '@/object-record/record-index/hooks/useOpenRecordFromIndexView';
@ -166,6 +167,7 @@ export const useOpenRecordTableCellV2 = (recordTableId: string) => {
openFieldInput({ openFieldInput({
fieldDefinition, fieldDefinition,
recordId, recordId,
prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
}); });
setCurrentTableCellInEditModePosition(cellPosition); setCurrentTableCellInEditModePosition(cellPosition);
@ -174,11 +176,11 @@ export const useOpenRecordTableCellV2 = (recordTableId: string) => {
value: initialValue, value: initialValue,
recordId, recordId,
fieldDefinition, fieldDefinition,
fieldComponentInstanceId: getRecordFieldInputId( fieldComponentInstanceId: getRecordFieldInputInstanceId({
recordId, recordId,
fieldDefinition.metadata.fieldName, fieldName: fieldDefinition.metadata.fieldName,
'record-table-cell', prefix: RECORD_TABLE_CELL_INPUT_ID_PREFIX,
), }),
}); });
toggleClickOutside(false); 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 { RecordTitleCellFieldInput } from '@/object-record/record-title-cell/components/RecordTitleCellFieldInput';
import { useRecordTitleCell } from '@/object-record/record-title-cell/hooks/useRecordTitleCell'; import { useRecordTitleCell } from '@/object-record/record-title-cell/hooks/useRecordTitleCell';
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType'; 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 = { type RecordTitleCellProps = {
loading?: boolean; loading?: boolean;
@ -37,54 +37,46 @@ export const RecordTitleCell = ({
const { closeRecordTitleCell } = useRecordTitleCell(); const { closeRecordTitleCell } = useRecordTitleCell();
const handleEnter: FieldInputEvent = (persistField) => { const closeCell = () => {
closeRecordTitleCell({ closeRecordTitleCell({
recordId, recordId,
fieldMetadataId: fieldDefinition?.fieldMetadataId, fieldName: fieldDefinition.metadata.fieldName,
containerType, containerType,
}); });
};
const handleEnter: FieldInputEvent = (persistField) => {
closeCell();
persistField(); persistField();
}; };
const handleEscape: FieldInputEvent = (persistField) => { const handleEscape = () => {
closeRecordTitleCell({ closeCell();
recordId,
fieldMetadataId: fieldDefinition?.fieldMetadataId,
containerType,
});
persistField();
}; };
const handleTab: FieldInputEvent = (persistField) => { const handleTab: FieldInputEvent = (persistField) => {
closeRecordTitleCell({ closeCell();
recordId,
fieldMetadataId: fieldDefinition?.fieldMetadataId,
containerType,
});
persistField(); persistField();
}; };
const handleShiftTab: FieldInputEvent = (persistField) => { const handleShiftTab: FieldInputEvent = (persistField) => {
closeRecordTitleCell({ closeCell();
recordId,
fieldMetadataId: fieldDefinition?.fieldMetadataId,
containerType,
});
persistField(); persistField();
}; };
const handleClickOutside: FieldInputClickOutsideEvent = (persistField) => { const handleClickOutside: FieldInputClickOutsideEvent = (persistField) => {
closeRecordTitleCell({ closeCell();
recordId,
fieldMetadataId: fieldDefinition?.fieldMetadataId,
containerType,
});
persistField(); persistField();
}; };
const recordTitleCellContextValue: RecordTitleCellContextProps = { const recordTitleCellContextValue: RecordTitleCellContextProps = {
editModeContent: ( editModeContent: (
<RecordTitleCellFieldInput <RecordTitleCellFieldInput
instanceId={getRecordFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
prefix: containerType,
})}
onEnter={handleEnter} onEnter={handleEnter}
onEscape={handleEscape} onEscape={handleEscape}
onTab={handleTab} onTab={handleTab}
@ -93,7 +85,9 @@ export const RecordTitleCell = ({
sizeVariant={sizeVariant} sizeVariant={sizeVariant}
/> />
), ),
displayModeContent: <RecordTitleCellFieldDisplay />, displayModeContent: (
<RecordTitleCellFieldDisplay containerType={containerType} />
),
editModeContentOnly: isFieldInputOnly, editModeContentOnly: isFieldInputOnly,
loading: loading, loading: loading,
isReadOnly, isReadOnly,
@ -103,11 +97,11 @@ export const RecordTitleCell = ({
return ( return (
<RecordFieldComponentInstanceContext.Provider <RecordFieldComponentInstanceContext.Provider
value={{ value={{
instanceId: getRecordTitleCellId( instanceId: getRecordFieldInputInstanceId({
recordId, recordId,
fieldDefinition?.fieldMetadataId, fieldName: fieldDefinition.metadata.fieldName,
containerType, prefix: containerType,
), }),
}} }}
> >
<FieldFocusContextProvider> <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 { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
import { RecordTitleCellSingleTextDisplayMode } from '@/object-record/record-title-cell/components/RecordTitleCellTextFieldDisplay'; import { RecordTitleCellSingleTextDisplayMode } from '@/object-record/record-title-cell/components/RecordTitleCellTextFieldDisplay';
import { RecordTitleFullNameFieldDisplay } from '@/object-record/record-title-cell/components/RecordTitleFullNameFieldDisplay'; import { RecordTitleFullNameFieldDisplay } from '@/object-record/record-title-cell/components/RecordTitleFullNameFieldDisplay';
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
import { useContext } from 'react'; import { useContext } from 'react';
export const RecordTitleCellFieldDisplay = () => { export const RecordTitleCellFieldDisplay = ({
containerType,
}: {
containerType: RecordTitleCellContainerType;
}) => {
const { fieldDefinition } = useContext(FieldContext); const { fieldDefinition } = useContext(FieldContext);
if (!isFieldText(fieldDefinition) && !isFieldFullName(fieldDefinition)) { if (!isFieldText(fieldDefinition) && !isFieldFullName(fieldDefinition)) {
@ -15,9 +20,9 @@ export const RecordTitleCellFieldDisplay = () => {
return ( return (
<> <>
{isFieldText(fieldDefinition) ? ( {isFieldText(fieldDefinition) ? (
<RecordTitleCellSingleTextDisplayMode /> <RecordTitleCellSingleTextDisplayMode containerType={containerType} />
) : isFieldFullName(fieldDefinition) ? ( ) : isFieldFullName(fieldDefinition) ? (
<RecordTitleFullNameFieldDisplay /> <RecordTitleFullNameFieldDisplay containerType={containerType} />
) : null} ) : null}
</> </>
); );

View File

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

View File

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

View File

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

View File

@ -2,13 +2,15 @@ import styled from '@emotion/styled';
import { ClipboardEvent, useEffect, useRef, useState } from 'react'; import { ClipboardEvent, useEffect, useRef, useState } from 'react';
import { Key } from 'ts-key-enum'; 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 { FieldDoubleText } from '@/object-record/record-field/types/FieldDoubleText';
import { TextInputV2 } from '@/ui/input/components/TextInputV2'; 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 { 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 { splitFullName } from '~/utils/format/spiltFullName';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly'; import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
import { isDefined } from 'twenty-shared/utils';
const StyledContainer = styled.div` const StyledContainer = styled.div`
display: flex; display: flex;
@ -82,33 +84,39 @@ export const RecordTitleDoubleTextInput = ({
const [focusPosition, setFocusPosition] = useState<'left' | 'right'>('left'); const [focusPosition, setFocusPosition] = useState<'left' | 'right'>('left');
useScopedHotkeys( const instanceId = useAvailableComponentInstanceIdOrThrow(
Key.Enter, RecordFieldComponentInstanceContext,
() => { );
useHotkeysOnFocusedElement({
keys: [Key.Enter],
callback: () => {
onEnter({ onEnter({
firstValue: firstInternalValue, firstValue: firstInternalValue,
secondValue: secondInternalValue, secondValue: secondInternalValue,
}); });
}, },
hotkeyScope, scope: hotkeyScope,
[onEnter, firstInternalValue, secondInternalValue], focusId: instanceId,
); dependencies: [onEnter, firstInternalValue, secondInternalValue],
});
useScopedHotkeys( useHotkeysOnFocusedElement({
[Key.Escape], keys: [Key.Escape],
() => { callback: () => {
onEscape({ onEscape({
firstValue: firstInternalValue, firstValue: firstInternalValue,
secondValue: secondInternalValue, secondValue: secondInternalValue,
}); });
}, },
hotkeyScope, scope: hotkeyScope,
[onEscape, firstInternalValue, secondInternalValue], focusId: instanceId,
); dependencies: [onEscape, firstInternalValue, secondInternalValue],
});
useScopedHotkeys( useHotkeysOnFocusedElement({
'tab', keys: ['tab'],
() => { callback: () => {
if (focusPosition === 'left') { if (focusPosition === 'left') {
setFocusPosition('right'); setFocusPosition('right');
secondValueInputRef.current?.focus(); secondValueInputRef.current?.focus();
@ -119,13 +127,19 @@ export const RecordTitleDoubleTextInput = ({
}); });
} }
}, },
hotkeyScope, scope: hotkeyScope,
[onTab, firstInternalValue, secondInternalValue, focusPosition], focusId: instanceId,
); dependencies: [
onTab,
firstInternalValue,
secondInternalValue,
focusPosition,
],
});
useScopedHotkeys( useHotkeysOnFocusedElement({
'shift+tab', keys: ['shift+tab'],
() => { callback: () => {
if (focusPosition === 'right') { if (focusPosition === 'right') {
setFocusPosition('left'); setFocusPosition('left');
firstValueInputRef.current?.focus(); firstValueInputRef.current?.focus();
@ -136,9 +150,15 @@ export const RecordTitleDoubleTextInput = ({
}); });
} }
}, },
hotkeyScope, scope: hotkeyScope,
[onShiftTab, firstInternalValue, secondInternalValue, focusPosition], focusId: instanceId,
); dependencies: [
onShiftTab,
firstInternalValue,
secondInternalValue,
focusPosition,
],
});
useListenClickOutside({ useListenClickOutside({
refs: [containerRef], 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 { 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 { 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 { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { TitleInputHotkeyScope } from '@/ui/input/types/TitleInputHotkeyScope'; 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 { Theme, withTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
@ -33,8 +35,12 @@ const StyledEmptyText = withTheme(styled.div<{ theme: Theme }>`
color: ${({ theme }) => theme.font.color.tertiary}; color: ${({ theme }) => theme.font.color.tertiary};
`); `);
export const RecordTitleFullNameFieldDisplay = () => { export const RecordTitleFullNameFieldDisplay = ({
const { fieldDefinition } = useContext(FieldContext); containerType,
}: {
containerType: string;
}) => {
const { recordId, fieldDefinition } = useContext(FieldContext);
const { openInlineCell } = useInlineCell(); const { openInlineCell } = useInlineCell();
@ -45,14 +51,29 @@ export const RecordTitleFullNameFieldDisplay = () => {
.join(' ') .join(' ')
.trim(); .trim();
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const recordTitleCellId = getRecordFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
prefix: containerType,
});
return ( return (
<StyledDiv <StyledDiv
onClick={() => { onClick={() => {
setHotkeyScopeAndMemorizePreviousScope({ pushFocusItemToFocusStack({
scope: TitleInputHotkeyScope.TitleInput, focusId: recordTitleCellId,
component: {
type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: recordTitleCellId,
},
hotkeyScope: {
scope: TitleInputHotkeyScope.TitleInput,
},
memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY, memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
}); });
openInlineCell(); 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 { 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 { isInlineCellInEditModeScopedState } from '@/object-record/record-inline-cell/states/isInlineCellInEditModeScopedState';
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType'; 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 { TitleInputHotkeyScope } from '@/ui/input/types/TitleInputHotkeyScope';
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId'; import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
@ -25,26 +25,30 @@ export const useRecordTitleCell = () => {
({ set }) => ({ set }) =>
({ ({
recordId, recordId,
fieldMetadataId, fieldName,
containerType, containerType,
}: { }: {
recordId: string; recordId: string;
fieldMetadataId: string; fieldName: string;
containerType: RecordTitleCellContainerType; containerType: RecordTitleCellContainerType;
}) => { }) => {
set( set(
isInlineCellInEditModeScopedState( isInlineCellInEditModeScopedState(
getRecordTitleCellId(recordId, fieldMetadataId, containerType), getRecordFieldInputInstanceId({
recordId,
fieldName,
prefix: containerType,
}),
), ),
false, false,
); );
removeFocusItemFromFocusStackById({ removeFocusItemFromFocusStackById({
focusId: getRecordTitleCellId( focusId: getRecordFieldInputInstanceId({
recordId, recordId,
fieldMetadataId, fieldName,
containerType, prefix: containerType,
), }),
}); });
goBackToPreviousDropdownFocusId(); goBackToPreviousDropdownFocusId();
@ -58,47 +62,47 @@ export const useRecordTitleCell = () => {
({ set, snapshot }) => ({ set, snapshot }) =>
({ ({
recordId, recordId,
fieldMetadataId, fieldName,
containerType, containerType,
customEditHotkeyScopeForField, customEditHotkeyScopeForField,
}: { }: {
recordId: string; recordId: string;
fieldMetadataId: string; fieldName: string;
containerType: RecordTitleCellContainerType; containerType: RecordTitleCellContainerType;
customEditHotkeyScopeForField?: HotkeyScope; customEditHotkeyScopeForField?: HotkeyScope;
}) => { }) => {
if (isDefined(customEditHotkeyScopeForField)) { if (isDefined(customEditHotkeyScopeForField)) {
pushFocusItemToFocusStack({ pushFocusItemToFocusStack({
focusId: getRecordTitleCellId( focusId: getRecordFieldInputInstanceId({
recordId, recordId,
fieldMetadataId, fieldName,
containerType, prefix: containerType,
), }),
component: { component: {
type: FocusComponentType.OPENED_FIELD_INPUT, type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: getRecordTitleCellId( instanceId: getRecordFieldInputInstanceId({
recordId, recordId,
fieldMetadataId, fieldName,
containerType, prefix: containerType,
), }),
}, },
hotkeyScope: customEditHotkeyScopeForField, hotkeyScope: customEditHotkeyScopeForField,
memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY, memoizeKey: INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY,
}); });
} else { } else {
pushFocusItemToFocusStack({ pushFocusItemToFocusStack({
focusId: getRecordTitleCellId( focusId: getRecordFieldInputInstanceId({
recordId, recordId,
fieldMetadataId, fieldName,
containerType, prefix: containerType,
), }),
component: { component: {
type: FocusComponentType.OPENED_FIELD_INPUT, type: FocusComponentType.OPENED_FIELD_INPUT,
instanceId: getRecordTitleCellId( instanceId: getRecordFieldInputInstanceId({
recordId, recordId,
fieldMetadataId, fieldName,
containerType, prefix: containerType,
), }),
}, },
hotkeyScope: { hotkeyScope: {
scope: TitleInputHotkeyScope.TitleInput, scope: TitleInputHotkeyScope.TitleInput,
@ -107,11 +111,11 @@ export const useRecordTitleCell = () => {
}); });
} }
const recordTitleCellId = getRecordTitleCellId( const recordTitleCellId = getRecordFieldInputInstanceId({
recordId, recordId,
fieldMetadataId, fieldName,
containerType, prefix: containerType,
); });
set(isInlineCellInEditModeScopedState(recordTitleCellId), true); set(isInlineCellInEditModeScopedState(recordTitleCellId), true);
const recordIndexFieldDefinitions = snapshot const recordIndexFieldDefinitions = snapshot
@ -119,7 +123,7 @@ export const useRecordTitleCell = () => {
.getValue(); .getValue();
const fieldDefinition = recordIndexFieldDefinitions.find( const fieldDefinition = recordIndexFieldDefinitions.find(
(field) => field.fieldMetadataId === fieldMetadataId, (field) => field.metadata.fieldName === fieldName,
); );
if (!fieldDefinition) { 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'; import { isDefined } from 'twenty-shared/utils';
export const getRecordFieldInputId = (
recordId: string, export const getRecordFieldInputInstanceId = ({
fieldName?: string, recordId,
prefix?: string, fieldName,
): string => { prefix,
}: {
recordId: string;
fieldName?: string;
prefix?: string;
}): string => {
if (isDefined(prefix)) { if (isDefined(prefix)) {
return `${prefix}-${recordId}-${fieldName}`; return `${prefix}-${recordId}-${fieldName}`;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -99,6 +99,7 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
/> />
<AdvancedSettingsWrapper animationDimension="width" hideDot> <AdvancedSettingsWrapper animationDimension="width" hideDot>
<StyledOptionInput <StyledOptionInput
instanceId={`select-option-value-${option.id}`}
value={option.value} value={option.value}
onChange={(input) => onChange={(input) =>
onChange({ onChange({
@ -133,6 +134,7 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
} }
/> />
<StyledOptionInput <StyledOptionInput
instanceId={`select-option-label-${option.id}`}
value={option.label} value={option.label}
onChange={(label) => { onChange={(label) => {
const optionNameHasBeenEdited = !( 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 ( return (
<> <>
<StyledInputsContainer> <StyledInputsContainer>
@ -150,6 +154,7 @@ export const SettingsDataModelObjectAboutForm = ({
defaultValue={objectMetadataItem?.labelSingular ?? ''} defaultValue={objectMetadataItem?.labelSingular ?? ''}
render={({ field: { onChange, value }, formState: { errors } }) => ( render={({ field: { onChange, value }, formState: { errors } }) => (
<TextInput <TextInput
instanceId={labelSingularTextInputId}
// TODO we should discuss on how to notify user about form validation schema issue, from now just displaying red borders // TODO we should discuss on how to notify user about form validation schema issue, from now just displaying red borders
noErrorHelper={true} noErrorHelper={true}
error={errors.labelSingular?.message} error={errors.labelSingular?.message}
@ -181,6 +186,7 @@ export const SettingsDataModelObjectAboutForm = ({
defaultValue={objectMetadataItem?.labelPlural ?? ''} defaultValue={objectMetadataItem?.labelPlural ?? ''}
render={({ field: { onChange, value }, formState: { errors } }) => ( render={({ field: { onChange, value }, formState: { errors } }) => (
<TextInput <TextInput
instanceId={labelPluralTextInputId}
// TODO we should discuss on how to notify user about form validation schema issue, from now just displaying red borders // TODO we should discuss on how to notify user about form validation schema issue, from now just displaying red borders
noErrorHelper={true} noErrorHelper={true}
error={errors.labelPlural?.message} error={errors.labelPlural?.message}
@ -210,6 +216,7 @@ export const SettingsDataModelObjectAboutForm = ({
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<TextArea <TextArea
textAreaId={descriptionTextAreaId}
placeholder={t`Write a description`} placeholder={t`Write a description`}
minRows={4} minRows={4}
value={value ?? undefined} value={value ?? undefined}
@ -264,6 +271,7 @@ export const SettingsDataModelObjectAboutForm = ({
}) => ( }) => (
<> <>
<TextInput <TextInput
instanceId={`${objectMetadataItem?.id}-${fieldName}`}
label={label} label={label}
placeholder={placeholder} placeholder={placeholder}
value={value} value={value}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -39,6 +39,7 @@ export const SettingsRoleLabelContainer = ({
return ( return (
<StyledHeaderTitle> <StyledHeaderTitle>
<TitleInput <TitleInput
instanceId="role-label-input"
disabled={!settingsDraftRole.isEditable} disabled={!settingsDraftRole.isEditable}
sizeVariant="md" sizeVariant="md"
value={settingsDraftRole.label} 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 { SettingSecurityNewSSOIdentityFormValues } from '@/settings/security/types/SSOIdentityProvider';
import { TextInput } from '@/ui/input/components/TextInput'; import { TextInput } from '@/ui/input/components/TextInput';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { ReactElement, useMemo } from 'react'; import { ReactElement, useMemo } from 'react';
import { Controller, useFormContext } from 'react-hook-form'; 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 { H2Title, IconComponent, IconKey } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout'; import { Section } from 'twenty-ui/layout';
import { IdentityProviderType } from '~/generated/graphql';
const StyledInputsContainer = styled.div` const StyledInputsContainer = styled.div`
display: grid; display: grid;
@ -88,6 +88,7 @@ export const SettingsSSOIdentitiesProvidersForm = () => {
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<TextInput <TextInput
instanceId="sso-identity-provider-name"
autoComplete="off" autoComplete="off"
label={t`Name`} label={t`Name`}
value={value} value={value}

View File

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

View File

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

View File

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

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