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 4f760bf95851..06aa66012f55 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 027f7f87557a..9801608508e1 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 9cd27d017e52..b10743f3533c 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 32d1951bcf5b..000000000000 --- 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 a75afeb82cee..8df2da80928a 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 d61ebc25b23e..25c01d745c0b 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 b560a76f3d8e..e06b79c9c548 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 7063110ac052..29cd280ed0e7 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 4dae422ca2fd..d7d3efe0335e 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 a0757a18af57..c2fa722ed634 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 dce19bb82e84..bc415e406aa7 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 494ec3d9cb63..000000000000 --- 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 19a027551a21..1294d6ca2998 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 eb60717a4125..b545e455c8b0 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 b0279e17eece..185661f2a996 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 521473627095..d85646096a94 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 27cfbf2d3c86..60cce744e2ee 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 fe6ac8ecaf43..d4b64f7f5239 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 288d7aca88af..84d0168696d1 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 393a2f96aaa5..000000000000 --- 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 47ab23f5b17e..5cb2d9a436cf 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 a6fca0e4d1fc..9874373e81fd 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 000000000000..49b27cdfe3eb --- /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 000000000000..c75e6cf8a8a5 --- /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 000000000000..3116430e10c3 --- /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 87667d22f25f..828fa028a1cc 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 e3b835c9be8c..000000000000 --- 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 000000000000..f584c4cabcc9 --- /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 a4a1b6b2e42a..0ce3ef74b026 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 1441c109ad84..dd1ce2cd7072 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 73518d41906d..9c0bf8b7ba08 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 e4d84e248a60..792b33adc009 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 cf7846adfc2c..af16309867fb 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 e9508f5365d2..b9e279b0b77a 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 d5e4ef29c464..18da2c62b7ec 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 4e1269b3e1c2..5b97f13dd2fe 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 7114005329c1..becc29aae6f9 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 907d16dda6bd..0fc54fbdaf37 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 000000000000..1e371c0eb397 --- /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 b7e0f0102ee2..9a225f4571d0 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 000000000000..b95be3578766 --- /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 fe3cae76e3da..3378e36ddca2 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 fa7616383a80..3f69b0dc387d 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 000000000000..f1c46ba194eb --- /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 000000000000..6edb8fec2116 --- /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 5cf5cc621456..86cc303c811a 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 000000000000..9b2124030610 --- /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 df0cd28ca53c..487b1a16f841 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 fa5d5c1e82c8..402bc97dcd2c 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 7c3ebf2ec112..1a4c51e441d4 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 1d9f4baa5d0c..ba93d808f9d9 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 0e3e21018532..bcb3508dc359 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 736f1621060b..215d5197310c 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 (