From 09bfb617b2944ed6be28ff7c3971ad99eb8693b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Mon, 3 Jun 2024 17:15:05 +0200 Subject: [PATCH] Right drawer to edit records (#5551) This PR introduces a new side panel to edit records and the ability to minimize the side panel. The goal is leverage this sidepanel to be able to create records while being in another show page. I'm opening the PR for feedback since it involved refactoring and therefore already touches a lot of files, even though it was quick to implement. Screenshot 2024-05-23 at 17 41 37 --- .../components/RightDrawerCalendarEvent.tsx | 6 +- .../useOpenCalendarEventRightDrawer.test.tsx | 12 +- .../hooks/useOpenCalendarEventRightDrawer.ts | 8 +- .../states/viewableCalendarEventIdState.ts | 6 - .../comment/__stories__/Comment.stories.tsx | 8 +- .../__stories__/CommentHeader.stories.tsx | 8 +- .../activities/components/ActivityEditor.tsx | 2 - .../emails/components/EmailThreadHeader.tsx | 2 - .../hooks/__tests__/useEmailThread.test.tsx | 25 ++- .../activities/emails/hooks/useEmailThread.ts | 14 +- .../hooks/useRightDrawerEmailThread.ts | 10 +- .../states/viewableEmailThreadIdState.ts | 6 - .../useOpenActivityRightDrawer.test.tsx | 10 +- .../useOpenCreateActivityDrawer.test.tsx | 10 +- .../hooks/useOpenActivityRightDrawer.ts | 11 +- .../hooks/useOpenCreateActivityDrawer.ts | 6 +- .../components/ActivityActionBar.tsx | 48 +---- .../components/RightDrawerActivityTopBar.tsx | 29 --- .../create/RightDrawerCreateActivity.tsx | 8 +- .../edit/RightDrawerEditActivity.tsx | 8 +- .../states/viewableActivityIdState.ts | 6 - .../object-record/hooks/useCreateOneRecord.ts | 6 + .../RelationFieldInput.stories.tsx | 1 + .../components/RightDrawerRecord.tsx | 40 ++++ .../states/viewableRecordIdState.ts | 6 + .../states/viewableRecordNameSingularState.ts | 6 + .../components/RecordShowContainer.tsx | 187 +++++++++--------- .../components/RecordShowContainerEffect.tsx | 41 ---- .../record-show/hooks/useRecordShowPage.ts | 98 +++++++++ .../RecordDetailRelationSection.tsx | 12 +- .../components/RecordTableRow.tsx | 1 + .../perf/RecordTableCell.perf.stories.tsx | 2 + .../contexts/RecordTableRowContext.ts | 1 + .../RecordTableCellSoftFocusMode.tsx | 16 +- .../record-table-cell/hooks/__mocks__/cell.ts | 1 + .../hooks/useOpenRecordTableCellFromCell.ts | 11 +- .../hooks/useOpenRecordTableCellV2.ts | 33 +++- .../components/RelationPicker.tsx | 27 ++- .../SingleEntitySelectMenuItems.tsx | 2 +- .../SingleEntitySelectMenuItemsWithSearch.tsx | 28 ++- .../useAddNewRecordAndOpenRightDrawer.ts | 115 +++++++++++ .../src/modules/ui/layout/page/PageBody.tsx | 51 ++++- .../ui/layout/page/SubMenuTopBarContainer.tsx | 4 +- .../right-drawer/components/RightDrawer.tsx | 31 ++- .../components/RightDrawerRouter.tsx | 24 ++- .../components/RightDrawerTopBar.tsx | 116 +++++++++++ .../RightDrawerTopBarCloseButton.tsx | 4 +- .../RightDrawerTopBarExpandButton.tsx | 15 +- .../RightDrawerTopBarMinimizeButton.tsx | 22 +++ .../components/StyledRightDrawerTopBar.tsx | 9 +- .../RightDrawerTopBar.stories.tsx} | 8 +- .../constants/RightDrawerPageIcons.ts | 9 + .../constants/RightDrawerPageTitles.ts | 9 + .../right-drawer/hooks/useRightDrawer.ts | 51 +++++ .../states/isRightDrawerMinimizedState.ts | 6 + .../right-drawer/types/RightDrawerPages.ts | 1 + .../components/ShowPageLeftContainer.tsx | 41 ++-- .../components/ShowPageRightContainer.tsx | 28 ++- .../ui/layout/tab/components/TabList.tsx | 2 +- .../pages/object-record/RecordIndexPage.tsx | 1 - .../pages/object-record/RecordShowPage.tsx | 100 ++-------- 61 files changed, 957 insertions(+), 452 deletions(-) delete mode 100644 packages/twenty-front/src/modules/activities/calendar/states/viewableCalendarEventIdState.ts delete mode 100644 packages/twenty-front/src/modules/activities/emails/states/viewableEmailThreadIdState.ts delete mode 100644 packages/twenty-front/src/modules/activities/right-drawer/components/RightDrawerActivityTopBar.tsx delete mode 100644 packages/twenty-front/src/modules/activities/states/viewableActivityIdState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerRecord.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-right-drawer/states/viewableRecordIdState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-right-drawer/states/viewableRecordNameSingularState.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainerEffect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPage.ts create mode 100644 packages/twenty-front/src/modules/object-record/relation-picker/hooks/useAddNewRecordAndOpenRightDrawer.ts create mode 100644 packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBar.tsx create mode 100644 packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarMinimizeButton.tsx rename packages/twenty-front/src/modules/{activities/right-drawer/components/__stories__/RightDrawerActivityTopBar.stories.tsx => ui/layout/right-drawer/components/__stories__/RightDrawerTopBar.stories.tsx} (70%) create mode 100644 packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageIcons.ts create mode 100644 packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageTitles.ts create mode 100644 packages/twenty-front/src/modules/ui/layout/right-drawer/states/isRightDrawerMinimizedState.ts diff --git a/packages/twenty-front/src/modules/activities/calendar/right-drawer/components/RightDrawerCalendarEvent.tsx b/packages/twenty-front/src/modules/activities/calendar/right-drawer/components/RightDrawerCalendarEvent.tsx index 4f760bf958..06aa66012f 100644 --- a/packages/twenty-front/src/modules/activities/calendar/right-drawer/components/RightDrawerCalendarEvent.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/right-drawer/components/RightDrawerCalendarEvent.tsx @@ -2,18 +2,18 @@ import { useRecoilValue } from 'recoil'; import { CalendarEventDetails } from '@/activities/calendar/components/CalendarEventDetails'; import { FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE } from '@/activities/calendar/graphql/operation-signatures/FindOneCalendarEventOperationSignature'; -import { viewableCalendarEventIdState } from '@/activities/calendar/states/viewableCalendarEventIdState'; import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { useSetRecordInStore } from '@/object-record/record-store/hooks/useSetRecordInStore'; export const RightDrawerCalendarEvent = () => { const { setRecords } = useSetRecordInStore(); - const viewableCalendarEventId = useRecoilValue(viewableCalendarEventIdState); + const viewableRecordId = useRecoilValue(viewableRecordIdState); const { record: calendarEvent } = useFindOneRecord({ objectNameSingular: FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE.objectNameSingular, - objectRecordId: viewableCalendarEventId ?? '', + objectRecordId: viewableRecordId ?? '', recordGqlFields: FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE.fields, onCompleted: (record) => setRecords([record]), }); diff --git a/packages/twenty-front/src/modules/activities/calendar/right-drawer/hooks/__tests__/useOpenCalendarEventRightDrawer.test.tsx b/packages/twenty-front/src/modules/activities/calendar/right-drawer/hooks/__tests__/useOpenCalendarEventRightDrawer.test.tsx index 027f7f8755..9801608508 100644 --- a/packages/twenty-front/src/modules/activities/calendar/right-drawer/hooks/__tests__/useOpenCalendarEventRightDrawer.test.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/right-drawer/hooks/__tests__/useOpenCalendarEventRightDrawer.test.tsx @@ -2,7 +2,7 @@ import { act, renderHook } from '@testing-library/react'; import { RecoilRoot, useRecoilValue } from 'recoil'; import { useOpenCalendarEventRightDrawer } from '@/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer'; -import { viewableCalendarEventIdState } from '@/activities/calendar/states/viewableCalendarEventIdState'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState'; describe('useOpenCalendarEventRightDrawer', () => { @@ -10,26 +10,24 @@ describe('useOpenCalendarEventRightDrawer', () => { const { result } = renderHook( () => { const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState); - const viewableCalendarEventId = useRecoilValue( - viewableCalendarEventIdState, - ); + const viewableRecordId = useRecoilValue(viewableRecordIdState); return { ...useOpenCalendarEventRightDrawer(), isRightDrawerOpen, - viewableCalendarEventId, + viewableRecordId, }; }, { wrapper: RecoilRoot }, ); expect(result.current.isRightDrawerOpen).toBe(false); - expect(result.current.viewableCalendarEventId).toBeNull(); + expect(result.current.viewableRecordId).toBeNull(); act(() => { result.current.openCalendarEventRightDrawer('1234'); }); expect(result.current.isRightDrawerOpen).toBe(true); - expect(result.current.viewableCalendarEventId).toBe('1234'); + expect(result.current.viewableRecordId).toBe('1234'); }); }); diff --git a/packages/twenty-front/src/modules/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer.ts b/packages/twenty-front/src/modules/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer.ts index 9cd27d017e..b10743f353 100644 --- a/packages/twenty-front/src/modules/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer.ts +++ b/packages/twenty-front/src/modules/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer.ts @@ -1,6 +1,6 @@ import { useSetRecoilState } from 'recoil'; -import { viewableCalendarEventIdState } from '@/activities/calendar/states/viewableCalendarEventIdState'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; @@ -9,14 +9,12 @@ import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope export const useOpenCalendarEventRightDrawer = () => { const { openRightDrawer } = useRightDrawer(); const setHotkeyScope = useSetHotkeyScope(); - const setViewableCalendarEventId = useSetRecoilState( - viewableCalendarEventIdState, - ); + const setViewableRecordId = useSetRecoilState(viewableRecordIdState); const openCalendarEventRightDrawer = (calendarEventId: string) => { setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); openRightDrawer(RightDrawerPages.ViewCalendarEvent); - setViewableCalendarEventId(calendarEventId); + setViewableRecordId(calendarEventId); }; return { openCalendarEventRightDrawer }; diff --git a/packages/twenty-front/src/modules/activities/calendar/states/viewableCalendarEventIdState.ts b/packages/twenty-front/src/modules/activities/calendar/states/viewableCalendarEventIdState.ts deleted file mode 100644 index 32d1951bcf..0000000000 --- a/packages/twenty-front/src/modules/activities/calendar/states/viewableCalendarEventIdState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createState } from 'twenty-ui'; - -export const viewableCalendarEventIdState = createState({ - key: 'viewableCalendarEventIdState', - defaultValue: null, -}); diff --git a/packages/twenty-front/src/modules/activities/comment/__stories__/Comment.stories.tsx b/packages/twenty-front/src/modules/activities/comment/__stories__/Comment.stories.tsx index a75afeb82c..8df2da8092 100644 --- a/packages/twenty-front/src/modules/activities/comment/__stories__/Comment.stories.tsx +++ b/packages/twenty-front/src/modules/activities/comment/__stories__/Comment.stories.tsx @@ -3,7 +3,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { useSetRecoilState } from 'recoil'; import { ComponentDecorator } from 'twenty-ui'; -import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { ActivityActionBar } from '../../right-drawer/components/ActivityActionBar'; import { Comment } from '../Comment'; @@ -11,11 +11,11 @@ import { Comment } from '../Comment'; import { mockComment, mockCommentWithLongValues } from './mock-comment'; const CommentSetterEffect = () => { - const setViewableActivity = useSetRecoilState(viewableActivityIdState); + const setViewableRecord = useSetRecoilState(viewableRecordIdState); useEffect(() => { - setViewableActivity('test-id'); - }, [setViewableActivity]); + setViewableRecord('test-id'); + }, [setViewableRecord]); return null; }; diff --git a/packages/twenty-front/src/modules/activities/comment/__stories__/CommentHeader.stories.tsx b/packages/twenty-front/src/modules/activities/comment/__stories__/CommentHeader.stories.tsx index d61ebc25b2..25c01d745c 100644 --- a/packages/twenty-front/src/modules/activities/comment/__stories__/CommentHeader.stories.tsx +++ b/packages/twenty-front/src/modules/activities/comment/__stories__/CommentHeader.stories.tsx @@ -4,7 +4,7 @@ import { DateTime } from 'luxon'; import { useSetRecoilState } from 'recoil'; import { ActivityActionBar } from '@/activities/right-drawer/components/ActivityActionBar'; -import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; import { avatarUrl } from '~/testing/mock-data/users'; @@ -13,11 +13,11 @@ import { CommentHeader } from '../CommentHeader'; import { mockComment, mockCommentWithLongValues } from './mock-comment'; const CommentHeaderSetterEffect = () => { - const setViewableActivity = useSetRecoilState(viewableActivityIdState); + const setViewableRecord = useSetRecoilState(viewableRecordIdState); useEffect(() => { - setViewableActivity('test-id'); - }, [setViewableActivity]); + setViewableRecord('test-id'); + }, [setViewableRecord]); return null; }; diff --git a/packages/twenty-front/src/modules/activities/components/ActivityEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityEditor.tsx index b560a76f3d..e06b79c9c5 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityEditor.tsx @@ -7,7 +7,6 @@ import { ActivityComments } from '@/activities/components/ActivityComments'; import { ActivityCreationDate } from '@/activities/components/ActivityCreationDate'; import { ActivityEditorFields } from '@/activities/components/ActivityEditorFields'; import { ActivityTitleEffect } from '@/activities/components/ActivityTitleEffect'; -import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { ActivityTitle } from './ActivityTitle'; @@ -68,7 +67,6 @@ export const ActivityEditor = ({ - diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadHeader.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadHeader.tsx index 7063110ac0..29cd280ed0 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadHeader.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadHeader.tsx @@ -1,5 +1,4 @@ import styled from '@emotion/styled'; -import { IconMail, Tag } from 'twenty-ui'; import { beautifyPastDateRelativeToNow } from '~/utils/date-utils'; @@ -43,7 +42,6 @@ export const EmailThreadHeader = ({ }: EmailThreadHeaderProps) => { return ( - {}} /> {subject} diff --git a/packages/twenty-front/src/modules/activities/emails/hooks/__tests__/useEmailThread.test.tsx b/packages/twenty-front/src/modules/activities/emails/hooks/__tests__/useEmailThread.test.tsx index 4dae422ca2..d7d3efe033 100644 --- a/packages/twenty-front/src/modules/activities/emails/hooks/__tests__/useEmailThread.test.tsx +++ b/packages/twenty-front/src/modules/activities/emails/hooks/__tests__/useEmailThread.test.tsx @@ -2,7 +2,7 @@ import { act, renderHook } from '@testing-library/react'; import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil'; import { useEmailThread } from '@/activities/emails/hooks/useEmailThread'; -import { viewableEmailThreadIdState } from '@/activities/emails/states/viewableEmailThreadIdState'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState'; const viewableEmailThreadId = '1234'; @@ -13,24 +13,22 @@ describe('useEmailThread', () => { () => { const emailThread = useEmailThread(); const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState); - const viewableEmailThreadId = useRecoilValue( - viewableEmailThreadIdState, - ); + const viewableRecordId = useRecoilValue(viewableRecordIdState); - return { ...emailThread, isRightDrawerOpen, viewableEmailThreadId }; + return { ...emailThread, isRightDrawerOpen, viewableRecordId }; }, { wrapper: RecoilRoot }, ); expect(result.current.isRightDrawerOpen).toBe(false); - expect(result.current.viewableEmailThreadId).toBeNull(); + expect(result.current.viewableRecordId).toBeNull(); act(() => { result.current.openEmailThread(viewableEmailThreadId); }); expect(result.current.isRightDrawerOpen).toBe(true); - expect(result.current.viewableEmailThreadId).toBe(viewableEmailThreadId); + expect(result.current.viewableRecordId).toBe(viewableEmailThreadId); }); it('should close email thread if trying to open the same thread id', () => { @@ -40,15 +38,16 @@ describe('useEmailThread', () => { const [isRightDrawerOpen, setIsRightDrawerOpen] = useRecoilState( isRightDrawerOpenState, ); - const [viewableEmailThreadId, setViewableEmailThreadId] = - useRecoilState(viewableEmailThreadIdState); + const [viewableRecordId, setViewableRecordId] = useRecoilState( + viewableRecordIdState, + ); return { ...emailThread, isRightDrawerOpen, - viewableEmailThreadId, + viewableRecordId, setIsRightDrawerOpen, - setViewableEmailThreadId, + setViewableRecordId, }; }, { wrapper: RecoilRoot }, @@ -56,7 +55,7 @@ describe('useEmailThread', () => { act(() => { result.current.setIsRightDrawerOpen(true); - result.current.setViewableEmailThreadId(viewableEmailThreadId); + result.current.setViewableRecordId(viewableEmailThreadId); }); act(() => { @@ -64,6 +63,6 @@ describe('useEmailThread', () => { }); expect(result.current.isRightDrawerOpen).toBe(false); - expect(result.current.viewableEmailThreadId).toBeNull(); + expect(result.current.viewableRecordId).toBeNull(); }); }); diff --git a/packages/twenty-front/src/modules/activities/emails/hooks/useEmailThread.ts b/packages/twenty-front/src/modules/activities/emails/hooks/useEmailThread.ts index a0757a18af..c2fa722ed6 100644 --- a/packages/twenty-front/src/modules/activities/emails/hooks/useEmailThread.ts +++ b/packages/twenty-front/src/modules/activities/emails/hooks/useEmailThread.ts @@ -1,13 +1,13 @@ import { useRecoilCallback } from 'recoil'; import { useOpenEmailThreadRightDrawer } from '@/activities/emails/right-drawer/hooks/useOpenEmailThreadRightDrawer'; -import { viewableEmailThreadIdState } from '@/activities/emails/states/viewableEmailThreadIdState'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState'; export const useEmailThread = () => { const { closeRightDrawer } = useRightDrawer(); - const openEmailThredRightDrawer = useOpenEmailThreadRightDrawer(); + const openEmailThreadRightDrawer = useOpenEmailThreadRightDrawer(); const openEmailThread = useRecoilCallback( ({ snapshot, set }) => @@ -17,19 +17,19 @@ export const useEmailThread = () => { .getValue(); const viewableEmailThreadId = snapshot - .getLoadable(viewableEmailThreadIdState) + .getLoadable(viewableRecordIdState) .getValue(); if (isRightDrawerOpen && viewableEmailThreadId === threadId) { - set(viewableEmailThreadIdState, null); + set(viewableRecordIdState, null); closeRightDrawer(); return; } - openEmailThredRightDrawer(); - set(viewableEmailThreadIdState, threadId); + openEmailThreadRightDrawer(); + set(viewableRecordIdState, threadId); }, - [closeRightDrawer, openEmailThredRightDrawer], + [closeRightDrawer, openEmailThreadRightDrawer], ); return { openEmailThread }; diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useRightDrawerEmailThread.ts b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useRightDrawerEmailThread.ts index dce19bb82e..bc415e406a 100644 --- a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useRightDrawerEmailThread.ts +++ b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useRightDrawerEmailThread.ts @@ -4,16 +4,16 @@ import gql from 'graphql-tag'; import { useRecoilValue } from 'recoil'; import { fetchAllThreadMessagesOperationSignatureFactory } from '@/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory'; -import { viewableEmailThreadIdState } from '@/activities/emails/states/viewableEmailThreadIdState'; import { EmailThreadMessage as EmailThreadMessageType } from '@/activities/emails/types/EmailThreadMessage'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; export const useRightDrawerEmailThread = () => { - const viewableEmailThreadId = useRecoilValue(viewableEmailThreadIdState); + const viewableRecordId = useRecoilValue(viewableRecordIdState); const apolloClient = useApolloClient(); const thread = apolloClient.readFragment({ - id: `TimelineThread:${viewableEmailThreadId}`, + id: `TimelineThread:${viewableRecordId}`, fragment: gql` fragment timelineThread on TimelineThread { id @@ -25,7 +25,7 @@ export const useRightDrawerEmailThread = () => { const FETCH_ALL_MESSAGES_OPERATION_SIGNATURE = fetchAllThreadMessagesOperationSignatureFactory({ - messageThreadId: viewableEmailThreadId, + messageThreadId: viewableRecordId, }); const { @@ -39,7 +39,7 @@ export const useRightDrawerEmailThread = () => { FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.objectNameSingular, orderBy: FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.variables.orderBy, recordGqlFields: FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.fields, - skip: !viewableEmailThreadId, + skip: !viewableRecordId, }); const fetchMoreMessages = useCallback(() => { diff --git a/packages/twenty-front/src/modules/activities/emails/states/viewableEmailThreadIdState.ts b/packages/twenty-front/src/modules/activities/emails/states/viewableEmailThreadIdState.ts deleted file mode 100644 index 494ec3d9cb..0000000000 --- a/packages/twenty-front/src/modules/activities/emails/states/viewableEmailThreadIdState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createState } from 'twenty-ui'; - -export const viewableEmailThreadIdState = createState({ - key: 'viewableEmailThreadIdState', - defaultValue: null, -}); diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenActivityRightDrawer.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenActivityRightDrawer.test.tsx index 19a027551a..1294d6ca29 100644 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenActivityRightDrawer.test.tsx +++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenActivityRightDrawer.test.tsx @@ -5,7 +5,7 @@ import { RecoilRoot, useRecoilValue } from 'recoil'; import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState'; -import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; const Wrapper = ({ children }: { children: ReactNode }) => ( @@ -18,12 +18,12 @@ describe('useOpenActivityRightDrawer', () => { const { result } = renderHook( () => { const openActivityRightDrawer = useOpenActivityRightDrawer(); - const viewableActivityId = useRecoilValue(viewableActivityIdState); + const viewableRecordId = useRecoilValue(viewableRecordIdState); const activityIdInDrawer = useRecoilValue(activityIdInDrawerState); return { openActivityRightDrawer, activityIdInDrawer, - viewableActivityId, + viewableRecordId, }; }, { @@ -32,11 +32,11 @@ describe('useOpenActivityRightDrawer', () => { ); expect(result.current.activityIdInDrawer).toBeNull(); - expect(result.current.viewableActivityId).toBeNull(); + expect(result.current.viewableRecordId).toBeNull(); act(() => { result.current.openActivityRightDrawer('123'); }); expect(result.current.activityIdInDrawer).toBe('123'); - expect(result.current.viewableActivityId).toBe('123'); + expect(result.current.viewableRecordId).toBe('123'); }); }); diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawer.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawer.test.tsx index eb60717a41..b545e455c8 100644 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawer.test.tsx +++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawer.test.tsx @@ -5,9 +5,9 @@ import { RecoilRoot, useRecoilValue, useSetRecoilState } from 'recoil'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState'; -import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; const mockUUID = '37873e04-2f83-4468-9ab7-3f87da6cafad'; @@ -28,7 +28,7 @@ describe('useOpenCreateActivityDrawer', () => { const { result } = renderHook( () => { const openActivityRightDrawer = useOpenCreateActivityDrawer(); - const viewableActivityId = useRecoilValue(viewableActivityIdState); + const viewableRecordId = useRecoilValue(viewableRecordIdState); const activityIdInDrawer = useRecoilValue(activityIdInDrawerState); const setObjectMetadataItems = useSetRecoilState( objectMetadataItemsState, @@ -36,7 +36,7 @@ describe('useOpenCreateActivityDrawer', () => { return { openActivityRightDrawer, activityIdInDrawer, - viewableActivityId, + viewableRecordId, setObjectMetadataItems, }; }, @@ -50,7 +50,7 @@ describe('useOpenCreateActivityDrawer', () => { }); expect(result.current.activityIdInDrawer).toBeNull(); - expect(result.current.viewableActivityId).toBeNull(); + expect(result.current.viewableRecordId).toBeNull(); await act(async () => { result.current.openActivityRightDrawer({ type: 'Note', @@ -58,6 +58,6 @@ describe('useOpenCreateActivityDrawer', () => { }); }); expect(result.current.activityIdInDrawer).toBe(mockUUID); - expect(result.current.viewableActivityId).toBe(mockUUID); + expect(result.current.viewableRecordId).toBe(mockUUID); }); }); diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenActivityRightDrawer.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenActivityRightDrawer.ts index b0279e17ee..185661f2a9 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useOpenActivityRightDrawer.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useOpenActivityRightDrawer.ts @@ -1,18 +1,17 @@ import { useRecoilState, useSetRecoilState } from 'recoil'; import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; -import { viewableActivityIdState } from '../states/viewableActivityIdState'; - export const useOpenActivityRightDrawer = () => { const { openRightDrawer, isRightDrawerOpen, rightDrawerPage } = useRightDrawer(); - const [viewableActivityId, setViewableActivityId] = useRecoilState( - viewableActivityIdState, + const [viewableRecordId, setViewableRecordId] = useRecoilState( + viewableRecordIdState, ); const setActivityIdInDrawer = useSetRecoilState(activityIdInDrawerState); const setHotkeyScope = useSetHotkeyScope(); @@ -21,13 +20,13 @@ export const useOpenActivityRightDrawer = () => { if ( isRightDrawerOpen && rightDrawerPage === RightDrawerPages.EditActivity && - viewableActivityId === activityId + viewableRecordId === activityId ) { return; } setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); - setViewableActivityId(activityId); + setViewableRecordId(activityId); setActivityIdInDrawer(activityId); openRightDrawer(RightDrawerPages.EditActivity); }; diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts index 5214736270..d85646096a 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts @@ -6,8 +6,8 @@ import { activityTargetableEntityArrayState } from '@/activities/states/activity import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState'; import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState'; import { temporaryActivityForEditorState } from '@/activities/states/temporaryActivityForEditorState'; -import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState'; import { ActivityType } from '@/activities/types/Activity'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; @@ -26,7 +26,7 @@ export const useOpenCreateActivityDrawer = () => { const setActivityTargetableEntityArray = useSetRecoilState( activityTargetableEntityArrayState, ); - const setViewableActivityId = useSetRecoilState(viewableActivityIdState); + const setViewableRecordId = useSetRecoilState(viewableRecordIdState); const setIsCreatingActivity = useSetRecoilState(isActivityInCreateModeState); @@ -59,7 +59,7 @@ export const useOpenCreateActivityDrawer = () => { setTemporaryActivityForEditor(createdActivityInCache); setIsCreatingActivity(true); setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); - setViewableActivityId(createdActivityInCache.id); + setViewableRecordId(createdActivityInCache.id); setActivityTargetableEntityArray(targetableObjects ?? []); openRightDrawer(RightDrawerPages.CreateActivity); setIsUpsertingActivityInDB(false); diff --git a/packages/twenty-front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx b/packages/twenty-front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx index 27cfbf2d3c..60cce744e2 100644 --- a/packages/twenty-front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx +++ b/packages/twenty-front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx @@ -1,23 +1,20 @@ import styled from '@emotion/styled'; import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards'; import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; -import { IconPlus, IconTrash } from 'twenty-ui'; +import { IconTrash } from 'twenty-ui'; -import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { useRefreshShowPageFindManyActivitiesQueries } from '@/activities/hooks/useRefreshShowPageFindManyActivitiesQueries'; import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState'; -import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState'; import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState'; import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState'; import { temporaryActivityForEditorState } from '@/activities/states/temporaryActivityForEditorState'; -import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState'; -import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState'; import { Activity } from '@/activities/types/Activity'; import { ActivityTarget } from '@/activities/types/ActivityTarget'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useDeleteRecordFromCache } from '@/object-record/cache/hooks/useDeleteRecordFromCache'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { mapToRecordId } from '@/object-record/utils/mapToObjectId'; import { IconButton } from '@/ui/input/button/components/IconButton'; @@ -30,12 +27,9 @@ const StyledButtonContainer = styled.div` `; export const ActivityActionBar = () => { - const viewableActivityId = useRecoilValue(viewableActivityIdState); + const viewableRecordId = useRecoilValue(viewableRecordIdState); const activityIdInDrawer = useRecoilValue(activityIdInDrawerState); - const activityTargetableEntityArray = useRecoilValue( - activityTargetableEntityArrayState, - ); const [, setIsRightDrawerOpen] = useRecoilState(isRightDrawerOpenState); const { deleteOneRecord: deleteOneActivity } = useDeleteOneRecord({ objectNameSingular: CoreObjectNameSingular.Activity, @@ -62,15 +56,9 @@ export const ActivityActionBar = () => { isUpsertingActivityInDBState, ); - const objectShowPageTargetableObject = useRecoilValue( - objectShowPageTargetableObjectState, - ); - const { refreshShowPageFindManyActivitiesQueries } = useRefreshShowPageFindManyActivitiesQueries(); - const openCreateActivity = useOpenCreateActivityDrawer(); - const deleteActivity = useRecoilCallback( ({ snapshot }) => async () => { @@ -86,7 +74,7 @@ export const ActivityActionBar = () => { setIsRightDrawerOpen(false); - if (!isNonEmptyString(viewableActivityId)) { + if (!isNonEmptyString(viewableRecordId)) { return; } @@ -111,13 +99,13 @@ export const ActivityActionBar = () => { await deleteManyActivityTargets(activityTargetIdsToDelete); } - await deleteOneActivity?.(viewableActivityId); + await deleteOneActivity?.(viewableRecordId); } }, [ activityIdInDrawer, setIsRightDrawerOpen, - viewableActivityId, + viewableRecordId, isActivityInCreateMode, temporaryActivityForEditor, deleteActivityFromCache, @@ -129,34 +117,10 @@ export const ActivityActionBar = () => { ], ); - const record = useRecoilValue( - recordStoreFamilyState(viewableActivityId ?? ''), - ); - - const addActivity = () => { - setIsRightDrawerOpen(false); - if (isDefined(record) && isDefined(objectShowPageTargetableObject)) { - openCreateActivity({ - type: record?.type, - customAssignee: record?.assignee, - targetableObjects: activityTargetableEntityArray, - }); - } - }; - const actionsAreDisabled = isUpsertingActivityInDB; - const isCreateActionDisabled = isActivityInCreateMode; - return ( - { - const isMobile = useIsMobile(); - - return ( - - - - {!isMobile && } - - {showActionBar && } - - ); -}; diff --git a/packages/twenty-front/src/modules/activities/right-drawer/components/create/RightDrawerCreateActivity.tsx b/packages/twenty-front/src/modules/activities/right-drawer/components/create/RightDrawerCreateActivity.tsx index fe6ac8ecaf..d4b64f7f52 100644 --- a/packages/twenty-front/src/modules/activities/right-drawer/components/create/RightDrawerCreateActivity.tsx +++ b/packages/twenty-front/src/modules/activities/right-drawer/components/create/RightDrawerCreateActivity.tsx @@ -1,17 +1,17 @@ import { useRecoilValue } from 'recoil'; -import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { RightDrawerActivity } from '../RightDrawerActivity'; export const RightDrawerCreateActivity = () => { - const viewableActivityId = useRecoilValue(viewableActivityIdState); + const viewableRecordId = useRecoilValue(viewableRecordIdState); return ( <> - {viewableActivityId && ( + {viewableRecordId && ( diff --git a/packages/twenty-front/src/modules/activities/right-drawer/components/edit/RightDrawerEditActivity.tsx b/packages/twenty-front/src/modules/activities/right-drawer/components/edit/RightDrawerEditActivity.tsx index 288d7aca88..84d0168696 100644 --- a/packages/twenty-front/src/modules/activities/right-drawer/components/edit/RightDrawerEditActivity.tsx +++ b/packages/twenty-front/src/modules/activities/right-drawer/components/edit/RightDrawerEditActivity.tsx @@ -1,16 +1,16 @@ import { useRecoilValue } from 'recoil'; -import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { RightDrawerActivity } from '../RightDrawerActivity'; export const RightDrawerEditActivity = () => { - const viewableActivityId = useRecoilValue(viewableActivityIdState); + const viewableRecordId = useRecoilValue(viewableRecordIdState); return ( <> - {viewableActivityId && ( - + {viewableRecordId && ( + )} ); diff --git a/packages/twenty-front/src/modules/activities/states/viewableActivityIdState.ts b/packages/twenty-front/src/modules/activities/states/viewableActivityIdState.ts deleted file mode 100644 index 393a2f96aa..0000000000 --- a/packages/twenty-front/src/modules/activities/states/viewableActivityIdState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createState } from 'twenty-ui'; - -export const viewableActivityIdState = createState({ - key: 'activities/viewable-activity-id', - defaultValue: null, -}); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts index 47ab23f5b1..5cb2d9a436 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { useApolloClient } from '@apollo/client'; import { v4 } from 'uuid'; @@ -28,6 +29,7 @@ export const useCreateOneRecord = < skipPostOptmisticEffect = false, }: useCreateOneRecordProps) => { const apolloClient = useApolloClient(); + const [loading, setLoading] = useState(false); const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, @@ -50,6 +52,8 @@ export const useCreateOneRecord = < const { objectMetadataItems } = useObjectMetadataItems(); const createOneRecord = async (input: Partial) => { + setLoading(true); + const idForCreation = input.id ?? v4(); const sanitizedInput = { @@ -94,6 +98,7 @@ export const useCreateOneRecord = < recordsToCreate: [record], objectMetadataItems, }); + setLoading(false); }, }); @@ -102,5 +107,6 @@ export const useCreateOneRecord = < return { createOneRecord, + loading, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx index a6fca0e4d1..9874373e81 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx @@ -74,6 +74,7 @@ const RelationFieldInputWithContext = ({ relationObjectMetadataNameSingular: CoreObjectNameSingular.WorkspaceMember, objectMetadataNameSingular: 'person', + relationFieldMetadataId: '20202020-8c37-4163-ba06-1dada334ce3e', }, }} entityId={entityId} diff --git a/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerRecord.tsx b/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerRecord.tsx new file mode 100644 index 0000000000..49b27cdfe3 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerRecord.tsx @@ -0,0 +1,40 @@ +import { useRecoilValue } from 'recoil'; + +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; +import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState'; +import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer'; +import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage'; +import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect'; +import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; + +export const RightDrawerRecord = () => { + const viewableRecordNameSingular = useRecoilValue( + viewableRecordNameSingularState, + ); + const viewableRecordId = useRecoilValue(viewableRecordIdState); + + if (!viewableRecordNameSingular) { + throw new Error(`Object name is not defined`); + } + + if (!viewableRecordId) { + throw new Error(`Record id is not defined`); + } + + const { objectNameSingular, objectRecordId } = useRecordShowPage( + viewableRecordNameSingular ?? '', + viewableRecordId ?? '', + ); + + return ( + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-right-drawer/states/viewableRecordIdState.ts b/packages/twenty-front/src/modules/object-record/record-right-drawer/states/viewableRecordIdState.ts new file mode 100644 index 0000000000..c75e6cf8a8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-right-drawer/states/viewableRecordIdState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const viewableRecordIdState = createState({ + key: 'activities/viewable-record-id', + defaultValue: null, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-right-drawer/states/viewableRecordNameSingularState.ts b/packages/twenty-front/src/modules/object-record/record-right-drawer/states/viewableRecordNameSingularState.ts new file mode 100644 index 0000000000..3116430e10 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-right-drawer/states/viewableRecordNameSingularState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const viewableRecordNameSingularState = createState({ + key: 'activities/viewable-record-name-singular', + defaultValue: null, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx index 87667d22f2..828fa028a1 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx @@ -27,6 +27,7 @@ import { ShowPageRightContainer } from '@/ui/layout/show-page/components/ShowPag import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard'; import { ShowPageRecoilScopeContext } from '@/ui/layout/states/ShowPageRecoilScopeContext'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { FieldMetadataType, FileFolder, @@ -39,12 +40,14 @@ type RecordShowContainerProps = { objectNameSingular: string; objectRecordId: string; loading: boolean; + isInRightDrawer?: boolean; }; export const RecordShowContainer = ({ objectNameSingular, objectRecordId, loading, + isInRightDrawer = false, }: RecordShowContainerProps) => { const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, @@ -129,114 +132,118 @@ export const RecordShowContainer = ({ ); const isReadOnly = objectMetadataItem.isRemote; + const isMobile = useIsMobile() || isInRightDrawer; const isPrefetchLoading = useIsPrefetchLoading(); - return ( - - - - {isDefined(recordFromStore) && ( - <> - - - - } - avatarType={recordIdentifier?.avatarType ?? 'rounded'} - onUploadPicture={ - objectNameSingular === 'person' ? onUploadPicture : undefined - } - /> - - {isPrefetchLoading ? ( - - ) : ( - inlineFieldMetadataItems.map((fieldMetadataItem, index) => ( - - - - )) - )} - - - {relationFieldMetadataItems?.map((fieldMetadataItem, index) => ( + const summary = ( + <> + {isDefined(recordFromStore) && ( + <> + + + + } + avatarType={recordIdentifier?.avatarType ?? 'rounded'} + onUploadPicture={ + objectNameSingular === 'person' ? onUploadPicture : undefined + } + /> + + {isPrefetchLoading ? ( + + ) : ( + inlineFieldMetadataItems.map((fieldMetadataItem, index) => ( - - ))} - - )} + )) + )} + + + {relationFieldMetadataItems?.map((fieldMetadataItem, index) => ( + + + + ))} + + )} + + ); + + return ( + + + + {!isMobile && summary} {recordFromStore ? ( ) : ( diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainerEffect.tsx deleted file mode 100644 index e3b835c9be..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainerEffect.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useEffect } from 'react'; -import { useRecoilState, useSetRecoilState } from 'recoil'; - -import { Activity } from '@/activities/types/Activity'; -import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; -import { recordLoadingFamilyState } from '@/object-record/record-store/states/recordLoadingFamilyState'; -import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; -import { isDefined } from '~/utils/isDefined'; - -export const RecordShowContainer = ({ - objectRecordId, - objectNameSingular, -}: { - objectRecordId: string; - objectNameSingular: string; -}) => { - const { record: activity, loading } = useFindOneRecord({ - objectRecordId, - objectNameSingular, - }); - - const setRecordStore = useSetRecoilState( - recordStoreFamilyState(objectRecordId), - ); - - const [recordLoading, setRecordLoading] = useRecoilState( - recordLoadingFamilyState(objectRecordId), - ); - - useEffect(() => { - if (loading !== recordLoading) { - setRecordLoading(loading); - } - }, [loading, recordLoading, setRecordLoading]); - - useEffect(() => { - if (!loading && isDefined(activity)) { - setRecordStore(activity); - } - }, [loading, setRecordStore, activity]); -}; diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPage.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPage.ts new file mode 100644 index 0000000000..f584c4cabc --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPage.ts @@ -0,0 +1,98 @@ +import { useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { useSetRecoilState } from 'recoil'; +import { useIcons } from 'twenty-ui'; + +import { useFavorites } from '@/favorites/hooks/useFavorites'; +import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; +import { findOneRecordForShowPageOperationSignatureFactory } from '@/object-record/record-show/graphql/operations/factories/findOneRecordForShowPageOperationSignatureFactory'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { isDefined } from '~/utils/isDefined'; +import { capitalize } from '~/utils/string/capitalize'; + +export const useRecordShowPage = ( + propsObjectNameSingular: string, + propsObjectRecordId: string, +) => { + const { + objectNameSingular: paramObjectNameSingular, + objectRecordId: paramObjectRecordId, + } = useParams(); + + const objectNameSingular = propsObjectNameSingular || paramObjectNameSingular; + const objectRecordId = propsObjectRecordId || paramObjectRecordId; + + if (!objectNameSingular || !objectRecordId) { + throw new Error('Object name or Record id is not defined'); + } + + const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular }); + const { labelIdentifierFieldMetadataItem } = + useLabelIdentifierFieldMetadataItem({ objectNameSingular }); + const { favorites, createFavorite, deleteFavorite } = useFavorites(); + const setEntityFields = useSetRecoilState( + recordStoreFamilyState(objectRecordId), + ); + const { getIcon } = useIcons(); + const headerIcon = getIcon(objectMetadataItem?.icon); + const FIND_ONE_RECORD_FOR_SHOW_PAGE_OPERATION_SIGNATURE = + findOneRecordForShowPageOperationSignatureFactory({ objectMetadataItem }); + const { record, loading } = useFindOneRecord({ + objectRecordId, + objectNameSingular, + recordGqlFields: FIND_ONE_RECORD_FOR_SHOW_PAGE_OPERATION_SIGNATURE.fields, + }); + + useEffect(() => { + if (isDefined(record)) { + setEntityFields(record); + } + }, [record, setEntityFields]); + + const correspondingFavorite = favorites.find( + (favorite) => favorite.recordId === objectRecordId, + ); + const isFavorite = isDefined(correspondingFavorite); + + const handleFavoriteButtonClick = async () => { + if (!objectNameSingular || !record) return; + + if (isFavorite) { + deleteFavorite(correspondingFavorite.id); + } else { + createFavorite(record, objectNameSingular); + } + }; + + const labelIdentifierFieldValue = + record?.[labelIdentifierFieldMetadataItem?.name ?? '']; + const pageName = + labelIdentifierFieldMetadataItem?.type === FieldMetadataType.FullName + ? [ + labelIdentifierFieldValue?.firstName, + labelIdentifierFieldValue?.lastName, + ].join(' ') + : isDefined(labelIdentifierFieldValue) + ? `${labelIdentifierFieldValue}` + : ''; + + const pageTitle = pageName.trim() + ? `${pageName} - ${capitalize(objectNameSingular)}` + : capitalize(objectNameSingular); + + return { + objectNameSingular, + objectRecordId, + headerIcon, + loading, + pageTitle, + pageName, + isFavorite, + handleFavoriteButtonClick, + record, + objectMetadataItem, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx index a4a1b6b2e4..0ce3ef74b0 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx @@ -17,6 +17,7 @@ import { RecordDetailSectionHeader } from '@/object-record/record-show/record-de import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { SingleEntitySelectMenuItemsWithSearch } from '@/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch'; +import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/relation-picker/hooks/useAddNewRecordAndOpenRightDrawer'; import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker'; import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope'; import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; @@ -72,7 +73,7 @@ export const RecordDetailRelationSection = ({ const relationRecordIds = relationRecords.map(({ id }) => id); - const dropdownId = `record-field-card-relation-picker-${fieldDefinition.label}`; + const dropdownId = `record-field-card-relation-picker-${fieldDefinition.label}-${entityId}`; const { closeDropdown, isDropdownOpen } = useDropdown(dropdownId); @@ -138,6 +139,14 @@ export const RecordDetailRelationSection = ({ ); }; + const { createNewRecordAndOpenRightDrawer } = + useAddNewRecordAndOpenRightDrawer({ + relationObjectMetadataNameSingular, + relationObjectMetadataItem, + relationFieldMetadataItem, + entityId, + }); + return ( } diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRow.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRow.tsx index 1441c109ad..dd1ce2cd70 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRow.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRow.tsx @@ -54,6 +54,7 @@ export const RecordTableRow = ({ getBasePathToShowPage({ objectNameSingular: objectMetadataItem.nameSingular, }) + recordId, + objectNameSingular: objectMetadataItem.nameSingular, isSelected: currentRowSelected, isReadOnly: objectMetadataItem.isRemote ?? false, isPendingRow, 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 73518d4190..9c0bf8b7ba 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 @@ -79,6 +79,8 @@ const meta: Meta = { > { + handleClick(); + /* + Disabling sidepanel access for now, TODO: launch + if (!isFieldInputOnly) { + openTableCell(undefined, true); + } + */ + }; + const isFirstColumn = columnIndex === 0; const customButtonIcon = useGetButtonIcon(); - const buttonIcon = isFirstColumn ? IconArrowUpRight : customButtonIcon; + const buttonIcon = isFirstColumn + ? IconArrowUpRight // IconLayoutSidebarRightExpand - Disabling sidepanel access for now + : customButtonIcon; const showButton = isDefined(buttonIcon) && @@ -136,7 +148,7 @@ export const RecordTableCellSoftFocusMode = ({ {editModeContentOnly ? editModeContent : nonEditModeContent} {showButton && ( - + )} ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__mocks__/cell.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__mocks__/cell.ts index e4d84e248a..792b33adc0 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__mocks__/cell.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__mocks__/cell.ts @@ -8,6 +8,7 @@ export const recordTableRow: RecordTableRowContextProps = { isSelected: false, recordId: 'recordId', pathToShowPage: '/', + objectNameSingular: 'objectNameSingular', 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 cf7846adfc..af16309867 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 @@ -31,9 +31,14 @@ export const useOpenRecordTableCellFromCell = () => { const cellPosition = useCurrentTableCellPosition(); const customCellHotkeyScope = useContext(CellHotkeyScopeContext); const { entityId, fieldDefinition } = useContext(FieldContext); - const { isReadOnly, pathToShowPage } = useContext(RecordTableRowContext); + const { isReadOnly, pathToShowPage, objectNameSingular } = useContext( + RecordTableRowContext, + ); - const openTableCell = (initialValue?: string) => { + const openTableCell = ( + initialValue?: string, + isActionButtonClick = false, + ) => { onOpenTableCell({ cellPosition, customCellHotkeyScope, @@ -41,7 +46,9 @@ export const useOpenRecordTableCellFromCell = () => { fieldDefinition, isReadOnly, pathToShowPage, + objectNameSingular, initialValue, + isActionButtonClick, }); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts index e9508f5365..b9e279b0b7 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts @@ -1,15 +1,19 @@ import { useNavigate } from 'react-router-dom'; -import { useRecoilCallback } from 'recoil'; +import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { useInitDraftValueV2 } from '@/object-record/record-field/hooks/useInitDraftValueV2'; import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; +import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/SoftFocusClickOutsideListenerId'; import { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/useLeaveTableFocus'; import { useMoveEditModeToTableCellPosition } from '@/object-record/record-table/hooks/internal/useMoveEditModeToCellPosition'; import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition'; +import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; +import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; @@ -28,9 +32,11 @@ export type OpenTableCellArgs = { cellPosition: TableCellPosition; isReadOnly: boolean; pathToShowPage: string; + objectNameSingular: string; customCellHotkeyScope: HotkeyScope | null; fieldDefinition: FieldDefinition; entityId: string; + isActionButtonClick: boolean; }; export const useOpenRecordTableCellV2 = (tableScopeId: string) => { @@ -48,6 +54,12 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => { const initDraftValue = useInitDraftValueV2(); + const { openRightDrawer } = useRightDrawer(); + const setViewableRecordId = useSetRecoilState(viewableRecordIdState); + const setViewableRecordNameSingular = useSetRecoilState( + viewableRecordNameSingularState, + ); + const openTableCell = useRecoilCallback( ({ snapshot }) => ({ @@ -55,9 +67,11 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => { cellPosition, isReadOnly, pathToShowPage, + objectNameSingular, customCellHotkeyScope, fieldDefinition, entityId, + isActionButtonClick, }: OpenTableCellArgs) => { if (isReadOnly) { return; @@ -78,9 +92,19 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => { fieldValue, }); - if (isFirstColumnCell && !isEmpty) { + if (isFirstColumnCell && !isEmpty && !isActionButtonClick) { leaveTableFocus(); navigate(pathToShowPage); + + return; + } + + if (isFirstColumnCell && !isEmpty && isActionButtonClick) { + leaveTableFocus(); + setViewableRecordId(entityId); + setViewableRecordNameSingular(objectNameSingular); + openRightDrawer(RightDrawerPages.ViewRecord); + return; } @@ -112,10 +136,13 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => { setDragSelectionStartEnabled, toggleClickOutsideListener, leaveTableFocus, - navigate, setHotkeyScope, initDraftValue, moveEditModeToTableCellPosition, + openRightDrawer, + setViewableRecordId, + setViewableRecordNameSingular, + navigate, ], ); diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/RelationPicker.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/RelationPicker.tsx index d5e4ef29c4..18da2c62b7 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/RelationPicker.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/RelationPicker.tsx @@ -1,9 +1,12 @@ -import { useEffect } from 'react'; +import { useContext, useEffect } from 'react'; import { IconForbid } from 'twenty-ui'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect'; +import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/relation-picker/hooks/useAddNewRecordAndOpenRightDrawer'; import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker'; import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; @@ -39,11 +42,33 @@ export const RelationPicker = ({ selectedEntity: EntityForSelect | null | undefined, ) => onSubmit(selectedEntity ?? null); + const { objectMetadataItem: relationObjectMetadataItem } = + useObjectMetadataItem({ + objectNameSingular: + fieldDefinition.metadata.relationObjectMetadataNameSingular, + }); + + const relationFieldMetadataItem = relationObjectMetadataItem.fields.find( + ({ id }) => id === fieldDefinition.metadata.relationFieldMetadataId, + ); + + const { entityId } = useContext(FieldContext); + + const { createNewRecordAndOpenRightDrawer } = + useAddNewRecordAndOpenRightDrawer({ + relationObjectMetadataNameSingular: + fieldDefinition.metadata.relationObjectMetadataNameSingular, + relationObjectMetadataItem, + relationFieldMetadataItem, + entityId, + }); + return ( ))} - {showCreateButton && !loading && ( + {showCreateButton && ( <> {entitiesToSelect.length > 0 && } void; + onCreate?: ((searchInput?: string) => void) | (() => void); relationObjectNameSingular: string; relationPickerScopeId?: string; selectedRelationRecordIds: string[]; @@ -54,8 +54,7 @@ export const SingleEntitySelectMenuItemsWithSearch = ({ relationPickerSearchFilterState, ); - const showCreateButton = - isDefined(onCreate) && relationPickerSearchFilter !== ''; + const showCreateButton = isDefined(onCreate); const entities = useFilteredSearchEntityQuery({ filters: [ @@ -71,6 +70,20 @@ export const SingleEntitySelectMenuItemsWithSearch = ({ objectNameSingular: relationObjectNameSingular, }); + let onCreateWithInput = undefined; + + if (isDefined(onCreate)) { + onCreateWithInput = () => { + if (onCreate.length > 0) { + (onCreate as (searchInput?: string) => void)( + relationPickerSearchFilter, + ); + } else { + (onCreate as () => void)(); + } + }; + } + return ( <> { + const setViewableRecordId = useSetRecoilState(viewableRecordIdState); + const setViewableRecordNameSingular = useSetRecoilState( + viewableRecordNameSingularState, + ); + + const { createOneRecord } = useCreateOneRecord({ + objectNameSingular: relationObjectMetadataNameSingular, + }); + + const { updateOneRecord } = useUpdateOneRecord({ + objectNameSingular: + relationFieldMetadataItem?.relationDefinition?.targetObjectMetadata + .nameSingular ?? 'workspaceMember', + }); + + const { openRightDrawer } = useRightDrawer(); + + if ( + relationObjectMetadataNameSingular === 'workspaceMember' || + !isDefined( + relationFieldMetadataItem?.relationDefinition?.targetObjectMetadata + .nameSingular, + ) + ) { + return { + createNewRecordAndOpenRightDrawer: undefined, + }; + } + + return { + createNewRecordAndOpenRightDrawer: async (searchInput?: string) => { + const newRecordId = v4(); + const labelIdentifierType = getLabelIdentifierFieldMetadataItem( + relationObjectMetadataItem, + )?.type; + const createRecordPayload: { + id: string; + name: + | string + | { firstName: string | undefined; lastName: string | undefined }; + [key: string]: any; + } = + labelIdentifierType === FieldMetadataType.FullName + ? { + id: newRecordId, + name: + searchInput && searchInput.split(' ').length > 1 + ? { + firstName: searchInput.split(' ')[0], + lastName: searchInput.split(' ').slice(1).join(' '), + } + : { firstName: searchInput, lastName: '' }, + } + : { id: newRecordId, name: searchInput ?? '' }; + + if ( + relationFieldMetadataItem?.relationDefinition?.direction === + RelationDefinitionType.ManyToOne + ) { + createRecordPayload[ + `${relationFieldMetadataItem?.relationDefinition?.targetFieldMetadata.name}Id` + ] = entityId; + } + + await createOneRecord(createRecordPayload); + + if ( + relationFieldMetadataItem?.relationDefinition?.direction === + RelationDefinitionType.OneToMany + ) { + await updateOneRecord({ + idToUpdate: entityId, + updateOneRecordInput: { + [`${relationFieldMetadataItem?.relationDefinition?.targetFieldMetadata.name}Id`]: + newRecordId, + }, + }); + } + + setViewableRecordId(newRecordId); + setViewableRecordNameSingular(relationObjectMetadataNameSingular); + openRightDrawer(RightDrawerPages.ViewRecord); + }, + }; +}; diff --git a/packages/twenty-front/src/modules/ui/layout/page/PageBody.tsx b/packages/twenty-front/src/modules/ui/layout/page/PageBody.tsx index 4e1269b3e1..5b97f13dd2 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/PageBody.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/PageBody.tsx @@ -1 +1,50 @@ -export { RightDrawerContainer as PageBody } from './RightDrawerContainer'; +import { ReactNode } from 'react'; +import styled from '@emotion/styled'; +import { MOBILE_VIEWPORT } from 'twenty-ui'; + +import { RightDrawer } from '@/ui/layout/right-drawer/components/RightDrawer'; + +import { PagePanel } from './PagePanel'; + +type PageBodyProps = { + children: ReactNode; +}; + +const StyledMainContainer = styled.div` + background: ${({ theme }) => theme.background.noisy}; + box-sizing: border-box; + display: flex; + flex: 1 1 auto; + flex-direction: row; + gap: ${({ theme }) => theme.spacing(2)}; + min-height: 0; + padding-bottom: ${({ theme }) => theme.spacing(3)}; + padding-right: ${({ theme }) => theme.spacing(3)}; + padding-left: 0; + width: 100%; + + @media (max-width: ${MOBILE_VIEWPORT}px) { + padding-left: ${({ theme }) => theme.spacing(3)}; + padding-bottom: 0; + } +`; + +type LeftContainerProps = { + isRightDrawerOpen?: boolean; +}; + +const StyledLeftContainer = styled.div` + display: flex; + flex-direction: column; + position: relative; + width: 100%; +`; + +export const PageBody = ({ children }: PageBodyProps) => ( + + + {children} + + + +); diff --git a/packages/twenty-front/src/modules/ui/layout/page/SubMenuTopBarContainer.tsx b/packages/twenty-front/src/modules/ui/layout/page/SubMenuTopBarContainer.tsx index 7114005329..becc29aae6 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/SubMenuTopBarContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/SubMenuTopBarContainer.tsx @@ -4,8 +4,8 @@ import { IconComponent } from 'twenty-ui'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; +import { PageBody } from './PageBody'; import { PageHeader } from './PageHeader'; -import { RightDrawerContainer } from './RightDrawerContainer'; type SubMenuTopBarContainerProps = { children: JSX.Element | JSX.Element[]; @@ -32,7 +32,7 @@ export const SubMenuTopBarContainer = ({ return ( {isMobile && } - {children} + {children} ); }; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx index 907d16dda6..0fc54fbdaf 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx @@ -7,6 +7,7 @@ import { Key } from 'ts-key-enum'; import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener'; import { isRightDrawerAnimationCompletedState } from '@/ui/layout/right-drawer/states/isRightDrawerAnimationCompleted'; +import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState'; import { rightDrawerCloseEventState } from '@/ui/layout/right-drawer/states/rightDrawerCloseEventsState'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; @@ -46,6 +47,8 @@ export const RightDrawer = () => { isRightDrawerOpenState, ); + const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState); + const isRightDrawerExpanded = useRecoilValue(isRightDrawerExpandedState); const [, setIsRightDrawerAnimationCompleted] = useRecoilState( isRightDrawerAnimationCompletedState, @@ -69,8 +72,11 @@ export const RightDrawer = () => { const isRightDrawerOpen = snapshot .getLoadable(isRightDrawerOpenState) .getValue(); + const isRightDrawerMinimized = snapshot + .getLoadable(isRightDrawerMinimizedState) + .getValue(); - if (isRightDrawerOpen) { + if (isRightDrawerOpen && !isRightDrawerMinimized) { set(rightDrawerCloseEventState, event); closeRightDrawer(); } @@ -115,6 +121,13 @@ export const RightDrawer = () => { closed: { x: '100%', }, + minimized: { + x: '0%', + width: 'auto', + height: 'auto', + bottom: '0', + top: 'auto', + }, }; const handleAnimationComplete = () => { setIsRightDrawerAnimationCompleted(isRightDrawerOpen); @@ -122,8 +135,20 @@ export const RightDrawer = () => { return ( , - topBar: , + topBar: , }, [RightDrawerPages.EditActivity]: { page: , - topBar: , + topBar: , }, [RightDrawerPages.ViewEmailThread]: { page: , - topBar: , + topBar: , }, [RightDrawerPages.ViewCalendarEvent]: { page: , - topBar: , + topBar: , + }, + [RightDrawerPages.ViewRecord]: { + page: , + topBar: , }, }; @@ -53,10 +59,14 @@ export const RightDrawerRouter = () => { ? RIGHT_DRAWER_PAGES_CONFIG[rightDrawerPage] : {}; + const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState); + return ( {topBar} - {page} + {!isRightDrawerMinimized && ( + {page} + )} ); }; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBar.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBar.tsx new file mode 100644 index 0000000000..1e371c0eb3 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBar.tsx @@ -0,0 +1,116 @@ +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import { Chip, ChipAccent, ChipSize, useIcons } from 'twenty-ui'; + +import { ActivityActionBar } from '@/activities/right-drawer/components/ActivityActionBar'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState'; +import { RightDrawerTopBarCloseButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarCloseButton'; +import { RightDrawerTopBarExpandButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarExpandButton'; +import { RightDrawerTopBarMinimizeButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarMinimizeButton'; +import { StyledRightDrawerTopBar } from '@/ui/layout/right-drawer/components/StyledRightDrawerTopBar'; +import { RIGHT_DRAWER_PAGE_ICONS } from '@/ui/layout/right-drawer/constants/RightDrawerPageIcons'; +import { RIGHT_DRAWER_PAGE_TITLES } from '@/ui/layout/right-drawer/constants/RightDrawerPageTitles'; +import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState'; +import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; + +const StyledTopBarWrapper = styled.div` + display: flex; +`; + +const StyledMinimizeTopBarTitleContainer = styled.div` + align-items: center; + display: flex; + gap: ${({ theme }) => theme.spacing(1)}; + height: 24px; + width: 168px; +`; + +const StyledMinimizeTopBarTitle = styled.div` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const StyledMinimizeTopBarIcon = styled.div` + align-items: center; + display: flex; +`; + +export const RightDrawerTopBar = ({ page }: { page: RightDrawerPages }) => { + const isMobile = useIsMobile(); + + const [isRightDrawerMinimized, setIsRightDrawerMinimized] = useRecoilState( + isRightDrawerMinimizedState, + ); + + const theme = useTheme(); + + const handleOnclick = () => { + if (isRightDrawerMinimized) { + setIsRightDrawerMinimized(false); + } + }; + + const { getIcon } = useIcons(); + + const PageIcon = getIcon(RIGHT_DRAWER_PAGE_ICONS[page]); + + const viewableRecordNameSingular = useRecoilValue( + viewableRecordNameSingularState, + ); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: viewableRecordNameSingular ?? 'company', + }); + + const ObjectIcon = getIcon(objectMetadataItem.icon); + + const label = + page === RightDrawerPages.ViewRecord + ? objectMetadataItem.labelSingular + : RIGHT_DRAWER_PAGE_TITLES[page]; + + const Icon = page === RightDrawerPages.ViewRecord ? ObjectIcon : PageIcon; + + return ( + + {!isRightDrawerMinimized && + (page === RightDrawerPages.EditActivity || + page === RightDrawerPages.CreateActivity) && } + {!isRightDrawerMinimized && + page !== RightDrawerPages.EditActivity && + page !== RightDrawerPages.CreateActivity && ( + } + size={ChipSize.Large} + accent={ChipAccent.TextSecondary} + clickable={false} + /> + )} + {isRightDrawerMinimized && ( + + + + + {label} + + )} + + {!isMobile && !isRightDrawerMinimized && ( + + )} + {!isMobile && !isRightDrawerMinimized && ( + + )} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarCloseButton.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarCloseButton.tsx index b7e0f0102e..9a225f4571 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarCloseButton.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarCloseButton.tsx @@ -1,4 +1,4 @@ -import { IconChevronsRight } from 'twenty-ui'; +import { IconX } from 'twenty-ui'; import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; @@ -13,7 +13,7 @@ export const RightDrawerTopBarCloseButton = () => { return ( { - const [isRightDrawerExpanded, setIsRightDrawerExpanded] = useRecoilState( - isRightDrawerExpandedState, - ); + const { isRightDrawerExpanded, downsizeRightDrawer, expandRightDrawer } = + useRightDrawer(); const handleButtonClick = () => { - setIsRightDrawerExpanded(!isRightDrawerExpanded); + if (isRightDrawerExpanded === true) { + downsizeRightDrawer(); + return; + } + expandRightDrawer(); }; return ( diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarMinimizeButton.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarMinimizeButton.tsx new file mode 100644 index 0000000000..b95be35787 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarMinimizeButton.tsx @@ -0,0 +1,22 @@ +import { IconMinus } from 'twenty-ui'; + +import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; +import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; + +export const RightDrawerTopBarMinimizeButton = () => { + const { isRightDrawerMinimized, minimizeRightDrawer, maximizeRightDrawer } = + useRightDrawer(); + + const handleButtonClick = () => { + isRightDrawerMinimized ? maximizeRightDrawer() : minimizeRightDrawer(); + }; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/StyledRightDrawerTopBar.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/StyledRightDrawerTopBar.tsx index fe3cae76e3..3378e36ddc 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/StyledRightDrawerTopBar.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/StyledRightDrawerTopBar.tsx @@ -1,6 +1,8 @@ import styled from '@emotion/styled'; -export const StyledRightDrawerTopBar = styled.div` +export const StyledRightDrawerTopBar = styled.div<{ + isRightDrawerMinimized: boolean; +}>` align-items: center; background: ${({ theme }) => theme.background.secondary}; border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; @@ -9,9 +11,12 @@ export const StyledRightDrawerTopBar = styled.div` flex-direction: row; font-size: ${({ theme }) => theme.font.size.md}; gap: ${({ theme }) => theme.spacing(1)}; - height: 56px; + height: ${({ isRightDrawerMinimized }) => + isRightDrawerMinimized ? '40px' : '56px'}; justify-content: space-between; padding-left: ${({ theme }) => theme.spacing(2)}; padding-right: ${({ theme }) => theme.spacing(2)}; + cursor: ${({ isRightDrawerMinimized }) => + isRightDrawerMinimized ? 'pointer' : 'default'}; `; diff --git a/packages/twenty-front/src/modules/activities/right-drawer/components/__stories__/RightDrawerActivityTopBar.stories.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/__stories__/RightDrawerTopBar.stories.tsx similarity index 70% rename from packages/twenty-front/src/modules/activities/right-drawer/components/__stories__/RightDrawerActivityTopBar.stories.tsx rename to packages/twenty-front/src/modules/ui/layout/right-drawer/components/__stories__/RightDrawerTopBar.stories.tsx index fa7616383a..3f69b0dc38 100644 --- a/packages/twenty-front/src/modules/activities/right-drawer/components/__stories__/RightDrawerActivityTopBar.stories.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/__stories__/RightDrawerTopBar.stories.tsx @@ -3,11 +3,11 @@ import { Meta, StoryObj } from '@storybook/react'; import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; import { graphqlMocks } from '~/testing/graphqlMocks'; -import { RightDrawerActivityTopBar } from '../RightDrawerActivityTopBar'; +import { RightDrawerTopBar } from '../RightDrawerTopBar'; -const meta: Meta = { +const meta: Meta = { title: 'Modules/Activities/RightDrawer/RightDrawerActivityTopBar', - component: RightDrawerActivityTopBar, + component: RightDrawerTopBar, decorators: [ (Story) => (
@@ -22,6 +22,6 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = {}; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageIcons.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageIcons.ts new file mode 100644 index 0000000000..f1c46ba194 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageIcons.ts @@ -0,0 +1,9 @@ +import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; + +export const RIGHT_DRAWER_PAGE_ICONS = { + [RightDrawerPages.CreateActivity]: 'IconNote', + [RightDrawerPages.EditActivity]: 'IconNote', + [RightDrawerPages.ViewEmailThread]: 'IconMail', + [RightDrawerPages.ViewCalendarEvent]: 'IconCalendarEvent', + [RightDrawerPages.ViewRecord]: 'Icon123', +}; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageTitles.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageTitles.ts new file mode 100644 index 0000000000..6edb8fec21 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageTitles.ts @@ -0,0 +1,9 @@ +import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; + +export const RIGHT_DRAWER_PAGE_TITLES = { + [RightDrawerPages.CreateActivity]: 'Create Activity', + [RightDrawerPages.EditActivity]: 'Edit Activity', + [RightDrawerPages.ViewEmailThread]: 'Email Thread', + [RightDrawerPages.ViewCalendarEvent]: 'Calendar Event', + [RightDrawerPages.ViewRecord]: 'Record Editor', +}; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts index 5cf5cc6214..86cc303c81 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts @@ -1,5 +1,6 @@ import { useRecoilCallback, useRecoilState } from 'recoil'; +import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState'; import { rightDrawerCloseEventState } from '@/ui/layout/right-drawer/states/rightDrawerCloseEventsState'; import { isRightDrawerExpandedState } from '../states/isRightDrawerExpandedState'; @@ -9,6 +10,8 @@ import { RightDrawerPages } from '../types/RightDrawerPages'; export const useRightDrawer = () => { const [isRightDrawerOpen] = useRecoilState(isRightDrawerOpenState); + const [isRightDrawerExpanded] = useRecoilState(isRightDrawerExpandedState); + const [isRightDrawerMinimized] = useRecoilState(isRightDrawerMinimizedState); const [rightDrawerPage] = useRecoilState(rightDrawerPageState); @@ -18,6 +21,7 @@ export const useRightDrawer = () => { set(rightDrawerPageState, rightDrawerPage); set(isRightDrawerExpandedState, false); set(isRightDrawerOpenState, true); + set(isRightDrawerMinimizedState, false); }, [], ); @@ -27,6 +31,47 @@ export const useRightDrawer = () => { () => { set(isRightDrawerExpandedState, false); set(isRightDrawerOpenState, false); + set(isRightDrawerMinimizedState, false); + }, + [], + ); + + const minimizeRightDrawer = useRecoilCallback( + ({ set }) => + () => { + set(isRightDrawerExpandedState, false); + set(isRightDrawerOpenState, true); + set(isRightDrawerMinimizedState, true); + }, + [], + ); + + const maximizeRightDrawer = useRecoilCallback( + ({ set }) => + () => { + set(isRightDrawerMinimizedState, false); + set(isRightDrawerExpandedState, false); + set(isRightDrawerOpenState, true); + }, + [], + ); + + const expandRightDrawer = useRecoilCallback( + ({ set }) => + () => { + set(isRightDrawerExpandedState, true); + set(isRightDrawerOpenState, true); + set(isRightDrawerMinimizedState, false); + }, + [], + ); + + const downsizeRightDrawer = useRecoilCallback( + ({ set }) => + () => { + set(isRightDrawerExpandedState, false); + set(isRightDrawerOpenState, true); + set(isRightDrawerMinimizedState, false); }, [], ); @@ -50,8 +95,14 @@ export const useRightDrawer = () => { return { rightDrawerPage, isRightDrawerOpen, + isRightDrawerExpanded, + isRightDrawerMinimized, openRightDrawer, closeRightDrawer, + minimizeRightDrawer, + maximizeRightDrawer, + expandRightDrawer, + downsizeRightDrawer, isSameEventThanRightDrawerClose, }; }; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/states/isRightDrawerMinimizedState.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/states/isRightDrawerMinimizedState.ts new file mode 100644 index 0000000000..9b21240306 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/states/isRightDrawerMinimizedState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const isRightDrawerMinimizedState = createState({ + key: 'ui/layout/is-right-drawer-minimized', + defaultValue: false, +}); diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts index df0cd28ca5..487b1a16f8 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts @@ -3,4 +3,5 @@ export enum RightDrawerPages { EditActivity = 'edit-activity', ViewEmailThread = 'view-email-thread', ViewCalendarEvent = 'view-calendar-event', + ViewRecord = 'view-record', } 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 fa5d5c1e82..402bc97dcd 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 @@ -4,22 +4,23 @@ import styled from '@emotion/styled'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; -const StyledOuterContainer = styled.div` +const StyledOuterContainer = styled.div<{ isMobile: boolean }>` background: ${({ theme }) => theme.background.secondary}; border-bottom-left-radius: 8px; - border-right: ${({ theme }) => - useIsMobile() ? 'none' : `1px solid ${theme.border.color.medium}`}; + border-right: ${({ theme, isMobile }) => + isMobile ? 'none' : `1px solid ${theme.border.color.medium}`}; border-top-left-radius: 8px; display: flex; flex-direction: column; gap: ${({ theme }) => theme.spacing(3)}; z-index: 10; + width: 'auto'; `; -const StyledInnerContainer = styled.div` +const StyledInnerContainer = styled.div<{ isMobile: boolean }>` display: flex; flex-direction: column; - width: ${() => (useIsMobile() ? `100%` : '348px')}; + width: ${({ isMobile }) => (isMobile ? `100%` : '348px')}; `; const StyledIntermediateContainer = styled.div` @@ -29,24 +30,30 @@ const StyledIntermediateContainer = styled.div` `; export type ShowPageLeftContainerProps = { + forceMobile: boolean; children: ReactNode; }; export const ShowPageLeftContainer = ({ + forceMobile = false, children, }: ShowPageLeftContainerProps) => { - const isMobile = useIsMobile(); - return isMobile ? ( - - {children} - - ) : ( - - - - {children} - - + const isMobile = useIsMobile() || forceMobile; + return ( + + {isMobile ? ( + + {children} + + ) : ( + + + + {children} + + + + )} ); }; diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx index 7c3ebf2ec1..1a4c51e441 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx @@ -24,12 +24,12 @@ import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -const StyledShowPageRightContainer = styled.div` +const StyledShowPageRightContainer = styled.div<{ isMobile: boolean }>` display: flex; flex: 1 0 0; flex-direction: column; justify-content: start; - overflow: ${() => (useIsMobile() ? 'none' : 'hidden')}; + overflow: ${(isMobile) => (isMobile ? 'none' : 'hidden')}; width: calc(100% + 4px); `; @@ -53,6 +53,8 @@ type ShowPageRightContainerProps = { tasks?: boolean; notes?: boolean; emails?: boolean; + summary?: JSX.Element; + isRightDrawer?: boolean; loading: boolean; }; @@ -63,8 +65,12 @@ export const ShowPageRightContainer = ({ notes, emails, loading, + summary, + isRightDrawer = false, }: ShowPageRightContainerProps) => { - const { activeTabIdState } = useTabList(TAB_LIST_COMPONENT_ID); + const { activeTabIdState } = useTabList( + TAB_LIST_COMPONENT_ID + isRightDrawer, + ); const activeTabId = useRecoilValue(activeTabIdState); const shouldDisplayCalendarTab = @@ -80,12 +86,20 @@ export const ShowPageRightContainer = ({ CoreObjectNameSingular.Company) || targetableObject.targetObjectNameSingular === CoreObjectNameSingular.Person; + const isMobile = useIsMobile() || isRightDrawer; + const TASK_TABS = [ + { + id: 'summary', + title: 'Summary', + Icon: IconCheckbox, + hide: !isMobile, + }, { id: 'timeline', title: 'Timeline', Icon: IconTimelineEvent, - hide: !timeline, + hide: !timeline || isRightDrawer, }, { id: 'tasks', @@ -127,14 +141,15 @@ export const ShowPageRightContainer = ({ ]; return ( - + + {activeTabId === 'summary' && summary} {activeTabId === 'timeline' && ( <> @@ -157,6 +172,7 @@ export const ShowPageRightContainer = ({ {activeTabId === 'logs' && ( )} + {} ); }; 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 1d9f4baa5d..ba93d808f9 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 @@ -35,7 +35,7 @@ const StyledContainer = styled.div` `; export const TabList = ({ tabs, tabListId, loading }: TabListProps) => { - const initialActiveTabId = tabs[0].id; + const initialActiveTabId = tabs.find((tab) => !tab.hide)?.id || ''; const { activeTabIdState, setActiveTabId } = useTabList(tabListId); diff --git a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx index 0e3e210185..bcb3508dc3 100644 --- a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx @@ -35,7 +35,6 @@ export const RecordIndexPage = () => { const handleAddButtonClick = async () => { setPendingRecordId(v4()); - setSelectedTableCellEditMode(-1, 0); setHotkeyScope(DEFAULT_CELL_SCOPE.scope, DEFAULT_CELL_SCOPE.customScopes); }; diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx index 736f162106..215d519731 100644 --- a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx @@ -1,17 +1,9 @@ -import { useEffect } from 'react'; import { useParams } from 'react-router-dom'; -import { useSetRecoilState } from 'recoil'; -import { useIcons } from 'twenty-ui'; -import { useFavorites } from '@/favorites/hooks/useFavorites'; -import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer'; -import { findOneRecordForShowPageOperationSignatureFactory } from '@/object-record/record-show/graphql/operations/factories/findOneRecordForShowPageOperationSignatureFactory'; +import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage'; import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect'; import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; -import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { PageBody } from '@/ui/layout/page/PageBody'; import { PageContainer } from '@/ui/layout/page/PageContainer'; import { PageFavoriteButton } from '@/ui/layout/page/PageFavoriteButton'; @@ -19,93 +11,29 @@ import { PageHeader } from '@/ui/layout/page/PageHeader'; import { ShowPageAddButton } from '@/ui/layout/show-page/components/ShowPageAddButton'; import { ShowPageMoreButton } from '@/ui/layout/show-page/components/ShowPageMoreButton'; import { PageTitle } from '@/ui/utilities/page-title/PageTitle'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { isDefined } from '~/utils/isDefined'; -import { capitalize } from '~/utils/string/capitalize'; export const RecordShowPage = () => { - const { objectNameSingular, objectRecordId } = useParams<{ + const parameters = useParams<{ objectNameSingular: string; objectRecordId: string; }>(); - if (!objectNameSingular) { - throw new Error(`Object name is not defined`); - } - - if (!objectRecordId) { - throw new Error(`Record id is not defined`); - } - - const { objectMetadataItem } = useObjectMetadataItem({ + const { objectNameSingular, - }); - - const { labelIdentifierFieldMetadataItem } = - useLabelIdentifierFieldMetadataItem({ - objectNameSingular, - }); - - const { favorites, createFavorite, deleteFavorite } = useFavorites(); - - const setEntityFields = useSetRecoilState( - recordStoreFamilyState(objectRecordId), - ); - - const { getIcon } = useIcons(); - - const headerIcon = getIcon(objectMetadataItem?.icon); - - const FIND_ONE_RECORD_FOR_SHOW_PAGE_OPERATION_SIGNATURE = - findOneRecordForShowPageOperationSignatureFactory({ objectMetadataItem }); - - const { record, loading } = useFindOneRecord({ objectRecordId, - objectNameSingular, - recordGqlFields: FIND_ONE_RECORD_FOR_SHOW_PAGE_OPERATION_SIGNATURE.fields, - }); - - useEffect(() => { - if (!record) { - return; - } - - setEntityFields(record); - }, [record, setEntityFields]); - - const correspondingFavorite = favorites.find( - (favorite) => favorite.recordId === objectRecordId, + headerIcon, + loading, + pageTitle, + pageName, + isFavorite, + handleFavoriteButtonClick, + record, + objectMetadataItem, + } = useRecordShowPage( + parameters.objectNameSingular ?? '', + parameters.objectRecordId ?? '', ); - const isFavorite = isDefined(correspondingFavorite); - - const handleFavoriteButtonClick = async () => { - if (!objectNameSingular || !record) return; - - if (isFavorite && isDefined(record)) { - deleteFavorite(correspondingFavorite.id); - } else { - createFavorite(record, objectNameSingular); - } - }; - - const labelIdentifierFieldValue = - record?.[labelIdentifierFieldMetadataItem?.name ?? '']; - - const pageName = - labelIdentifierFieldMetadataItem?.type === FieldMetadataType.FullName - ? [ - labelIdentifierFieldValue?.firstName, - labelIdentifierFieldValue?.lastName, - ].join(' ') - : isDefined(labelIdentifierFieldValue) - ? `${labelIdentifierFieldValue}` - : ''; - - const pageTitle = pageName.trim() - ? `${pageName} - ${capitalize(objectNameSingular)}` - : capitalize(objectNameSingular); - return (