diff --git a/package.json b/package.json index 970bf9934..b2ceb7c5f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/twenty-front/src/hooks/useScrollRestoration.ts b/packages/twenty-front/src/hooks/useScrollRestoration.ts deleted file mode 100644 index de11e854a..000000000 --- a/packages/twenty-front/src/hooks/useScrollRestoration.ts +++ /dev/null @@ -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]); -}; diff --git a/packages/twenty-front/src/hooks/useScrollToPosition.ts b/packages/twenty-front/src/hooks/useScrollToPosition.ts deleted file mode 100644 index b656ca90e..000000000 --- a/packages/twenty-front/src/hooks/useScrollToPosition.ts +++ /dev/null @@ -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 }; -}; diff --git a/packages/twenty-front/src/index.css b/packages/twenty-front/src/index.css index 4efa4d2e1..e7b2dce9b 100644 --- a/packages/twenty-front/src/index.css +++ b/packages/twenty-front/src/index.css @@ -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; diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx index f0b8fab57..f040cfdd7 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx @@ -107,6 +107,7 @@ export const CalendarEventDetails = ({ }), useUpdateRecord: () => [() => undefined, { loading: false }], maxWidth: 300, + isReadOnly: false, }} > ((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, diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx index d6406f084..fd6bd3cd3 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx +++ b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx @@ -2,13 +2,12 @@ import { useContext } from 'react'; import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips'; import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords'; -import { useOpenActivityTargetInlineCellEditMode } from '@/activities/inline-cell/hooks/useOpenActivityTargetInlineCellEditMode'; -import { useUpdateActivityTargetFromInlineCell } from '@/activities/inline-cell/hooks/useUpdateActivityTargetFromInlineCell'; +import { useOpenActivityTargetCellEditMode } from '@/activities/inline-cell/hooks/useOpenActivityTargetCellEditMode'; +import { useUpdateActivityTargetFromCell } from '@/activities/inline-cell/hooks/useUpdateActivityTargetFromCell'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { FieldContextProvider } from '@/object-record/record-field/components/FieldContextProvider'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider'; -import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { RecordInlineCellContainer } from '@/object-record/record-inline-cell/components/RecordInlineCellContainer'; import { RecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext'; @@ -39,18 +38,15 @@ export const ActivityTargetsInlineCell = ({ const { closeInlineCell } = useInlineCell(componentInstanceId); - const { fieldDefinition } = useContext(FieldContext); + const { fieldDefinition, isReadOnly } = useContext(FieldContext); - const isFieldReadOnly = useIsFieldValueReadOnly(); + const { openActivityTargetCellEditMode } = + useOpenActivityTargetCellEditMode(); - const { openActivityTargetInlineCellEditMode } = - useOpenActivityTargetInlineCellEditMode(); - - const { updateActivityTargetFromInlineCell } = - useUpdateActivityTargetFromInlineCell({ - activityObjectNameSingular, - activityId: activityRecordId, - }); + const { updateActivityTargetFromCell } = useUpdateActivityTargetFromCell({ + activityObjectNameSingular, + activityId: activityRecordId, + }); return ( { - updateActivityTargetFromInlineCell({ + updateActivityTargetFromCell({ recordPickerInstanceId: componentInstanceId, morphItem, activityTargetWithTargetRecords: @@ -102,7 +98,7 @@ export const ActivityTargetsInlineCell = ({ /> ), onOpenEditMode: () => { - openActivityTargetInlineCellEditMode({ + openActivityTargetCellEditMode({ recordPickerInstanceId: componentInstanceId, activityTargetObjectRecords, }); diff --git a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useOpenActivityTargetInlineCellEditMode.ts b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useOpenActivityTargetCellEditMode.ts similarity index 91% rename from packages/twenty-front/src/modules/activities/inline-cell/hooks/useOpenActivityTargetInlineCellEditMode.ts rename to packages/twenty-front/src/modules/activities/inline-cell/hooks/useOpenActivityTargetCellEditMode.ts index 729b201a2..2635d612c 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useOpenActivityTargetInlineCellEditMode.ts +++ b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useOpenActivityTargetCellEditMode.ts @@ -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 }; }; diff --git a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromInlineCell.ts b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromCell.ts similarity index 90% rename from packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromInlineCell.ts rename to packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromCell.ts index 28a2155a8..3c494c915 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromInlineCell.ts +++ b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromCell.ts @@ -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 }; }; diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/components/EventList.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/components/EventList.tsx index 30f6bc268..211393380 100644 --- a/packages/twenty-front/src/modules/activities/timeline-activities/components/EventList.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/components/EventList.tsx @@ -45,7 +45,6 @@ export const EventList = ({ events, targetableObject }: EventListProps) => { return ( diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffValue.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffValue.tsx index 8bbf8de17..c518ad70f 100644 --- a/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffValue.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffValue.tsx @@ -54,6 +54,7 @@ export const EventFieldDiffValue = ({ defaultValue: fieldMetadataItem.defaultValue, }, hotkeyScope: 'field-event-diff', + isReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectRecords.ts b/packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectRecords.ts new file mode 100644 index 000000000..b3c8131b9 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectRecords.ts @@ -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((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; +}; diff --git a/packages/twenty-front/src/modules/auth/components/AuthModal.tsx b/packages/twenty-front/src/modules/auth/components/AuthModal.tsx index 52354a520..e50386d83 100644 --- a/packages/twenty-front/src/modules/auth/components/AuthModal.tsx +++ b/packages/twenty-front/src/modules/auth/components/AuthModal.tsx @@ -12,10 +12,7 @@ type AuthModalProps = { children: React.ReactNode }; export const AuthModal = ({ children }: AuthModalProps) => ( - + {children} diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuList.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuList.tsx index 33aecbf48..616c8e64f 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuList.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuList.tsx @@ -79,10 +79,7 @@ export const CommandMenuList = ({ - + { @@ -66,10 +67,8 @@ export const MainNavigationDrawerItems = () => { )} diff --git a/packages/twenty-front/src/modules/object-record/hooks/useAttachRelatedRecordFromRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useAttachRelatedRecordFromRecord.ts index f293a9c13..51b3b9b9d 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useAttachRelatedRecordFromRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useAttachRelatedRecordFromRecord.ts @@ -73,7 +73,7 @@ export const useAttachRelatedRecordFromRecord = ({ getRelatedRecordFromCache(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`]; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx index 0b13d619b..e0ed92291 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx @@ -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 }} > @@ -258,7 +251,6 @@ export const RecordBoard = () => { - { } }, 800); - const scrollWrapperRef = useContext(RecordBoardScrollWrapperContext); + const { scrollWrapperHTMLElement } = useScrollWrapperElement(); const { ref: cardRef } = useInView({ - root: scrollWrapperRef?.ref.current, + root: scrollWrapperHTMLElement, rootMargin: '1000px', }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx index 51fbbd267..48e075171 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx @@ -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[]; }) => { - 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, }} > { + const isRecordReadOnly = useIsRecordReadOnly({ + recordId, + }); + return ( - - {(draggableProvided) => ( -
- -
- )} -
+ + + {(draggableProvided) => ( +
+ +
+ )} +
+
); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/contexts/RecordBoardCardContext.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/contexts/RecordBoardCardContext.ts index 407d76a45..db5c0ce77 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/contexts/RecordBoardCardContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/contexts/RecordBoardCardContext.ts @@ -2,6 +2,7 @@ import { createContext } from 'react'; type RecordBoardCardContextProps = { recordId: string; + isRecordReadOnly: boolean; }; export const RecordBoardCardContext = diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo.tsx index 5cf24118a..815b74c64 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo.tsx @@ -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) => ( - - - + )); }, ); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx index 1fcc71f26..13b2f4989 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldContextProvider.tsx index 6d6a5f0f5..f9037f09f 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldContextProvider.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldContextProvider.tsx @@ -78,6 +78,7 @@ export const FieldContextProvider = ({ hotkeyScope: InlineCellHotkeyScope.InlineCell, clearable, overridenIsFieldEmpty, + isReadOnly: false, }} > {children} diff --git a/packages/twenty-front/src/modules/object-record/record-field/contexts/FieldContext.ts b/packages/twenty-front/src/modules/object-record/record-field/contexts/FieldContext.ts index ab07755d1..cb62086c1 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/contexts/FieldContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/contexts/FieldContext.ts @@ -32,6 +32,7 @@ export type GenericFieldContextType = { overridenIsFieldEmpty?: boolean; displayedMaxRows?: number; isDisplayModeFixHeight?: boolean; + isReadOnly: boolean; onOpenEditMode?: () => void; onCloseEditMode?: () => void; isLabelHidden?: boolean; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useGetButtonIcon.test.tsx b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useGetButtonIcon.test.tsx index 12e3d8036..62094e068 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useGetButtonIcon.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useGetButtonIcon.test.tsx @@ -23,6 +23,7 @@ const getWrapper = recordId, hotkeyScope: 'hotkeyScope', isLabelIdentifier: false, + isReadOnly: false, }} > {children} diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldEmpty.test.tsx b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldEmpty.test.tsx index 0e32c1da8..9dbbdaca5 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldEmpty.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldEmpty.test.tsx @@ -16,6 +16,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => ( recordId, hotkeyScope: 'hotkeyScope', isLabelIdentifier: false, + isReadOnly: false, }} > {children} diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldInputOnly.test.tsx b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldInputOnly.test.tsx index 0d9d62368..0be22d108 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldInputOnly.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldInputOnly.test.tsx @@ -22,6 +22,7 @@ const getWrapper = recordId, hotkeyScope: 'hotkeyScope', isLabelIdentifier: false, + isReadOnly: false, }} > {children} diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldValueReadOnly.test.tsx b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldValueReadOnly.test.tsx index 274a251a0..29b5b5f97 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldValueReadOnly.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldValueReadOnly.test.tsx @@ -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, isRecordDeleted: boolean) => - ({ children }: { children: ReactNode }) => { - return ( - - - - - - {children} - - - - - - ); - }; - 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); }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx index be8d16d1a..4aabb9110 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx @@ -105,6 +105,7 @@ const getWrapper = hotkeyScope: 'hotkeyScope', isLabelIdentifier: false, useUpdateRecord: useUpdateOneRecordMutation, + isReadOnly: false, }} > {children} diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx index 824618748..9ae6197d6 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx @@ -342,6 +342,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => { hotkeyScope: 'hotkeyScope', isLabelIdentifier: false, useUpdateRecord: useUpdateOneRecordMutation, + isReadOnly: false, }} > {children} diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsFieldValueReadOnly.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsFieldValueReadOnly.ts index b83abf667..b89cbcdfc 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsFieldValueReadOnly.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsFieldValueReadOnly.ts @@ -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; +}; +export const useIsFieldValueReadOnly = ({ + fieldDefinition, + isRecordReadOnly, +}: UseIsFieldValueReadOnlyParams) => { const { metadata, type } = fieldDefinition; - const recordDeletedAt = useRecoilValue( - 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, }); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsRecordReadOnly.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsRecordReadOnly.ts new file mode 100644 index 000000000..c4f7a5040 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsRecordReadOnly.ts @@ -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( + recordStoreFamilySelector({ + recordId, + fieldName: 'deletedAt', + }), + ); + + const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission(); + + return hasObjectReadOnlyPermission || isDefined(recordDeletedAt); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useOpenFieldInputEditMode.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useOpenFieldInputEditMode.ts index d6679129c..af2909b86 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useOpenFieldInputEditMode.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useOpenFieldInputEditMode.ts @@ -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,33 +25,82 @@ export const useOpenFieldInputEditMode = () => { const { openRelationFromManyFieldInput } = useOpenRelationFromManyFieldInput(); - const openFieldInput = ({ - fieldDefinition, - recordId, - }: { - fieldDefinition: FieldDefinition; - recordId: string; - }) => { - if (isFieldRelationToOneObject(fieldDefinition)) { - openRelationToOneFieldInput({ - fieldName: fieldDefinition.metadata.fieldName, - recordId: recordId, - }); - } + const { openActivityTargetCellEditMode } = + useOpenActivityTargetCellEditMode(); - if (isFieldRelationFromManyObjects(fieldDefinition)) { - if ( - isDefined(fieldDefinition.metadata.relationObjectMetadataNameSingular) - ) { - openRelationFromManyFieldInput({ - fieldName: fieldDefinition.metadata.fieldName, - objectNameSingular: + const openFieldInput = useRecoilCallback( + ({ snapshot }) => + ({ + fieldDefinition, + recordId, + }: { + fieldDefinition: FieldDefinition; + recordId: string; + }) => { + if (isFieldRelationToOneObject(fieldDefinition)) { + openRelationToOneFieldInput({ + fieldName: fieldDefinition.metadata.fieldName, + recordId: recordId, + }); + } + + if ( + isFieldRelationFromManyObjects(fieldDefinition) && + ['taskTarget', 'noteTarget'].includes( fieldDefinition.metadata.relationObjectMetadataNameSingular, - recordId: recordId, - }); - } - } - }; + ) + ) { + const fieldValue = snapshot + .getLoadable>( + 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, + ) + ) { + openRelationFromManyFieldInput({ + fieldName: fieldDefinition.metadata.fieldName, + objectNameSingular: + fieldDefinition.metadata.relationObjectMetadataNameSingular, + recordId: recordId, + }); + } + } + }, + [ + openActivityTargetCellEditMode, + openRelationFromManyFieldInput, + openRelationToOneFieldInput, + ], + ); return { openFieldInput: openFieldInput, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RelationFromManyFieldDisplay.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RelationFromManyFieldDisplay.perf.stories.tsx index 3ae75c133..b6aa64607 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RelationFromManyFieldDisplay.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RelationFromManyFieldDisplay.perf.stories.tsx @@ -70,6 +70,7 @@ const meta: Meta = { ...relationFromManyFieldDisplayMock.fieldDefinition, } as unknown as FieldDefinition, hotkeyScope: 'hotkey-scope', + isReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx index 899dbac7a..ee489f663 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx @@ -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; @@ -57,8 +86,22 @@ export const RelationFromManyFieldInput = ({ { + if (isRelationFromActivityTargets) { + updateActivityTargetFromCell({ + morphItem, + activityTargetWithTargetRecords: activityTargetObjectRecords, + recordPickerInstanceId, + }); + } else { + updateRelation(morphItem); + } + }} + onCreate={ + !isRelationFromActivityTargets + ? createNewRecordAndOpenRightDrawer + : undefined + } onClickOutside={handleSubmit} layoutDirection={ layoutDirection === 'downward' diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/AddressFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/AddressFieldInput.stories.tsx index 27110bbb3..076422aa7 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/AddressFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/AddressFieldInput.stories.tsx @@ -75,6 +75,7 @@ const AddressInputWithContext = ({ recordId: recordId ?? '123', hotkeyScope: 'hotkey-scope', isLabelIdentifier: false, + isReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx index b7379db65..e72b09cd0 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx @@ -66,6 +66,7 @@ const BooleanFieldInputWithContext = ({ recordId: recordId ?? '123', hotkeyScope: 'hotkey-scope', isLabelIdentifier: false, + isReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx index 034ffb678..28de98428 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx @@ -70,6 +70,7 @@ const NumberFieldInputWithContext = ({ recordId: '123', hotkeyScope: 'hotkey-scope', isLabelIdentifier: false, + isReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RatingFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RatingFieldInput.stories.tsx index 4816ce221..8fb65b8fd 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RatingFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RatingFieldInput.stories.tsx @@ -68,6 +68,7 @@ const RatingFieldInputWithContext = ({ recordId: recordId ?? '123', hotkeyScope: 'hotkey-scope', isLabelIdentifier: false, + isReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFromManyFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFromManyFieldInput.stories.tsx index 54f280c9a..b2cc54fbe 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFromManyFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFromManyFieldInput.stories.tsx @@ -95,6 +95,7 @@ const RelationManyFieldInputWithContext = () => { recordId: 'recordId', hotkeyScope: 'hotkey-scope', isLabelIdentifier: false, + isReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx index 926b141bf..2fc80c512 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx @@ -88,6 +88,7 @@ const RelationToOneFieldInputWithContext = ({ recordId: recordId, hotkeyScope: 'hotkey-scope', isLabelIdentifier: false, + isReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/__tests__/isFieldValueReadOnly.test.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/__tests__/isFieldValueReadOnly.test.ts index db435778f..69f49bb33 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/__tests__/isFieldValueReadOnly.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/__tests__/isFieldValueReadOnly.test.ts @@ -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, + describe('when checking field types', () => { + it('should return true if fieldType is RICH_TEXT', () => { + const result = isFieldValueReadOnly({ + fieldType: FieldMetadataType.RICH_TEXT, + }); + + expect(result).toBe(true); }); - expect(result).toBe(true); - }); + it('should return true if fieldType is RICH_TEXT_V2', () => { + const result = isFieldValueReadOnly({ + fieldType: FieldMetadataType.RICH_TEXT_V2, + }); - it('should return true if fieldType is FieldMetadataType.ACTOR', () => { - const result = isFieldValueReadOnly({ - fieldType: FieldMetadataType.ACTOR, - contextStoreCurrentViewType: ContextStoreViewType.Table, + expect(result).toBe(true); }); - expect(result).toBe(true); - }); + it('should return true if fieldType is ACTOR', () => { + const result = isFieldValueReadOnly({ + fieldType: FieldMetadataType.ACTOR, + }); - it('should return true if fieldType is FieldMetadataType.RICH_TEXT', () => { - const result = isFieldValueReadOnly({ - fieldType: FieldMetadataType.RICH_TEXT, - contextStoreCurrentViewType: ContextStoreViewType.Table, + expect(result).toBe(true); }); - expect(result).toBe(true); + it('should return false for other field types', () => { + const result = isFieldValueReadOnly({ + fieldType: FieldMetadataType.TEXT, + }); + + expect(result).toBe(false); + }); }); - it('should return false if fieldType is not FieldMetadataType.ACTOR or FieldMetadataType.RICH_TEXT', () => { + it('should return false for standard editable fields', () => { const result = isFieldValueReadOnly({ + objectNameSingular: 'company', + fieldName: 'name', fieldType: FieldMetadataType.TEXT, - contextStoreCurrentViewType: ContextStoreViewType.Table, - }); - - expect(result).toBe(false); - }); - - it('should return false if none of the conditions are met', () => { - const result = isFieldValueReadOnly({ - contextStoreCurrentViewType: ContextStoreViewType.Table, + isRecordReadOnly: false, }); expect(result).toBe(false); diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueReadOnly.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueReadOnly.ts index 443d344f0..40efed913 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueReadOnly.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueReadOnly.ts @@ -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 }) || diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx index 25cb0ecfe..96f113ed2 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx @@ -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: , diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx index 34613c42a..a073cdfbc 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx @@ -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` diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellDisplayMode.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellDisplayMode.tsx index 74518ed29..85c21af79 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellDisplayMode.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellDisplayMode.tsx @@ -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 = diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx index d75cc5b58..605e5b9e1 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx @@ -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 ( <> @@ -109,6 +114,7 @@ export const FieldsCard = ({ useUpdateRecord: useUpdateOneObjectRecordMutation, hotkeyScope: InlineCellHotkeyScope.InlineCell, isDisplayModeFixHeight: true, + isReadOnly: isRecordReadOnly, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/SummaryCard.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/SummaryCard.tsx index f787bdd08..2cfcbc03f 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/SummaryCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/SummaryCard.tsx @@ -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 ( diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx index fdd205792..ebf9a0349 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx @@ -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" /> - {!isReadOnly && ( + {!isFieldReadOnly && ( 0} rightAdornment={ - !isReadOnly && ( + !isFieldReadOnly && ( = { objectMetadataItem: mockedCompanyObjectMetadataItem, }), hotkeyScope: 'hotkey-scope', + isReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyBottomEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyBottomEffect.tsx index 7e259e825..d5ff4bb8c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyBottomEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyBottomEffect.tsx @@ -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 = () => { diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx index 575dd0673..2949c146f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx @@ -102,7 +102,6 @@ export const RecordTableWithWrappers = ({ > diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx index 0dfcd31a6..0ad261a81 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx @@ -137,6 +137,7 @@ const meta: Meta = { ...mockPerformance.fieldDefinition, }, hotkeyScope: 'hotkey-scope', + isReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRowContext.ts b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRowContext.ts index 2957ab8d1..ee9706133 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRowContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRowContext.ts @@ -8,6 +8,7 @@ export type RecordTableRowContextValue = { isSelected: boolean; inView: boolean; isPendingRow?: boolean; + isReadOnly?: boolean; }; export const [RecordTableRowContextProvider, useRecordTableRowContextOrThrow] = diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFetchMoreLoader.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFetchMoreLoader.tsx index 0fd58bb75..68efaf749 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFetchMoreLoader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFetchMoreLoader.tsx @@ -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) { diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect.tsx index 64b994226..3be71c787 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect.tsx @@ -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(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx index e68e4ad15..1e5144f2e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx @@ -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(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx index c170246a2..6322f5274 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx @@ -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} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContext.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContext.tsx index 944b28904..ab2955b94 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContext.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContext.tsx @@ -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 ( {children} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx index 387a973fe..2754eb8f9 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx @@ -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)) { diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx index 1cd4b5664..0716666d7 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx @@ -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} /> ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx index 63405f479..253acbfd9 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx @@ -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 ( <> diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusModeHotkeysSetterEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusModeHotkeysSetterEffect.tsx index 0745447ef..323521a8d 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusModeHotkeysSetterEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusModeHotkeysSetterEffect.tsx @@ -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; } diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellInGroup.test.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellInGroup.test.tsx index a6e53dfa3..9a3678dab 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellInGroup.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellInGroup.test.tsx @@ -54,6 +54,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => ( recordId: 'recordId', hotkeyScope: TableHotkeyScope.Table, isLabelIdentifier: false, + isReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellNoGroup.test.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellNoGroup.test.tsx index 4a4181977..2bfef2b4f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellNoGroup.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellNoGroup.test.tsx @@ -54,6 +54,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => ( recordId: 'recordId', hotkeyScope: TableHotkeyScope.Table, isLabelIdentifier: false, + isReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts index 4cdd47fb9..a53e9d37c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts @@ -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, diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx index b91c74bd7..41bc79b69 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx @@ -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 ( ` ${({ 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 ( <> - + { return ( diff --git a/packages/twenty-front/src/modules/settings/components/SettingsPageContainer.tsx b/packages/twenty-front/src/modules/settings/components/SettingsPageContainer.tsx index 1a34c49c5..921e3efc4 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsPageContainer.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsPageContainer.tsx @@ -30,11 +30,7 @@ export const SettingsPageContainer = ({ }: { children: ReactNode; }) => ( - + {children} ); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx index a881c7b85..5283e52d9 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx @@ -146,6 +146,7 @@ export const SettingsDataModelFieldPreview = ({ defaultValue: fieldMetadataItem.defaultValue, }, hotkeyScope: 'field-preview', + isReadOnly: false, }} > {fieldMetadataItem.type === FieldMetadataType.BOOLEAN ? ( diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx index d172bcbed..1cfc26ecd 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx @@ -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 = ({ return ( <> - + {hasMaxHeight ? ( @@ -69,10 +68,7 @@ export const DropdownMenuItemsContainer = ({ )} ) : ( - + theme.border.radius.sm}; } `; diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/ShowPageContainer.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/ShowPageContainer.tsx index 17e35ad41..3aebf9a30 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/components/ShowPageContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/components/ShowPageContainer.tsx @@ -32,7 +32,6 @@ export const ShowPageContainer = ({ children }: ShowPageContainerProps) => { return isMobile ? ( {children} diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageActivityContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageActivityContainer.tsx index 7a0a6b4fb..d5954316e 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageActivityContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageActivityContainer.tsx @@ -70,7 +70,6 @@ export const ShowPageActivityContainer = ({ return ( diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageLeftContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageLeftContainer.tsx index dc4cc7e1c..f9ea8ccc2 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageLeftContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageLeftContainer.tsx @@ -47,7 +47,6 @@ export const ShowPageLeftContainer = ({ ) : ( diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx index 1080a730a..6831196d7 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx @@ -79,7 +79,6 @@ export const TabList = ({ /> diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/states/internal/currentHotkeyScopeState.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/states/internal/currentHotkeyScopeState.ts index 9670524d8..900af17f0 100644 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/states/internal/currentHotkeyScopeState.ts +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/states/internal/currentHotkeyScopeState.ts @@ -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({ key: 'currentHotkeyScopeState', diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx b/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx index 0e7af457f..cc9bfbfeb 100644 --- a/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx @@ -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(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) => { + 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 ( - + - - {children} - - + {children} + ); }; diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/components/internal/ScrollWrapperInitEffect.tsx b/packages/twenty-front/src/modules/ui/utilities/scroll/components/internal/ScrollWrapperInitEffect.tsx new file mode 100644 index 000000000..1e5568e04 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/scroll/components/internal/ScrollWrapperInitEffect.tsx @@ -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 <>; +}; diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx b/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx deleted file mode 100644 index 4d5a34a45..000000000 --- a/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { createContext, RefObject } from 'react'; - -type ScrollWrapperContextValue = { - ref: RefObject; - 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({ - 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'); - } -}; diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/__tests__/useScrollWrapperScopedRef.test.tsx b/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/__tests__/useScrollWrapperScopedRef.test.tsx deleted file mode 100644 index 7048479e0..000000000 --- a/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/__tests__/useScrollWrapperScopedRef.test.tsx +++ /dev/null @@ -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(); - }); -}); diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useScrollToPosition.ts b/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useScrollToPosition.ts new file mode 100644 index 000000000..46bcb5c13 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useScrollToPosition.ts @@ -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 }; +}; diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useScrollWrapperElement.ts b/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useScrollWrapperElement.ts new file mode 100644 index 000000000..81decd96e --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useScrollWrapperElement.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useScrollWrapperScopedRef.ts b/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useScrollWrapperScopedRef.ts deleted file mode 100644 index bc1012c82..000000000 --- a/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useScrollWrapperScopedRef.ts +++ /dev/null @@ -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; -}; diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useToggleScrollWrapper.ts b/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useToggleScrollWrapper.ts index 5264d50e5..1ee8860fd 100644 --- a/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useToggleScrollWrapper.ts +++ b/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useToggleScrollWrapper.ts @@ -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, + }; }; diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/states/scrollWrapperInstanceComponentState.ts b/packages/twenty-front/src/modules/ui/utilities/scroll/states/scrollWrapperInstanceComponentState.ts deleted file mode 100644 index f2069bf32..000000000 --- a/packages/twenty-front/src/modules/ui/utilities/scroll/states/scrollWrapperInstanceComponentState.ts +++ /dev/null @@ -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({ - key: 'scrollWrapperInstanceComponentState', - defaultValue: null, - componentInstanceContext: ScrollWrapperComponentInstanceContext, - }); diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/states/scrollWrappeScrollBottomComponentState.ts b/packages/twenty-front/src/modules/ui/utilities/scroll/states/scrollWrapperScrollBottomComponentState.ts similarity index 100% rename from packages/twenty-front/src/modules/ui/utilities/scroll/states/scrollWrappeScrollBottomComponentState.ts rename to packages/twenty-front/src/modules/ui/utilities/scroll/states/scrollWrapperScrollBottomComponentState.ts diff --git a/packages/twenty-front/src/pages/settings/Releases.tsx b/packages/twenty-front/src/pages/settings/Releases.tsx index 88552fc54..e767b337f 100644 --- a/packages/twenty-front/src/pages/settings/Releases.tsx +++ b/packages/twenty-front/src/pages/settings/Releases.tsx @@ -118,10 +118,7 @@ export const Releases = () => { ]} > - + {releases.map((release) => ( diff --git a/packages/twenty-front/src/testing/decorators/getFieldDecorator.tsx b/packages/twenty-front/src/testing/decorators/getFieldDecorator.tsx index 4e4312b11..a7e4bfaec 100644 --- a/packages/twenty-front/src/testing/decorators/getFieldDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/getFieldDecorator.tsx @@ -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, }} > {children} diff --git a/yarn.lock b/yarn.lock index 9be168c89..8b1dfc268 100644 --- a/yarn.lock +++ b/yarn.lock @@ -46580,23 +46580,6 @@ __metadata: languageName: node linkType: hard -"overlayscrollbars-react@npm:^0.5.4": - version: 0.5.6 - resolution: "overlayscrollbars-react@npm:0.5.6" - peerDependencies: - overlayscrollbars: ^2.0.0 - react: ">=16.8.0" - checksum: 10c0/59a2aad3664d81dc4dee5e747b72bb2645047e0e6d300d9583244167d1e4b19c4cc263932e4b112d5c24358430f78d15d34ea389beb8845e3b5319ccc57db8d8 - languageName: node - linkType: hard - -"overlayscrollbars@npm:^2.6.1": - version: 2.10.0 - resolution: "overlayscrollbars@npm:2.10.0" - checksum: 10c0/d8eead54a8459cade66cac347524858a41f2d207737ea7bbb9df690c242cfd6804023bac85d245168893284cae32e11ed19acf236fad5e8e47116018d7e7d43a - languageName: node - linkType: hard - "override-require@npm:^1.1.1": version: 1.1.1 resolution: "override-require@npm:1.1.1" @@ -55457,8 +55440,6 @@ __metadata: nodemailer: "npm:^6.9.8" nx: "npm:18.3.3" openapi-types: "npm:^12.1.3" - overlayscrollbars: "npm:^2.6.1" - overlayscrollbars-react: "npm:^0.5.4" passport: "npm:^0.7.0" passport-google-oauth20: "npm:^2.0.0" passport-jwt: "npm:^4.0.1"