Fix a bunch of bugs on Record Page Layouts (#17342)
## Display task target and note target relations https://github.com/user-attachments/assets/b291cc38-33b9-45b8-b685-e5890824176e ## Drop summary card in right drawer ### Before <img width="1362" height="2160" alt="CleanShot 2026-01-22 at 14 27 09@2x" src="https://github.com/user-attachments/assets/79f45d8f-976b-4999-8f62-5cb4b87b3ef6" /> ### After <img width="1362" height="2160" alt="CleanShot 2026-01-22 at 14 26 43@2x" src="https://github.com/user-attachments/assets/fb67ddbb-0585-48bd-8ef6-c0139bba4062" /> ## Add a tooltip to the see all action https://github.com/user-attachments/assets/ab05cfab-8a6c-4caf-bfe9-698c8b7904e6 ## Rename the Fields tab to Home ### Before <img width="544" height="354" alt="CleanShot 2026-01-22 at 14 28 26@2x" src="https://github.com/user-attachments/assets/62f7faa2-3a18-40af-8cdf-3fb9f7c7708c" /> ### After <img width="544" height="354" alt="CleanShot 2026-01-22 at 14 28 06@2x" src="https://github.com/user-attachments/assets/76c21ec9-3723-4933-b34f-88b6403cbad5" />
This commit is contained in:
committed by
GitHub
parent
267691a403
commit
fb2c9e12bc
+1
-4
@@ -1,7 +1,6 @@
|
||||
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { PageLayoutLeftPanel } from '@/page-layout/components/PageLayoutLeftPanel';
|
||||
import { PageLayoutTabHeader } from '@/page-layout/components/PageLayoutTabHeader';
|
||||
import { PageLayoutTabList } from '@/page-layout/components/PageLayoutTabList';
|
||||
import { PageLayoutTabListEffect } from '@/page-layout/components/PageLayoutTabListEffect';
|
||||
import { PAGE_LAYOUT_LEFT_PANEL_CONTAINER_WIDTH } from '@/page-layout/constants/PageLayoutLeftPanelContainerWidth';
|
||||
@@ -21,8 +20,8 @@ import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTab
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
@@ -129,8 +128,6 @@ export const PageLayoutRendererContent = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<PageLayoutTabHeader />
|
||||
|
||||
<StyledScrollWrapper
|
||||
componentInstanceId={getScrollWrapperInstanceIdFromPageLayoutId(
|
||||
currentPageLayout.id,
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { SummaryCard } from '@/object-record/record-show/components/SummaryCard';
|
||||
import { useCurrentPageLayout } from '@/page-layout/hooks/useCurrentPageLayout';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { PageLayoutType } from '~/generated/graphql';
|
||||
|
||||
export const PageLayoutTabHeader = () => {
|
||||
const { currentPageLayout } = useCurrentPageLayout();
|
||||
const targetRecordIdentifier = useTargetRecord();
|
||||
const { isInRightDrawer } = useLayoutRenderingContext();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
if (
|
||||
currentPageLayout?.type !== PageLayoutType.RECORD_PAGE ||
|
||||
!(isMobile || isInRightDrawer)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SummaryCard
|
||||
objectNameSingular={targetRecordIdentifier.targetObjectNameSingular}
|
||||
objectRecordId={targetRecordIdentifier.id}
|
||||
isInRightDrawer={isInRightDrawer}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+2
-2
@@ -19,8 +19,8 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
id: 'company-tab-fields',
|
||||
title: 'Fields',
|
||||
icon: 'IconList',
|
||||
title: 'Home',
|
||||
icon: 'IconHome',
|
||||
position: 100,
|
||||
layoutMode: 'vertical-list',
|
||||
pageLayoutId: DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID,
|
||||
|
||||
+2
-2
@@ -15,8 +15,8 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
id: 'note-tab-fields',
|
||||
title: 'Fields',
|
||||
icon: 'IconList',
|
||||
title: 'Home',
|
||||
icon: 'IconHome',
|
||||
position: 100,
|
||||
layoutMode: 'vertical-list',
|
||||
pageLayoutId: DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID,
|
||||
|
||||
+2
-2
@@ -19,8 +19,8 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
id: 'opportunity-tab-fields',
|
||||
title: 'Fields',
|
||||
icon: 'IconList',
|
||||
title: 'Home',
|
||||
icon: 'IconHome',
|
||||
position: 100,
|
||||
layoutMode: 'vertical-list',
|
||||
pageLayoutId: DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID,
|
||||
|
||||
+2
-2
@@ -15,8 +15,8 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
id: 'person-tab-fields',
|
||||
title: 'Fields',
|
||||
icon: 'IconList',
|
||||
title: 'Home',
|
||||
icon: 'IconHome',
|
||||
position: 100,
|
||||
layoutMode: 'vertical-list',
|
||||
pageLayoutId: DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID,
|
||||
|
||||
@@ -15,8 +15,8 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
id: 'default-tab-fields',
|
||||
title: 'Fields',
|
||||
icon: 'IconList',
|
||||
title: 'Home',
|
||||
icon: 'IconHome',
|
||||
position: 100,
|
||||
layoutMode: 'vertical-list',
|
||||
pageLayoutId: DEFAULT_RECORD_PAGE_LAYOUT_ID,
|
||||
|
||||
+2
-2
@@ -15,8 +15,8 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
id: 'task-tab-fields',
|
||||
title: 'Fields',
|
||||
icon: 'IconList',
|
||||
title: 'Home',
|
||||
icon: 'IconHome',
|
||||
position: 100,
|
||||
layoutMode: 'vertical-list',
|
||||
pageLayoutId: DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID,
|
||||
|
||||
+2
-2
@@ -21,10 +21,10 @@ export const DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT: PageLayout = {
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
id: 'workflow-run-tab-fields',
|
||||
title: 'Fields',
|
||||
title: 'Home',
|
||||
position: 100,
|
||||
layoutMode: 'vertical-list',
|
||||
icon: 'IconList',
|
||||
icon: 'IconHome',
|
||||
pageLayoutId: DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
|
||||
+2
-2
@@ -21,8 +21,8 @@ export const DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT: PageLayout = {
|
||||
{
|
||||
__typename: 'PageLayoutTab',
|
||||
id: 'workflow-version-tab-fields',
|
||||
title: 'Fields',
|
||||
icon: 'IconList',
|
||||
title: 'Home',
|
||||
icon: 'IconHome',
|
||||
position: 100,
|
||||
layoutMode: 'vertical-list',
|
||||
pageLayoutId: DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID,
|
||||
|
||||
+6
-12
@@ -3,30 +3,24 @@ import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguratio
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useTemporaryFieldsConfiguration = (
|
||||
objectNameSingular: string,
|
||||
): FieldsConfiguration | null => {
|
||||
): FieldsConfiguration => {
|
||||
const { t } = useLingui();
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const configuration = useMemo<FieldsConfiguration | null>(() => {
|
||||
const configuration = useMemo<FieldsConfiguration>(() => {
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return null;
|
||||
throw new Error('Object metadata item is not defined');
|
||||
}
|
||||
|
||||
const fieldsToDisplay = objectMetadataItem.fields.filter(
|
||||
(field) =>
|
||||
field.type !== FieldMetadataType.RELATION &&
|
||||
field.type !== FieldMetadataType.MORPH_RELATION &&
|
||||
field.type !== FieldMetadataType.RICH_TEXT_V2,
|
||||
);
|
||||
const fieldsToDisplay = objectMetadataItem.fields;
|
||||
|
||||
if (fieldsToDisplay.length === 0) {
|
||||
return null;
|
||||
throw new Error('No fields to display');
|
||||
}
|
||||
|
||||
const generalFields: Array<{ fieldMetadataId: string; position: number }> =
|
||||
@@ -72,7 +66,7 @@ export const useTemporaryFieldsConfiguration = (
|
||||
}
|
||||
|
||||
if (sections.length === 0) {
|
||||
return null;
|
||||
throw new Error('No sections to display');
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
+32
-9
@@ -9,16 +9,22 @@ import { isFieldWidget } from '@/page-layout/widgets/field/utils/isFieldWidget';
|
||||
import { useCurrentWidget } from '@/page-layout/widgets/hooks/useCurrentWidget';
|
||||
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector';
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { AppPath, ViewFilterOperand } from 'twenty-shared/types';
|
||||
import { getAppPath, isDefined } from 'twenty-shared/utils';
|
||||
import { IconArrowUpRight } from 'twenty-ui/display';
|
||||
import {
|
||||
AppTooltip,
|
||||
IconArrowUpRight,
|
||||
TooltipDelay,
|
||||
TooltipPosition,
|
||||
} from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { RelationType } from '~/generated-metadata/graphql';
|
||||
import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
display: flex;
|
||||
@@ -120,13 +126,30 @@ export const WidgetActionFieldSeeAll = () => {
|
||||
filterQueryParams,
|
||||
);
|
||||
|
||||
const tooltipId = `widget-see-all-${widget.id}`;
|
||||
const relationLabelPlural =
|
||||
relationObjectMetadataItem.labelPlural.toLowerCase();
|
||||
const tooltipContent = t`See all ${relationLabelPlural} linked to this record`;
|
||||
|
||||
return (
|
||||
<StyledLink to={filterLinkHref} data-testid="widget-see-all-link">
|
||||
<StyledSeeAllButton
|
||||
Icon={IconArrowUpRight}
|
||||
accent="secondary"
|
||||
isMobile={isMobile}
|
||||
<>
|
||||
<div id={tooltipId}>
|
||||
<StyledLink to={filterLinkHref} data-testid="widget-see-all-link">
|
||||
<StyledSeeAllButton
|
||||
Icon={IconArrowUpRight}
|
||||
accent="secondary"
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
</StyledLink>
|
||||
</div>
|
||||
<AppTooltip
|
||||
anchorSelect={`#${tooltipId}`}
|
||||
content={tooltipContent}
|
||||
place={TooltipPosition.Top}
|
||||
delay={TooltipDelay.mediumDelay}
|
||||
offset={5}
|
||||
noArrow
|
||||
/>
|
||||
</StyledLink>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+42
-12
@@ -1,4 +1,6 @@
|
||||
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
|
||||
@@ -6,6 +8,7 @@ import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordF
|
||||
import { RecordFieldsScopeContextProvider } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
|
||||
import { RecordFieldListComponentInstanceContext } from '@/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext';
|
||||
import { recordFieldListHoverPositionComponentState } from '@/object-record/record-field-list/states/recordFieldListHoverPositionComponentState';
|
||||
import { isActivityTargetField } from '@/object-record/record-field-list/utils/categorizeRelationFields';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||
@@ -90,7 +93,9 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => {
|
||||
targetRecord.targetObjectNameSingular,
|
||||
);
|
||||
|
||||
if (sectionsWithFieldIndices.length === 0) {
|
||||
const hasFieldsToDisplay = sectionsWithFieldIndices.length > 0;
|
||||
|
||||
if (!hasFieldsToDisplay) {
|
||||
return (
|
||||
<RightDrawerProvider value={{ isInRightDrawer }}>
|
||||
<StyledContainer>
|
||||
@@ -133,6 +138,11 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => {
|
||||
<>
|
||||
{section.fields.map(
|
||||
({ field: fieldMetadataItem, globalIndex }) => {
|
||||
const isActivityTarget = isActivityTargetField(
|
||||
fieldMetadataItem.name,
|
||||
targetRecord.targetObjectNameSingular,
|
||||
);
|
||||
|
||||
return (
|
||||
<FieldContext.Provider
|
||||
key={targetRecord.id + fieldMetadataItem.id}
|
||||
@@ -174,17 +184,37 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => {
|
||||
})}`,
|
||||
}}
|
||||
>
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: getRecordFieldInputInstanceId({
|
||||
recordId: targetRecord.id,
|
||||
fieldName: fieldMetadataItem.name,
|
||||
prefix: instanceId,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<RecordInlineCell loading={recordLoading} />
|
||||
</RecordFieldComponentInstanceContext.Provider>
|
||||
{isActivityTarget ? (
|
||||
<ActivityTargetsInlineCell
|
||||
componentInstanceId={getRecordFieldInputInstanceId(
|
||||
{
|
||||
recordId: targetRecord.id,
|
||||
fieldName: fieldMetadataItem.name,
|
||||
prefix: instanceId,
|
||||
},
|
||||
)}
|
||||
activityObjectNameSingular={
|
||||
targetRecord.targetObjectNameSingular as
|
||||
| CoreObjectNameSingular.Note
|
||||
| CoreObjectNameSingular.Task
|
||||
}
|
||||
activityRecordId={targetRecord.id}
|
||||
showLabel={true}
|
||||
maxWidth={200}
|
||||
/>
|
||||
) : (
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: getRecordFieldInputInstanceId({
|
||||
recordId: targetRecord.id,
|
||||
fieldName: fieldMetadataItem.name,
|
||||
prefix: instanceId,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<RecordInlineCell loading={recordLoading} />
|
||||
</RecordFieldComponentInstanceContext.Provider>
|
||||
)}
|
||||
</FieldContext.Provider>
|
||||
);
|
||||
},
|
||||
|
||||
+12
-9
@@ -1,4 +1,3 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems';
|
||||
|
||||
type UseFieldsWidgetFieldMetadataItemsProps = {
|
||||
@@ -7,13 +6,17 @@ type UseFieldsWidgetFieldMetadataItemsProps = {
|
||||
|
||||
export const useFieldsWidgetFieldMetadataItems = ({
|
||||
objectNameSingular,
|
||||
}: UseFieldsWidgetFieldMetadataItemsProps): FieldMetadataItem[] => {
|
||||
const { inlineFieldMetadataItems } = useFieldListFieldMetadataItems({
|
||||
objectNameSingular,
|
||||
excludeFieldMetadataIds: [],
|
||||
excludeCreatedAtAndUpdatedAt: true,
|
||||
showRelationSections: false,
|
||||
});
|
||||
}: UseFieldsWidgetFieldMetadataItemsProps) => {
|
||||
const { inlineFieldMetadataItems, legacyActivityTargetFieldMetadataItems } =
|
||||
useFieldListFieldMetadataItems({
|
||||
objectNameSingular,
|
||||
excludeFieldMetadataIds: [],
|
||||
excludeCreatedAtAndUpdatedAt: true,
|
||||
showRelationSections: true,
|
||||
});
|
||||
|
||||
return inlineFieldMetadataItems ?? [];
|
||||
return {
|
||||
inlineFieldMetadataItems,
|
||||
legacyActivityTargetFieldMetadataItems,
|
||||
};
|
||||
};
|
||||
|
||||
+11
-9
@@ -10,21 +10,23 @@ export const useFieldsWidgetSectionsWithFields = (
|
||||
) => {
|
||||
const isMobile = useIsMobile();
|
||||
const { isInRightDrawer } = useLayoutRenderingContext();
|
||||
const fieldMetadataItems = useFieldsWidgetFieldMetadataItems({
|
||||
objectNameSingular,
|
||||
});
|
||||
const { inlineFieldMetadataItems, legacyActivityTargetFieldMetadataItems } =
|
||||
useFieldsWidgetFieldMetadataItems({
|
||||
objectNameSingular,
|
||||
});
|
||||
const temporaryConfiguration =
|
||||
useTemporaryFieldsConfiguration(objectNameSingular);
|
||||
|
||||
const context = buildWidgetVisibilityContext({ isMobile, isInRightDrawer });
|
||||
|
||||
const allFieldMetadataItems = [
|
||||
...legacyActivityTargetFieldMetadataItems,
|
||||
...inlineFieldMetadataItems,
|
||||
];
|
||||
|
||||
const sectionsWithFields = filterAndOrderFieldsFromConfiguration({
|
||||
configuration: temporaryConfiguration ?? {
|
||||
__typename: 'FieldsConfiguration',
|
||||
configurationType: 'FIELDS',
|
||||
sections: [],
|
||||
},
|
||||
availableFieldMetadataItems: fieldMetadataItems,
|
||||
configuration: temporaryConfiguration,
|
||||
availableFieldMetadataItems: allFieldMetadataItems,
|
||||
context,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user