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 4f760bf95..06aa66012 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 027f7f875..980160850 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 9cd27d017..b10743f35 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 32d1951bc..000000000 --- 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 a75afeb82..8df2da809 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 d61ebc25b..25c01d745 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 b560a76f3..e06b79c9c 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 7063110ac..29cd280ed 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 4dae422ca..d7d3efe03 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 a0757a18a..c2fa722ed 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 dce19bb82..bc415e406 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 494ec3d9c..000000000 --- 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 19a027551..1294d6ca2 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 eb60717a4..b545e455c 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 b0279e17e..185661f2a 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 521473627..d85646096 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 27cfbf2d3..60cce744e 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 fe6ac8eca..d4b64f7f5 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 288d7aca8..84d016869 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 393a2f96a..000000000 --- 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 47ab23f5b..5cb2d9a43 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 a6fca0e4d..9874373e8 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 000000000..49b27cdfe --- /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 000000000..c75e6cf8a --- /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 000000000..3116430e1 --- /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 87667d22f..828fa028a 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 e3b835c9b..000000000 --- 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 000000000..f584c4cab --- /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 a4a1b6b2e..0ce3ef74b 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 1441c109a..dd1ce2cd7 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 73518d419..9c0bf8b7b 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 e4d84e248..792b33adc 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 cf7846adf..af1630986 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 e9508f536..b9e279b0b 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 d5e4ef29c..18da2c62b 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 4e1269b3e..5b97f13dd 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 711400532..becc29aae 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 907d16dda..0fc54fbda 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 000000000..1e371c0eb --- /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 b7e0f0102..9a225f457 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 000000000..b95be3578 --- /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 fe3cae76e..3378e36dd 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 fa7616383..3f69b0dc3 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 000000000..f1c46ba19 --- /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 000000000..6edb8fec2 --- /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 5cf5cc621..86cc303c8 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 000000000..9b2124030 --- /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 df0cd28ca..487b1a16f 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 fa5d5c1e8..402bc97dc 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 7c3ebf2ec..1a4c51e44 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 1d9f4baa5..ba93d808f 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 0e3e21018..bcb3508dc 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 736f16210..215d51973 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 (