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

@ -138,8 +138,6 @@
"next-mdx-remote": "^4.4.1",
"nodemailer": "^6.9.8",
"openapi-types": "^12.1.3",
"overlayscrollbars": "^2.6.1",
"overlayscrollbars-react": "^0.5.4",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",

View File

@ -1,37 +0,0 @@
import { useEffect } from 'react';
import { useNavigation } from 'react-router-dom';
import { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState';
import { scrollWrapperScrollTopComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollTopComponentState';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDefined } from 'twenty-shared/utils';
/**
* @deprecated We should now use useScrollToPosition instead
* Note that `location.key` is used in the cache key, not `location.pathname`,
* so the same path navigated to at different points in the history stack will
* not share the same scroll position.
*/
export const useScrollRestoration = (viewportHeight?: number) => {
const { state } = useNavigation();
const [scrollTop, setScrollTop] = useRecoilComponentStateV2(
scrollWrapperScrollTopComponentState,
);
const overlayScrollbars = useRecoilComponentValueV2(
scrollWrapperInstanceComponentState,
);
const scrollWrapper = overlayScrollbars?.elements().viewport;
const skip = isDefined(viewportHeight) && scrollTop > viewportHeight;
useEffect(() => {
if (state === 'loading') {
setScrollTop(scrollWrapper?.scrollTop ?? 0);
} else if (state === 'idle' && isDefined(scrollWrapper) && !skip) {
scrollWrapper.scrollTo({ top: scrollTop });
}
}, [state, scrollWrapper, skip, scrollTop, setScrollTop]);
};

View File

@ -1,25 +0,0 @@
import { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilCallback } from 'recoil';
export const useScrollToPosition = () => {
const scrollWrapperInstanceState = useRecoilComponentCallbackStateV2(
scrollWrapperInstanceComponentState,
);
const scrollToPosition = useRecoilCallback(
({ snapshot }) =>
(scrollPositionInPx: number) => {
const overlayScrollbars = snapshot
.getLoadable(scrollWrapperInstanceState)
.getValue();
const scrollWrapper = overlayScrollbars?.elements().viewport;
scrollWrapper?.scrollTo({ top: scrollPositionInPx });
},
[scrollWrapperInstanceState],
);
return { scrollToPosition };
};

View File

@ -17,10 +17,6 @@ form {
display: contents;
}
html, body {
overscroll-behavior: none;
}
/* https://stackoverflow.com/questions/44543157/how-to-hide-the-google-invisible-recaptcha-badge */
.grecaptcha-badge {
visibility: hidden !important;

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,15 +38,12 @@ 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({
const { updateActivityTargetFromCell } = useUpdateActivityTargetFromCell({
activityObjectNameSingular,
activityId: activityRecordId,
});
@ -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;
};

View File

@ -12,10 +12,7 @@ type AuthModalProps = { children: React.ReactNode };
export const AuthModal = ({ children }: AuthModalProps) => (
<Modal padding={'none'} modalVariant="primary">
<ScrollWrapper
contextProviderName="modalContent"
componentInstanceId="scroll-wrapper-modal-content"
>
<ScrollWrapper componentInstanceId="scroll-wrapper-modal-content">
<StyledContent>{children}</StyledContent>
</ScrollWrapper>
</Modal>

View File

@ -79,10 +79,7 @@ export const CommandMenuList = ({
<CommandMenuDefaultSelectionEffect
selectableItemIds={selectableItemIds}
/>
<ScrollWrapper
contextProviderName="commandMenu"
componentInstanceId={`scroll-wrapper-command-menu`}
>
<ScrollWrapper componentInstanceId={`scroll-wrapper-command-menu`}>
<StyledInnerList>
<SelectableList
selectableListId="command-menu-list"

View File

@ -24,6 +24,7 @@ const StyledMainSection = styled(NavigationDrawerSection)`
`;
const StyledInnerContainer = styled.div`
height: 100%;
width: 100%;
`;
export const MainNavigationDrawerItems = () => {
@ -66,10 +67,8 @@ export const MainNavigationDrawerItems = () => {
</StyledMainSection>
)}
<ScrollWrapper
contextProviderName="navigationDrawer"
componentInstanceId={`scroll-wrapper-navigation-drawer`}
defaultEnableXScroll={false}
scrollbarVariant="no-padding"
>
<StyledInnerContainer>
<NavigationDrawerOpenedSection />

View File

@ -73,7 +73,7 @@ export const useAttachRelatedRecordFromRecord = ({
getRelatedRecordFromCache<ObjectRecord>(relatedRecordId);
if (!cachedRelatedRecord) {
throw new Error('could not find cached related record');
throw new Error('Could not find cached related record');
}
const previousRecordId = cachedRelatedRecord?.[`${fieldOnRelatedObject}Id`];

View File

@ -35,7 +35,6 @@ import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { ViewType } from '@/views/types/ViewType';
import { useScrollRestoration } from '~/hooks/useScrollRestoration';
const StyledContainer = styled.div`
display: flex;
@ -61,11 +60,6 @@ const StyledBoardContentContainer = styled.div`
height: calc(100% - 48px);
`;
const RecordBoardScrollRestoreEffect = () => {
useScrollRestoration();
return null;
};
export const RecordBoard = () => {
const { updateOneRecord, selectFieldMetadataItem, recordBoardId } =
useContext(RecordBoardContext);
@ -239,7 +233,6 @@ export const RecordBoard = () => {
value={{ instanceId: recordBoardId }}
>
<ScrollWrapper
contextProviderName="recordBoard"
componentInstanceId={`scroll-wrapper-record-board-${recordBoardId}`}
>
<RecordBoardStickyHeaderEffect />
@ -258,7 +251,6 @@ export const RecordBoard = () => {
</StyledColumnContainer>
</DragDropContext>
</StyledContainer>
<RecordBoardScrollRestoreEffect />
<DragSelect
dragSelectable={boardRef}
onDragSelectionEnd={handleDragSelectionEnd}

View File

@ -16,7 +16,7 @@ import { RecordValueSetterEffect } from '@/object-record/record-store/components
import { AppPath } from '@/types/AppPath';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { RecordBoardScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
@ -144,10 +144,10 @@ export const RecordBoardCard = () => {
}
}, 800);
const scrollWrapperRef = useContext(RecordBoardScrollWrapperContext);
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
const { ref: cardRef } = useInView({
root: scrollWrapperRef?.ref.current,
root: scrollWrapperHTMLElement,
rootMargin: '1000px',
});

View File

@ -11,6 +11,7 @@ import {
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
@ -21,7 +22,7 @@ export const RecordBoardCardBody = ({
}: {
fieldDefinitions: RecordBoardFieldDefinition<FieldMetadata>[];
}) => {
const { recordId } = useContext(RecordBoardCardContext);
const { recordId, isRecordReadOnly } = useContext(RecordBoardCardContext);
const { updateOneRecord } = useContext(RecordBoardContext);
@ -45,6 +46,13 @@ export const RecordBoardCardBody = ({
recordId,
maxWidth: 156,
isLabelIdentifier: false,
isReadOnly: isFieldValueReadOnly({
objectNameSingular:
fieldDefinition.metadata.objectMetadataNameSingular,
fieldName: fieldDefinition.metadata.fieldName,
fieldType: fieldDefinition.type,
isRecordReadOnly,
}),
fieldDefinition: {
disableTooltip: false,
fieldMetadataId: fieldDefinition.fieldMetadataId,
@ -60,6 +68,7 @@ export const RecordBoardCardBody = ({
},
useUpdateRecord: useUpdateOneRecordHook,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
isDisplayModeFixHeight: true,
}}
>
<RecordFieldComponentInstanceContext.Provider

View File

@ -1,6 +1,8 @@
import { Draggable } from '@hello-pangea/dnd';
import { RecordBoardCard } from '@/object-record/record-board/record-board-card/components/RecordBoardCard';
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
export const RecordBoardCardDraggableContainer = ({
recordId,
@ -9,7 +11,12 @@ export const RecordBoardCardDraggableContainer = ({
recordId: string;
index: number;
}) => {
const isRecordReadOnly = useIsRecordReadOnly({
recordId,
});
return (
<RecordBoardCardContext.Provider value={{ recordId, isRecordReadOnly }}>
<Draggable key={recordId} draggableId={recordId} index={index}>
{(draggableProvided) => (
<div
@ -26,5 +33,6 @@ export const RecordBoardCardDraggableContainer = ({
</div>
)}
</Draggable>
</RecordBoardCardContext.Provider>
);
};

View File

@ -2,6 +2,7 @@ import { createContext } from 'react';
type RecordBoardCardContextProps = {
recordId: string;
isRecordReadOnly: boolean;
};
export const RecordBoardCardContext =

View File

@ -1,7 +1,6 @@
import React from 'react';
import { RecordBoardCardDraggableContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardDraggableContainer';
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
type RecordBoardColumnCardsMemoProps = {
recordIds: string[];
@ -10,9 +9,11 @@ type RecordBoardColumnCardsMemoProps = {
export const RecordBoardColumnCardsMemo = React.memo(
({ recordIds }: RecordBoardColumnCardsMemoProps) => {
return recordIds.map((recordId, index) => (
<RecordBoardCardContext.Provider value={{ recordId }} key={recordId}>
<RecordBoardCardDraggableContainer recordId={recordId} index={index} />
</RecordBoardCardContext.Provider>
<RecordBoardCardDraggableContainer
key={recordId}
recordId={recordId}
index={index}
/>
));
},
);

View File

@ -11,9 +11,9 @@ import { RecordGroupDefinitionType } from '@/object-record/record-group/types/Re
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { Tag } from 'twenty-ui/components';
import { IconDotsVertical, IconPlus } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { Tag } from 'twenty-ui/components';
const StyledHeader = styled.div`
align-items: center;

View File

@ -78,6 +78,7 @@ export const FieldContextProvider = ({
hotkeyScope: InlineCellHotkeyScope.InlineCell,
clearable,
overridenIsFieldEmpty,
isReadOnly: false,
}}
>
{children}

View File

@ -32,6 +32,7 @@ export type GenericFieldContextType = {
overridenIsFieldEmpty?: boolean;
displayedMaxRows?: number;
isDisplayModeFixHeight?: boolean;
isReadOnly: boolean;
onOpenEditMode?: () => void;
onCloseEditMode?: () => void;
isLabelHidden?: boolean;

View File

@ -23,6 +23,7 @@ const getWrapper =
recordId,
hotkeyScope: 'hotkeyScope',
isLabelIdentifier: false,
isReadOnly: false,
}}
>
<RecoilRoot>{children}</RecoilRoot>

View File

@ -16,6 +16,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
recordId,
hotkeyScope: 'hotkeyScope',
isLabelIdentifier: false,
isReadOnly: false,
}}
>
<RecoilRoot>{children}</RecoilRoot>

View File

@ -22,6 +22,7 @@ const getWrapper =
recordId,
hotkeyScope: 'hotkeyScope',
isLabelIdentifier: false,
isReadOnly: false,
}}
>
<RecoilRoot>{children}</RecoilRoot>

View File

@ -1,79 +1,28 @@
import { renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import {
actorFieldDefinition,
phonesFieldDefinition,
} from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
import { JestRecordStoreSetter } from '~/testing/jest/JestRecordStoreSetter';
import { phonesFieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { useIsFieldValueReadOnly } from '../useIsFieldValueReadOnly';
const recordId = 'recordId';
const mockInstanceId = 'mock-instance-id';
const getWrapper =
(fieldDefinition: FieldDefinition<FieldMetadata>, isRecordDeleted: boolean) =>
({ children }: { children: ReactNode }) => {
return (
<RecoilRoot>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: mockInstanceId }}
>
<JestObjectMetadataItemSetter>
<JestRecordStoreSetter
records={[
{
id: recordId,
deletedAt: isRecordDeleted ? new Date().toISOString() : null,
__typename: 'standardObject',
} as ObjectRecord,
]}
>
<FieldContext.Provider
value={{
fieldDefinition,
recordId,
hotkeyScope: 'hotkeyScope',
isLabelIdentifier: false,
}}
>
{children}
</FieldContext.Provider>
</JestRecordStoreSetter>
</JestObjectMetadataItemSetter>
</ContextStoreComponentInstanceContext.Provider>
</RecoilRoot>
);
};
describe('useIsFieldValueReadOnly', () => {
it('should take fieldDefinition into account', () => {
const { result } = renderHook(() => useIsFieldValueReadOnly(), {
wrapper: getWrapper(phonesFieldDefinition, false),
});
it('should return true if the field is read only', () => {
const { result } = renderHook(() =>
useIsFieldValueReadOnly({
fieldDefinition: phonesFieldDefinition,
isRecordReadOnly: false,
}),
);
expect(result.current).toBe(false);
const { result: result2 } = renderHook(() => useIsFieldValueReadOnly(), {
wrapper: getWrapper(actorFieldDefinition, false),
});
expect(result2.current).toBe(true);
});
it('should take isRecordDeleted into account', () => {
const { result } = renderHook(() => useIsFieldValueReadOnly(), {
wrapper: getWrapper(phonesFieldDefinition, true),
});
it('should return true if the record is read only', () => {
const { result } = renderHook(() =>
useIsFieldValueReadOnly({
fieldDefinition: phonesFieldDefinition,
isRecordReadOnly: true,
}),
);
expect(result.current).toBe(true);
});

View File

@ -105,6 +105,7 @@ const getWrapper =
hotkeyScope: 'hotkeyScope',
isLabelIdentifier: false,
useUpdateRecord: useUpdateOneRecordMutation,
isReadOnly: false,
}}
>
{children}

View File

@ -342,6 +342,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => {
hotkeyScope: 'hotkeyScope',
isLabelIdentifier: false,
useUpdateRecord: useUpdateOneRecordMutation,
isReadOnly: false,
}}
>
{children}

View File

@ -1,45 +1,22 @@
import { useContext } from 'react';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRecoilValue } from 'recoil';
import { FieldContext } from '../contexts/FieldContext';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldValueReadOnly } from '../utils/isFieldValueReadOnly';
import { isDefined } from 'twenty-shared/utils';
export const useIsFieldValueReadOnly = () => {
const { fieldDefinition, recordId } = useContext(FieldContext);
type UseIsFieldValueReadOnlyParams = {
isRecordReadOnly: boolean;
fieldDefinition: FieldDefinition<FieldMetadata>;
};
export const useIsFieldValueReadOnly = ({
fieldDefinition,
isRecordReadOnly,
}: UseIsFieldValueReadOnlyParams) => {
const { metadata, type } = fieldDefinition;
const recordDeletedAt = useRecoilValue<ObjectRecord | null>(
recordStoreFamilySelector({
recordId,
fieldName: 'deletedAt',
}),
);
const contextStoreCurrentViewType = useRecoilComponentValueV2(
contextStoreCurrentViewTypeComponentState,
);
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: metadata.objectMetadataNameSingular ?? '',
});
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
return isFieldValueReadOnly({
objectNameSingular: metadata.objectMetadataNameSingular,
fieldName: metadata.fieldName,
fieldType: type,
isObjectRemote: objectMetadataItem.isRemote,
isRecordDeleted: isDefined(recordDeletedAt),
hasObjectReadOnlyPermission,
contextStoreCurrentViewType,
isRecordReadOnly,
});
};

View File

@ -0,0 +1,24 @@
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
type UseIsRecordReadOnlyParams = {
recordId: string;
};
export const useIsRecordReadOnly = ({
recordId,
}: UseIsRecordReadOnlyParams) => {
const recordDeletedAt = useRecoilValue<ObjectRecord | null>(
recordStoreFamilySelector({
recordId,
fieldName: 'deletedAt',
}),
);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
return hasObjectReadOnlyPermission || isDefined(recordDeletedAt);
};

View File

@ -1,9 +1,23 @@
import { useOpenActivityTargetCellEditMode } from '@/activities/inline-cell/hooks/useOpenActivityTargetCellEditMode';
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 { useOpenRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput';
import { useOpenRelationToOneFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import {
FieldMetadata,
FieldRelationFromManyValue,
FieldRelationValue,
} from '@/object-record/record-field/types/FieldMetadata';
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const useOpenFieldInputEditMode = () => {
@ -11,7 +25,12 @@ export const useOpenFieldInputEditMode = () => {
const { openRelationFromManyFieldInput } =
useOpenRelationFromManyFieldInput();
const openFieldInput = ({
const { openActivityTargetCellEditMode } =
useOpenActivityTargetCellEditMode();
const openFieldInput = useRecoilCallback(
({ snapshot }) =>
({
fieldDefinition,
recordId,
}: {
@ -25,9 +44,47 @@ export const useOpenFieldInputEditMode = () => {
});
}
if (
isFieldRelationFromManyObjects(fieldDefinition) &&
['taskTarget', 'noteTarget'].includes(
fieldDefinition.metadata.relationObjectMetadataNameSingular,
)
) {
const fieldValue = snapshot
.getLoadable<FieldRelationValue<FieldRelationFromManyValue>>(
recordStoreFamilySelector({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
}),
)
.getValue();
const activity = snapshot
.getLoadable(recordStoreFamilyState(recordId))
.getValue();
const objectMetadataItems = snapshot
.getLoadable(objectMetadataItemsState)
.getValue();
const activityTargetObjectRecords = getActivityTargetObjectRecords({
activityRecord: activity as Task | Note,
objectMetadataItems,
activityTargets: fieldValue as NoteTarget[] | TaskTarget[],
});
openActivityTargetCellEditMode({
recordPickerInstanceId: `relation-from-many-field-input-${recordId}`,
activityTargetObjectRecords,
});
return;
}
if (isFieldRelationFromManyObjects(fieldDefinition)) {
if (
isDefined(fieldDefinition.metadata.relationObjectMetadataNameSingular)
isDefined(
fieldDefinition.metadata.relationObjectMetadataNameSingular,
)
) {
openRelationFromManyFieldInput({
fieldName: fieldDefinition.metadata.fieldName,
@ -37,7 +94,13 @@ export const useOpenFieldInputEditMode = () => {
});
}
}
};
},
[
openActivityTargetCellEditMode,
openRelationFromManyFieldInput,
openRelationToOneFieldInput,
],
);
return {
openFieldInput: openFieldInput,

View File

@ -70,6 +70,7 @@ const meta: Meta = {
...relationFromManyFieldDisplayMock.fieldDefinition,
} as unknown as FieldDefinition<FieldMetadata>,
hotkeyScope: 'hotkey-scope',
isReadOnly: false,
}}
>
<RelationFieldValueSetterEffect />

View File

@ -1,7 +1,13 @@
import { useContext } from 'react';
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
import { useUpdateActivityTargetFromCell } from '@/activities/inline-cell/hooks/useUpdateActivityTargetFromCell';
import { NoteTarget } from '@/activities/types/NoteTarget';
import { TaskTarget } from '@/activities/types/TaskTarget';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useRelationField } from '@/object-record/record-field/meta-types/hooks/useRelationField';
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
@ -22,11 +28,34 @@ export const RelationFromManyFieldInput = ({
const recordPickerInstanceId = `relation-from-many-field-input-${recordId}`;
const { updateRelation } = useUpdateRelationFromManyFieldInput();
const fieldName = fieldDefinition.metadata.fieldName;
const objectMetadataNameSingular =
fieldDefinition.metadata.objectMetadataNameSingular;
const { updateActivityTargetFromCell } = useUpdateActivityTargetFromCell({
activityObjectNameSingular: objectMetadataNameSingular as
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task,
activityId: recordId,
});
const { fieldValue } = useRelationField();
const handleSubmit = () => {
onSubmit?.(() => {});
};
const isRelationFromActivityTargets =
(fieldName === 'noteTargets' &&
objectMetadataNameSingular === CoreObjectNameSingular.Note) ||
(fieldName === 'taskTargets' &&
objectMetadataNameSingular === CoreObjectNameSingular.Task);
const { activityTargetObjectRecords } = useActivityTargetObjectRecords(
recordId,
fieldValue as NoteTarget[] | TaskTarget[],
);
const relationFieldDefinition =
fieldDefinition as FieldDefinition<FieldRelationMetadata>;
@ -57,8 +86,22 @@ export const RelationFromManyFieldInput = ({
<MultipleRecordPicker
componentInstanceId={recordPickerInstanceId}
onSubmit={handleSubmit}
onChange={updateRelation}
onCreate={createNewRecordAndOpenRightDrawer}
onChange={(morphItem) => {
if (isRelationFromActivityTargets) {
updateActivityTargetFromCell({
morphItem,
activityTargetWithTargetRecords: activityTargetObjectRecords,
recordPickerInstanceId,
});
} else {
updateRelation(morphItem);
}
}}
onCreate={
!isRelationFromActivityTargets
? createNewRecordAndOpenRightDrawer
: undefined
}
onClickOutside={handleSubmit}
layoutDirection={
layoutDirection === 'downward'

View File

@ -75,6 +75,7 @@ const AddressInputWithContext = ({
recordId: recordId ?? '123',
hotkeyScope: 'hotkey-scope',
isLabelIdentifier: false,
isReadOnly: false,
}}
>
<AddressValueSetterEffect value={value} />

View File

@ -66,6 +66,7 @@ const BooleanFieldInputWithContext = ({
recordId: recordId ?? '123',
hotkeyScope: 'hotkey-scope',
isLabelIdentifier: false,
isReadOnly: false,
}}
>
<BooleanFieldValueSetterEffect

View File

@ -93,6 +93,7 @@ const DateFieldInputWithContext = ({
recordId: '123',
hotkeyScope: 'hotkey-scope',
isLabelIdentifier: false,
isReadOnly: false,
}}
>
<StorybookFieldInputDropdownFocusIdSetterEffect />

View File

@ -70,6 +70,7 @@ const NumberFieldInputWithContext = ({
recordId: '123',
hotkeyScope: 'hotkey-scope',
isLabelIdentifier: false,
isReadOnly: false,
}}
>
<StorybookFieldInputDropdownFocusIdSetterEffect />

View File

@ -68,6 +68,7 @@ const RatingFieldInputWithContext = ({
recordId: recordId ?? '123',
hotkeyScope: 'hotkey-scope',
isLabelIdentifier: false,
isReadOnly: false,
}}
>
<RatingFieldValueSetterEffect value={value} />

View File

@ -95,6 +95,7 @@ const RelationManyFieldInputWithContext = () => {
recordId: 'recordId',
hotkeyScope: 'hotkey-scope',
isLabelIdentifier: false,
isReadOnly: false,
}}
>
<RelationWorkspaceSetterEffect />

View File

@ -88,6 +88,7 @@ const RelationToOneFieldInputWithContext = ({
recordId: recordId,
hotkeyScope: 'hotkey-scope',
isLabelIdentifier: false,
isReadOnly: false,
}}
>
<RecordFieldComponentInstanceContext.Provider

View File

@ -63,6 +63,7 @@ const TextFieldInputWithContext = ({
},
hotkeyScope: 'hotkey-scope',
isLabelIdentifier: false,
isReadOnly: false,
}}
>
<StorybookFieldInputDropdownFocusIdSetterEffect />

View File

@ -1,142 +1,90 @@
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly';
import { FieldMetadataType } from '~/generated/graphql';
describe('isFieldValueReadOnly', () => {
it('should return true if fieldName is noteTargets or taskTargets', () => {
it('should return true if record is read only', () => {
const result = isFieldValueReadOnly({
fieldName: 'noteTargets',
contextStoreCurrentViewType: ContextStoreViewType.Table,
});
expect(result).toBe(true);
const result2 = isFieldValueReadOnly({
fieldName: 'taskTargets',
contextStoreCurrentViewType: ContextStoreViewType.Table,
});
expect(result2).toBe(true);
});
it('should return true if fieldName is noteTargets or taskTargets but is not in table or kanban view', () => {
const result = isFieldValueReadOnly({
fieldName: 'noteTargets',
contextStoreCurrentViewType: ContextStoreViewType.ShowPage,
});
expect(result).toBe(false);
const result2 = isFieldValueReadOnly({
fieldName: 'taskTargets',
contextStoreCurrentViewType: ContextStoreViewType.ShowPage,
});
expect(result2).toBe(false);
});
it('should return false if fieldName is not noteTargets or taskTargets', () => {
const result = isFieldValueReadOnly({
fieldName: 'test',
contextStoreCurrentViewType: ContextStoreViewType.Table,
});
expect(result).toBe(false);
});
it('should return true if isObjectRemote is true', () => {
const result = isFieldValueReadOnly({
isObjectRemote: true,
contextStoreCurrentViewType: ContextStoreViewType.Table,
isRecordReadOnly: true,
});
expect(result).toBe(true);
});
it('should return false if isObjectRemote is false', () => {
it('should return true if object is a workflow sub object', () => {
const result = isFieldValueReadOnly({
isObjectRemote: false,
contextStoreCurrentViewType: ContextStoreViewType.Table,
});
expect(result).toBe(false);
});
it('should return true if isRecordDeleted is true', () => {
const result = isFieldValueReadOnly({
isRecordDeleted: true,
contextStoreCurrentViewType: ContextStoreViewType.Table,
objectNameSingular: 'workflowRun',
});
expect(result).toBe(true);
});
it('should return false if isRecordDeleted is false', () => {
it('should return true if object is a calendar event', () => {
const result = isFieldValueReadOnly({
isRecordDeleted: false,
contextStoreCurrentViewType: ContextStoreViewType.Table,
});
expect(result).toBe(false);
});
it('should return true if objectNameSingular is Workflow and fieldName is not name', () => {
const result = isFieldValueReadOnly({
objectNameSingular: 'workflow',
fieldName: 'test',
contextStoreCurrentViewType: ContextStoreViewType.Table,
objectNameSingular: CoreObjectNameSingular.CalendarEvent,
});
expect(result).toBe(true);
});
it('should return false if objectNameSingular is Workflow and fieldName is name', () => {
it('should return true if object is a workflow and field is not name', () => {
const result = isFieldValueReadOnly({
objectNameSingular: 'Workflow',
objectNameSingular: CoreObjectNameSingular.Workflow,
fieldName: 'description',
});
expect(result).toBe(true);
});
it('should return false if object is a workflow and field is name', () => {
const result = isFieldValueReadOnly({
objectNameSingular: CoreObjectNameSingular.Workflow,
fieldName: 'name',
contextStoreCurrentViewType: ContextStoreViewType.Table,
});
expect(result).toBe(false);
});
it('should return true if isWorkflowSubObjectMetadata is true', () => {
const result = isFieldValueReadOnly({
objectNameSingular: 'workflowVersion',
contextStoreCurrentViewType: ContextStoreViewType.Table,
});
expect(result).toBe(true);
});
it('should return true if fieldType is FieldMetadataType.ACTOR', () => {
const result = isFieldValueReadOnly({
fieldType: FieldMetadataType.ACTOR,
contextStoreCurrentViewType: ContextStoreViewType.Table,
});
expect(result).toBe(true);
});
it('should return true if fieldType is FieldMetadataType.RICH_TEXT', () => {
describe('when checking field types', () => {
it('should return true if fieldType is RICH_TEXT', () => {
const result = isFieldValueReadOnly({
fieldType: FieldMetadataType.RICH_TEXT,
contextStoreCurrentViewType: ContextStoreViewType.Table,
});
expect(result).toBe(true);
});
it('should return false if fieldType is not FieldMetadataType.ACTOR or FieldMetadataType.RICH_TEXT', () => {
it('should return true if fieldType is RICH_TEXT_V2', () => {
const result = isFieldValueReadOnly({
fieldType: FieldMetadataType.RICH_TEXT_V2,
});
expect(result).toBe(true);
});
it('should return true if fieldType is ACTOR', () => {
const result = isFieldValueReadOnly({
fieldType: FieldMetadataType.ACTOR,
});
expect(result).toBe(true);
});
it('should return false for other field types', () => {
const result = isFieldValueReadOnly({
fieldType: FieldMetadataType.TEXT,
contextStoreCurrentViewType: ContextStoreViewType.Table,
});
expect(result).toBe(false);
});
});
it('should return false if none of the conditions are met', () => {
it('should return false for standard editable fields', () => {
const result = isFieldValueReadOnly({
contextStoreCurrentViewType: ContextStoreViewType.Table,
objectNameSingular: 'company',
fieldName: 'name',
fieldType: FieldMetadataType.TEXT,
isRecordReadOnly: false,
});
expect(result).toBe(false);

View File

@ -1,51 +1,25 @@
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata';
import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor';
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
type isFieldValueReadOnlyParams = {
objectNameSingular?: string;
fieldName?: string;
fieldType?: FieldMetadataType;
isObjectRemote?: boolean;
isRecordDeleted?: boolean;
hasObjectReadOnlyPermission?: boolean;
contextStoreCurrentViewType: ContextStoreViewType | null;
isRecordReadOnly?: boolean;
};
export const isFieldValueReadOnly = ({
objectNameSingular,
fieldName,
fieldType,
isObjectRemote = false,
isRecordDeleted = false,
hasObjectReadOnlyPermission = false,
contextStoreCurrentViewType,
isRecordReadOnly = false,
}: isFieldValueReadOnlyParams) => {
const isTableViewOrKanbanView =
contextStoreCurrentViewType === ContextStoreViewType.Table ||
contextStoreCurrentViewType === ContextStoreViewType.Kanban;
const isTargetField =
fieldName === 'noteTargets' || fieldName === 'taskTargets';
if (isTableViewOrKanbanView && isTargetField) {
return true;
}
if (isObjectRemote) {
return true;
}
if (isRecordDeleted) {
return true;
}
if (hasObjectReadOnlyPermission) {
if (isRecordReadOnly) {
return true;
}
@ -64,6 +38,20 @@ export const isFieldValueReadOnly = ({
return true;
}
if (
objectNameSingular !== CoreObjectNameSingular.Note &&
fieldName === 'noteTargets'
) {
return true;
}
if (
objectNameSingular !== CoreObjectNameSingular.Task &&
fieldName === 'taskTargets'
) {
return true;
}
if (
isDefined(fieldType) &&
(isFieldActor({ type: fieldType }) ||

View File

@ -11,7 +11,6 @@ import {
} from '@/object-record/record-field/types/FieldInputEvent';
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
@ -46,14 +45,13 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
isDisplayModeFixHeight,
onOpenEditMode,
onCloseEditMode,
isReadOnly,
} = useContext(FieldContext);
const buttonIcon = useGetButtonIcon();
const isFieldInputOnly = useIsFieldInputOnly();
const isFieldReadOnly = useIsFieldValueReadOnly();
const { closeInlineCell } = useInlineCell();
const handleEnter: FieldInputEvent = (persistField) => {
@ -134,7 +132,7 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
};
const RecordInlineCellContextValue: RecordInlineCellContextProps = {
readonly: isFieldReadOnly,
readonly: isReadOnly,
buttonIcon: buttonIcon,
IconLabel: fieldDefinition.iconName
? getIcon(fieldDefinition.iconName)
@ -152,7 +150,7 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
onTab={handleTab}
onShiftTab={handleShiftTab}
onClickOutside={handleClickOutside}
isReadOnly={isFieldReadOnly}
isReadOnly={isReadOnly}
/>
),
displayModeContent: <FieldDisplay />,

View File

@ -9,13 +9,13 @@ import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInput
import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata';
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { useRecordInlineCellContext } from './RecordInlineCellContext';
import {
AppTooltip,
OverflowingTextWithTooltip,
TooltipDelay,
} from 'twenty-ui/display';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { useRecordInlineCellContext } from './RecordInlineCellContext';
const StyledIconContainer = styled.div`
align-items: center;
@ -38,7 +38,6 @@ const StyledLabelAndIconContainer = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
height: 18px;
padding-top: 3px;
`;
const StyledValueContainer = styled.div`

View File

@ -63,13 +63,8 @@ export const RecordInlineCellDisplayMode = ({
const { t } = useLingui();
const {
editModeContentOnly,
showLabel,
label,
buttonIcon,
} = useRecordInlineCellContext();
const { editModeContentOnly, showLabel, label, buttonIcon } =
useRecordInlineCellContext();
const isDisplayModeContentEmpty = useIsFieldEmpty();
const showEditButton =

View File

@ -6,6 +6,7 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
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';
@ -84,6 +85,10 @@ export const FieldsCard = ({
),
);
const isRecordReadOnly = useIsRecordReadOnly({
recordId: objectRecordId,
});
return (
<>
<PropertyBox>
@ -109,6 +114,7 @@ export const FieldsCard = ({
useUpdateRecord: useUpdateOneObjectRecordMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
isDisplayModeFixHeight: true,
isReadOnly: isRecordReadOnly,
}}
>
<ActivityTargetsInlineCell
@ -146,6 +152,7 @@ export const FieldsCard = ({
useUpdateRecord: useUpdateOneObjectRecordMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
isDisplayModeFixHeight: true,
isReadOnly: isRecordReadOnly,
}}
>
<RecordFieldComponentInstanceContext.Provider
@ -182,6 +189,7 @@ export const FieldsCard = ({
useUpdateRecord: useUpdateOneObjectRecordMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
isDisplayModeFixHeight: true,
isReadOnly: isRecordReadOnly,
}}
>
<RecordDetailRelationSection

View File

@ -1,6 +1,7 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
import { RecordTitleCell } from '@/object-record/record-title-cell/components/RecordTitleCell';
@ -55,6 +56,10 @@ export const ObjectRecordShowPageBreadcrumb = ({
objectRecordId,
});
const isRecordReadOnly = useIsRecordReadOnly({
recordId: objectRecordId,
});
if (loading) {
return null;
}
@ -87,6 +92,7 @@ export const ObjectRecordShowPageBreadcrumb = ({
hotkeyScope: InlineCellHotkeyScope.InlineCell,
isCentered: false,
isDisplayModeFixHeight: true,
isReadOnly: isRecordReadOnly,
}}
>
<RecordTitleCell sizeVariant="xs" />

View File

@ -1,6 +1,7 @@
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
@ -10,8 +11,8 @@ import { RecordTitleCell } from '@/object-record/record-title-cell/components/Re
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useRecoilValue } from 'recoil';
import { FieldMetadataType } from '~/generated/graphql';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated/graphql';
type SummaryCardProps = {
objectNameSingular: string;
@ -54,6 +55,10 @@ export const SummaryCard = ({
}),
);
const isRecordReadOnly = useIsRecordReadOnly({
recordId: objectRecordId,
});
return (
<ShowPageSummaryCard
isMobile={isMobile}
@ -88,6 +93,7 @@ export const SummaryCard = ({
hotkeyScope: InlineCellHotkeyScope.InlineCell,
isCentered: !isMobile,
isDisplayModeFixHeight: true,
isReadOnly: isRecordReadOnly,
}}
>
<RecordTitleCell sizeVariant="md" />

View File

@ -17,6 +17,7 @@ import {
RecordUpdateHookParams,
} from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
@ -209,7 +210,14 @@ export const RecordDetailRelationRecordsListItem = ({
[isExpanded],
);
const isReadOnly = useIsFieldValueReadOnly();
const isRecordReadOnly = useIsRecordReadOnly({
recordId: relationRecord.id,
});
const isFieldReadOnly = useIsFieldValueReadOnly({
fieldDefinition,
isRecordReadOnly,
});
return (
<>
@ -226,7 +234,7 @@ export const RecordDetailRelationRecordsListItem = ({
accent="tertiary"
/>
</StyledClickableZone>
{!isReadOnly && (
{!isFieldReadOnly && (
<DropdownScope dropdownScopeId={dropdownScopeId}>
<Dropdown
dropdownId={dropdownScopeId}
@ -279,6 +287,7 @@ export const RecordDetailRelationRecordsListItem = ({
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
isReadOnly: false,
}}
>
<RecordFieldComponentInstanceContext.Provider

View File

@ -6,6 +6,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
@ -190,7 +191,14 @@ export const RecordDetailRelationSection = ({
recordId,
});
const isReadOnly = useIsFieldValueReadOnly();
const isRecordReadOnly = useIsRecordReadOnly({
recordId,
});
const isFieldReadOnly = useIsFieldValueReadOnly({
fieldDefinition,
isRecordReadOnly,
});
if (loading) return null;
@ -250,7 +258,7 @@ export const RecordDetailRelationSection = ({
hideRightAdornmentOnMouseLeave={!isDropdownOpen && !isMobile}
areRecordsAvailable={relationRecords.length > 0}
rightAdornment={
!isReadOnly && (
!isFieldReadOnly && (
<DropdownScope dropdownScopeId={dropdownId}>
<StyledAddDropdown
dropdownId={dropdownId}

View File

@ -46,6 +46,7 @@ const meta: Meta<typeof RecordDetailRelationSection> = {
objectMetadataItem: mockedCompanyObjectMetadataItem,
}),
hotkeyScope: 'hotkey-scope',
isReadOnly: false,
}}
>
<Story />

View File

@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { scrollWrapperScrollBottomComponentState } from '@/ui/utilities/scroll/states/scrollWrappeScrollBottomComponentState';
import { scrollWrapperScrollBottomComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollBottomComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordTableStickyBottomEffect = () => {

View File

@ -102,7 +102,6 @@ export const RecordTableWithWrappers = ({
>
<EntityDeleteContext.Provider value={deleteOneRecord}>
<ScrollWrapper
contextProviderName="recordTableWithWrappers"
componentInstanceId={`record-table-scroll-${recordTableId}`}
>
<RecordUpdateContext.Provider value={updateRecordMutation}>

View File

@ -137,6 +137,7 @@ const meta: Meta = {
...mockPerformance.fieldDefinition,
},
hotkeyScope: 'hotkey-scope',
isReadOnly: false,
}}
>
<RelationFieldValueSetterEffect />

View File

@ -8,6 +8,7 @@ export type RecordTableRowContextValue = {
isSelected: boolean;
inView: boolean;
isPendingRow?: boolean;
isReadOnly?: boolean;
};
export const [RecordTableRowContextProvider, useRecordTableRowContextOrThrow] =

View File

@ -1,12 +1,11 @@
import styled from '@emotion/styled';
import { useContext } from 'react';
import { useInView } from 'react-intersection-observer';
import { useRecoilCallback } from 'recoil';
import { isRecordIndexLoadMoreLockedComponentState } from '@/object-record/record-index/states/isRecordIndexLoadMoreLockedComponentState';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2';
import { RecordTableWithWrappersScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { GRAY_SCALE } from 'twenty-ui/theme';
@ -38,9 +37,7 @@ export const RecordTableBodyFetchMoreLoader = () => {
[setRecordTableLastRowVisible, isRecordTableLoadMoreLocked],
);
const scrollWrapperRef = useContext(
RecordTableWithWrappersScrollWrapperContext,
);
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
const hasRecordTableFetchedAllRecordsComponents = useRecoilComponentValueV2(
hasRecordTableFetchedAllRecordsComponentStateV2,
@ -53,9 +50,7 @@ export const RecordTableBodyFetchMoreLoader = () => {
onChange: onLastRowVisible,
delay: 1000,
rootMargin: '1000px',
root: scrollWrapperRef?.ref.current?.querySelector(
'[data-overlayscrollbars-viewport]',
),
root: scrollWrapperHTMLElement,
});
if (!showLoadingMoreRow) {

View File

@ -11,11 +11,11 @@ import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record
import { tableEncounteredUnrecoverableErrorComponentState } from '@/object-record/record-table/states/tableEncounteredUnrecoverableErrorComponentState';
import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState';
import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState';
import { useScrollToPosition } from '@/ui/utilities/scroll/hooks/useScrollToPosition';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { isNonEmptyString, isNull } from '@sniptt/guards';
import { useScrollToPosition } from '~/hooks/useScrollToPosition';
export const RecordTableNoRecordGroupBodyEffect = () => {
const { objectNameSingular } = useRecordTableContextOrThrow();

View File

@ -8,9 +8,9 @@ import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/
import { recordIndexHasFetchedAllRecordsByGroupComponentState } from '@/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState';
import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useScrollToPosition } from '@/ui/utilities/scroll/hooks/useScrollToPosition';
import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';
import { isNonEmptyString, isNull } from '@sniptt/guards';
import { useScrollToPosition } from '~/hooks/useScrollToPosition';
export const RecordTableRecordGroupBodyEffect = () => {
const { objectNameSingular } = useRecordTableContextOrThrow();

View File

@ -3,7 +3,6 @@ import { ReactNode, useContext } from 'react';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { CellHotkeyScopeContext } from '@/object-record/record-table/contexts/CellHotkeyScopeContext';
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
@ -52,11 +51,11 @@ export const RecordTableCellBaseContainer = ({
}: {
children: ReactNode;
}) => {
const { isReadOnly } = useContext(FieldContext);
const { setIsFocused } = useFieldFocus();
const { openTableCell } = useOpenRecordTableCellFromCell();
const { theme } = useContext(ThemeContext);
const isReadOnly = useIsFieldValueReadOnly();
const { hasSoftFocus, cellPosition } = useContext(RecordTableCellContext);
const { onMoveSoftFocusToCurrentCell, onCellMouseEnter } =
@ -98,7 +97,7 @@ export const RecordTableCellBaseContainer = ({
fontColorExtraLight={theme.font.color.extraLight}
fontColorMedium={theme.border.color.medium}
hasSoftFocus={hasSoftFocus}
isReadOnly={isReadOnly}
isReadOnly={isReadOnly ?? false}
>
{children}
</StyledBaseContainer>

View File

@ -1,6 +1,6 @@
import { ReactNode, useContext } from 'react';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
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';
@ -8,16 +8,17 @@ import { useRecordIndexContextOrThrow } from '@/object-record/record-index/conte
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 { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { 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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ReactNode, useContext } from 'react';
import { useIsMobile } from 'twenty-ui/utilities';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { isRecordTableScrolledLeftComponentState } from '../../states/isRecordTableScrolledLeftComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useIsMobile } from 'twenty-ui/utilities';
export const RecordTableCellFieldContext = ({
children,
@ -27,7 +28,8 @@ export const RecordTableCellFieldContext = ({
const { objectMetadataItem } = useRecordTableContextOrThrow();
const { indexIdentifierUrl } = useRecordIndexContextOrThrow();
const { columnDefinition } = useContext(RecordTableCellContext);
const { recordId } = useRecordTableRowContextOrThrow();
const { recordId, isReadOnly: isTableRowReadOnly } =
useRecordTableRowContextOrThrow();
const updateRecord = useContext(RecordUpdateContext);
const isMobile = useIsMobile();
@ -70,6 +72,11 @@ export const RecordTableCellFieldContext = ({
const customHotkeyScope = computedHotkeyScope(columnDefinition);
const isFieldReadOnly = useIsFieldValueReadOnly({
fieldDefinition: columnDefinition,
isRecordReadOnly: isTableRowReadOnly ?? false,
});
return (
<FieldContext.Provider
value={{
@ -87,6 +94,7 @@ export const RecordTableCellFieldContext = ({
}),
displayedMaxRows: 1,
isLabelHidden,
isReadOnly: isFieldReadOnly,
}}
>
{children}

View File

@ -1,10 +1,10 @@
import { ReactNode, useContext } from 'react';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableCellFieldContext } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContext';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { ReactNode, useContext } from 'react';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { RecordTableCellFieldContext } from './RecordTableCellFieldContext';
export const RecordTableCellFieldContextWrapper = ({
children,
@ -12,6 +12,7 @@ export const RecordTableCellFieldContextWrapper = ({
children: ReactNode;
}) => {
const { columnDefinition } = useContext(RecordTableCellContext);
const { recordId } = useRecordTableRowContextOrThrow();
if (isUndefinedOrNull(columnDefinition)) {

View File

@ -1,5 +1,5 @@
import { FieldInput } from '@/object-record/record-field/components/FieldInput';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import {
FieldInputClickOutsideEvent,
FieldInputEvent,
@ -7,12 +7,13 @@ import {
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
import { useContext } from 'react';
import { useRecoilCallback } from 'recoil';
export const RecordTableCellFieldInput = () => {
const { onMoveFocus, onCloseTableCell } = useRecordTableBodyContextOrThrow();
const isFieldReadOnly = useIsFieldValueReadOnly();
const { isReadOnly } = useContext(FieldContext);
const handleEnter: FieldInputEvent = (persistField) => {
persistField();
@ -77,7 +78,7 @@ export const RecordTableCellFieldInput = () => {
onShiftTab={handleShiftTab}
onSubmit={handleSubmit}
onTab={handleTab}
isReadOnly={isFieldReadOnly}
isReadOnly={isReadOnly}
/>
);
};

View File

@ -10,12 +10,11 @@ import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/rec
import { isSoftFocusUsingMouseState } from '@/object-record/record-table/states/isSoftFocusUsingMouseState';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer';
import { isDefined } from 'twenty-shared/utils';
import { IconArrowUpRight } from 'twenty-ui/display';
import { useIsMobile } from 'twenty-ui/utilities';
import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer';
type RecordTableCellSoftFocusModeProps = {
editModeContent: ReactElement;
@ -27,12 +26,10 @@ export const RecordTableCellSoftFocusMode = ({
nonEditModeContent,
}: RecordTableCellSoftFocusModeProps) => {
const { columnIndex, columnDefinition } = useContext(RecordTableCellContext);
const { recordId } = useContext(FieldContext);
const { recordId, isReadOnly } = useContext(FieldContext);
const { onActionMenuDropdownOpened } = useRecordTableBodyContextOrThrow();
const isFieldReadOnly = useIsFieldValueReadOnly();
const { openTableCell } = useOpenRecordTableCellFromCell();
const editModeContentOnly = useIsFieldInputOnly();
@ -52,7 +49,7 @@ export const RecordTableCellSoftFocusMode = ({
}, [isSoftFocusUsingMouse]);
const handleClick = () => {
if (!isFieldInputOnly && !isFieldReadOnly) {
if (!isFieldInputOnly && !isReadOnly) {
openTableCell();
}
};
@ -85,13 +82,13 @@ export const RecordTableCellSoftFocusMode = ({
const showButton =
isDefined(buttonIcon) &&
!editModeContentOnly &&
!isFieldReadOnly &&
!isReadOnly &&
!(isMobile && isFirstColumn);
const dontShowContent = isEmpty && isFieldReadOnly;
const dontShowContent = isEmpty && isReadOnly;
const showPlaceholder =
!editModeContentOnly && !isFieldReadOnly && isFirstColumn && isEmpty;
!editModeContentOnly && !isReadOnly && isFirstColumn && isEmpty;
return (
<>

View File

@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react';
import { useContext, useEffect, useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
@ -11,14 +11,12 @@ import { isSoftFocusUsingMouseState } from '@/object-record/record-table/states/
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
export const RecordTableCellSoftFocusModeHotkeysSetterEffect = () => {
const isFieldReadOnly = useIsFieldValueReadOnly();
const { openTableCell } = useOpenRecordTableCellFromCell();
const { isReadOnly } = useContext(FieldContext);
const isFieldInputOnly = useIsFieldInputOnly();
@ -50,7 +48,7 @@ export const RecordTableCellSoftFocusModeHotkeysSetterEffect = () => {
useScopedHotkeys(
Key.Enter,
() => {
if (isFieldReadOnly) {
if (isReadOnly) {
return;
}
@ -67,7 +65,7 @@ export const RecordTableCellSoftFocusModeHotkeysSetterEffect = () => {
useScopedHotkeys(
'*',
(keyboardEvent) => {
if (isFieldReadOnly) {
if (isReadOnly) {
return;
}

View File

@ -54,6 +54,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
recordId: 'recordId',
hotkeyScope: TableHotkeyScope.Table,
isLabelIdentifier: false,
isReadOnly: false,
}}
>
<RecordTableRowContextProvider value={recordTableRowContextValue}>

View File

@ -54,6 +54,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
recordId: 'recordId',
hotkeyScope: TableHotkeyScope.Table,
isLabelIdentifier: false,
isReadOnly: false,
}}
>
<RecordTableRowContextProvider value={recordTableRowContextValue}>

View File

@ -1,7 +1,6 @@
import { useContext } from 'react';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { CellHotkeyScopeContext } from '@/object-record/record-table/contexts/CellHotkeyScopeContext';
@ -29,7 +28,7 @@ export type OpenTableCellArgs = {
export const useOpenRecordTableCellFromCell = () => {
const customCellHotkeyScope = useContext(CellHotkeyScopeContext);
const { recordId, fieldDefinition } = useContext(FieldContext);
const { recordId, fieldDefinition, isReadOnly } = useContext(FieldContext);
const { pathToShowPage, objectNameSingular } =
useRecordTableRowContextOrThrow();
@ -38,8 +37,6 @@ export const useOpenRecordTableCellFromCell = () => {
const cellPosition = useCurrentTableCellPosition();
const isFieldReadOnly = useIsFieldValueReadOnly();
const openTableCell = (
initialValue?: string,
isActionButtonClick = false,
@ -50,7 +47,7 @@ export const useOpenRecordTableCellFromCell = () => {
customCellHotkeyScope,
recordId,
fieldDefinition,
isReadOnly: isFieldReadOnly,
isReadOnly,
pathToShowPage,
objectNameSingular,
initialValue,

View File

@ -4,7 +4,7 @@ import { RecordTableAggregateFooterCell } from '@/object-record/record-table/rec
import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext';
import { FIRST_TH_WIDTH } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState';
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isUndefined } from '@sniptt/guards';
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
@ -86,14 +86,11 @@ export const RecordTableAggregateFooter = ({
visibleTableColumnsComponentSelector,
);
const overlayScrollbarsInstance = useRecoilComponentValueV2(
scrollWrapperInstanceComponentState,
);
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
const hasHorizontalOverflow = overlayScrollbarsInstance
? overlayScrollbarsInstance.elements().scrollOffsetElement.scrollWidth >
overlayScrollbarsInstance.elements().scrollOffsetElement.clientWidth
: false;
const hasHorizontalOverflow =
(scrollWrapperHTMLElement?.scrollWidth ?? 0) >
(scrollWrapperHTMLElement?.clientWidth ?? 0);
return (
<StyledTableRow

View File

@ -1,16 +1,13 @@
import { Theme } from '@emotion/react';
import styled from '@emotion/styled';
import { useContext } from 'react';
import { HIDDEN_TABLE_COLUMN_DROPDOWN_ID } from '@/object-record/record-table/constants/HiddenTableColumnDropdownId';
import { RecordTableHeaderPlusButtonContent } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useScrollWrapperScopedRef } from '@/ui/utilities/scroll/hooks/useScrollWrapperScopedRef';
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
import { useTheme } from '@emotion/react';
import { IconPlus } from 'twenty-ui/display';
import { ThemeContext } from 'twenty-ui/theme';
const StyledPlusIconHeaderCell = styled.th<{
theme: Theme;
isTableWiderThanScreen: boolean;
}>`
${({ theme }) => {
@ -54,20 +51,17 @@ const HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID =
'hidden-table-columns-dropdown-hotkey-scope-id';
export const RecordTableHeaderLastColumn = () => {
const { theme } = useContext(ThemeContext);
const theme = useTheme();
const scrollWrapper = useScrollWrapperScopedRef('recordTableWithWrappers');
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
const isTableWiderThanScreen =
(scrollWrapper.ref.current?.clientWidth ?? 0) <
(scrollWrapper.ref.current?.scrollWidth ?? 0);
(scrollWrapperHTMLElement?.clientWidth ?? 0) <
(scrollWrapperHTMLElement?.scrollWidth ?? 0);
return (
<>
<StyledPlusIconHeaderCell
theme={theme}
isTableWiderThanScreen={isTableWiderThanScreen}
>
<StyledPlusIconHeaderCell isTableWiderThanScreen={isTableWiderThanScreen}>
<Dropdown
dropdownId={HIDDEN_TABLE_COLUMN_DROPDOWN_ID}
clickableComponent={

View File

@ -1,4 +1,4 @@
import { ReactNode, useContext, useEffect } from 'react';
import { ReactNode, useEffect } from 'react';
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
@ -6,7 +6,7 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableDraggableTr } from '@/object-record/record-table/record-table-row/components/RecordTableDraggableTr';
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
import { RecordTableWithWrappersScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useInView } from 'react-intersection-observer';
@ -34,14 +34,10 @@ export const RecordTableRowWrapper = ({
const { onIndexRecordsLoaded } = useRecordIndexContextOrThrow();
const scrollWrapperRef = useContext(
RecordTableWithWrappersScrollWrapperContext,
);
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
const { ref: elementRef, inView } = useInView({
root: scrollWrapperRef.ref.current?.querySelector(
'[data-overlayscrollbars-viewport]',
),
root: scrollWrapperHTMLElement,
rootMargin: '1000px',
});

View File

@ -41,10 +41,7 @@ export const SettingsNavigationDrawerItems = () => {
return (
<ScrollWrapper
contextProviderName="navigationDrawer"
componentInstanceId={`scroll-wrapper-settings-navigation-drawer`}
scrollbarVariant="no-padding"
heightMode="fit-content"
defaultEnableXScroll={false}
>
<StyledInnerContainer>

View File

@ -30,11 +30,7 @@ export const SettingsPageContainer = ({
}: {
children: ReactNode;
}) => (
<ScrollWrapper
contextProviderName="settingsPageContainer"
heightMode="full"
componentInstanceId={'scroll-wrapper-settings-page-container'}
>
<ScrollWrapper componentInstanceId={'scroll-wrapper-settings-page-container'}>
<StyledSettingsPageContainer>{children}</StyledSettingsPageContainer>
</ScrollWrapper>
);

View File

@ -146,6 +146,7 @@ export const SettingsDataModelFieldPreview = ({
defaultValue: fieldMetadataItem.defaultValue,
},
hotkeyScope: 'field-preview',
isReadOnly: false,
}}
>
{fieldMetadataItem.type === FieldMetadataType.BOOLEAN ? (

View File

@ -4,11 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { Heading } from '@/spreadsheet-import/components/Heading';
import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import {
ImportedRow,
ImportedStructuredRow,
SpreadsheetImportField,
} from '@/spreadsheet-import/types';
import { ImportedRow, ImportedStructuredRow } from '@/spreadsheet-import/types';
import { findUnmatchedRequiredFields } from '@/spreadsheet-import/utils/findUnmatchedRequiredFields';
import { getMatchedColumns } from '@/spreadsheet-import/utils/getMatchedColumns';
import { normalizeTableData } from '@/spreadsheet-import/utils/normalizeTableData';
@ -21,13 +17,14 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { initialComputedColumnsSelector } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/states/initialComputedColumnsState';
import { UnmatchColumn } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/UnmatchColumn';
import { initialComputedColumnsSelector } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/states/initialComputedColumnsState';
import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep';
import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType';
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
import { SpreadsheetImportField } from '@/spreadsheet-import/types/SpreadsheetImportField';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { Trans, useLingui } from '@lingui/react/macro';
import { useRecoilState } from 'recoil';
@ -273,11 +270,7 @@ export const MatchColumnsStep = <T extends string>({
return (
<>
<ScrollWrapper
contextProviderName="modalContent"
componentInstanceId="scroll-wrapper-modal-content"
heightMode="full"
>
<ScrollWrapper componentInstanceId="scroll-wrapper-modal-content">
<StyledContent>
<Heading
title={t`Match Columns`}

View File

@ -55,7 +55,6 @@ export const DropdownMenuItemsContainer = ({
>
{hasMaxHeight ? (
<StyledScrollWrapper
contextProviderName="dropdownMenuItemsContainer"
componentInstanceId={`scroll-wrapper-dropdown-menu-${id}`}
>
<StyledDropdownMenuItemsInternalContainer>
@ -69,10 +68,7 @@ export const DropdownMenuItemsContainer = ({
)}
</StyledDropdownMenuItemsExternalContainer>
) : (
<ScrollWrapper
contextProviderName="dropdownMenuItemsContainer"
componentInstanceId={`scroll-wrapper-dropdown-menu-${id}`}
>
<ScrollWrapper componentInstanceId={`scroll-wrapper-dropdown-menu-${id}`}>
<StyledDropdownMenuItemsExternalContainer
hasMaxHeight={hasMaxHeight}
className={className}

View File

@ -30,17 +30,7 @@ const StyledLayout = styled.div`
scrollbar-width: 4px;
width: 100%;
*::-webkit-scrollbar {
height: 4px;
width: 4px;
}
*::-webkit-scrollbar-corner {
background-color: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: ${({ theme }) => theme.border.radius.sm};
}
`;

View File

@ -32,7 +32,6 @@ export const ShowPageContainer = ({ children }: ShowPageContainerProps) => {
return isMobile ? (
<StyledOuterContainer>
<StyledScrollWrapper
contextProviderName="showPageContainer"
componentInstanceId={'scroll-wrapper-show-page-container'}
>
<StyledInnerContainer>{children}</StyledInnerContainer>

View File

@ -70,7 +70,6 @@ export const ShowPageActivityContainer = ({
return (
<ScrollWrapper
contextProviderName="showPageActivityContainer"
componentInstanceId={`scroll-wrapper-tab-list-${targetableObject.id}`}
>
<StyledShowPageActivityContainer>

View File

@ -47,7 +47,6 @@ export const ShowPageLeftContainer = ({
</StyledInnerContainer>
) : (
<ScrollWrapper
contextProviderName="showPageLeftContainer"
componentInstanceId={`scroll-wrapper-show-page-left-container`}
>
<StyledIntermediateContainer>

View File

@ -79,7 +79,6 @@ export const TabList = ({
/>
<ScrollWrapper
defaultEnableYScroll={false}
contextProviderName="tabList"
componentInstanceId={`scroll-wrapper-tab-list-${componentInstanceId}`}
>
<StyledContainer className={className}>

View File

@ -1,6 +1,6 @@
import { createState } from 'twenty-ui/utilities';
import { INITIAL_HOTKEYS_SCOPE } from '../../constants/InitialHotkeysScope';
import { HotkeyScope } from '../../types/HotkeyScope';
import { createState } from 'twenty-ui/utilities';
export const currentHotkeyScopeState = createState<HotkeyScope>({
key: 'currentHotkeyScopeState',

View File

@ -1,102 +1,44 @@
import styled from '@emotion/styled';
import { OverlayScrollbars } from 'overlayscrollbars';
import { useOverlayScrollbars } from 'overlayscrollbars-react';
import { useEffect, useRef } from 'react';
import {
ContextProviderName,
getContextByProviderName,
} from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
import { ScrollWrapperInitEffect } from '@/ui/utilities/scroll/components/internal/ScrollWrapperInitEffect';
import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext';
import { scrollWrapperScrollBottomComponentState } from '@/ui/utilities/scroll/states/scrollWrappeScrollBottomComponentState';
import { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState';
import { scrollWrapperScrollBottomComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollBottomComponentState';
import { scrollWrapperScrollLeftComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollLeftComponentState';
import { scrollWrapperScrollTopComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollTopComponentState';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { css } from '@emotion/react';
import 'overlayscrollbars/overlayscrollbars.css';
type HeightMode = 'full' | 'fit-content';
const StyledScrollWrapper = styled.div<{
heightMode: HeightMode;
scrollbarVariant: 'with-padding' | 'no-padding';
}>`
const StyledScrollWrapper = styled.div`
&.scroll-wrapper-x-enabled {
overflow-x: scroll;
}
&.scroll-wrapper-y-enabled {
overflow-y: scroll;
}
display: flex;
height: ${({ heightMode }) => {
switch (heightMode) {
case 'full':
return '100%';
case 'fit-content':
return 'fit-content';
}
}};
width: 100%;
.os-scrollbar-handle {
background-color: ${({ theme }) => theme.border.color.strong};
}
// Keep horizontal scrollbar always visible
.os-scrollbar-horizontal {
&.os-scrollbar-auto-hide {
opacity: 1;
visibility: visible;
}
.os-scrollbar-track {
visibility: visible !important;
}
}
.os-scrollbar {
transition:
opacity 300ms,
visibility 300ms,
top 300ms,
right 300ms,
bottom 300ms,
left 300ms;
}
${({ scrollbarVariant }) =>
scrollbarVariant === 'no-padding' &&
css`
.os-scrollbar {
--os-size: 6px;
padding: 0px;
}
`}
height: 100%;
`;
const StyledInnerContainer = styled.div`
height: 100%;
width: 100%;
`;
export type ScrollWrapperProps = {
children: React.ReactNode;
className?: string;
heightMode?: HeightMode;
defaultEnableXScroll?: boolean;
defaultEnableYScroll?: boolean;
contextProviderName: ContextProviderName;
componentInstanceId: string;
scrollbarVariant?: 'with-padding' | 'no-padding';
};
export const ScrollWrapper = ({
componentInstanceId,
children,
className,
heightMode = 'full',
defaultEnableXScroll = true,
defaultEnableYScroll = true,
contextProviderName,
scrollbarVariant = 'with-padding',
}: ScrollWrapperProps) => {
const scrollableRef = useRef<HTMLDivElement>(null);
const Context = getContextByProviderName(contextProviderName);
const setScrollTop = useSetRecoilComponentStateV2(
scrollWrapperScrollTopComponentState,
componentInstanceId,
@ -112,8 +54,8 @@ export const ScrollWrapper = ({
componentInstanceId,
);
const handleScroll = (overlayScroll: OverlayScrollbars) => {
const target = overlayScroll.elements().scrollOffsetElement;
const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {
const target = event.currentTarget;
setScrollTop(target.scrollTop);
setScrollLeft(target.scrollLeft);
setScrollBottom(
@ -121,103 +63,21 @@ export const ScrollWrapper = ({
);
};
const setOverlayScrollbars = useSetRecoilComponentStateV2(
scrollWrapperInstanceComponentState,
componentInstanceId,
);
const [initialize, instance] = useOverlayScrollbars({
options: {
scrollbars: {
autoHide: 'scroll',
autoHideDelay: 500,
},
overflow: {
x: defaultEnableXScroll ? undefined : 'hidden',
y: defaultEnableYScroll ? undefined : 'hidden',
},
},
events: {
updated: (osInstance) => {
const {
scrollOffsetElement: target,
scrollbarVertical,
scrollbarHorizontal,
} = osInstance.elements();
if (scrollbarVertical !== null) {
scrollbarVertical.track.dataset.selectDisable = 'true';
}
if (scrollbarHorizontal !== null) {
scrollbarHorizontal.track.dataset.selectDisable = 'true';
}
setScrollBottom(
target.scrollHeight - target.clientHeight - target.scrollTop,
);
},
scroll: (osInstance) => {
const { scrollOffsetElement: target, scrollbarVertical } =
osInstance.elements();
// Hide vertical scrollbar by default
if (scrollbarVertical !== null) {
scrollbarVertical.track.style.visibility = 'hidden';
}
// Show vertical scrollbar based on scroll direction
const isVerticalScroll =
target.scrollTop !== Number(target.dataset.lastScrollTop || '0');
if (
isVerticalScroll === true &&
scrollbarVertical !== null &&
target.scrollHeight > target.clientHeight
) {
scrollbarVertical.track.style.visibility = 'visible';
}
// Update vertical scroll positions
target.dataset.lastScrollTop = target.scrollTop.toString();
handleScroll(osInstance);
},
},
});
useEffect(() => {
const currentRef = scrollableRef.current;
if (currentRef !== null) {
initialize(currentRef);
}
return () => {
// Reset vertical scroll component-specific Recoil state
setScrollTop(0);
setOverlayScrollbars(null);
instance()?.destroy();
};
}, [initialize, instance, setScrollTop, setOverlayScrollbars]);
useEffect(() => {
setOverlayScrollbars(instance());
}, [instance, setOverlayScrollbars]);
return (
<ScrollWrapperComponentInstanceContext.Provider
value={{ instanceId: componentInstanceId }}
>
<Context.Provider
value={{
ref: scrollableRef,
id: contextProviderName,
}}
>
<ScrollWrapperInitEffect
defaultEnableXScroll={defaultEnableXScroll}
defaultEnableYScroll={defaultEnableYScroll}
/>
<StyledScrollWrapper
ref={scrollableRef}
id={`scroll-wrapper-${componentInstanceId}`}
className={className}
heightMode={heightMode}
scrollbarVariant={scrollbarVariant}
onScroll={handleScroll}
>
<StyledInnerContainer>{children}</StyledInnerContainer>
</StyledScrollWrapper>
</Context.Provider>
</ScrollWrapperComponentInstanceContext.Provider>
);
};

View File

@ -0,0 +1,27 @@
import { useToggleScrollWrapper } from '@/ui/utilities/scroll/hooks/useToggleScrollWrapper';
import { useEffect } from 'react';
export type ScrollWrapperInitEffectProps = {
defaultEnableXScroll?: boolean;
defaultEnableYScroll?: boolean;
};
export const ScrollWrapperInitEffect = ({
defaultEnableXScroll = true,
defaultEnableYScroll = true,
}: ScrollWrapperInitEffectProps) => {
const { toggleScrollXWrapper, toggleScrollYWrapper } =
useToggleScrollWrapper();
useEffect(() => {
toggleScrollXWrapper(defaultEnableXScroll);
toggleScrollYWrapper(defaultEnableYScroll);
}, [
defaultEnableXScroll,
defaultEnableYScroll,
toggleScrollXWrapper,
toggleScrollYWrapper,
]);
return <></>;
};

View File

@ -1,98 +0,0 @@
import { createContext, RefObject } from 'react';
type ScrollWrapperContextValue = {
ref: RefObject<HTMLDivElement>;
id: string;
};
export type ContextProviderName =
| 'eventList'
| 'commandMenu'
| 'recordBoard'
| 'recordTableWithWrappers'
| 'settingsPageContainer'
| 'dropdownMenuItemsContainer'
| 'showPageContainer'
| 'showPageLeftContainer'
| 'tabList'
| 'releases'
| 'test'
| 'showPageActivityContainer'
| 'navigationDrawer'
| 'aggregateFooterCell'
| 'modalContent';
const createScrollWrapperContext = (id: string) =>
createContext<ScrollWrapperContextValue>({
ref: { current: null },
id,
});
export const EventListScrollWrapperContext =
createScrollWrapperContext('eventList');
export const CommandMenuScrollWrapperContext =
createScrollWrapperContext('commandMenu');
export const RecordBoardScrollWrapperContext =
createScrollWrapperContext('recordBoard');
export const RecordTableWithWrappersScrollWrapperContext =
createScrollWrapperContext('recordTableWithWrappers');
export const SettingsPageContainerScrollWrapperContext =
createScrollWrapperContext('settingsPageContainer');
export const DropdownMenuItemsContainerScrollWrapperContext =
createScrollWrapperContext('dropdownMenuItemsContainer');
export const ShowPageContainerScrollWrapperContext =
createScrollWrapperContext('showPageContainer');
export const ShowPageLeftContainerScrollWrapperContext =
createScrollWrapperContext('showPageLeftContainer');
export const TabListScrollWrapperContext =
createScrollWrapperContext('tabList');
export const ReleasesScrollWrapperContext =
createScrollWrapperContext('releases');
export const ShowPageActivityContainerScrollWrapperContext =
createScrollWrapperContext('showPageActivityContainer');
export const NavigationDrawerScrollWrapperContext =
createScrollWrapperContext('navigationDrawer');
export const TestScrollWrapperContext = createScrollWrapperContext('test');
export const AggregateFooterCellScrollWrapperContext =
createScrollWrapperContext('aggregateFooterCell');
export const ModalContentScrollWrapperContext =
createScrollWrapperContext('modalContent');
export const getContextByProviderName = (
contextProviderName: ContextProviderName,
) => {
switch (contextProviderName) {
case 'eventList':
return EventListScrollWrapperContext;
case 'commandMenu':
return CommandMenuScrollWrapperContext;
case 'recordBoard':
return RecordBoardScrollWrapperContext;
case 'recordTableWithWrappers':
return RecordTableWithWrappersScrollWrapperContext;
case 'settingsPageContainer':
return SettingsPageContainerScrollWrapperContext;
case 'dropdownMenuItemsContainer':
return DropdownMenuItemsContainerScrollWrapperContext;
case 'showPageContainer':
return ShowPageContainerScrollWrapperContext;
case 'showPageLeftContainer':
return ShowPageLeftContainerScrollWrapperContext;
case 'tabList':
return TabListScrollWrapperContext;
case 'releases':
return ReleasesScrollWrapperContext;
case 'test':
return TestScrollWrapperContext;
case 'showPageActivityContainer':
return ShowPageActivityContainerScrollWrapperContext;
case 'navigationDrawer':
return NavigationDrawerScrollWrapperContext;
case 'aggregateFooterCell':
return AggregateFooterCellScrollWrapperContext;
case 'modalContent':
return ModalContentScrollWrapperContext;
default:
throw new Error('Context Provider not available');
}
};

View File

@ -1,19 +0,0 @@
import { renderHook } from '@testing-library/react';
import { useScrollWrapperScopedRef } from '@/ui/utilities/scroll/hooks/useScrollWrapperScopedRef';
jest.mock('react', () => {
const originalModule = jest.requireActual('react');
return {
...originalModule,
useContext: () => ({ current: {} }),
};
});
describe('useScrollWrapperScopedRef', () => {
it('should return the scrollWrapperRef if available', () => {
const { result } = renderHook(() => useScrollWrapperScopedRef('test'));
expect(result.current).toBeDefined();
});
});

View File

@ -0,0 +1,11 @@
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
export const useScrollToPosition = () => {
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
const scrollToPosition = (scrollPositionInPx: number) => {
scrollWrapperHTMLElement?.scrollTo({ top: scrollPositionInPx });
};
return { scrollToPosition };
};

View File

@ -0,0 +1,17 @@
import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
export const useScrollWrapperElement = (targetComponentInstanceId?: string) => {
const instanceId = useAvailableComponentInstanceIdOrThrow(
ScrollWrapperComponentInstanceContext,
targetComponentInstanceId,
);
const scrollWrapperHTMLElement = document.getElementById(
`scroll-wrapper-${instanceId}`,
);
return {
scrollWrapperHTMLElement,
};
};

View File

@ -1,22 +0,0 @@
import { useContext } from 'react';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import {
ContextProviderName,
getContextByProviderName,
} from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
export const useScrollWrapperScopedRef = (
contextProviderName: ContextProviderName,
) => {
const Context = getContextByProviderName(contextProviderName);
const scrollWrapperRef = useContext(Context);
if (isUndefinedOrNull(scrollWrapperRef))
throw new Error(
`Using a scroll ref without a ScrollWrapper : verify that you are using a ScrollWrapper if you intended to do so.`,
);
return scrollWrapperRef;
};

View File

@ -1,34 +1,38 @@
import { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
export const useToggleScrollWrapper = () => {
const instanceOverlay = useRecoilComponentValueV2(
scrollWrapperInstanceComponentState,
export const useToggleScrollWrapper = (targetComponentInstanceId?: string) => {
const instanceId = useAvailableComponentInstanceIdOrThrow(
ScrollWrapperComponentInstanceContext,
targetComponentInstanceId,
);
const toggleScrollXWrapper = (isEnabled: boolean) => {
if (!instanceOverlay) {
return;
if (isEnabled) {
document
.getElementById(`scroll-wrapper-${instanceId}`)
?.classList.add('scroll-wrapper-x-enabled');
} else {
document
.getElementById(`scroll-wrapper-${instanceId}`)
?.classList.remove('scroll-wrapper-x-enabled');
}
instanceOverlay.options({
overflow: {
x: isEnabled ? 'scroll' : 'hidden',
},
});
};
const toggleScrollYWrapper = (isEnabled: boolean) => {
if (!instanceOverlay) {
return;
if (isEnabled) {
document
.getElementById(`scroll-wrapper-${instanceId}`)
?.classList.add('scroll-wrapper-y-enabled');
} else {
document
.getElementById(`scroll-wrapper-${instanceId}`)
?.classList.remove('scroll-wrapper-y-enabled');
}
instanceOverlay.options({
overflow: {
y: isEnabled ? 'scroll' : 'hidden',
},
});
};
return { toggleScrollXWrapper, toggleScrollYWrapper };
return {
toggleScrollXWrapper,
toggleScrollYWrapper,
};
};

View File

@ -1,10 +0,0 @@
import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
import { OverlayScrollbars } from 'overlayscrollbars';
export const scrollWrapperInstanceComponentState =
createComponentStateV2<OverlayScrollbars | null>({
key: 'scrollWrapperInstanceComponentState',
defaultValue: null,
componentInstanceContext: ScrollWrapperComponentInstanceContext,
});

View File

@ -118,10 +118,7 @@ export const Releases = () => {
]}
>
<SettingsPageContainer>
<ScrollWrapper
contextProviderName="releases"
componentInstanceId="scroll-wrapper-releases"
>
<ScrollWrapper componentInstanceId="scroll-wrapper-releases">
<StyledReleaseContainer>
{releases.map((release) => (
<React.Fragment key={release.slug}>

View File

@ -12,11 +12,11 @@ import {
} from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
import { getCompaniesMock } from '~/testing/mock-data/companies';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { getPeopleRecordConnectionMock } from '~/testing/mock-data/people';
import { mockedTasks } from '~/testing/mock-data/tasks';
import { isDefined } from 'twenty-shared/utils';
const RecordMockSetterEffect = ({
companies,
@ -141,6 +141,7 @@ export const getFieldDecorator =
objectMetadataItem,
}),
hotkeyScope: 'hotkey-scope',
isReadOnly: false,
}}
>
<RecordMockSetterEffect

View File

@ -56,6 +56,7 @@ export const useMockFieldContext = ({
hotkeyScope:
customHotkeyScope ?? InlineCellHotkeyScope.InlineCell,
clearable,
isReadOnly: false,
}}
>
{children}

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