diff --git a/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx b/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx index ea0502a876..cf1d15db70 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx @@ -25,7 +25,7 @@ const meta: Meta = { (Story) => ( = { return ( { recordId: objectRecordId, }} > - - diff --git a/packages/twenty-front/src/modules/object-record/record-merge/components/MergePreviewTab.tsx b/packages/twenty-front/src/modules/object-record/record-merge/components/MergePreviewTab.tsx index 635ff43e7a..728b8c1b4c 100644 --- a/packages/twenty-front/src/modules/object-record/record-merge/components/MergePreviewTab.tsx +++ b/packages/twenty-front/src/modules/object-record/record-merge/components/MergePreviewTab.tsx @@ -27,7 +27,7 @@ export const MergePreviewTab = ({ return ( ` + flex: 1; + overflow-y: auto; + background: ${({ theme }) => theme.background.primary}; + padding-bottom: ${({ theme, isInRightDrawer }) => + isInRightDrawer ? theme.spacing(16) : 0}; +`; + +export const PageLayoutDispatcher = ({ + targetRecordIdentifier, + isInRightDrawer = false, +}: { + targetRecordIdentifier: TargetRecordIdentifier; + isInRightDrawer?: boolean; +}) => { + const recordDeletedAt = useRecoilValue( + recordStoreFamilySelector({ + recordId: targetRecordIdentifier.id, + fieldName: 'deletedAt', + }), + ); + + if ( + targetRecordIdentifier.targetObjectNameSingular === + CoreObjectNameSingular.Dashboard + ) { + return ( + <> + + + + + {recordDeletedAt && ( + + + + )} + + + + + + + + + {isInRightDrawer && ( + , + , + ]} + /> + )} + + + ); + } + + return ( + <> + + + + + + + ); +}; 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 04819d4421..6087b0734a 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 @@ -20,15 +20,12 @@ const StyledShowPageBannerContainer = styled.div` type RecordShowContainerProps = { objectNameSingular: string; objectRecordId: string; - loading: boolean; isInRightDrawer?: boolean; - isNewRightDrawerItemLoading?: boolean; }; export const RecordShowContainer = ({ objectNameSingular, objectRecordId, - loading, isInRightDrawer = false, }: RecordShowContainerProps) => { const { objectMetadataItem } = useObjectMetadataItem({ @@ -47,7 +44,6 @@ export const RecordShowContainer = ({ ); const { layout, tabs } = useRecordShowContainerTabs( - loading, objectNameSingular as CoreObjectNameSingular, isInRightDrawer, objectMetadataItem, @@ -70,12 +66,12 @@ export const RecordShowContainer = ({ diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts index 9848032b88..f0e4303734 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts +++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts @@ -38,7 +38,6 @@ const OBJECT_SPECIFIC_LAYOUTS: Partial< }; export const useRecordShowContainerTabs = ( - loading: boolean, targetObjectNameSingular: CoreObjectNameSingular, isInRightDrawer: boolean, objectMetadataItem: ObjectMetadataItem, @@ -101,7 +100,7 @@ export const useRecordShowContainerTabs = ( title, Icon, cards, - hide: loading || shouldHide, + hide: shouldHide, }; }) // When isInRightDrawer === true, we merge first and second tab into first tab diff --git a/packages/twenty-front/src/modules/object-record/record-show/utils/__tests__/evaluateTabVisibility.test.ts b/packages/twenty-front/src/modules/object-record/record-show/utils/__tests__/evaluateTabVisibility.test.ts new file mode 100644 index 0000000000..0558e1ca21 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-show/utils/__tests__/evaluateTabVisibility.test.ts @@ -0,0 +1,271 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { + type TabVisibilityContext, + evaluateTabVisibility, +} from '@/object-record/record-show/utils/evaluateTabVisibility'; +import { type ObjectPermissions } from 'twenty-shared/types'; +import { FeatureFlagKey, FieldMetadataType } from '~/generated/graphql'; + +const makeObjectMetadataItem = ( + overrides: Partial = {}, +): ObjectMetadataItem => { + return { + id: '1', + nameSingular: CoreObjectNameSingular.Company, + namePlural: 'companies', + labelSingular: 'Company', + labelPlural: 'Companies', + isActive: true, + isCustom: false, + isLabelSyncedWithName: false, + isRemote: false, + isSearchable: true, + isSystem: false, + isUIReadOnly: false, + createdAt: '', + updatedAt: '', + description: '', + fields: [], + readableFields: [], + updatableFields: [], + labelIdentifierFieldMetadataId: '', + indexMetadatas: [], + icon: '', + shortcut: '', + standardOverrides: undefined, + imageIdentifierFieldMetadataId: undefined, + duplicateCriteria: undefined, + ...overrides, + }; +}; + +const makeObjectPermissions = ( + overrides: Partial = {}, +): ObjectPermissions => { + return { + canReadObjectRecords: true, + canUpdateObjectRecords: true, + canSoftDeleteObjectRecords: true, + canDestroyObjectRecords: true, + restrictedFields: {}, + ...overrides, + }; +}; + +describe('evaluateTabVisibility', () => { + const baseContext: TabVisibilityContext = { + isMobile: false, + isInRightDrawer: false, + currentWorkspace: { + featureFlags: [{ key: FeatureFlagKey.IS_AI_ENABLED, value: true }], + }, + objectMetadataItems: [makeObjectMetadataItem()], + objectPermissionsByObjectMetadataId: { + '1': { ...makeObjectPermissions(), objectMetadataId: '1' }, + }, + targetObjectMetadataItem: makeObjectMetadataItem(), + }; + + const baseConfig = { + ifMobile: false, + ifDesktop: false, + ifInRightDrawer: false, + ifFeaturesDisabled: [], + ifRequiredObjectsInactive: [], + ifRelationsMissing: [], + }; + + it('returns true if ifMobile and isMobile', () => { + expect( + evaluateTabVisibility( + { ...baseConfig, ifMobile: true }, + { ...baseContext, isMobile: true }, + ), + ).toBe(true); + }); + + it('returns true if ifDesktop and not isMobile', () => { + expect( + evaluateTabVisibility( + { ...baseConfig, ifDesktop: true }, + { ...baseContext, isMobile: false }, + ), + ).toBe(true); + }); + + it('returns true if ifInRightDrawer and isInRightDrawer', () => { + expect( + evaluateTabVisibility( + { ...baseConfig, ifInRightDrawer: true }, + { ...baseContext, isInRightDrawer: true }, + ), + ).toBe(true); + }); + + it('returns true if feature flag is disabled', () => { + expect( + evaluateTabVisibility( + { + ...baseConfig, + ifFeaturesDisabled: [FeatureFlagKey.IS_CALENDAR_VIEW_ENABLED], + }, + baseContext, + ), + ).toBe(true); + }); + + it('returns false if feature flag is enabled', () => { + const ctx = { + ...baseContext, + currentWorkspace: { + featureFlags: [ + { key: FeatureFlagKey.IS_AI_ENABLED, value: true }, + { key: FeatureFlagKey.IS_CALENDAR_VIEW_ENABLED, value: true }, + ], + }, + }; + expect( + evaluateTabVisibility( + { + ...baseConfig, + ifFeaturesDisabled: [FeatureFlagKey.IS_CALENDAR_VIEW_ENABLED], + }, + ctx, + ), + ).toBe(false); + }); + + it('returns true if required object is inactive', () => { + const ctx = { + ...baseContext, + objectMetadataItems: [ + ...baseContext.objectMetadataItems, + makeObjectMetadataItem({ + id: '2', + nameSingular: CoreObjectNameSingular.Person, + isActive: false, + }), + ], + }; + expect( + evaluateTabVisibility( + { + ...baseConfig, + ifRequiredObjectsInactive: [CoreObjectNameSingular.Person], + }, + ctx, + ), + ).toBe(true); + }); + + it('returns true if relation is missing', () => { + const ctx = { + ...baseContext, + targetObjectMetadataItem: makeObjectMetadataItem({ fields: [] }), + }; + expect( + evaluateTabVisibility( + { ...baseConfig, ifRelationsMissing: ['contact'] }, + ctx, + ), + ).toBe(true); + }); + + it('returns true if no read permission', () => { + const ctx = { + ...baseContext, + objectMetadataItems: [ + ...baseContext.objectMetadataItems, + makeObjectMetadataItem({ + id: '2', + nameSingular: CoreObjectNameSingular.Person, + }), + ], + objectPermissionsByObjectMetadataId: { + ...baseContext.objectPermissionsByObjectMetadataId, + '2': { + ...makeObjectPermissions({ canReadObjectRecords: false }), + objectMetadataId: '2', + }, + }, + }; + expect( + evaluateTabVisibility( + { + ...baseConfig, + ifNoReadPermission: true, + ifNoReadPermissionObject: CoreObjectNameSingular.Person, + }, + ctx, + ), + ).toBe(true); + }); + + it('returns false if no hide conditions are met', () => { + expect(evaluateTabVisibility(baseConfig, baseContext)).toBe(false); + }); + + it('returns false if required object is active', () => { + expect( + evaluateTabVisibility( + { + ...baseConfig, + ifRequiredObjectsInactive: [CoreObjectNameSingular.Company], + }, + baseContext, + ), + ).toBe(false); + }); + + it('returns false if relation exists and is active', () => { + const ctx = { + ...baseContext, + targetObjectMetadataItem: makeObjectMetadataItem({ + fields: [ + { + name: 'contact', + type: FieldMetadataType.RELATION, + isActive: true, + } as any, + ], + }), + }; + expect( + evaluateTabVisibility( + { ...baseConfig, ifRelationsMissing: ['contact'] }, + ctx, + ), + ).toBe(false); + }); + + it('returns false if read permission exists', () => { + const ctx = { + ...baseContext, + objectMetadataItems: [ + ...baseContext.objectMetadataItems, + makeObjectMetadataItem({ + id: '2', + nameSingular: CoreObjectNameSingular.Person, + }), + ], + objectPermissionsByObjectMetadataId: { + ...baseContext.objectPermissionsByObjectMetadataId, + '2': { + ...makeObjectPermissions({ canReadObjectRecords: true }), + objectMetadataId: '2', + }, + }, + }; + expect( + evaluateTabVisibility( + { + ...baseConfig, + ifNoReadPermission: true, + ifNoReadPermissionObject: CoreObjectNameSingular.Person, + }, + ctx, + ), + ).toBe(false); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/record-show/utils/getCardComponent.tsx b/packages/twenty-front/src/modules/object-record/record-show/utils/getCardComponent.tsx index ef629da36d..c47ded4f59 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/utils/getCardComponent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/utils/getCardComponent.tsx @@ -4,7 +4,6 @@ import { FilesCard } from '@/activities/files/components/FilesCard'; import { NotesCard } from '@/activities/notes/components/NotesCard'; import { TasksCard } from '@/activities/tasks/components/TasksCard'; import { TimelineCard } from '@/activities/timeline-activities/components/TimelineCard'; -import { DashboardCard } from '@/dashboards/components/DashboardCard'; import { FieldsCard } from '@/object-record/record-show/components/FieldsCard'; import { type CardConfiguration, @@ -26,9 +25,9 @@ const CardRenderer = ({ Component: React.ComponentType<{ configuration?: T }> | React.ComponentType; configuration?: T; }) => { - const { targetRecord } = useLayoutRenderingContext(); + const { targetRecordIdentifier } = useLayoutRenderingContext(); - if (!targetRecord) { + if (!targetRecordIdentifier) { return null; } @@ -87,7 +86,8 @@ export const getCardComponent = ( return ; case CardType.DashboardCard: - return ; + throw new Error('Dashboard are handled separately currently'); + default: assertUnreachable(cardType); } diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx index ed56896b04..5b95121192 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRenderer.tsx @@ -22,7 +22,7 @@ export const PageLayoutRenderer = ({ return ( ; + targetRecordIdentifier: TargetRecordIdentifier | undefined; layoutType: PageLayoutType; diff --git a/packages/twenty-front/src/modules/ui/layout/contexts/TargetRecordIdentifier.ts b/packages/twenty-front/src/modules/ui/layout/contexts/TargetRecordIdentifier.ts new file mode 100644 index 0000000000..1f0a84e260 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/contexts/TargetRecordIdentifier.ts @@ -0,0 +1,6 @@ +import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; + +export type TargetRecordIdentifier = Pick< + ActivityTargetableObject, + 'id' | 'targetObjectNameSingular' +>; diff --git a/packages/twenty-front/src/modules/ui/layout/contexts/useTargetRecord.ts b/packages/twenty-front/src/modules/ui/layout/contexts/useTargetRecord.ts index 7c6c8ace34..eaa9477b93 100644 --- a/packages/twenty-front/src/modules/ui/layout/contexts/useTargetRecord.ts +++ b/packages/twenty-front/src/modules/ui/layout/contexts/useTargetRecord.ts @@ -1,13 +1,13 @@ import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; export const useTargetRecord = () => { - const { targetRecord } = useLayoutRenderingContext(); + const { targetRecordIdentifier } = useLayoutRenderingContext(); - if (!targetRecord) { + if (!targetRecordIdentifier) { throw new Error( - 'useTargetRecord must be used within a record page context (targetRecord is required)', + 'useTargetRecord must be used within a record page context (targetRecordIdentifier is required)', ); } - return targetRecord; + return targetRecordIdentifier; }; diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx index d5b913f533..1f52b5e3ca 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx @@ -1,28 +1,27 @@ import { RecordShowRightDrawerActionMenu } from '@/action-menu/components/RecordShowRightDrawerActionMenu'; import { RecordShowRightDrawerOpenRecordButton } from '@/action-menu/components/RecordShowRightDrawerOpenRecordButton'; -import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext'; import { FieldsCard } from '@/object-record/record-show/components/FieldsCard'; import { SummaryCard } from '@/object-record/record-show/components/SummaryCard'; import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout'; import { getCardComponent } from '@/object-record/record-show/utils/getCardComponent'; -import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext'; import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter'; import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer'; import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId'; import { TabList } from '@/ui/layout/tab-list/components/TabList'; -import { PageLayoutType } from '~/generated/graphql'; +import { type TargetRecordIdentifier } from '@/ui/layout/contexts/TargetRecordIdentifier'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext'; import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { css } from '@emotion/react'; import styled from '@emotion/styled'; import React from 'react'; -const StyledShowPageRightContainer = styled.div<{ isMobile: boolean }>` +const StyledShowPageRightContainer = styled.div` display: flex; flex-direction: column; height: 100%; @@ -35,11 +34,11 @@ const StyledShowPageRightContainer = styled.div<{ isMobile: boolean }>` const StyledTabListContainer = styled.div<{ shouldDisplay: boolean }>` ${({ shouldDisplay }) => !shouldDisplay && - ` - visibility: hidden; - height: 0; - overflow: hidden; - `} + css` + height: 0; + overflow: hidden; + visibility: hidden; + `} `; const StyledTabList = styled(TabList)` @@ -57,10 +56,7 @@ const StyledContentContainer = styled.div<{ isInRightDrawer: boolean }>` type ShowPageSubContainerProps = { layout?: RecordLayout; tabs: SingleTabProps[]; - targetableObject: Pick< - ActivityTargetableObject, - 'targetObjectNameSingular' | 'id' - >; + targetRecordIdentifier: TargetRecordIdentifier; isInRightDrawer?: boolean; loading: boolean; }; @@ -68,7 +64,7 @@ type ShowPageSubContainerProps = { export const ShowPageSubContainer = ({ tabs, layout, - targetableObject, + targetRecordIdentifier, loading, isInRightDrawer = false, }: ShowPageSubContainerProps) => { @@ -78,7 +74,7 @@ export const ShowPageSubContainer = ({ const tabListComponentId = getShowPageTabListComponentId({ pageId: commandMenuPageComponentInstance?.instanceId, - targetObjectId: targetableObject.id, + targetObjectId: targetRecordIdentifier.id, }); const activeTabId = useRecoilComponentValue( activeTabIdComponentState, @@ -89,8 +85,8 @@ export const ShowPageSubContainer = ({ const summaryCard = ( ); @@ -118,52 +114,43 @@ export const ShowPageSubContainer = ({ layout && !layout.hideSummaryAndFields && !isMobile && !isInRightDrawer; return ( - - - {displaySummaryAndFields && ( - - {summaryCard} - {fieldsCard} - + {displaySummaryAndFields && ( + + {summaryCard} + {fieldsCard} + + )} + + 1}> + + + {(isMobile || isInRightDrawer) && summaryCard} + + {renderActiveTabContent()} + + {isInRightDrawer && ( + , + , + ]} + /> )} - - 1}> - - - {(isMobile || isInRightDrawer) && summaryCard} - - {renderActiveTabContent()} - - {isInRightDrawer && ( - , - , - ]} - /> - )} - - - + + ); }; diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx index 44f98df29a..bcd1894297 100644 --- a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx @@ -6,15 +6,14 @@ import { TimelineActivityContext } from '@/activities/timeline-activities/contex import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; -import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer'; -import { RecordShowEffect } from '@/object-record/record-show/components/RecordShowEffect'; +import { PageLayoutDispatcher } from '@/object-record/record-show/components/PageLayoutDispatcher'; +import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage'; import { computeRecordShowComponentInstanceId } from '@/object-record/record-show/utils/computeRecordShowComponentInstanceId'; import { PageHeaderToggleCommandMenuButton } from '@/ui/layout/page-header/components/PageHeaderToggleCommandMenuButton'; import { PageBody } from '@/ui/layout/page/components/PageBody'; import { PageContainer } from '@/ui/layout/page/components/PageContainer'; import { RecordShowPageHeader } from '~/pages/object-record/RecordShowPageHeader'; import { RecordShowPageTitle } from '~/pages/object-record/RecordShowPageTitle'; -import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage'; export const RecordShowPage = () => { const parameters = useParams<{ @@ -58,14 +57,11 @@ export const RecordShowPage = () => { recordId: objectRecordId, }} > - -