Extract DashboardCard from ShowPageSubContainer (#15073)
This PR determines the minimal setup required to render dashboards without errors while extracting them from the record show page. The ultimate goal is to create a `PageLayoutRenderer` component that takes any page layout configuration and correctly renders dashboards or record pages. Currently, the `DashboardCard` component renders itself the `PageLayoutRenderer` component. The next step is to reverse the flow of control and make `PageLayoutRenderer` take a configuration and decide whether it should render a dashboard or something else. This PR takes into consideration two comments left by Charles on [the first PR scaffolding the refactor](https://github.com/twentyhq/twenty/pull/15021): renaming `targetRecord` to `targetRecordIdentifier` and adding tests to `evaluateTabVisibility()`. ## Demo to assert this PR doesn't break everything https://github.com/user-attachments/assets/b5b99cf3-08fa-43d3-8da1-79018bc63641
This commit is contained in:
committed by
GitHub
parent
7d747c9876
commit
23de047787
+1
-1
@@ -25,7 +25,7 @@ const meta: Meta<typeof CalendarEventsCard> = {
|
||||
(Story) => (
|
||||
<LayoutRenderingProvider
|
||||
value={{
|
||||
targetRecord: {
|
||||
targetRecordIdentifier: {
|
||||
id: '1',
|
||||
targetObjectNameSingular: CoreObjectNameSingular.Company,
|
||||
},
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ const meta: Meta<typeof TimelineCard> = {
|
||||
return (
|
||||
<LayoutRenderingProvider
|
||||
value={{
|
||||
targetRecord: {
|
||||
targetRecordIdentifier: {
|
||||
id: '1',
|
||||
targetObjectNameSingular: CoreObjectNameSingular.Company,
|
||||
},
|
||||
|
||||
+7
-11
@@ -6,8 +6,7 @@ import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/c
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { INFORMATION_BANNER_HEIGHT } from '@/information-banner/constants/InformationBannerHeight';
|
||||
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 { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
@@ -89,15 +88,12 @@ export const CommandMenuRecordPage = () => {
|
||||
recordId: objectRecordId,
|
||||
}}
|
||||
>
|
||||
<RecordShowEffect
|
||||
objectNameSingular={objectNameSingular}
|
||||
recordId={objectRecordId}
|
||||
/>
|
||||
<RecordShowContainer
|
||||
objectNameSingular={objectNameSingular}
|
||||
objectRecordId={objectRecordId}
|
||||
loading={false}
|
||||
isInRightDrawer={true}
|
||||
<PageLayoutDispatcher
|
||||
targetRecordIdentifier={{
|
||||
id: objectRecordId,
|
||||
targetObjectNameSingular: objectNameSingular,
|
||||
}}
|
||||
isInRightDrawer
|
||||
/>
|
||||
</TimelineActivityContext.Provider>
|
||||
</StyledRightDrawerRecord>
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ export const MergePreviewTab = ({
|
||||
return (
|
||||
<LayoutRenderingProvider
|
||||
value={{
|
||||
targetRecord: {
|
||||
targetRecordIdentifier: {
|
||||
id: recordId,
|
||||
targetObjectNameSingular: objectNameSingular,
|
||||
},
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ export const MergeRecordTab = ({
|
||||
return (
|
||||
<LayoutRenderingProvider
|
||||
value={{
|
||||
targetRecord: {
|
||||
targetRecordIdentifier: {
|
||||
id: recordId,
|
||||
targetObjectNameSingular: objectNameSingular,
|
||||
},
|
||||
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import { RecordShowRightDrawerActionMenu } from '@/action-menu/components/RecordShowRightDrawerActionMenu';
|
||||
import { RecordShowRightDrawerOpenRecordButton } from '@/action-menu/components/RecordShowRightDrawerOpenRecordButton';
|
||||
import { DashboardCard } from '@/dashboards/components/DashboardCard';
|
||||
import { InformationBannerDeletedRecord } from '@/information-banner/components/deleted-record/InformationBannerDeletedRecord';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
|
||||
import { RecordShowContainerContextStoreTargetedRecordsEffect } from '@/object-record/record-show/components/RecordShowContainerContextStoreTargetedRecordsEffect';
|
||||
import { RecordShowEffect } from '@/object-record/record-show/components/RecordShowEffect';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { type TargetRecordIdentifier } from '@/ui/layout/contexts/TargetRecordIdentifier';
|
||||
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { PageLayoutType } from '~/generated/graphql';
|
||||
|
||||
const StyledShowPageBannerContainer = styled.div`
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const StyledShowPageRightContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const StyledContentContainer = styled.div<{ isInRightDrawer: boolean }>`
|
||||
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<string | null>(
|
||||
recordStoreFamilySelector({
|
||||
recordId: targetRecordIdentifier.id,
|
||||
fieldName: 'deletedAt',
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
targetRecordIdentifier.targetObjectNameSingular ===
|
||||
CoreObjectNameSingular.Dashboard
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<RecordShowEffect
|
||||
objectNameSingular={targetRecordIdentifier.targetObjectNameSingular}
|
||||
recordId={targetRecordIdentifier.id}
|
||||
/>
|
||||
|
||||
<RecordShowContainerContextStoreTargetedRecordsEffect
|
||||
recordId={targetRecordIdentifier.id}
|
||||
/>
|
||||
|
||||
{recordDeletedAt && (
|
||||
<StyledShowPageBannerContainer>
|
||||
<InformationBannerDeletedRecord
|
||||
recordId={targetRecordIdentifier.id}
|
||||
objectNameSingular={
|
||||
targetRecordIdentifier.targetObjectNameSingular
|
||||
}
|
||||
/>
|
||||
</StyledShowPageBannerContainer>
|
||||
)}
|
||||
|
||||
<StyledShowPageRightContainer>
|
||||
<LayoutRenderingProvider
|
||||
value={{
|
||||
targetRecordIdentifier: {
|
||||
id: targetRecordIdentifier.id,
|
||||
targetObjectNameSingular:
|
||||
targetRecordIdentifier.targetObjectNameSingular,
|
||||
},
|
||||
layoutType: PageLayoutType.RECORD_PAGE,
|
||||
isInRightDrawer,
|
||||
}}
|
||||
>
|
||||
<StyledContentContainer isInRightDrawer={isInRightDrawer}>
|
||||
<DashboardCard />
|
||||
</StyledContentContainer>
|
||||
</LayoutRenderingProvider>
|
||||
|
||||
{isInRightDrawer && (
|
||||
<RightDrawerFooter
|
||||
actions={[
|
||||
<RecordShowRightDrawerActionMenu />,
|
||||
<RecordShowRightDrawerOpenRecordButton
|
||||
objectNameSingular={
|
||||
targetRecordIdentifier.targetObjectNameSingular
|
||||
}
|
||||
recordId={targetRecordIdentifier.id}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</StyledShowPageRightContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<RecordShowEffect
|
||||
objectNameSingular={targetRecordIdentifier.targetObjectNameSingular}
|
||||
recordId={targetRecordIdentifier.id}
|
||||
/>
|
||||
|
||||
<LayoutRenderingProvider
|
||||
value={{
|
||||
targetRecordIdentifier: {
|
||||
id: targetRecordIdentifier.id,
|
||||
targetObjectNameSingular:
|
||||
targetRecordIdentifier.targetObjectNameSingular,
|
||||
},
|
||||
layoutType: PageLayoutType.RECORD_PAGE,
|
||||
isInRightDrawer,
|
||||
}}
|
||||
>
|
||||
<RecordShowContainer
|
||||
objectNameSingular={targetRecordIdentifier.targetObjectNameSingular}
|
||||
objectRecordId={targetRecordIdentifier.id}
|
||||
isInRightDrawer={isInRightDrawer}
|
||||
/>
|
||||
</LayoutRenderingProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+2
-6
@@ -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 = ({
|
||||
<ShowPageSubContainer
|
||||
tabs={tabs}
|
||||
layout={layout}
|
||||
targetableObject={{
|
||||
targetRecordIdentifier={{
|
||||
id: objectRecordId,
|
||||
targetObjectNameSingular: objectMetadataItem?.nameSingular,
|
||||
}}
|
||||
isInRightDrawer={isInRightDrawer}
|
||||
loading={isPrefetchLoading || loading || recordLoading}
|
||||
loading={isPrefetchLoading || recordLoading}
|
||||
/>
|
||||
</ShowPageContainer>
|
||||
</RightDrawerProvider>
|
||||
|
||||
+1
-2
@@ -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
|
||||
|
||||
+271
@@ -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> = {},
|
||||
): 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> = {},
|
||||
): 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);
|
||||
});
|
||||
});
|
||||
+4
-4
@@ -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 = <T extends CardConfiguration>({
|
||||
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 = <T extends CardType>(
|
||||
return <CardRenderer Component={WorkflowRunCard} />;
|
||||
|
||||
case CardType.DashboardCard:
|
||||
return <CardRenderer Component={DashboardCard} />;
|
||||
throw new Error('Dashboard are handled separately currently');
|
||||
|
||||
default:
|
||||
assertUnreachable(cardType);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const PageLayoutRenderer = ({
|
||||
return (
|
||||
<LayoutRenderingProvider
|
||||
value={{
|
||||
targetRecord: undefined,
|
||||
targetRecordIdentifier: undefined,
|
||||
layoutType: PageLayoutType.DASHBOARD,
|
||||
isInRightDrawer: false,
|
||||
}}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { type PageLayoutType } from '~/generated/graphql';
|
||||
import { createRequiredContext } from '~/utils/createRequiredContext';
|
||||
import { type TargetRecordIdentifier } from './TargetRecordIdentifier';
|
||||
|
||||
export type LayoutRenderingContextType = {
|
||||
// Optional target record - only present for record pages that display data about a specific record
|
||||
// Undefined for dashboards which are standalone
|
||||
// Uses ActivityTargetableObject shape for compatibility with existing components
|
||||
targetRecord?: Pick<
|
||||
ActivityTargetableObject,
|
||||
'id' | 'targetObjectNameSingular'
|
||||
>;
|
||||
targetRecordIdentifier: TargetRecordIdentifier | undefined;
|
||||
|
||||
layoutType: PageLayoutType;
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
|
||||
export type TargetRecordIdentifier = Pick<
|
||||
ActivityTargetableObject,
|
||||
'id' | 'targetObjectNameSingular'
|
||||
>;
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
+49
-62
@@ -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 = (
|
||||
<SummaryCard
|
||||
objectNameSingular={targetableObject.targetObjectNameSingular}
|
||||
objectRecordId={targetableObject.id}
|
||||
objectNameSingular={targetRecordIdentifier.targetObjectNameSingular}
|
||||
objectRecordId={targetRecordIdentifier.id}
|
||||
isInRightDrawer={isInRightDrawer}
|
||||
/>
|
||||
);
|
||||
@@ -118,52 +114,43 @@ export const ShowPageSubContainer = ({
|
||||
layout && !layout.hideSummaryAndFields && !isMobile && !isInRightDrawer;
|
||||
|
||||
return (
|
||||
<LayoutRenderingProvider
|
||||
value={{
|
||||
targetRecord: {
|
||||
id: targetableObject.id,
|
||||
targetObjectNameSingular: targetableObject.targetObjectNameSingular,
|
||||
},
|
||||
layoutType: PageLayoutType.RECORD_PAGE,
|
||||
isInRightDrawer,
|
||||
}}
|
||||
<TabListComponentInstanceContext.Provider
|
||||
value={{ instanceId: tabListComponentId }}
|
||||
>
|
||||
<TabListComponentInstanceContext.Provider
|
||||
value={{ instanceId: tabListComponentId }}
|
||||
>
|
||||
{displaySummaryAndFields && (
|
||||
<ShowPageLeftContainer forceMobile={isMobile}>
|
||||
{summaryCard}
|
||||
{fieldsCard}
|
||||
</ShowPageLeftContainer>
|
||||
{displaySummaryAndFields && (
|
||||
<ShowPageLeftContainer forceMobile={isMobile}>
|
||||
{summaryCard}
|
||||
{fieldsCard}
|
||||
</ShowPageLeftContainer>
|
||||
)}
|
||||
<StyledShowPageRightContainer>
|
||||
<StyledTabListContainer shouldDisplay={visibleTabs.length > 1}>
|
||||
<StyledTabList
|
||||
behaveAsLinks={!isInRightDrawer}
|
||||
loading={loading}
|
||||
tabs={tabs}
|
||||
isInRightDrawer={isInRightDrawer}
|
||||
componentInstanceId={tabListComponentId}
|
||||
/>
|
||||
</StyledTabListContainer>
|
||||
{(isMobile || isInRightDrawer) && summaryCard}
|
||||
<StyledContentContainer isInRightDrawer={isInRightDrawer}>
|
||||
{renderActiveTabContent()}
|
||||
</StyledContentContainer>
|
||||
{isInRightDrawer && (
|
||||
<RightDrawerFooter
|
||||
actions={[
|
||||
<RecordShowRightDrawerActionMenu />,
|
||||
<RecordShowRightDrawerOpenRecordButton
|
||||
objectNameSingular={
|
||||
targetRecordIdentifier.targetObjectNameSingular
|
||||
}
|
||||
recordId={targetRecordIdentifier.id}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<StyledShowPageRightContainer isMobile={isMobile}>
|
||||
<StyledTabListContainer shouldDisplay={visibleTabs.length > 1}>
|
||||
<StyledTabList
|
||||
behaveAsLinks={!isInRightDrawer}
|
||||
loading={loading}
|
||||
tabs={tabs}
|
||||
isInRightDrawer={isInRightDrawer}
|
||||
componentInstanceId={tabListComponentId}
|
||||
/>
|
||||
</StyledTabListContainer>
|
||||
{(isMobile || isInRightDrawer) && summaryCard}
|
||||
<StyledContentContainer isInRightDrawer={isInRightDrawer}>
|
||||
{renderActiveTabContent()}
|
||||
</StyledContentContainer>
|
||||
{isInRightDrawer && (
|
||||
<RightDrawerFooter
|
||||
actions={[
|
||||
<RecordShowRightDrawerActionMenu />,
|
||||
<RecordShowRightDrawerOpenRecordButton
|
||||
objectNameSingular={targetableObject.targetObjectNameSingular}
|
||||
recordId={targetableObject.id}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</StyledShowPageRightContainer>
|
||||
</TabListComponentInstanceContext.Provider>
|
||||
</LayoutRenderingProvider>
|
||||
</StyledShowPageRightContainer>
|
||||
</TabListComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
>
|
||||
<RecordShowEffect
|
||||
objectNameSingular={objectNameSingular}
|
||||
recordId={objectRecordId}
|
||||
/>
|
||||
<RecordShowContainer
|
||||
objectNameSingular={objectNameSingular}
|
||||
objectRecordId={objectRecordId}
|
||||
loading={false}
|
||||
<PageLayoutDispatcher
|
||||
targetRecordIdentifier={{
|
||||
id: objectRecordId,
|
||||
targetObjectNameSingular: objectNameSingular,
|
||||
}}
|
||||
/>
|
||||
</TimelineActivityContext.Provider>
|
||||
</PageBody>
|
||||
|
||||
Reference in New Issue
Block a user