Various fixes (#11108)

Fixes many bug regarding TableCell and InlineCells
This commit is contained in:
Charles Bochet
2025-03-22 14:19:10 +01:00
committed by GitHub
parent 692e08f0d4
commit ccf60284cf
61 changed files with 473 additions and 374 deletions

View File

@ -17,6 +17,7 @@ import { FieldContext } from '@/object-record/record-field/contexts/FieldContext
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
@ -102,7 +103,6 @@ export const CalendarEventDetails = ({
value={{
recordId: calendarEvent.id,
hotkeyScope: 'calendar-event-details',
recoilScopeId: `${calendarEvent.id}-${fieldName}`,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsFieldDefinition({
field: fieldsByName[fieldName],
@ -116,7 +116,7 @@ export const CalendarEventDetails = ({
>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: `${calendarEvent.id}-${fieldName}`,
instanceId: getRecordFieldInputId(calendarEvent.id, fieldName),
}}
>
<RecordInlineCell readonly />

View File

@ -14,7 +14,7 @@ import {
import { TextInput } from '@/ui/input/components/TextInput';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useMemo, useState } from 'react';
import { useState } from 'react';
import { isDefined } from 'twenty-shared';
import {
IconCalendar,
@ -94,11 +94,6 @@ export const AttachmentRow = ({
const [attachmentFileName, setAttachmentFileName] =
useState(originalFileName);
const fieldContext = useMemo(
() => ({ recoilScopeId: attachment?.id ?? '' }),
[attachment?.id],
);
const { destroyOneRecord: destroyOneAttachment } = useDestroyOneRecord({
objectNameSingular: CoreObjectNameSingular.Attachment,
});
@ -161,7 +156,13 @@ export const AttachmentRow = ({
};
return (
<FieldContext.Provider value={fieldContext as GenericFieldContextType}>
<FieldContext.Provider
value={
{
recordId: attachment.id,
} as GenericFieldContextType
}
>
<ActivityRow disabled>
<StyledLeftContent>
<AttachmentIcon attachmentType={attachment.type} />

View File

@ -1,23 +1,21 @@
import { useContext } from 'react';
import { Key } from 'ts-key-enum';
import { IconArrowUpRight, IconPencil } from 'twenty-ui';
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
import { useOpenActivityTargetInlineCellEditMode } from '@/activities/inline-cell/hooks/useOpenActivityTargetInlineCellEditMode';
import { useUpdateActivityTargetFromInlineCell } from '@/activities/inline-cell/hooks/useUpdateActivityTargetFromInlineCell';
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { RecordInlineCellContainer } from '@/object-record/record-inline-cell/components/RecordInlineCellContainer';
import { RecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext';
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope';
type ActivityTargetsInlineCellProps = {
activityRecordId: string;
@ -26,6 +24,7 @@ type ActivityTargetsInlineCellProps = {
activityObjectNameSingular:
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task;
componentInstanceId: string;
};
export const ActivityTargetsInlineCell = ({
@ -33,26 +32,17 @@ export const ActivityTargetsInlineCell = ({
showLabel = true,
maxWidth,
activityObjectNameSingular,
componentInstanceId,
}: ActivityTargetsInlineCellProps) => {
const { activityTargetObjectRecords } =
useActivityTargetObjectRecords(activityRecordId);
const multipleRecordPickerInstanceId = `multiple-record-picker-target-${activityRecordId}`;
const { closeInlineCell } = useInlineCell();
const { closeInlineCell } = useInlineCell(componentInstanceId);
const { fieldDefinition } = useContext(FieldContext);
const isFieldReadOnly = useIsFieldValueReadOnly();
useScopedHotkeys(
Key.Escape,
() => {
closeInlineCell();
},
ActivityEditorHotkeyScope.ActivityTargets,
);
const { FieldContextProvider: ActivityTargetsContextProvider } =
useFieldContext({
objectNameSingular: activityObjectNameSingular,
@ -72,7 +62,11 @@ export const ActivityTargetsInlineCell = ({
});
return (
<RecordFieldInputScope recordFieldInputScopeId={activityRecordId}>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: componentInstanceId,
}}
>
<FieldFocusContextProvider>
{ActivityTargetsContextProvider && (
<ActivityTargetsContextProvider>
@ -80,20 +74,20 @@ export const ActivityTargetsInlineCell = ({
value={{
buttonIcon: IconPencil,
customEditHotkeyScope:
ActivityEditorHotkeyScope.ActivityTargets,
MultipleRecordPickerHotkeyScope.MultipleRecordPicker,
IconLabel: showLabel ? IconArrowUpRight : undefined,
showLabel: showLabel,
readonly: isFieldReadOnly,
labelWidth: fieldDefinition?.labelWidth,
editModeContent: (
<MultipleRecordPicker
componentInstanceId={multipleRecordPickerInstanceId}
componentInstanceId={componentInstanceId}
onClickOutside={() => {
closeInlineCell();
}}
onChange={(morphItem) => {
updateActivityTargetFromInlineCell({
recordPickerInstanceId: multipleRecordPickerInstanceId,
recordPickerInstanceId: componentInstanceId,
morphItem,
activityTargetWithTargetRecords:
activityTargetObjectRecords,
@ -113,7 +107,7 @@ export const ActivityTargetsInlineCell = ({
),
onOpenEditMode: () => {
openActivityTargetInlineCellEditMode({
recordPickerInstanceId: multipleRecordPickerInstanceId,
recordPickerInstanceId: componentInstanceId,
activityTargetObjectRecords,
});
},
@ -124,6 +118,6 @@ export const ActivityTargetsInlineCell = ({
</ActivityTargetsContextProvider>
)}
</FieldFocusContextProvider>
</RecordFieldInputScope>
</RecordFieldComponentInstanceContext.Provider>
);
};

View File

@ -96,6 +96,7 @@ export const NoteCard = ({
{NoteTargetsContextProvider && (
<NoteTargetsContextProvider>
<ActivityTargetsInlineCell
componentInstanceId={`note-card-${note.id}-targets`}
activityRecordId={note.id}
activityObjectNameSingular={CoreObjectNameSingular.Note}
/>

View File

@ -135,6 +135,7 @@ export const TaskRow = ({ task }: { task: Task }) => {
activityRecordId={task.id}
showLabel={false}
maxWidth={200}
componentInstanceId={`task-row-targets-${task.id}`}
/>
</TaskTargetsContextProvider>
)}

View File

@ -1,5 +1,4 @@
export enum ActivityEditorHotkeyScope {
ActivityTitle = 'activity-title',
ActivityBody = 'activity-body',
ActivityTargets = 'activity-targets',
}

View File

@ -22,6 +22,8 @@ import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/reco
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
@ -29,7 +31,7 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { AnimatePresence, motion } from 'framer-motion';
import { useRef } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
import { useIsMobile } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
@ -65,9 +67,23 @@ export const CommandMenuContainer = ({
useCommandMenuHotKeys();
const handleClickOutside = useRecoilCallback(
({ snapshot }) =>
() => {
const hotkeyScope = snapshot
.getLoadable(currentHotkeyScopeState)
.getValue();
if (hotkeyScope.scope === AppHotkeyScope.CommandMenuOpen) {
closeCommandMenu();
}
},
[closeCommandMenu],
);
useListenClickOutside({
refs: [commandMenuRef],
callback: closeCommandMenu,
callback: handleClickOutside,
listenerId: 'COMMAND_MENU_LISTENER_ID',
excludeClassNames: ['page-header-command-menu-button'],
});

View File

@ -77,7 +77,12 @@ export const RecordChip = ({
avatarType={recordChipData.avatarType}
avatarUrl={recordChipData.avatarUrl ?? ''}
className={className}
variant={variant}
variant={
variant ??
(!forceDisableClick
? AvatarChipVariant.Regular
: AvatarChipVariant.Transparent)
}
to={to ?? getLinkToShowPage(objectNameSingular, record)}
onClick={(clickEvent) => {
// TODO refactor wrapper event listener to avoid colliding events

View File

@ -59,7 +59,6 @@ export const useFieldContext = ({
key={objectRecordId + fieldMetadataItem.id}
value={{
recordId: objectRecordId,
recoilScopeId: objectRecordId + fieldMetadataItem.id,
isLabelIdentifier,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,

View File

@ -13,6 +13,7 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { useContext } from 'react';
export const RecordBoardCardBody = ({
@ -43,7 +44,6 @@ export const RecordBoardCardBody = ({
value={{
recordId,
maxWidth: 156,
recoilScopeId: `board-card-${recordId}-${fieldDefinition.fieldMetadataId}`,
isLabelIdentifier: false,
fieldDefinition: {
disableTooltip: false,
@ -64,7 +64,11 @@ export const RecordBoardCardBody = ({
>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: `board-card-${recordId}-${fieldDefinition.fieldMetadataId}`,
instanceId: getRecordFieldInputId(
recordId,
fieldDefinition.metadata.fieldName,
'record-board-card',
),
}}
>
<RecordInlineCell />

View File

@ -10,11 +10,8 @@ import { PhonesFieldInput } from '@/object-record/record-field/meta-types/input/
import { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input/components/RawJsonFieldInput';
import { RelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput';
import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput';
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones';
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { ArrayFieldInput } from '@/object-record/record-field/meta-types/input/components/ArrayFieldInput';
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
@ -30,6 +27,7 @@ import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/is
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
import { isFieldRating } from '@/object-record/record-field/types/guards/isFieldRating';
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
import { FieldContext } from '../contexts/FieldContext';
import { BooleanFieldInput } from '../meta-types/input/components/BooleanFieldInput';
@ -58,7 +56,6 @@ type FieldInputProps = {
};
export const FieldInput = ({
recordFieldInputdId,
onCancel,
onSubmit,
onEnter,
@ -71,9 +68,7 @@ export const FieldInput = ({
const { fieldDefinition } = useContext(FieldContext);
return (
<RecordFieldInputScope
recordFieldInputScopeId={getScopeIdFromComponentId(recordFieldInputdId)}
>
<>
{isFieldRelationToOneObject(fieldDefinition) ? (
<RelationToOneFieldInput onSubmit={onSubmit} onCancel={onCancel} />
) : isFieldRelationFromManyObjects(fieldDefinition) ? (
@ -173,6 +168,6 @@ export const FieldInput = ({
) : (
<></>
)}
</RecordFieldInputScope>
</>
);
};

View File

@ -23,7 +23,6 @@ export type GenericFieldContextType = {
fieldDefinition: FieldDefinition<FieldMetadata>;
useUpdateRecord?: RecordUpdateHook;
recordId: string;
recoilScopeId?: string;
hotkeyScope: string;
isLabelIdentifier: boolean;
labelIdentifierLink?: string;

View File

@ -1,22 +0,0 @@
import { RecordFieldInputScopeInternalContext } from '@/object-record/record-field/scopes/scope-internal-context/RecordFieldInputScopeInternalContext';
import { recordFieldInputDraftValueComponentSelector } from '@/object-record/record-field/states/selectors/recordFieldInputDraftValueComponentSelector';
import { FieldInputDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
import { extractComponentSelector } from '@/ui/utilities/state/component-state/utils/extractComponentSelector';
export const useRecordFieldInputStates = <FieldValue>(
recordFieldInputId?: string,
) => {
const scopeId = useAvailableScopeIdOrThrow(
RecordFieldInputScopeInternalContext,
getScopeIdOrUndefinedFromComponentId(recordFieldInputId),
);
return {
scopeId,
getDraftValueSelector: extractComponentSelector<
FieldInputDraftValue<FieldValue> | undefined
>(recordFieldInputDraftValueComponentSelector, scopeId),
};
};

View File

@ -9,7 +9,6 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'
import { computeDraftValueFromFieldValue } from '@/object-record/record-field/utils/computeDraftValueFromFieldValue';
import { computeDraftValueFromString } from '@/object-record/record-field/utils/computeDraftValueFromString';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { extractComponentSelector } from '@/ui/utilities/state/component-state/utils/extractComponentSelector';
export const useInitDraftValueV2 = <FieldValue>() => {
@ -19,19 +18,19 @@ export const useInitDraftValueV2 = <FieldValue>() => {
value,
recordId,
fieldDefinition,
fieldComponentInstanceId,
}: {
value?: string;
recordId: string;
fieldDefinition: FieldDefinition<FieldMetadata>;
fieldComponentInstanceId: string;
}) => {
const recordFieldInputScopeId = `${getRecordFieldInputId(
recordId,
fieldDefinition?.metadata?.fieldName,
)}`;
const getDraftValueSelector = extractComponentSelector<
FieldInputDraftValue<FieldValue> | undefined
>(recordFieldInputDraftValueComponentSelector, recordFieldInputScopeId);
>(
recordFieldInputDraftValueComponentSelector,
fieldComponentInstanceId,
);
const recordFieldValue = snapshot
.getLoadable(

View File

@ -1,13 +1,22 @@
import { useSetRecoilState } from 'recoil';
import { useRecordFieldInputStates } from '@/object-record/record-field/hooks/internal/useRecordFieldInputStates';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { recordFieldInputDraftValueComponentSelector } from '@/object-record/record-field/states/selectors/recordFieldInputDraftValueComponentSelector';
import { FieldInputDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { extractComponentSelector } from '@/ui/utilities/state/component-state/utils/extractComponentSelector';
export const useRecordFieldInput = <FieldValue>(
recordFieldInputId?: string,
) => {
const { scopeId, getDraftValueSelector } =
useRecordFieldInputStates<FieldValue>(recordFieldInputId);
export const useRecordFieldInput = <FieldValue>() => {
const recordFieldComponentInstanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
const getDraftValueSelector = extractComponentSelector<
FieldInputDraftValue<FieldValue> | undefined
>(
recordFieldInputDraftValueComponentSelector,
recordFieldComponentInstanceId,
);
const setDraftValue = useSetRecoilState(getDraftValueSelector());
@ -26,7 +35,6 @@ export const useRecordFieldInput = <FieldValue>(
};
return {
scopeId,
setDraftValue,
getDraftValueSelector,
isDraftValueEmpty,

View File

@ -19,7 +19,6 @@ export const FieldContextProvider = ({
value={{
recordId: recordId ?? '1',
isLabelIdentifier: false,
recoilScopeId: '1',
hotkeyScope: 'hotkey-scope',
fieldDefinition,
useUpdateRecord: () => [() => undefined, {}],

View File

@ -41,7 +41,7 @@ export const useAddressField = () => {
};
const { setDraftValue, getDraftValueSelector } =
useRecordFieldInput<FieldAddressValue>(`${recordId}-${fieldName}`);
useRecordFieldInput<FieldAddressValue>();
const draftValue = useRecoilValue(getDraftValueSelector());

View File

@ -56,7 +56,7 @@ export const useCurrencyField = () => {
};
const { setDraftValue, getDraftValueSelector } =
useRecordFieldInput<FieldCurrencyValue>(`${recordId}-${fieldName}`);
useRecordFieldInput<FieldCurrencyValue>();
const draftValue = useRecoilValue(getDraftValueSelector());

View File

@ -25,9 +25,7 @@ export const useDateField = () => {
}),
);
const { setDraftValue } = useRecordFieldInput<FieldDateValue>(
`${recordId}-${fieldName}`,
);
const { setDraftValue } = useRecordFieldInput<FieldDateValue>();
return {
fieldDefinition,

View File

@ -29,9 +29,7 @@ export const useDateTimeField = () => {
}),
);
const { setDraftValue } = useRecordFieldInput<FieldDateTimeValue>(
`${recordId}-${fieldName}`,
);
const { setDraftValue } = useRecordFieldInput<FieldDateTimeValue>();
return {
fieldDefinition,

View File

@ -27,7 +27,7 @@ export const useEmailsField = () => {
);
const { setDraftValue, getDraftValueSelector } =
useRecordFieldInput<FieldEmailsValue>(`${recordId}-${fieldName}`);
useRecordFieldInput<FieldEmailsValue>();
const draftValue = useRecoilValue(getDraftValueSelector());

View File

@ -41,7 +41,7 @@ export const useFullNameField = () => {
};
const { setDraftValue, getDraftValueSelector } =
useRecordFieldInput<FieldFullNameValue>(`${recordId}-${fieldName}`);
useRecordFieldInput<FieldFullNameValue>();
const draftValue = useRecoilValue(getDraftValueSelector());

View File

@ -43,7 +43,7 @@ export const useJsonField = () => {
};
const { setDraftValue, getDraftValueSelector } =
useRecordFieldInput<FieldJsonValue>(`${recordId}-${fieldName}`);
useRecordFieldInput<FieldJsonValue>();
const draftValue = useRecoilValue(getDraftValueSelector());

View File

@ -27,7 +27,7 @@ export const useLinksField = () => {
);
const { setDraftValue, getDraftValueSelector } =
useRecordFieldInput<FieldLinksValue>(`${recordId}-${fieldName}`);
useRecordFieldInput<FieldLinksValue>();
const draftValue = useRecoilValue(getDraftValueSelector());

View File

@ -35,7 +35,7 @@ export const useMultiSelectField = () => {
const persistField = usePersistField();
const { setDraftValue, getDraftValueSelector } =
useRecordFieldInput<FieldMultiSelectValue>(`${recordId}-${fieldName}`);
useRecordFieldInput<FieldMultiSelectValue>();
const draftValue = useRecoilValue(getDraftValueSelector());
return {

View File

@ -55,7 +55,7 @@ export const useNumberField = () => {
};
const { setDraftValue, getDraftValueSelector } =
useRecordFieldInput<FieldNumberValue>(`${recordId}-${fieldName}`);
useRecordFieldInput<FieldNumberValue>();
const draftValue = useRecoilValue(getDraftValueSelector());

View File

@ -27,7 +27,7 @@ export const usePhonesField = () => {
);
const { setDraftValue, getDraftValueSelector } =
useRecordFieldInput<FieldPhonesValue>(`${recordId}-${fieldName}`);
useRecordFieldInput<FieldPhonesValue>();
const draftValue = useRecoilValue(getDraftValueSelector());

View File

@ -28,9 +28,8 @@ export const useRelationField = <T extends ObjectRecord | ObjectRecord[]>() => {
recordStoreFamilySelector({ recordId, fieldName }),
);
const { getDraftValueSelector } = useRecordFieldInput<FieldRelationValue<T>>(
`${recordId}-${fieldName}`,
);
const { getDraftValueSelector } =
useRecordFieldInput<FieldRelationValue<T>>();
const draftValue = useRecoilValue(getDraftValueSelector());
const initialSearchValue = draftValue;

View File

@ -35,7 +35,7 @@ export const useRichTextField = () => {
const fieldRichTextValue = isFieldRichTextValue(fieldValue) ? fieldValue : '';
const { setDraftValue, getDraftValueSelector } =
useRecordFieldInput<FieldRichTextValue>(`${recordId}-${fieldName}`);
useRecordFieldInput<FieldRichTextValue>();
const draftValue = useRecoilValue(getDraftValueSelector());

View File

@ -40,7 +40,7 @@ export const useRichTextV2Field = () => {
: ({ blocknote: null, markdown: null } as FieldRichTextV2Value);
const { setDraftValue, getDraftValueSelector } =
useRecordFieldInput<FieldRichTextValue>(`${recordId}-${fieldName}`);
useRecordFieldInput<FieldRichTextValue>();
const draftValue = useRecoilValue(getDraftValueSelector());

View File

@ -30,7 +30,7 @@ export const useSelectField = () => {
const persistField = usePersistField();
const { setDraftValue, getDraftValueSelector } =
useRecordFieldInput<FieldSelectValue>(`${recordId}-${fieldName}`);
useRecordFieldInput<FieldSelectValue>();
const draftValue = useRecoilValue(getDraftValueSelector());
return {

View File

@ -28,7 +28,7 @@ export const useTextField = () => {
const fieldTextValue = isFieldTextValue(fieldValue) ? fieldValue : '';
const { setDraftValue, getDraftValueSelector } =
useRecordFieldInput<FieldTextValue>(`${recordId}-${fieldName}`);
useRecordFieldInput<FieldTextValue>();
const draftValue = useRecoilValue(getDraftValueSelector());

View File

@ -12,6 +12,8 @@ import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
const AddressValueSetterEffect = ({
value,
@ -49,32 +51,42 @@ const AddressInputWithContext = ({
return (
<div>
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'text',
label: 'Address',
type: FieldMetadataType.ADDRESS,
iconName: 'IconTag',
metadata: {
fieldName: 'Address',
placeHolder: 'Enter text',
objectMetadataNameSingular: 'person',
},
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputId(
recordId ?? '',
'Address',
'record-table-cell',
),
}}
recordId={recordId}
>
<AddressValueSetterEffect value={value} />
<AddressInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
value={value}
hotkeyScope="hotkey-scope"
onTab={onTab}
onShiftTab={onShiftTab}
/>
</FieldContextProvider>
<div data-testid="data-field-input-click-outside-div" />
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'text',
label: 'Address',
type: FieldMetadataType.ADDRESS,
iconName: 'IconTag',
metadata: {
fieldName: 'Address',
placeHolder: 'Enter text',
objectMetadataNameSingular: 'person',
},
}}
recordId={recordId}
>
<AddressValueSetterEffect value={value} />
<AddressInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
value={value}
hotkeyScope="hotkey-scope"
onTab={onTab}
onShiftTab={onShiftTab}
/>
</FieldContextProvider>
<div data-testid="data-field-input-click-outside-div" />
</RecordFieldComponentInstanceContext.Provider>
</div>
);
};

View File

@ -7,6 +7,8 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco
import { FieldMetadataType } from '~/generated/graphql';
import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import {
BooleanFieldInput,
BooleanFieldInputProps,
@ -39,23 +41,36 @@ const BooleanFieldInputWithContext = ({
onSubmit,
}: BooleanFieldInputWithContextProps) => {
return (
<FieldContextProvider
fieldDefinition={{
defaultValue: false,
fieldMetadataId: 'boolean',
label: 'Boolean',
iconName: 'Icon123',
type: FieldMetadataType.BOOLEAN,
metadata: {
fieldName: 'Boolean',
objectMetadataNameSingular: 'person',
},
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputId(
recordId ?? '',
'Boolean',
'record-table-cell',
),
}}
recordId={recordId}
>
<BooleanFieldValueSetterEffect value={value} recordId={recordId ?? ''} />
<BooleanFieldInput onSubmit={onSubmit} testId="boolean-field-input" />
</FieldContextProvider>
<FieldContextProvider
fieldDefinition={{
defaultValue: false,
fieldMetadataId: 'boolean',
label: 'Boolean',
iconName: 'Icon123',
type: FieldMetadataType.BOOLEAN,
metadata: {
fieldName: 'Boolean',
objectMetadataNameSingular: 'person',
},
}}
recordId={recordId}
>
<BooleanFieldValueSetterEffect
value={value}
recordId={recordId ?? ''}
/>
<BooleanFieldInput onSubmit={onSubmit} testId="boolean-field-input" />
</FieldContextProvider>
</RecordFieldComponentInstanceContext.Provider>
);
};

View File

@ -6,6 +6,8 @@ import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope
import { FieldMetadataType } from '~/generated/graphql';
import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect';
import { useDateTimeField } from '../../../hooks/useDateTimeField';
import {
@ -66,7 +68,15 @@ const DateFieldInputWithContext = ({
}, [setHotkeyScope]);
return (
<div>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputId(
recordId ?? '',
'Date',
'record-table-cell',
),
}}
>
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'date',
@ -90,7 +100,7 @@ const DateFieldInputWithContext = ({
/>
</FieldContextProvider>
<div data-testid="data-field-input-click-outside-div"></div>
</div>
</RecordFieldComponentInstanceContext.Provider>
);
};

View File

@ -7,6 +7,8 @@ import { FieldMetadataType } from '~/generated/graphql';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { useNumberField } from '../../../hooks/useNumberField';
@ -43,7 +45,15 @@ const NumberFieldInputWithContext = ({
}, [setHotKeyScope]);
return (
<div>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputId(
recordId ?? '',
'Number',
'record-table-cell',
),
}}
>
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'number',
@ -69,7 +79,7 @@ const NumberFieldInputWithContext = ({
/>
</FieldContextProvider>
<div data-testid="data-field-input-click-outside-div" />
</div>
</RecordFieldComponentInstanceContext.Provider>
);
};

View File

@ -7,6 +7,8 @@ import { isDefined } from 'twenty-shared';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { FieldRatingValue } from '../../../../types/FieldMetadata';
import { useRatingField } from '../../../hooks/useRatingField';
import { RatingFieldInput, RatingFieldInputProps } from '../RatingFieldInput';
@ -42,22 +44,32 @@ const RatingFieldInputWithContext = ({
}, [setHotKeyScope]);
return (
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'rating',
label: 'Rating',
type: FieldMetadataType.RATING,
iconName: 'Icon123',
metadata: {
fieldName: 'Rating',
objectMetadataNameSingular: 'person',
},
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputId(
recordId ?? '',
'Rating',
'record-table-cell',
),
}}
recordId={recordId}
>
<RatingFieldValueSetterEffect value={value} />
<RatingFieldInput onSubmit={onSubmit} />
</FieldContextProvider>
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'rating',
label: 'Rating',
type: FieldMetadataType.RATING,
iconName: 'Icon123',
metadata: {
fieldName: 'Rating',
objectMetadataNameSingular: 'person',
},
}}
recordId={recordId}
>
<RatingFieldValueSetterEffect value={value} />
<RatingFieldInput onSubmit={onSubmit} />
</FieldContextProvider>
</RecordFieldComponentInstanceContext.Provider>
);
};

View File

@ -1,13 +1,14 @@
import { Decorator, Meta, StoryObj } from '@storybook/react';
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
import { useEffect } from 'react';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { FieldMetadataType } from '~/generated/graphql';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { Decorator, Meta, StoryObj } from '@storybook/react';
import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { FieldContextProvider } from '../../../components/FieldContextProvider';
import { useTextField } from '../../../hooks/useTextField';
import { TextFieldInput, TextFieldInputProps } from '../TextFieldInput';
@ -43,7 +44,11 @@ const TextFieldInputWithContext = ({
}, [setHotKeyScope]);
return (
<div>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: 'record-field-component-instance-id',
}}
>
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'text',
@ -69,7 +74,7 @@ const TextFieldInputWithContext = ({
/>
</FieldContextProvider>
<div data-testid="data-field-input-click-outside-div" />
</div>
</RecordFieldComponentInstanceContext.Provider>
);
};

View File

@ -1,21 +0,0 @@
import { ReactNode } from 'react';
import { RecordFieldInputScopeInternalContext } from '@/object-record/record-field/scopes/scope-internal-context/RecordFieldInputScopeInternalContext';
type RecordFieldInputScopeProps = {
children: ReactNode;
recordFieldInputScopeId: string;
};
export const RecordFieldInputScope = ({
children,
recordFieldInputScopeId,
}: RecordFieldInputScopeProps) => {
return (
<RecordFieldInputScopeInternalContext.Provider
value={{ scopeId: recordFieldInputScopeId }}
>
{children}
</RecordFieldInputScopeInternalContext.Provider>
);
};

View File

@ -1,7 +0,0 @@
import { createScopeInternalContext } from '@/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext';
import { RecoilComponentStateKey } from '@/ui/utilities/state/component-state/types/RecoilComponentStateKey';
type RecordFieldInputScopeInternalContextProps = RecoilComponentStateKey;
export const RecordFieldInputScopeInternalContext =
createScopeInternalContext<RecordFieldInputScopeInternalContextProps>();

View File

@ -12,15 +12,19 @@ import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFie
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope';
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilCallback } from 'recoil';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { RecordInlineCellContainer } from './RecordInlineCellContainer';
import {
@ -42,6 +46,10 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
onCloseEditMode,
} = useContext(FieldContext);
const recordFieldComponentInstanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
const buttonIcon = useGetButtonIcon();
const isFieldInputOnly = useIsFieldInputOnly();
@ -78,15 +86,22 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
closeInlineCell();
};
const handleClickOutside: FieldInputClickOutsideEvent = (
persistField,
event,
) => {
event.stopImmediatePropagation();
const handleClickOutside: FieldInputClickOutsideEvent = useRecoilCallback(
({ snapshot }) =>
(persistField, event) => {
const hotkeyScope = snapshot
.getLoadable(currentHotkeyScopeState)
.getValue();
if (hotkeyScope.scope !== InlineCellHotkeyScope.InlineCell) {
return;
}
event.stopImmediatePropagation();
persistField();
closeInlineCell();
};
persistField();
closeInlineCell();
},
[closeInlineCell],
);
const { getIcon } = useIcons();
const { openFieldInput, closeFieldInput } = useOpenFieldInputEditMode();
@ -132,10 +147,7 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
isCentered,
editModeContent: (
<FieldInput
recordFieldInputdId={getRecordFieldInputId(
recordId,
fieldDefinition?.metadata?.fieldName,
)}
recordFieldInputdId={recordFieldComponentInstanceId}
onEnter={handleEnter}
onCancel={handleCancel}
onEscape={handleEscape}

View File

@ -1,10 +1,10 @@
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/states/recordFieldInputIsFieldInErrorComponentState';
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState';
import { RecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import styled from '@emotion/styled';
@ -37,21 +37,19 @@ export const RecordInlineCellEditMode = ({
children,
}: RecordInlineCellEditModeProps) => {
const { isCentered } = useContext(RecordInlineCellContext);
const { recordId, fieldDefinition } = useContext(FieldContext);
const instanceId = getRecordFieldInputId(
recordId,
fieldDefinition?.metadata?.fieldName,
const recordFieldComponentInstanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
const setFieldInputLayoutDirection = useSetRecoilComponentStateV2(
recordFieldInputLayoutDirectionComponentState,
instanceId,
recordFieldComponentInstanceId,
);
const setFieldInputLayoutDirectionLoading = useSetRecoilComponentStateV2(
recordFieldInputLayoutDirectionLoadingComponentState,
instanceId,
recordFieldComponentInstanceId,
);
const setFieldInputLayoutDirectionMiddleware = {

View File

@ -6,22 +6,28 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH
import { isDefined } from 'twenty-shared';
import { useInitDraftValueV2 } from '@/object-record/record-field/hooks/useInitDraftValueV2';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { useRecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { isInlineCellInEditModeScopedState } from '../states/isInlineCellInEditModeScopedState';
import { InlineCellHotkeyScope } from '../types/InlineCellHotkeyScope';
export const useInlineCell = () => {
const {
recoilScopeId = '',
recordId,
fieldDefinition,
} = useContext(FieldContext);
export const useInlineCell = (
recordFieldComponentInstanceIdFromProps?: string,
) => {
const { recordId, fieldDefinition } = useContext(FieldContext);
const recordFieldComponentInstanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
recordFieldComponentInstanceIdFromProps,
);
const [isInlineCellInEditMode, setIsInlineCellInEditMode] = useRecoilState(
isInlineCellInEditModeScopedState(recoilScopeId),
isInlineCellInEditModeScopedState(recordFieldComponentInstanceId),
);
const { onOpenEditMode, onCloseEditMode } = useRecordInlineCellContext();
@ -50,7 +56,15 @@ export const useInlineCell = () => {
const openInlineCell = (customEditHotkeyScopeForField?: string) => {
onOpenEditMode?.();
setIsInlineCellInEditMode(true);
initFieldInputDraftValue({ recordId, fieldDefinition });
initFieldInputDraftValue({
recordId,
fieldDefinition,
fieldComponentInstanceId: getRecordFieldInputId(
recordId,
fieldDefinition.metadata.fieldName,
'inline-cell',
),
});
if (isDefined(customEditHotkeyScopeForField)) {
setHotkeyScopeAndMemorizePreviousScope(customEditHotkeyScopeForField);

View File

@ -15,6 +15,7 @@ import { useRecordShowContainerActions } from '@/object-record/record-show/hooks
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { RecordDetailDuplicatesSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailDuplicatesSection';
import { RecordDetailRelationSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSection';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
import { FieldMetadataType } from '~/generated/graphql';
@ -97,7 +98,6 @@ export const FieldsCard = ({
value={{
recordId: objectRecordId,
maxWidth: 200,
recoilScopeId: objectRecordId + fieldMetadataItem.id,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
@ -110,22 +110,21 @@ export const FieldsCard = ({
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: objectRecordId + fieldMetadataItem.id,
}}
>
<ActivityTargetsInlineCell
activityObjectNameSingular={
objectNameSingular as
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task
}
activityRecordId={objectRecordId}
showLabel={true}
maxWidth={200}
/>
</RecordFieldComponentInstanceContext.Provider>
<ActivityTargetsInlineCell
componentInstanceId={getRecordFieldInputId(
objectRecordId,
fieldMetadataItem.name,
'fields-card',
)}
activityObjectNameSingular={
objectNameSingular as
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task
}
activityRecordId={objectRecordId}
showLabel={true}
maxWidth={200}
/>
</FieldContext.Provider>
),
)}
@ -135,7 +134,6 @@ export const FieldsCard = ({
value={{
recordId: objectRecordId,
maxWidth: 200,
recoilScopeId: objectRecordId + fieldMetadataItem.id,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
@ -150,7 +148,11 @@ export const FieldsCard = ({
>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: `${objectRecordId}-${fieldMetadataItem.id}`,
instanceId: getRecordFieldInputId(
objectRecordId,
fieldMetadataItem.name,
'fields-card',
),
}}
>
<RecordInlineCell loading={recordLoading} />
@ -169,7 +171,6 @@ export const FieldsCard = ({
key={objectRecordId + fieldMetadataItem.id}
value={{
recordId: objectRecordId,
recoilScopeId: objectRecordId + fieldMetadataItem.id,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,

View File

@ -68,8 +68,6 @@ export const ObjectRecordShowPageBreadcrumb = ({
<FieldContext.Provider
value={{
recordId: objectRecordId,
recoilScopeId:
objectRecordId + labelIdentifierFieldMetadataItem?.id,
isLabelIdentifier: false,
fieldDefinition: {
type:

View File

@ -70,8 +70,6 @@ export const SummaryCard = ({
<FieldContext.Provider
value={{
recordId: objectRecordId,
recoilScopeId:
objectRecordId + labelIdentifierFieldMetadataItem?.id,
isLabelIdentifier: false,
fieldDefinition: {
type:

View File

@ -37,6 +37,7 @@ import { RecordDetailRecordsListItem } from '@/object-record/record-show/record-
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@ -268,7 +269,6 @@ export const RecordDetailRelationRecordsListItem = ({
value={{
recordId: relationRecord.id,
maxWidth: 200,
recoilScopeId: `${relationRecord.id}-${fieldMetadataItem.id}`,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
@ -283,7 +283,11 @@ export const RecordDetailRelationRecordsListItem = ({
>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: `${relationRecord.id}-${fieldMetadataItem.id}`,
instanceId: getRecordFieldInputId(
relationRecord.id,
fieldMetadataItem.name,
'record-detail',
),
}}
>
<RecordInlineCell />

View File

@ -13,7 +13,10 @@ export const useUpsertRecordsInStore = () => {
.getValue();
if (JSON.stringify(currentRecord) !== JSON.stringify(record)) {
set(recordStoreFamilyState(record.id), record);
set(recordStoreFamilyState(record.id), {
...currentRecord,
...record,
});
}
}
},

View File

@ -62,7 +62,10 @@ export const useSetRecordTableData = ({
.getValue();
if (JSON.stringify(currentRecord) !== JSON.stringify(record)) {
set(recordStoreFamilyState(record.id), record);
set(recordStoreFamilyState(record.id), {
...currentRecord,
...record,
});
}
}

View File

@ -1,9 +1,9 @@
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/states/recordFieldInputIsFieldInErrorComponentState';
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import styled from '@emotion/styled';
@ -14,7 +14,7 @@ import {
offset,
useFloating,
} from '@floating-ui/react';
import { ReactElement, useContext } from 'react';
import { ReactElement } from 'react';
const StyledEditableCellEditModeContainer = styled.div<RecordTableCellEditModeProps>`
align-items: center;
@ -35,25 +35,21 @@ export type RecordTableCellEditModeProps = {
export const RecordTableCellEditMode = ({
children,
}: RecordTableCellEditModeProps) => {
const { recordId, fieldDefinition } = useContext(FieldContext);
const isFieldInError = useRecoilComponentValueV2(
recordFieldInputIsFieldInErrorComponentState,
);
const instanceId = getRecordFieldInputId(
recordId,
fieldDefinition?.metadata?.fieldName,
const recordFieldComponentInstanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldComponentInstanceContext,
);
const setFieldInputLayoutDirection = useSetRecoilComponentStateV2(
recordFieldInputLayoutDirectionComponentState,
instanceId,
recordFieldComponentInstanceId,
);
const setFieldInputLayoutDirectionLoading = useSetRecoilComponentStateV2(
recordFieldInputLayoutDirectionLoadingComponentState,
instanceId,
recordFieldComponentInstanceId,
);
const setFieldInputLayoutDirectionMiddleware = {

View File

@ -16,6 +16,7 @@ import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/co
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@ -72,7 +73,6 @@ export const RecordTableCellFieldContextWrapper = ({
return (
<FieldContext.Provider
value={{
recoilScopeId: recordId + columnDefinition.label,
recordId,
fieldDefinition: columnDefinition,
useUpdateRecord: () => [updateRecord, {}],
@ -89,7 +89,13 @@ export const RecordTableCellFieldContextWrapper = ({
}}
>
<RecordFieldComponentInstanceContext.Provider
value={{ instanceId: recordId + columnDefinition.label }}
value={{
instanceId: getRecordFieldInputId(
recordId,
columnDefinition.metadata.fieldName,
'record-table-cell',
),
}}
>
{children}
</RecordFieldComponentInstanceContext.Provider>

View File

@ -3,9 +3,13 @@ import { useContext } from 'react';
import { FieldInput } from '@/object-record/record-field/components/FieldInput';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
import { useRecoilCallback } from 'recoil';
export const RecordTableCellFieldInput = () => {
const { recordId, fieldDefinition } = useContext(FieldContext);
@ -31,16 +35,22 @@ export const RecordTableCellFieldInput = () => {
onCloseTableCell();
};
const handleClickOutside = (
persistField: () => void,
event: MouseEvent | TouchEvent,
) => {
event.stopImmediatePropagation();
const handleClickOutside: FieldInputClickOutsideEvent = useRecoilCallback(
({ snapshot }) =>
(persistField, event) => {
const hotkeyScope = snapshot
.getLoadable(currentHotkeyScopeState)
.getValue();
if (hotkeyScope.scope !== TableHotkeyScope.CellEditMode) {
return;
}
event.stopImmediatePropagation();
persistField();
onCloseTableCell();
};
persistField();
onCloseTableCell();
},
[onCloseTableCell],
);
const handleEscape: FieldInputEvent = (persistField) => {
persistField();

View File

@ -24,6 +24,7 @@ import { recordIndexOpenRecordInState } from '@/object-record/record-index/state
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
@ -158,6 +159,11 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
value: initialValue,
recordId,
fieldDefinition,
fieldComponentInstanceId: getRecordFieldInputId(
recordId,
fieldDefinition.metadata.fieldName,
'record-table-cell',
),
});
toggleClickOutsideListener(false);

View File

@ -8,6 +8,7 @@ import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEv
import { useInlineCell } from '../../record-inline-cell/hooks/useInlineCell';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { RecordTitleCellContainer } from '@/object-record/record-title-cell/components/RecordTitleCellContainer';
import {
RecordTitleCellContext,
@ -30,7 +31,13 @@ export const RecordTitleCell = ({
const isFieldInputOnly = useIsFieldInputOnly();
const { closeInlineCell } = useInlineCell();
const { closeInlineCell } = useInlineCell(
getRecordFieldInputId(
recordId,
fieldDefinition?.metadata?.fieldName,
'title',
),
);
const handleEnter: FieldInputEvent = (persistField) => {
persistField();
@ -64,10 +71,6 @@ export const RecordTitleCell = ({
const recordTitleCellContextValue: RecordTitleCellContextProps = {
editModeContent: (
<RecordTitleCellFieldInput
recordFieldInputId={getRecordFieldInputId(
recordId,
fieldDefinition?.metadata?.fieldName,
)}
onEnter={handleEnter}
onEscape={handleEscape}
onTab={handleTab}
@ -82,10 +85,20 @@ export const RecordTitleCell = ({
};
return (
<FieldFocusContextProvider>
<RecordTitleCellContext.Provider value={recordTitleCellContextValue}>
<RecordTitleCellContainer />
</RecordTitleCellContext.Provider>
</FieldFocusContextProvider>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputId(
recordId,
fieldDefinition?.metadata?.fieldName,
'title',
),
}}
>
<FieldFocusContextProvider>
<RecordTitleCellContext.Provider value={recordTitleCellContextValue}>
<RecordTitleCellContainer />
</RecordTitleCellContext.Provider>
</FieldFocusContextProvider>
</RecordFieldComponentInstanceContext.Provider>
);
};

View File

@ -1,8 +1,5 @@
import { useContext } from 'react';
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
@ -11,7 +8,6 @@ import { RecordTitleCellTextFieldInput } from '@/object-record/record-title-cell
import { RecordTitleFullNameFieldInput } from '@/object-record/record-title-cell/components/RecordTitleFullNameFieldInput';
type RecordTitleCellFieldInputProps = {
recordFieldInputId: string;
onClickOutside?: (
persist: () => void,
event: MouseEvent | TouchEvent,
@ -25,7 +21,6 @@ type RecordTitleCellFieldInputProps = {
export const RecordTitleCellFieldInput = ({
sizeVariant,
recordFieldInputId,
onEnter,
onEscape,
onShiftTab,
@ -39,9 +34,7 @@ export const RecordTitleCellFieldInput = ({
}
return (
<RecordFieldInputScope
recordFieldInputScopeId={getScopeIdFromComponentId(recordFieldInputId)}
>
<>
{isFieldText(fieldDefinition) ? (
<RecordTitleCellTextFieldInput
onEnter={onEnter}
@ -61,6 +54,6 @@ export const RecordTitleCellFieldInput = ({
sizeVariant={sizeVariant}
/>
) : null}
</RecordFieldInputScope>
</>
);
};

View File

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

View File

@ -9,6 +9,7 @@ import { FieldDisplay } from '@/object-record/record-field/components/FieldDispl
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { BooleanFieldInput } from '@/object-record/record-field/meta-types/input/components/BooleanFieldInput';
import { RatingFieldInput } from '@/object-record/record-field/meta-types/input/components/RatingFieldInput';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { SettingsDataModelSetFieldValueEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetFieldValueEffect';
import { SettingsDataModelSetPreviewRecordEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetRecordEffect';
import { useFieldPreviewValue } from '@/settings/data-model/fields/preview/hooks/useFieldPreviewValue';
@ -98,59 +99,65 @@ export const SettingsDataModelFieldPreview = ({
return (
<>
{previewRecord ? (
<SettingsDataModelSetPreviewRecordEffect
fieldName={fieldName}
record={previewRecord}
/>
) : (
<SettingsDataModelSetFieldValueEffect
recordId={recordId}
fieldName={fieldName}
value={fieldPreviewValue}
/>
)}
<StyledFieldPreview shrink={shrink}>
{!!withFieldLabel && (
<StyledFieldLabel>
<FieldIcon
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
{fieldMetadataItem.label}:
</StyledFieldLabel>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: 'record-field-component-instance-id',
}}
>
{previewRecord ? (
<SettingsDataModelSetPreviewRecordEffect
fieldName={fieldName}
record={previewRecord}
/>
) : (
<SettingsDataModelSetFieldValueEffect
recordId={recordId}
fieldName={fieldName}
value={fieldPreviewValue}
/>
)}
<FieldContext.Provider
value={{
recordId,
isLabelIdentifier,
fieldDefinition: {
type: fieldMetadataItem.type,
iconName: 'FieldIcon',
fieldMetadataId: fieldMetadataItem.id || '',
label: fieldMetadataItem.label,
metadata: {
fieldName,
objectMetadataNameSingular: objectMetadataItem.nameSingular,
relationObjectMetadataNameSingular:
relationObjectMetadataItem?.nameSingular,
options: fieldMetadataItem.options ?? [],
settings: fieldMetadataItem.settings,
},
defaultValue: fieldMetadataItem.defaultValue,
},
hotkeyScope: 'field-preview',
}}
>
{fieldMetadataItem.type === FieldMetadataType.BOOLEAN ? (
<BooleanFieldInput readonly />
) : fieldMetadataItem.type === FieldMetadataType.RATING ? (
<RatingFieldInput readonly />
) : (
<FieldDisplay />
<StyledFieldPreview shrink={shrink}>
{!!withFieldLabel && (
<StyledFieldLabel>
<FieldIcon
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
{fieldMetadataItem.label}:
</StyledFieldLabel>
)}
</FieldContext.Provider>
</StyledFieldPreview>
<FieldContext.Provider
value={{
recordId,
isLabelIdentifier,
fieldDefinition: {
type: fieldMetadataItem.type,
iconName: 'FieldIcon',
fieldMetadataId: fieldMetadataItem.id || '',
label: fieldMetadataItem.label,
metadata: {
fieldName,
objectMetadataNameSingular: objectMetadataItem.nameSingular,
relationObjectMetadataNameSingular:
relationObjectMetadataItem?.nameSingular,
options: fieldMetadataItem.options ?? [],
settings: fieldMetadataItem.settings,
},
defaultValue: fieldMetadataItem.defaultValue,
},
hotkeyScope: 'field-preview',
}}
>
{fieldMetadataItem.type === FieldMetadataType.BOOLEAN ? (
<BooleanFieldInput readonly />
) : fieldMetadataItem.type === FieldMetadataType.RATING ? (
<RatingFieldInput readonly />
) : (
<FieldDisplay />
)}
</FieldContext.Provider>
</StyledFieldPreview>
</RecordFieldComponentInstanceContext.Provider>
</>
);
};

View File

@ -8,8 +8,6 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { CurrencyPickerHotkeyScope } from '../types/CurrencyPickerHotkeyScope';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { CurrencyPickerDropdownSelect } from './CurrencyPickerDropdownSelect';
const StyledDropdownButtonContainer = styled.div`
@ -69,10 +67,6 @@ export const CurrencyPickerDropdownButton = ({
closeDropdown();
};
const { toggleClickOutsideListener } = useClickOutsideListener(
TableHotkeyScope.CellEditMode,
);
const currency = currencies.find(({ value }) => value === valueCode);
const currencyCode = currency?.value ?? CurrencyCode.USD;
@ -98,8 +92,6 @@ export const CurrencyPickerDropdownButton = ({
}
dropdownPlacement="bottom-start"
dropdownOffset={{ x: 0, y: 4 }}
onOpen={() => toggleClickOutsideListener(false)}
onClose={() => toggleClickOutsideListener(true)}
/>
);
};

View File

@ -5,6 +5,7 @@ import { useRecoilCallback } from 'recoil';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import {
RecordFieldValueSelectorContextProvider,
useSetRecordValue,
@ -124,26 +125,32 @@ export const getFieldDecorator =
});
return (
<RecordFieldValueSelectorContextProvider>
<FieldContext.Provider
value={{
recordId: record.id,
isLabelIdentifier,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: 0,
objectMetadataItem,
}),
hotkeyScope: 'hotkey-scope',
}}
>
<RecordMockSetterEffect
companies={companies}
people={people}
tasks={tasks}
/>
<Story />
</FieldContext.Provider>
</RecordFieldValueSelectorContextProvider>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: 'record-field-component-instance-id',
}}
>
<RecordFieldValueSelectorContextProvider>
<FieldContext.Provider
value={{
recordId: record.id,
isLabelIdentifier,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: 0,
objectMetadataItem,
}),
hotkeyScope: 'hotkey-scope',
}}
>
<RecordMockSetterEffect
companies={companies}
people={people}
tasks={tasks}
/>
<Story />
</FieldContext.Provider>
</RecordFieldValueSelectorContextProvider>
</RecordFieldComponentInstanceContext.Provider>
);
};

View File

@ -46,7 +46,6 @@ export const useMockFieldContext = ({
key={objectRecordId + fieldMetadataItem.id}
value={{
recordId: objectRecordId,
recoilScopeId: objectRecordId + fieldMetadataItem.id,
isLabelIdentifier,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,

View File

@ -361,7 +361,7 @@ const StyledButton = styled('button', {
const StyledButtonWrapper = styled.div<
Pick<
ButtonProps,
'isLoading' | 'variant' | 'accent' | 'inverted' | 'disabled'
'isLoading' | 'variant' | 'accent' | 'inverted' | 'disabled' | 'fullWidth'
>
>`
${({ theme, variant, accent, inverted, disabled }) => css`
@ -409,7 +409,9 @@ const StyledButtonWrapper = styled.div<
max-width: ${({ isLoading, theme }) =>
isLoading ? `calc(100% - ${theme.spacing(8)})` : 'none'};
position: relative;
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
`;
export const Button = ({
@ -445,6 +447,7 @@ export const Button = ({
accent={accent}
inverted={inverted}
disabled={soon || disabled}
fullWidth={fullWidth}
>
{(isLoading || Icon) && (
<ButtonIcon Icon={Icon} isLoading={!!isLoading} />