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
@@ -1500,7 +1500,6 @@ export enum FeatureFlagKey {
|
||||
IS_NOTE_TARGET_MIGRATED = 'IS_NOTE_TARGET_MIGRATED',
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED',
|
||||
IS_SSE_DB_EVENTS_ENABLED = 'IS_SSE_DB_EVENTS_ENABLED',
|
||||
IS_TASK_TARGET_MIGRATED = 'IS_TASK_TARGET_MIGRATED',
|
||||
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED'
|
||||
|
||||
@@ -1472,7 +1472,6 @@ export enum FeatureFlagKey {
|
||||
IS_NOTE_TARGET_MIGRATED = 'IS_NOTE_TARGET_MIGRATED',
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED',
|
||||
IS_SSE_DB_EVENTS_ENABLED = 'IS_SSE_DB_EVENTS_ENABLED',
|
||||
IS_TASK_TARGET_MIGRATED = 'IS_TASK_TARGET_MIGRATED',
|
||||
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED'
|
||||
|
||||
-1
@@ -60,7 +60,6 @@ export const CommandMenuCalendarEventPage = () => {
|
||||
},
|
||||
layoutType: PageLayoutType.RECORD_PAGE,
|
||||
isInRightDrawer: true,
|
||||
isLegacyRecordShowPage: true,
|
||||
}}
|
||||
>
|
||||
<CalendarEventDetailsEffect record={calendarEvent} />
|
||||
|
||||
+2
-2
@@ -6,7 +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 { PageLayoutDispatcher } from '@/object-record/record-show/components/PageLayoutDispatcher';
|
||||
import { PageLayoutRecordPageRenderer } from '@/object-record/record-show/components/PageLayoutRecordPageRenderer';
|
||||
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext';
|
||||
@@ -80,7 +80,7 @@ export const CommandMenuRecordPage = () => {
|
||||
recordId: objectRecordId,
|
||||
}}
|
||||
>
|
||||
<PageLayoutDispatcher
|
||||
<PageLayoutRecordPageRenderer
|
||||
targetRecordIdentifier={{
|
||||
id: objectRecordId,
|
||||
targetObjectNameSingular: objectNameSingular,
|
||||
|
||||
+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;
|
||||
};
|
||||
@@ -8,18 +8,9 @@ import { usePageLayoutTabWithVisibleWidgetsOrThrow } from '@/page-layout/hooks/u
|
||||
import { useReorderPageLayoutWidgets } from '@/page-layout/hooks/useReorderPageLayoutWidgets';
|
||||
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
PageLayoutTabLayoutMode,
|
||||
PageLayoutType,
|
||||
} from '~/generated/graphql';
|
||||
import { PageLayoutTabLayoutMode, PageLayoutType } from '~/generated/graphql';
|
||||
|
||||
export const PageLayoutContent = () => {
|
||||
const isRecordPageEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_ENABLED,
|
||||
);
|
||||
|
||||
const isPageLayoutInEditMode = useRecoilComponentValue(
|
||||
isPageLayoutInEditModeComponentState,
|
||||
);
|
||||
@@ -37,10 +28,8 @@ export const PageLayoutContent = () => {
|
||||
const isRecordPageLayout =
|
||||
currentPageLayout.type === PageLayoutType.RECORD_PAGE;
|
||||
|
||||
const isCanvasLayout =
|
||||
isRecordPageEnabled && layoutMode === PageLayoutTabLayoutMode.CANVAS;
|
||||
const isVerticalList =
|
||||
isRecordPageEnabled && layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST;
|
||||
const isCanvasLayout = layoutMode === PageLayoutTabLayoutMode.CANVAS;
|
||||
const isVerticalList = layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST;
|
||||
|
||||
if (isCanvasLayout) {
|
||||
return <PageLayoutCanvasViewer widgets={activeTab.widgets} />;
|
||||
|
||||
@@ -11,9 +11,6 @@ export type LayoutRenderingContextType = {
|
||||
layoutType: PageLayoutType;
|
||||
|
||||
isInRightDrawer: boolean;
|
||||
|
||||
/** TODO: Remove once the traditional record show page is removed. */
|
||||
isLegacyRecordShowPage?: boolean;
|
||||
};
|
||||
|
||||
export const [LayoutRenderingProvider, useLayoutRenderingContext] =
|
||||
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { PAGE_LAYOUT_LEFT_PANEL_CONTAINER_WIDTH } from '@/page-layout/constants/PageLayoutLeftPanelContainerWidth';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
|
||||
const StyledOuterContainer = styled.div<{ isMobile: boolean }>`
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border-bottom-left-radius: 8px;
|
||||
border-right: ${({ theme, isMobile }) =>
|
||||
isMobile ? 'none' : `1px solid ${theme.border.color.medium}`};
|
||||
border-top-left-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(3)};
|
||||
z-index: 10;
|
||||
width: 'auto';
|
||||
`;
|
||||
|
||||
const StyledInnerContainer = styled.div<{ isMobile: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: ${({ isMobile }) =>
|
||||
isMobile ? `100%` : `${PAGE_LAYOUT_LEFT_PANEL_CONTAINER_WIDTH}px`};
|
||||
`;
|
||||
|
||||
const StyledIntermediateContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
export type ShowPageLeftContainerProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const ShowPageLeftContainer = ({
|
||||
children,
|
||||
}: ShowPageLeftContainerProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<StyledOuterContainer isMobile={isMobile}>
|
||||
{isMobile ? (
|
||||
<StyledInnerContainer isMobile={isMobile}>
|
||||
{children}
|
||||
</StyledInnerContainer>
|
||||
) : (
|
||||
<ScrollWrapper
|
||||
componentInstanceId={`scroll-wrapper-show-page-left-container`}
|
||||
>
|
||||
<StyledIntermediateContainer>
|
||||
<StyledInnerContainer isMobile={isMobile}>
|
||||
{children}
|
||||
</StyledInnerContainer>
|
||||
</StyledIntermediateContainer>
|
||||
</ScrollWrapper>
|
||||
)}
|
||||
</StyledOuterContainer>
|
||||
);
|
||||
};
|
||||
-158
@@ -1,158 +0,0 @@
|
||||
import { RecordShowRightDrawerActionMenu } from '@/action-menu/components/RecordShowRightDrawerActionMenu';
|
||||
import { RecordShowRightDrawerOpenRecordButton } from '@/action-menu/components/RecordShowRightDrawerOpenRecordButton';
|
||||
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 { 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 { 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';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const StyledShowPageRightContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: start;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const StyledTabListContainer = styled.div<{ shouldDisplay: boolean }>`
|
||||
${({ shouldDisplay }) =>
|
||||
!shouldDisplay &&
|
||||
css`
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledTabList = styled(TabList)`
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
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};
|
||||
`;
|
||||
|
||||
type ShowPageSubContainerProps = {
|
||||
layout?: RecordLayout;
|
||||
tabs: SingleTabProps[];
|
||||
targetRecordIdentifier: TargetRecordIdentifier;
|
||||
isInRightDrawer?: boolean;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export const ShowPageSubContainer = ({
|
||||
tabs,
|
||||
layout,
|
||||
targetRecordIdentifier,
|
||||
loading,
|
||||
isInRightDrawer = false,
|
||||
}: ShowPageSubContainerProps) => {
|
||||
const commandMenuPageComponentInstance = useComponentInstanceStateContext(
|
||||
CommandMenuPageComponentInstanceContext,
|
||||
);
|
||||
|
||||
const tabListComponentId = getShowPageTabListComponentId({
|
||||
pageId: commandMenuPageComponentInstance?.instanceId,
|
||||
targetObjectId: targetRecordIdentifier.id,
|
||||
});
|
||||
const activeTabId = useRecoilComponentValue(
|
||||
activeTabIdComponentState,
|
||||
tabListComponentId,
|
||||
);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const summaryCard = (
|
||||
<SummaryCard
|
||||
objectNameSingular={targetRecordIdentifier.targetObjectNameSingular}
|
||||
objectRecordId={targetRecordIdentifier.id}
|
||||
isInRightDrawer={isInRightDrawer}
|
||||
/>
|
||||
);
|
||||
|
||||
const fieldsCard = <FieldsCard />;
|
||||
|
||||
const renderActiveTabContent = () => {
|
||||
const activeTab = tabs.find((tab) => tab.id === activeTabId);
|
||||
if (!activeTab?.cards?.length) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{activeTab.cards.map((card, index) => (
|
||||
<React.Fragment key={`${activeTab.id}-card-${index}`}>
|
||||
{getCardComponent(card.type, card.configuration)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const visibleTabs = tabs.filter((tab) => !tab.hide);
|
||||
|
||||
const isInCommandMenu = isDefined(commandMenuPageComponentInstance);
|
||||
|
||||
const displaySummaryAndFields =
|
||||
layout && !layout.hideSummaryAndFields && !isMobile && !isInRightDrawer;
|
||||
|
||||
return (
|
||||
<TabListComponentInstanceContext.Provider
|
||||
value={{ instanceId: tabListComponentId }}
|
||||
>
|
||||
{displaySummaryAndFields && (
|
||||
<ShowPageLeftContainer>
|
||||
{summaryCard}
|
||||
{fieldsCard}
|
||||
</ShowPageLeftContainer>
|
||||
)}
|
||||
<StyledShowPageRightContainer>
|
||||
<StyledTabListContainer shouldDisplay={visibleTabs.length > 1}>
|
||||
<StyledTabList
|
||||
behaveAsLinks={!isInRightDrawer}
|
||||
loading={loading}
|
||||
tabs={tabs}
|
||||
isInRightDrawer={isInRightDrawer}
|
||||
componentInstanceId={tabListComponentId}
|
||||
/>
|
||||
</StyledTabListContainer>
|
||||
{(isMobile || isInRightDrawer) && !isInCommandMenu && summaryCard}
|
||||
<StyledContentContainer isInRightDrawer={isInRightDrawer}>
|
||||
{renderActiveTabContent()}
|
||||
</StyledContentContainer>
|
||||
{isInRightDrawer && (
|
||||
<RightDrawerFooter
|
||||
actions={[
|
||||
<RecordShowRightDrawerActionMenu />,
|
||||
<RecordShowRightDrawerOpenRecordButton
|
||||
objectNameSingular={
|
||||
targetRecordIdentifier.targetObjectNameSingular
|
||||
}
|
||||
recordId={targetRecordIdentifier.id}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</StyledShowPageRightContainer>
|
||||
</TabListComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
-1
@@ -1 +0,0 @@
|
||||
export const SHOW_PAGE_ADD_BUTTON_DROPDOWN_ID = 'show-page-add-button-dropdown';
|
||||
@@ -1,10 +0,0 @@
|
||||
import { type LayoutCard } from '@/ui/layout/tab-list/types/LayoutCard';
|
||||
import { type TabVisibilityConfig } from '@/ui/layout/tab-list/types/TabVisibilityConfig';
|
||||
|
||||
export type RecordLayoutTab = {
|
||||
title: string;
|
||||
position: number;
|
||||
icon: string;
|
||||
hide: TabVisibilityConfig;
|
||||
cards: LayoutCard[];
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { type FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
export type TabVisibilityConfig = {
|
||||
ifMobile: boolean;
|
||||
ifDesktop: boolean;
|
||||
ifInRightDrawer: boolean;
|
||||
ifFeaturesDisabled: FeatureFlagKey[];
|
||||
ifRequiredObjectsInactive: CoreObjectNameSingular[];
|
||||
ifRelationsMissing: string[];
|
||||
ifNoReadPermission?: boolean;
|
||||
ifNoReadPermissionObject?: CoreObjectNameSingular;
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainCo
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { MainContainerLayoutWithCommandMenu } from '@/object-record/components/MainContainerLayoutWithCommandMenu';
|
||||
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
|
||||
import { PageLayoutDispatcher } from '@/object-record/record-show/components/PageLayoutDispatcher';
|
||||
import { PageLayoutRecordPageRenderer } from '@/object-record/record-show/components/PageLayoutRecordPageRenderer';
|
||||
import { RecordShowPageSSESubscribeEffect } from '@/object-record/record-show/components/RecordShowPageSSESubscribeEffect';
|
||||
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
|
||||
import { computeRecordShowComponentInstanceId } from '@/object-record/record-show/utils/computeRecordShowComponentInstanceId';
|
||||
@@ -58,11 +58,12 @@ export const RecordShowPage = () => {
|
||||
recordId: objectRecordId,
|
||||
}}
|
||||
>
|
||||
<PageLayoutDispatcher
|
||||
<PageLayoutRecordPageRenderer
|
||||
targetRecordIdentifier={{
|
||||
id: objectRecordId,
|
||||
targetObjectNameSingular: objectNameSingular,
|
||||
}}
|
||||
isInRightDrawer={false}
|
||||
/>
|
||||
<RecordShowPageSSESubscribeEffect
|
||||
objectNameSingular={objectNameSingular}
|
||||
|
||||
-1
@@ -5,7 +5,6 @@ export enum FeatureFlagKey {
|
||||
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
|
||||
IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED = 'IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED',
|
||||
IS_MARKETPLACE_ENABLED = 'IS_MARKETPLACE_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
|
||||
|
||||
-2
@@ -226,7 +226,6 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_AI_ENABLED: false,
|
||||
IS_APPLICATION_ENABLED: false,
|
||||
IS_IMAP_SMTP_CALDAV_ENABLED: false,
|
||||
IS_RECORD_PAGE_LAYOUT_ENABLED: false,
|
||||
IS_PUBLIC_DOMAIN_ENABLED: false,
|
||||
IS_EMAILING_DOMAIN_ENABLED: false,
|
||||
IS_DASHBOARD_V2_ENABLED: false,
|
||||
@@ -264,7 +263,6 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_JSON_FILTER_ENABLED: false,
|
||||
IS_AI_ENABLED: false,
|
||||
IS_APPLICATION_ENABLED: false,
|
||||
IS_RECORD_PAGE_LAYOUT_ENABLED: false,
|
||||
IS_PUBLIC_DOMAIN_ENABLED: false,
|
||||
IS_EMAILING_DOMAIN_ENABLED: false,
|
||||
IS_DASHBOARD_V2_ENABLED: false,
|
||||
|
||||
-5
@@ -36,11 +36,6 @@ export const seedFeatureFlags = async ({
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_PUBLIC_DOMAIN_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
|
||||
-1
@@ -4,5 +4,4 @@ export const DEFAULT_FEATURE_FLAGS = [
|
||||
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
|
||||
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
|
||||
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_ENABLED,
|
||||
] as const satisfies FeatureFlagKey[];
|
||||
|
||||
Reference in New Issue
Block a user