Remove the code of old show pages (#17811)
Closes https://github.com/twentyhq/core-team-issues/issues/2213
This commit is contained in:
committed by
GitHub
parent
2d42b0a726
commit
b250de216e
+1
-5
@@ -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
|
||||
|
||||
-2
@@ -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 />
|
||||
|
||||
-2
@@ -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
|
||||
|
||||
+1
-5
@@ -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
|
||||
|
||||
-62
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
-79
@@ -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>
|
||||
);
|
||||
};
|
||||
-131
@@ -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];
|
||||
}, []),
|
||||
};
|
||||
};
|
||||
-82
@@ -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'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
-35
@@ -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: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
-23
@@ -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,
|
||||
},
|
||||
};
|
||||
-35
@@ -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: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
-35
@@ -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: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
-23
@@ -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,
|
||||
},
|
||||
};
|
||||
-27
@@ -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,
|
||||
},
|
||||
};
|
||||
-25
@@ -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,
|
||||
},
|
||||
};
|
||||
-25
@@ -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>;
|
||||
-273
@@ -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);
|
||||
});
|
||||
});
|
||||
-109
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user