Remove overlay-scroll-bar (#11258)

## What

- Deprecate overlayscrollbars as we decided to follow the native
behavior
- rework on performances (avoid calling recoil states too much at field
level which is quite expensive)
- Also implements:
https://github.com/twentyhq/core-team-issues/issues/569

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Charles Bochet
2025-04-04 16:13:48 +02:00
committed by GitHub
parent 6b184cc641
commit 2308091b13
101 changed files with 664 additions and 952 deletions

View File

@ -107,6 +107,7 @@ export const CalendarEventDetails = ({
}),
useUpdateRecord: () => [() => undefined, { loading: false }],
maxWidth: 300,
isReadOnly: false,
}}
>
<RecordFieldComponentInstanceContext.Provider

View File

@ -8,17 +8,14 @@ import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttac
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { Key } from 'ts-key-enum';
import { useDebouncedCallback } from 'use-debounce';
@ -26,6 +23,7 @@ import { ActivityRichTextEditorChangeOnActivityIdEffect } from '@/activities/com
import { Note } from '@/activities/types/Note';
import { Task } from '@/activities/types/Task';
import { CommandMenuHotkeyScope } from '@/command-menu/types/CommandMenuHotkeyScope';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
import { PartialBlock } from '@blocknote/core';
import '@blocknote/core/fonts/inter.css';
@ -56,17 +54,11 @@ export const ActivityRichTextEditor = ({
objectNameSingular: activityObjectNameSingular,
});
const contextStoreCurrentViewType = useRecoilComponentValueV2(
contextStoreCurrentViewTypeComponentState,
);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const isRecordReadOnly = useIsRecordReadOnly({ recordId: activityId });
const isReadOnly = isFieldValueReadOnly({
objectNameSingular: activityObjectNameSingular,
hasObjectReadOnlyPermission,
contextStoreCurrentViewType,
isRecordDeleted: activityInStore?.deletedAt !== null,
isRecordReadOnly,
});
const {

View File

@ -196,10 +196,7 @@ export const AttachmentList = ({
</StyledHeader>
</StyledModalHeader>
<ScrollWrapper
contextProviderName="modalContent"
componentInstanceId={`preview-modal-${previewedAttachment.id}`}
scrollbarVariant="no-padding"
heightMode="fit-content"
>
<StyledModalContent>
<Suspense

View File

@ -1,14 +1,12 @@
import { useRecoilValue } from 'recoil';
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
import { Note } from '@/activities/types/Note';
import { NoteTarget } from '@/activities/types/NoteTarget';
import { Task } from '@/activities/types/Task';
import { TaskTarget } from '@/activities/types/TaskTarget';
import { getActivityTargetObjectRecords } from '@/activities/utils/getActivityTargetObjectRecords';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
export const useActivityTargetObjectRecords = (
@ -25,49 +23,11 @@ export const useActivityTargetObjectRecords = (
return { activityTargetObjectRecords: [] };
}
const targets = activityTargets
? activityTargets
: activity && 'noteTargets' in activity && activity.noteTargets
? activity.noteTargets
: activity && 'taskTargets' in activity && activity.taskTargets
? activity.taskTargets
: [];
const activityTargetObjectRecords = targets
.map<ActivityTargetWithTargetRecord | undefined>((activityTarget) => {
if (!isDefined(activityTarget)) {
throw new Error(`Cannot find activity target`);
}
const correspondingObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
isDefined(activityTarget[objectMetadataItem.nameSingular]) &&
![CoreObjectNameSingular.Note, CoreObjectNameSingular.Task].includes(
objectMetadataItem.nameSingular as CoreObjectNameSingular,
),
);
if (!correspondingObjectMetadataItem) {
return undefined;
}
const targetObjectRecord = activityTarget[
correspondingObjectMetadataItem.nameSingular
] as ObjectRecord | undefined;
if (!isDefined(targetObjectRecord)) {
throw new Error(
`Cannot find target object record of type ${correspondingObjectMetadataItem.nameSingular}, make sure the request for activities eagerly loads for the target objects on activity target relation.`,
);
}
return {
activityTarget,
targetObject: targetObjectRecord,
targetObjectMetadataItem: correspondingObjectMetadataItem,
};
})
.filter(isDefined);
const activityTargetObjectRecords = getActivityTargetObjectRecords({
activityRecord: activity as Note | Task,
objectMetadataItems,
activityTargets,
});
return {
activityTargetObjectRecords,

View File

@ -2,13 +2,12 @@ import { useContext } from 'react';
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 { useOpenActivityTargetCellEditMode } from '@/activities/inline-cell/hooks/useOpenActivityTargetCellEditMode';
import { useUpdateActivityTargetFromCell } from '@/activities/inline-cell/hooks/useUpdateActivityTargetFromCell';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldContextProvider } from '@/object-record/record-field/components/FieldContextProvider';
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 { 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';
@ -39,18 +38,15 @@ export const ActivityTargetsInlineCell = ({
const { closeInlineCell } = useInlineCell(componentInstanceId);
const { fieldDefinition } = useContext(FieldContext);
const { fieldDefinition, isReadOnly } = useContext(FieldContext);
const isFieldReadOnly = useIsFieldValueReadOnly();
const { openActivityTargetCellEditMode } =
useOpenActivityTargetCellEditMode();
const { openActivityTargetInlineCellEditMode } =
useOpenActivityTargetInlineCellEditMode();
const { updateActivityTargetFromInlineCell } =
useUpdateActivityTargetFromInlineCell({
activityObjectNameSingular,
activityId: activityRecordId,
});
const { updateActivityTargetFromCell } = useUpdateActivityTargetFromCell({
activityObjectNameSingular,
activityId: activityRecordId,
});
return (
<RecordFieldComponentInstanceContext.Provider
@ -73,7 +69,7 @@ export const ActivityTargetsInlineCell = ({
MultipleRecordPickerHotkeyScope.MultipleRecordPicker,
IconLabel: showLabel ? IconArrowUpRight : undefined,
showLabel: showLabel,
readonly: isFieldReadOnly,
readonly: isReadOnly,
labelWidth: fieldDefinition?.labelWidth,
editModeContent: (
<MultipleRecordPicker
@ -82,7 +78,7 @@ export const ActivityTargetsInlineCell = ({
closeInlineCell();
}}
onChange={(morphItem) => {
updateActivityTargetFromInlineCell({
updateActivityTargetFromCell({
recordPickerInstanceId: componentInstanceId,
morphItem,
activityTargetWithTargetRecords:
@ -102,7 +98,7 @@ export const ActivityTargetsInlineCell = ({
/>
),
onOpenEditMode: () => {
openActivityTargetInlineCellEditMode({
openActivityTargetCellEditMode({
recordPickerInstanceId: componentInstanceId,
activityTargetObjectRecords,
});

View File

@ -7,21 +7,22 @@ import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
import { useRecoilCallback } from 'recoil';
type OpenActivityTargetInlineCellEditModeProps = {
type OpenActivityTargetCellEditModeProps = {
recordPickerInstanceId: string;
activityTargetObjectRecords: ActivityTargetWithTargetRecord[];
};
export const useOpenActivityTargetInlineCellEditMode = () => {
// TODO: deprecate this once we are supporting one to many through relations
export const useOpenActivityTargetCellEditMode = () => {
const { performSearch: multipleRecordPickerPerformSearch } =
useMultipleRecordPickerPerformSearch();
const openActivityTargetInlineCellEditMode = useRecoilCallback(
const openActivityTargetCellEditMode = useRecoilCallback(
({ set, snapshot }) =>
({
recordPickerInstanceId,
activityTargetObjectRecords,
}: OpenActivityTargetInlineCellEditModeProps) => {
}: OpenActivityTargetCellEditModeProps) => {
const objectMetadataItems = snapshot
.getLoadable(objectMetadataItemsState)
.getValue()
@ -82,5 +83,5 @@ export const useOpenActivityTargetInlineCellEditMode = () => {
[multipleRecordPickerPerformSearch],
);
return { openActivityTargetInlineCellEditMode };
return { openActivityTargetCellEditMode };
};

View File

@ -11,16 +11,17 @@ import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/typ
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { isNull } from '@sniptt/guards';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { v4 } from 'uuid';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
type UpdateActivityTargetFromInlineCellProps = {
type UpdateActivityTargetFromCellProps = {
recordPickerInstanceId: string;
morphItem: RecordPickerPickableMorphItem;
activityTargetWithTargetRecords: ActivityTargetWithTargetRecord[];
};
export const useUpdateActivityTargetFromInlineCell = ({
// TODO: deprecate this hook once we implement one-to-many relation through
export const useUpdateActivityTargetFromCell = ({
activityObjectNameSingular,
activityId,
}: {
@ -29,27 +30,37 @@ export const useUpdateActivityTargetFromInlineCell = ({
| CoreObjectNameSingular.Task;
activityId: string;
}) => {
const joinObjectNameSingular = getJoinObjectNameSingular(
activityObjectNameSingular,
);
const { createOneRecord: createOneActivityTarget } = useCreateOneRecord<
NoteTarget | TaskTarget
>({
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
objectNameSingular:
joinObjectNameSingular === ''
? activityObjectNameSingular
: joinObjectNameSingular,
});
const { deleteOneRecord: deleteOneActivityTarget } = useDeleteOneRecord({
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
objectNameSingular:
joinObjectNameSingular === ''
? activityObjectNameSingular
: joinObjectNameSingular,
});
const setActivityFromStore = useSetRecoilState(
recordStoreFamilyState(activityId),
);
const updateActivityTargetFromInlineCell = useRecoilCallback(
const updateActivityTargetFromCell = useRecoilCallback(
({ snapshot }) =>
async ({
morphItem,
activityTargetWithTargetRecords,
recordPickerInstanceId,
}: UpdateActivityTargetFromInlineCellProps) => {
}: UpdateActivityTargetFromCellProps) => {
const targetObjectName =
activityObjectNameSingular === CoreObjectNameSingular.Task
? 'task'
@ -179,5 +190,5 @@ export const useUpdateActivityTargetFromInlineCell = ({
],
);
return { updateActivityTargetFromInlineCell };
return { updateActivityTargetFromCell };
};

View File

@ -45,7 +45,6 @@ export const EventList = ({ events, targetableObject }: EventListProps) => {
return (
<ScrollWrapper
contextProviderName="eventList"
componentInstanceId={`scroll-wrapper-event-list-${targetableObject.id}`}
>
<StyledTimelineContainer>

View File

@ -54,6 +54,7 @@ export const EventFieldDiffValue = ({
defaultValue: fieldMetadataItem.defaultValue,
},
hotkeyScope: 'field-event-diff',
isReadOnly: false,
}}
>
<FieldDisplay />

View File

@ -0,0 +1,75 @@
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
import { Note } from '@/activities/types/Note';
import { NoteTarget } from '@/activities/types/NoteTarget';
import { Task } from '@/activities/types/Task';
import { TaskTarget } from '@/activities/types/TaskTarget';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
type GetActivityTargetObjectRecordsProps = {
activityRecord: Note | Task;
objectMetadataItems: ObjectMetadataItem[];
activityTargets?: NoteTarget[] | TaskTarget[];
};
export const getActivityTargetObjectRecords = ({
activityRecord,
objectMetadataItems,
activityTargets,
}: GetActivityTargetObjectRecordsProps) => {
if (!isDefined(activityRecord) && !isDefined(activityTargets)) {
return [];
}
const targets = activityTargets
? activityTargets
: activityRecord &&
'noteTargets' in activityRecord &&
activityRecord.noteTargets
? activityRecord.noteTargets
: activityRecord &&
'taskTargets' in activityRecord &&
activityRecord.taskTargets
? activityRecord.taskTargets
: [];
const activityTargetObjectRecords = targets
.map<ActivityTargetWithTargetRecord | undefined>((activityTarget) => {
if (!isDefined(activityTarget)) {
throw new Error(`Cannot find activity target`);
}
const correspondingObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
isDefined(activityTarget[objectMetadataItem.nameSingular]) &&
![CoreObjectNameSingular.Note, CoreObjectNameSingular.Task].includes(
objectMetadataItem.nameSingular as CoreObjectNameSingular,
),
);
if (!correspondingObjectMetadataItem) {
return undefined;
}
const targetObjectRecord = activityTarget[
correspondingObjectMetadataItem.nameSingular
] as ObjectRecord | undefined;
if (!isDefined(targetObjectRecord)) {
throw new Error(
`Cannot find target object record of type ${correspondingObjectMetadataItem.nameSingular}, make sure the request for activities eagerly loads for the target objects on activity target relation.`,
);
}
return {
activityTarget,
targetObject: targetObjectRecord,
targetObjectMetadataItem: correspondingObjectMetadataItem,
};
})
.filter(isDefined);
return activityTargetObjectRecords;
};