Remove the code of old show pages (#17811)

Closes https://github.com/twentyhq/core-team-issues/issues/2213
This commit is contained in:
Baptiste Devessier
2026-02-10 10:10:00 +01:00
committed by GitHub
parent 2d42b0a726
commit b250de216e
36 changed files with 10 additions and 1265 deletions
@@ -23,9 +23,6 @@ type RecordDetailRecordsListItemContainerProps = {
className?: string;
};
/**
* TODO: Remove noHorizontalPadding logic once the traditional record show page is removed.
*/
export const RecordDetailRecordsListItemContainer = ({
children,
className,
@@ -33,8 +30,7 @@ export const RecordDetailRecordsListItemContainer = ({
const layoutRenderingContext = useLayoutRenderingContext();
const isInRecordPageLayout =
layoutRenderingContext?.layoutType === PageLayoutType.RECORD_PAGE &&
!layoutRenderingContext?.isLegacyRecordShowPage;
layoutRenderingContext?.layoutType === PageLayoutType.RECORD_PAGE;
return (
<StyledListItem
@@ -28,8 +28,6 @@ const meta: Meta<typeof RecordDetailDuplicatesSection> = {
},
layoutType: PageLayoutType.RECORD_PAGE,
isInRightDrawer: false,
// TODO: Remove once the traditional record show page is removed.
isLegacyRecordShowPage: true,
}}
>
<Story />
@@ -43,8 +43,6 @@ const meta: Meta<typeof RecordDetailRelationSection> = {
},
layoutType: PageLayoutType.RECORD_PAGE,
isInRightDrawer: false,
// TODO: Remove once the traditional record show page is removed.
isLegacyRecordShowPage: true,
}}
>
<ContextStoreComponentInstanceContext.Provider
@@ -26,9 +26,6 @@ const StyledPropertyBoxContainer = styled('div', {
noHorizontalPadding ? 0 : theme.spacing(2)};
`;
/**
* TODO: Remove noHorizontalPadding logic once the traditional record show page is removed.
*/
export const PropertyBox = ({
children,
className,
@@ -37,8 +34,7 @@ export const PropertyBox = ({
const layoutRenderingContext = useLayoutRenderingContext();
const isInRecordPageLayout =
layoutRenderingContext.layoutType === PageLayoutType.RECORD_PAGE &&
!layoutRenderingContext.isLegacyRecordShowPage;
layoutRenderingContext.layoutType === PageLayoutType.RECORD_PAGE;
return (
<StyledPropertyBoxContainer
@@ -1,62 +0,0 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { PageLayoutRecordPageRenderer } from '@/object-record/record-show/components/PageLayoutRecordPageRenderer';
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
import { RecordShowEffect } from '@/object-record/record-show/components/RecordShowEffect';
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { type TargetRecordIdentifier } from '@/ui/layout/contexts/TargetRecordIdentifier';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey, PageLayoutType } from '~/generated/graphql';
export const PageLayoutDispatcher = ({
targetRecordIdentifier,
isInRightDrawer = false,
}: {
targetRecordIdentifier: TargetRecordIdentifier;
isInRightDrawer?: boolean;
}) => {
const isRecordPageEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_ENABLED,
);
if (
targetRecordIdentifier.targetObjectNameSingular ===
CoreObjectNameSingular.Dashboard ||
isRecordPageEnabled
) {
return (
<PageLayoutRecordPageRenderer
targetRecordIdentifier={targetRecordIdentifier}
isInRightDrawer={isInRightDrawer}
/>
);
}
return (
<>
<RecordShowEffect
objectNameSingular={targetRecordIdentifier.targetObjectNameSingular}
recordId={targetRecordIdentifier.id}
/>
<LayoutRenderingProvider
value={{
targetRecordIdentifier: {
id: targetRecordIdentifier.id,
targetObjectNameSingular:
targetRecordIdentifier.targetObjectNameSingular,
},
layoutType: PageLayoutType.RECORD_PAGE,
isInRightDrawer,
// TODO: Remove once the traditional record show page is removed.
isLegacyRecordShowPage: true,
}}
>
<RecordShowContainer
objectNameSingular={targetRecordIdentifier.targetObjectNameSingular}
objectRecordId={targetRecordIdentifier.id}
isInRightDrawer={isInRightDrawer}
/>
</LayoutRenderingProvider>
</>
);
};
@@ -1,79 +0,0 @@
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ShowPageContainer } from '@/ui/layout/page/components/ShowPageContainer';
import { RightDrawerProvider } from '@/ui/layout/right-drawer/contexts/RightDrawerContext';
import { InformationBannerDeletedRecord } from '@/information-banner/components/deleted-record/InformationBannerDeletedRecord';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { RecordShowContainerContextStoreTargetedRecordsEffect } from '@/object-record/record-show/components/RecordShowContainerContextStoreTargetedRecordsEffect';
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { useRecordShowContainerTabs } from '@/object-record/record-show/hooks/useRecordShowContainerTabs';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { ShowPageSubContainer } from '@/ui/layout/show-page/components/ShowPageSubContainer';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
const StyledShowPageBannerContainer = styled.div`
z-index: 1;
`;
type RecordShowContainerProps = {
objectNameSingular: string;
objectRecordId: string;
isInRightDrawer?: boolean;
};
export const RecordShowContainer = ({
objectNameSingular,
objectRecordId,
isInRightDrawer = false,
}: RecordShowContainerProps) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { isPrefetchLoading, recordLoading } = useRecordShowContainerData({
objectRecordId,
});
const recordDeletedAt = useRecoilValue<string | null>(
recordStoreFamilySelector({
recordId: objectRecordId,
fieldName: 'deletedAt',
}),
);
const { layout, tabs } = useRecordShowContainerTabs(
objectNameSingular as CoreObjectNameSingular,
isInRightDrawer,
objectMetadataItem,
);
return (
<RightDrawerProvider value={{ isInRightDrawer }}>
<RecordShowContainerContextStoreTargetedRecordsEffect
recordId={objectRecordId}
/>
{recordDeletedAt && (
<StyledShowPageBannerContainer>
<InformationBannerDeletedRecord
recordId={objectRecordId}
objectNameSingular={objectNameSingular}
/>
</StyledShowPageBannerContainer>
)}
<ShowPageContainer>
<ShowPageSubContainer
tabs={tabs}
layout={layout}
targetRecordIdentifier={{
id: objectRecordId,
targetObjectNameSingular: objectMetadataItem?.nameSingular,
}}
isInRightDrawer={isInRightDrawer}
loading={isPrefetchLoading || recordLoading}
/>
</ShowPageContainer>
</RightDrawerProvider>
);
};
@@ -1,131 +0,0 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { BASE_RECORD_LAYOUT } from '@/object-record/record-show/layouts/base-record-layout';
import { COMPANY_RECORD_LAYOUT } from '@/object-record/record-show/layouts/company-record-layout';
import { NOTE_RECORD_LAYOUT } from '@/object-record/record-show/layouts/note-record-layout';
import { OPPORTUNITY_RECORD_LAYOUT } from '@/object-record/record-show/layouts/opportunity-record-layout';
import { PERSON_RECORD_LAYOUT } from '@/object-record/record-show/layouts/person-record-layout';
import { TASK_RECORD_LAYOUT } from '@/object-record/record-show/layouts/task-record-layout';
import { WORKFLOW_RECORD_LAYOUT } from '@/object-record/record-show/layouts/workflow-record-layout';
import { WORKFLOW_RUN_RECORD_LAYOUT } from '@/object-record/record-show/layouts/workflow-run-record-layout';
import { WORKFLOW_VERSION_RECORD_LAYOUT } from '@/object-record/record-show/layouts/workflow-version-record-layout';
import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
import { evaluateTabVisibility } from '@/object-record/record-show/utils/evaluateTabVisibility';
import { type RecordLayoutTab } from '@/ui/layout/tab-list/types/RecordLayoutTab';
import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useLingui } from '@lingui/react/macro';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { IconHome, useIcons } from 'twenty-ui/display';
// Object-specific layouts that override or extend the base layout
const OBJECT_SPECIFIC_LAYOUTS: Partial<
Record<CoreObjectNameSingular, RecordLayout>
> = {
[CoreObjectNameSingular.Note]: NOTE_RECORD_LAYOUT,
[CoreObjectNameSingular.Task]: TASK_RECORD_LAYOUT,
[CoreObjectNameSingular.Company]: COMPANY_RECORD_LAYOUT,
[CoreObjectNameSingular.Person]: PERSON_RECORD_LAYOUT,
[CoreObjectNameSingular.Opportunity]: OPPORTUNITY_RECORD_LAYOUT,
[CoreObjectNameSingular.Workflow]: WORKFLOW_RECORD_LAYOUT,
[CoreObjectNameSingular.WorkflowVersion]: WORKFLOW_VERSION_RECORD_LAYOUT,
[CoreObjectNameSingular.WorkflowRun]: WORKFLOW_RUN_RECORD_LAYOUT,
};
export const useRecordShowContainerTabs = (
targetObjectNameSingular: CoreObjectNameSingular,
isInRightDrawer: boolean,
objectMetadataItem: ObjectMetadataItem,
): { layout: RecordLayout; tabs: SingleTabProps[] } => {
const { t } = useLingui();
const isMobile = useIsMobile();
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { getIcon } = useIcons();
// Merge base layout with object-specific layout
const recordLayout: RecordLayout = useMemo(() => {
return {
...BASE_RECORD_LAYOUT,
...(OBJECT_SPECIFIC_LAYOUTS[targetObjectNameSingular] || {}),
tabs: {
...BASE_RECORD_LAYOUT.tabs,
...(OBJECT_SPECIFIC_LAYOUTS[targetObjectNameSingular]?.tabs || {}),
},
};
}, [targetObjectNameSingular]);
return {
layout: recordLayout,
tabs: Object.entries(recordLayout.tabs)
.filter(
(entry): entry is [string, NonNullable<RecordLayoutTab>] =>
entry[1] !== null && entry[1] !== undefined,
)
.sort(([, a], [, b]) => a.position - b.position)
.map(([key, { title, icon, hide, cards }]) => {
const Icon = getIcon(icon);
// Special handling for fields tab
if (key === 'fields') {
return {
id: key,
title,
Icon,
cards,
hide:
!(isMobile || isInRightDrawer) ||
recordLayout.hideFieldsInSidePanel,
};
}
// Use extracted visibility evaluation logic
const shouldHide = evaluateTabVisibility(hide, {
isMobile,
isInRightDrawer,
currentWorkspace,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
targetObjectMetadataItem: objectMetadataItem,
});
return {
id: key,
title,
Icon,
cards,
hide: shouldHide,
};
})
// When isInRightDrawer === true, we merge first and second tab into first tab
.reduce<SingleTabProps[]>((acc, tab, index, array) => {
if (isInRightDrawer && array.length > 1) {
if (index === 0) {
return [
...acc,
{
id: 'home',
title: t`Home`,
Icon: IconHome,
cards: [
...(tab.hide ? [] : tab.cards),
...(array[1].hide ? [] : array[1].cards),
],
hide: false,
},
];
}
if (index === 1) {
return acc;
}
}
return [...acc, tab];
}, []),
};
};
@@ -1,82 +0,0 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { CardType } from '@/object-record/record-show/types/CardType';
import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
export const BASE_RECORD_LAYOUT: RecordLayout = {
tabs: {
fields: {
title: 'Fields',
icon: 'IconList',
position: 100,
cards: [{ type: CardType.FieldCard }],
hide: {
ifMobile: false,
ifDesktop: true,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
timeline: {
title: 'Timeline',
icon: 'IconTimelineEvent',
position: 200,
cards: [{ type: CardType.TimelineCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: true,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
tasks: {
title: 'Tasks',
icon: 'IconCheckbox',
position: 300,
cards: [{ type: CardType.TaskCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [CoreObjectNameSingular.Task],
ifRelationsMissing: ['taskTargets'],
ifNoReadPermission: true,
ifNoReadPermissionObject: CoreObjectNameSingular.Task,
},
},
notes: {
title: 'Notes',
icon: 'IconNotes',
position: 400,
cards: [{ type: CardType.NoteCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [CoreObjectNameSingular.Note],
ifRelationsMissing: ['noteTargets'],
ifNoReadPermission: true,
ifNoReadPermissionObject: CoreObjectNameSingular.Note,
},
},
files: {
title: 'Files',
icon: 'IconPaperclip',
position: 500,
cards: [{ type: CardType.FileCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [CoreObjectNameSingular.Attachment],
ifRelationsMissing: ['attachments'],
},
},
},
};
@@ -1,35 +0,0 @@
import { CardType } from '@/object-record/record-show/types/CardType';
import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
export const COMPANY_RECORD_LAYOUT: RecordLayout = {
tabs: {
emails: {
title: 'Emails',
position: 600,
icon: 'IconMail',
cards: [{ type: CardType.EmailCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
calendar: {
title: 'Calendar',
position: 700,
icon: 'IconCalendarEvent',
cards: [{ type: CardType.CalendarCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
},
};
@@ -1,23 +0,0 @@
import { CardType } from '@/object-record/record-show/types/CardType';
import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
export const NOTE_RECORD_LAYOUT: RecordLayout = {
tabs: {
richText: {
title: 'Note',
position: 101,
icon: 'IconNotes',
cards: [{ type: CardType.FieldRichTextCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
tasks: null,
notes: null,
},
};
@@ -1,35 +0,0 @@
import { CardType } from '@/object-record/record-show/types/CardType';
import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
export const OPPORTUNITY_RECORD_LAYOUT: RecordLayout = {
tabs: {
emails: {
title: 'Emails',
position: 600,
icon: 'IconMail',
cards: [{ type: CardType.EmailCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
calendar: {
title: 'Calendar',
position: 700,
icon: 'IconCalendarEvent',
cards: [{ type: CardType.CalendarCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
},
};
@@ -1,35 +0,0 @@
import { CardType } from '@/object-record/record-show/types/CardType';
import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
export const PERSON_RECORD_LAYOUT: RecordLayout = {
tabs: {
emails: {
title: 'Emails',
position: 600,
icon: 'IconMail',
cards: [{ type: CardType.EmailCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
calendar: {
title: 'Calendar',
position: 700,
icon: 'IconCalendarEvent',
cards: [{ type: CardType.CalendarCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
},
};
@@ -1,23 +0,0 @@
import { CardType } from '@/object-record/record-show/types/CardType';
import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
export const TASK_RECORD_LAYOUT: RecordLayout = {
tabs: {
richText: {
title: 'Note',
position: 101,
icon: 'IconNotes',
cards: [{ type: CardType.FieldRichTextCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
tasks: null,
notes: null,
},
};
@@ -1,27 +0,0 @@
import { CardType } from '@/object-record/record-show/types/CardType';
import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
export const WORKFLOW_RECORD_LAYOUT: RecordLayout = {
hideSummaryAndFields: true,
tabs: {
workflow: {
title: 'Flow',
position: 101,
icon: 'IconSettings',
cards: [{ type: CardType.WorkflowCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
timeline: null,
fields: null,
tasks: null,
notes: null,
files: null,
},
};
@@ -1,25 +0,0 @@
import { CardType } from '@/object-record/record-show/types/CardType';
import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
export const WORKFLOW_RUN_RECORD_LAYOUT: RecordLayout = {
tabs: {
workflowRun: {
title: 'Flow',
position: 101,
icon: 'IconSettings',
cards: [{ type: CardType.WorkflowRunCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
timeline: null,
tasks: null,
notes: null,
files: null,
},
};
@@ -1,25 +0,0 @@
import { CardType } from '@/object-record/record-show/types/CardType';
import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
export const WORKFLOW_VERSION_RECORD_LAYOUT: RecordLayout = {
tabs: {
workflowVersion: {
title: 'Flow',
position: 101,
icon: 'IconSettings',
cards: [{ type: CardType.WorkflowVersionCard }],
hide: {
ifMobile: false,
ifDesktop: false,
ifInRightDrawer: false,
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [],
ifRelationsMissing: [],
},
},
timeline: null,
tasks: null,
notes: null,
files: null,
},
};
@@ -1,7 +0,0 @@
import { type RecordLayoutTab } from '@/ui/layout/tab-list/types/RecordLayoutTab';
export type RecordLayout = {
hideSummaryAndFields?: boolean;
hideFieldsInSidePanel?: boolean;
tabs: Record<string, RecordLayoutTab | null>;
};
@@ -1,3 +0,0 @@
import { type RecordLayout } from '@/object-record/record-show/types/RecordLayout';
export type RecordLayoutMap = Record<string, RecordLayout | null>;
@@ -1,273 +0,0 @@
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: {},
rowLevelPermissionPredicates: [],
rowLevelPermissionPredicateGroups: [],
...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_RECORD_PAGE_LAYOUT_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_RECORD_PAGE_LAYOUT_ENABLED, value: true },
],
},
};
expect(
evaluateTabVisibility(
{
...baseConfig,
ifFeaturesDisabled: [FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_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);
});
});
@@ -1,109 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { type TabVisibilityConfig } from '@/ui/layout/tab-list/types/TabVisibilityConfig';
import { type ObjectPermissions } from 'twenty-shared/types';
import { FieldMetadataType, type FeatureFlagDto } from '~/generated/graphql';
export type TabVisibilityContext = {
isMobile: boolean;
isInRightDrawer: boolean;
currentWorkspace: {
featureFlags?: FeatureFlagDto[] | null;
} | null;
objectMetadataItems: ObjectMetadataItem[];
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
targetObjectMetadataItem: ObjectMetadataItem;
};
export const evaluateTabVisibility = (
hide: TabVisibilityConfig,
context: TabVisibilityContext,
): boolean => {
const {
isMobile,
isInRightDrawer,
currentWorkspace,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
} = context;
const baseHide =
(hide.ifMobile && isMobile) ||
(hide.ifDesktop && !isMobile) ||
(hide.ifInRightDrawer && isInRightDrawer);
if (baseHide) {
return true;
}
const featureNotEnabled =
hide.ifFeaturesDisabled.length > 0 &&
!hide.ifFeaturesDisabled.every((flagKey) => {
const featureFlags = currentWorkspace?.featureFlags;
if (!featureFlags) {
return false;
}
return !!featureFlags.find((flag) => flag.key === flagKey && flag.value);
});
if (featureNotEnabled) {
return true;
}
const requiredObjectInactive = hide.ifRequiredObjectsInactive.some(
(requiredObjectName) => {
const objectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === requiredObjectName,
);
return !objectMetadataItem?.isActive;
},
);
if (requiredObjectInactive) {
return true;
}
const relationMissing = hide.ifRelationsMissing.some((relationName) => {
return !context.targetObjectMetadataItem.fields.some(
(field) =>
field.name === relationName &&
field.type === FieldMetadataType.RELATION &&
field.isActive === true,
);
});
if (relationMissing) {
return true;
}
const noReadPermission =
hide.ifNoReadPermission === true &&
isDefined(hide.ifNoReadPermissionObject) &&
(() => {
const targetObjectMetadataId = objectMetadataItems.find(
(item) => item.nameSingular === hide.ifNoReadPermissionObject,
)?.id;
if (!isDefined(targetObjectMetadataId)) {
return false;
}
const objectPermissions = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
targetObjectMetadataId,
);
return objectPermissions.canReadObjectRecords === false;
})();
if (noReadPermission) {
return true;
}
return false;
};