From 9b7953e7a3ab3d0033a20839f42737bb26c1f9c3 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Wed, 21 Jan 2026 09:38:08 +0100
Subject: [PATCH] Fix widget injection order: place relation widgets after
FIELDS, Notes after relations (#17274)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
> [!NOTE]
> This code is temporary. It will be dropped once Record Page Layouts
can be fully configured.
## Before
https://github.com/user-attachments/assets/3f6aed00-2fd5-47d4-bd74-002a68030627
## After
The `usePageLayoutWithRelationWidgets` hook was appending relation
widgets to the end of the widget list. This resulted in incorrect
ordering where relation widgets appeared after Notes and other widgets
instead of immediately following the FIELDS widget.
## Changes
- **Widget injection logic**: Modified `injectRelationWidgetsIntoLayout`
to find the first FIELDS widget and insert relation widgets immediately
after it, rather than appending to end
- **Notes positioning**: Extract and reposition NOTES widget to appear
after all relation widgets, maintaining correct semantic order: `FIELDS
→ Relations → Notes → Other widgets`
- **Fallback behavior**: When no FIELDS widget exists, append relation
widgets to end as before
- **Test coverage**: Added 6 test cases covering injection order, Notes
positioning, and edge cases (missing widgets, empty relations,
non-record pages)
## Example
Before:
```
[FIELDS, NOTES, GRAPH] → [FIELDS, NOTES, GRAPH, Relation1, Relation2]
```
After:
```
[FIELDS, NOTES, GRAPH] → [FIELDS, Relation1, Relation2, NOTES, GRAPH]
```
> [!WARNING]
>
>
> Firewall rules blocked me from connecting to one or more
addresses (expand for details)
>
> #### I tried to connect to the following addresses, but was blocked by
firewall rules:
>
> - `googlechromelabs.github.io`
> - Triggering command: `/usr/local/bin/node /usr/local/bin/node
install.mjs` (dns block)
> -
`https://storage.googleapis.com/chrome-for-testing-public/127.0.6533.88/linux64/chrome-headless-shell-linux64.zip`
> - Triggering command: `/usr/local/bin/node /usr/local/bin/node
install.mjs` (http block)
> -
`https://storage.googleapis.com/chrome-for-testing-public/127.0.6533.88/linux64/chrome-linux64.zip`
> - Triggering command: `/usr/local/bin/node /usr/local/bin/node
install.mjs` (http block)
>
> If you need me to access, download, or install something from one of
these locations, you can either:
>
> - Configure [Actions setup
steps](https://gh.io/copilot/actions-setup-steps) to set up my
environment, which run before the firewall is enabled
> - Add the appropriate URLs or hosts to the custom allowlist in this
repository's [Copilot coding agent
settings](https://github.com/twentyhq/twenty/settings/copilot/coding_agent)
(admins only)
>
>
Original prompt
Start implementation
[Chronological Review: The conversation began with the user requesting
an update to the `usePageLayoutWithRelationWidgets` function to inject
relation widgets after the first FIELDS widget and to position a Note
widget after all relation widgets. The user then asked for a plan
without comments, followed by a request to start the implementation of
the plan.]
[Intent Mapping:
1. "Update #sym:usePageLayoutWithRelationWidgets. We should inject the
relation widgets directly after the first FIELDS widget."
2. "We want the Note widget to be after all the relation widgets we
inject."
3. "Prepare a plan. Don't write any comment."
4. "Start implementation."]
[Technical Inventory:
- Function: `usePageLayoutWithRelationWidgets`
- Context: This function is part of a page layout management system,
likely dealing with dynamic widget placement based on certain
conditions.]
[Code Archaeology:
- File:
`/Users/devessier/Web/twenty/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutWithRelationWidgets.ts`
- Function Purpose: To manage the layout of widgets on a page,
specifically handling relation widgets and their placement relative to
other widgets.]
[Progress Assessment:
- Completed: User has defined the requirements for widget placement.
- Pending: Implementation of the specified changes to the
`usePageLayoutWithRelationWidgets` function.]
[Context Validation: All necessary context for continuing the
implementation of the widget placement logic is captured, including the
specific order of widget injection.]
[Recent Commands Analysis:
- Last Agent Commands: The user requested to "Start implementation."
- Tool Results Summary: No specific tool results were generated as the
conversation focused on planning and implementation requests.
- Pre-Summary State: The agent was actively preparing to implement the
changes to the `usePageLayoutWithRelationWidgets` function when the
token budget was exceeded.
- Operation Context: The commands executed were directly related to the
user's goal of modifying the widget layout in the application.]
1. Conversation Overview:
- Primary Objectives:
- "Update #sym:usePageLayoutWithRelationWidgets. We should inject the
relation widgets directly after the first FIELDS widget."
- "We want the Note widget to be after all the relation widgets we
inject."
- "Prepare a plan. Don't write any comment."
- "Start implementation."
- Session Context: The user is focused on modifying the widget layout in
a page layout management system, specifically the order of relation and
Note widgets.
- User Intent Evolution: The user transitioned from defining
requirements to requesting a plan and then moving directly to
implementation.
2. Technical Foundation:
- Function: `usePageLayoutWithRelationWidgets`
- Purpose: To manage the layout of widgets on a page, particularly the
placement of relation widgets.
3. Codebase Status:
- File Name: `usePageLayoutWithRelationWidgets.ts`
- Purpose: To handle the layout of widgets dynamically based on
specified rules.
- Current State: Awaiting implementation of the user-defined widget
placement logic.
- Key Code Segments: The function signature is defined, but the
implementation details are yet to be added.
4. Problem Resolution:
- Issues Encountered: No specific technical problems were reported; the
focus was on planning and implementation.
- Solutions Implemented: None yet, as the implementation phase has just
begun.
- Debugging Context: No ongoing troubleshooting efforts were mentioned.
- Lessons Learned: The importance of clear widget placement requirements
was emphasized.
5. Progress Tracking:
- Completed Tasks: User has articulated the requirements for widget
placement.
- Partially Complete Work: Implementation of the specified changes is
pending.
- Validated Outcomes: No features have been confirmed working yet as
implementation has not started.
6. Active Work State:
- Current Focus: The user is preparing to implement the changes to the
`usePageLayoutWithRelationWidgets` function.
- Recent Context: The user has defined the order of widget placement and
is ready to start coding.
- Working Code: The function is currently defined but lacks the
implementation logic.
- Immediate Context: The user is focused on implementing the logic for
injecting relation widgets and positioning the Note widget.
7. Recent Operations:
- Last Agent Commands: "Start implementation."
- Tool Results Summary: No specific results were generated; the focus
was on user requests.
- Pre-Summary State: The agent was preparing to implement the changes to
the `usePageLayoutWithRelationWidgets` function.
- Operation Context: The commands executed were directly related to the
user's goal of modifying the widget layout.
8. Continuation Plan:
- Pending Task 1: Implement the logic to inject relation widgets after
the first FIELDS widget.
- Pending Task 2: Ensure the Note widget is positioned after all
relation widgets...
Created from [VS
Code](https://code.visualstudio.com/docs/copilot/copilot-coding-agent).
---
💬 We'd love your input! Share your thoughts on Copilot coding agent in
our [2 minute survey](https://gh.io/copilot-coding-agent-survey).
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Devessier <29370468+Devessier@users.noreply.github.com>
Co-authored-by: Devessier
---
.../usePageLayoutWithRelationWidgets.test.tsx | 317 ++++++++++++++++++
.../hooks/usePageLayoutWithRelationWidgets.ts | 43 ++-
2 files changed, 359 insertions(+), 1 deletion(-)
create mode 100644 packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutWithRelationWidgets.test.tsx
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutWithRelationWidgets.test.tsx b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutWithRelationWidgets.test.tsx
new file mode 100644
index 0000000000..6792ed9e2f
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/hooks/__tests__/usePageLayoutWithRelationWidgets.test.tsx
@@ -0,0 +1,317 @@
+import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
+import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems';
+import { usePageLayoutWithRelationWidgets } from '@/page-layout/hooks/usePageLayoutWithRelationWidgets';
+import { type PageLayout } from '@/page-layout/types/PageLayout';
+import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
+import { renderHook } from '@testing-library/react';
+import {
+ PageLayoutType,
+ WidgetConfigurationType,
+ WidgetType,
+} from '~/generated/graphql';
+
+jest.mock('@/ui/layout/contexts/LayoutRenderingContext');
+jest.mock(
+ '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems',
+);
+
+describe('usePageLayoutWithRelationWidgets', () => {
+ const mockBasePageLayout: PageLayout = {
+ __typename: 'PageLayout',
+ id: 'test-layout',
+ name: 'Test Layout',
+ type: PageLayoutType.RECORD_PAGE,
+ objectMetadataId: 'obj-1',
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ deletedAt: null,
+ tabs: [
+ {
+ __typename: 'PageLayoutTab',
+ id: 'tab-1',
+ title: 'Fields',
+ icon: 'IconList',
+ position: 100,
+ layoutMode: 'vertical-list',
+ pageLayoutId: 'test-layout',
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ deletedAt: null,
+ widgets: [
+ {
+ __typename: 'PageLayoutWidget',
+ id: 'widget-fields',
+ pageLayoutTabId: 'tab-1',
+ title: 'Fields',
+ type: WidgetType.FIELDS,
+ objectMetadataId: null,
+ gridPosition: {
+ __typename: 'GridPosition',
+ row: 0,
+ column: 0,
+ rowSpan: 1,
+ columnSpan: 12,
+ },
+ configuration: {
+ __typename: 'FieldsConfiguration',
+ configurationType: WidgetConfigurationType.FIELDS,
+ sections: [],
+ },
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ deletedAt: null,
+ },
+ {
+ __typename: 'PageLayoutWidget',
+ id: 'widget-notes',
+ pageLayoutTabId: 'tab-1',
+ title: 'Notes',
+ type: WidgetType.NOTES,
+ objectMetadataId: null,
+ gridPosition: {
+ __typename: 'GridPosition',
+ row: 1,
+ column: 0,
+ rowSpan: 1,
+ columnSpan: 12,
+ },
+ configuration: {
+ __typename: 'NotesConfiguration',
+ configurationType: WidgetConfigurationType.NOTES,
+ },
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ deletedAt: null,
+ },
+ {
+ __typename: 'PageLayoutWidget',
+ id: 'widget-other',
+ pageLayoutTabId: 'tab-1',
+ title: 'Other',
+ type: WidgetType.GRAPH,
+ objectMetadataId: null,
+ gridPosition: {
+ __typename: 'GridPosition',
+ row: 2,
+ column: 0,
+ rowSpan: 1,
+ columnSpan: 12,
+ },
+ configuration: {
+ __typename: 'BarChartConfiguration',
+ configurationType: WidgetConfigurationType.BAR_CHART,
+ layout: 'VERTICAL',
+ aggregateOperation: 'COUNT',
+ aggregateFieldMetadataId: 'id',
+ primaryAxisGroupByFieldMetadataId: 'createdAt',
+ primaryAxisOrderBy: 'FIELD_ASC',
+ displayDataLabel: false,
+ },
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ deletedAt: null,
+ },
+ ],
+ },
+ ],
+ };
+
+ const mockRelationFields: FieldMetadataItem[] = [
+ {
+ id: 'field-1',
+ label: 'Related Companies',
+ name: 'relatedCompanies',
+ type: 'RELATION',
+ isNullable: true,
+ isActive: true,
+ isSystem: false,
+ isCustom: false,
+ defaultValue: null,
+ options: null,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ fromRelationMetadata: null,
+ toRelationMetadata: null,
+ relationDefinition: null,
+ settings: null,
+ } as FieldMetadataItem,
+ {
+ id: 'field-2',
+ label: 'Related People',
+ name: 'relatedPeople',
+ type: 'RELATION',
+ isNullable: true,
+ isActive: true,
+ isSystem: false,
+ isCustom: false,
+ defaultValue: null,
+ options: null,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ fromRelationMetadata: null,
+ toRelationMetadata: null,
+ relationDefinition: null,
+ settings: null,
+ } as FieldMetadataItem,
+ ];
+
+ beforeEach(() => {
+ (useLayoutRenderingContext as jest.Mock).mockReturnValue({
+ targetRecordIdentifier: {
+ targetObjectNameSingular: 'company',
+ },
+ layoutType: PageLayoutType.RECORD_PAGE,
+ });
+
+ (useFieldListFieldMetadataItems as jest.Mock).mockReturnValue({
+ boxedRelationFieldMetadataItems: mockRelationFields,
+ });
+ });
+
+ it('should inject relation widgets after the first FIELDS widget', () => {
+ const { result } = renderHook(() =>
+ usePageLayoutWithRelationWidgets(mockBasePageLayout),
+ );
+
+ const firstTab = result.current?.tabs[0];
+ expect(firstTab).toBeDefined();
+
+ const widgets = firstTab?.widgets || [];
+ expect(widgets.length).toBe(5); // 1 FIELDS + 2 relation + 1 NOTES + 1 OTHER
+
+ // First widget should be FIELDS
+ expect(widgets[0].type).toBe(WidgetType.FIELDS);
+ expect(widgets[0].id).toBe('widget-fields');
+
+ // Next two should be relation widgets
+ expect(widgets[1].type).toBe(WidgetType.FIELD);
+ expect(widgets[1].title).toBe('Related Companies');
+ expect(widgets[2].type).toBe(WidgetType.FIELD);
+ expect(widgets[2].title).toBe('Related People');
+
+ // Then NOTES widget
+ expect(widgets[3].type).toBe(WidgetType.NOTES);
+ expect(widgets[3].id).toBe('widget-notes');
+
+ // Finally OTHER widget
+ expect(widgets[4].type).toBe(WidgetType.GRAPH);
+ expect(widgets[4].id).toBe('widget-other');
+ });
+
+ it('should handle layout with no FIELDS widget by appending to end', () => {
+ const layoutWithoutFields: PageLayout = {
+ ...mockBasePageLayout,
+ tabs: [
+ {
+ ...mockBasePageLayout.tabs[0],
+ widgets: [
+ {
+ __typename: 'PageLayoutWidget',
+ id: 'widget-other',
+ pageLayoutTabId: 'tab-1',
+ title: 'Other',
+ type: WidgetType.GRAPH,
+ objectMetadataId: null,
+ gridPosition: {
+ __typename: 'GridPosition',
+ row: 0,
+ column: 0,
+ rowSpan: 1,
+ columnSpan: 12,
+ },
+ configuration: {
+ __typename: 'BarChartConfiguration',
+ configurationType: WidgetConfigurationType.BAR_CHART,
+ layout: 'VERTICAL',
+ aggregateOperation: 'COUNT',
+ aggregateFieldMetadataId: 'id',
+ primaryAxisGroupByFieldMetadataId: 'createdAt',
+ primaryAxisOrderBy: 'FIELD_ASC',
+ displayDataLabel: false,
+ },
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ deletedAt: null,
+ },
+ ],
+ },
+ ],
+ };
+
+ const { result } = renderHook(() =>
+ usePageLayoutWithRelationWidgets(layoutWithoutFields),
+ );
+
+ const firstTab = result.current?.tabs[0];
+ const widgets = firstTab?.widgets || [];
+
+ expect(widgets.length).toBe(3); // 1 OTHER + 2 relation
+ expect(widgets[0].type).toBe(WidgetType.GRAPH);
+ expect(widgets[1].type).toBe(WidgetType.FIELD);
+ expect(widgets[2].type).toBe(WidgetType.FIELD);
+ });
+
+ it('should return unchanged layout when no relation fields exist', () => {
+ (useFieldListFieldMetadataItems as jest.Mock).mockReturnValue({
+ boxedRelationFieldMetadataItems: [],
+ });
+
+ const { result } = renderHook(() =>
+ usePageLayoutWithRelationWidgets(mockBasePageLayout),
+ );
+
+ expect(result.current).toEqual(mockBasePageLayout);
+ });
+
+ it('should return undefined when basePageLayout is undefined', () => {
+ const { result } = renderHook(() =>
+ usePageLayoutWithRelationWidgets(undefined),
+ );
+
+ expect(result.current).toBeUndefined();
+ });
+
+ it('should return unchanged layout when not a record page', () => {
+ (useLayoutRenderingContext as jest.Mock).mockReturnValue({
+ targetRecordIdentifier: {
+ targetObjectNameSingular: 'company',
+ },
+ layoutType: PageLayoutType.DASHBOARD,
+ });
+
+ const { result } = renderHook(() =>
+ usePageLayoutWithRelationWidgets(mockBasePageLayout),
+ );
+
+ expect(result.current).toEqual(mockBasePageLayout);
+ });
+
+ it('should handle layout without Note widget', () => {
+ const layoutWithoutNotes: PageLayout = {
+ ...mockBasePageLayout,
+ tabs: [
+ {
+ ...mockBasePageLayout.tabs[0],
+ widgets: [
+ mockBasePageLayout.tabs[0].widgets[0], // FIELDS widget
+ mockBasePageLayout.tabs[0].widgets[2], // OTHER widget
+ ],
+ },
+ ],
+ };
+
+ const { result } = renderHook(() =>
+ usePageLayoutWithRelationWidgets(layoutWithoutNotes),
+ );
+
+ const firstTab = result.current?.tabs[0];
+ const widgets = firstTab?.widgets || [];
+
+ expect(widgets.length).toBe(4); // 1 FIELDS + 2 relation + 1 OTHER
+
+ expect(widgets[0].type).toBe(WidgetType.FIELDS);
+ expect(widgets[1].type).toBe(WidgetType.FIELD);
+ expect(widgets[2].type).toBe(WidgetType.FIELD);
+ expect(widgets[3].type).toBe(WidgetType.GRAPH);
+ });
+});
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutWithRelationWidgets.ts b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutWithRelationWidgets.ts
index 158a3b5854..1f4928faa1 100644
--- a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutWithRelationWidgets.ts
+++ b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutWithRelationWidgets.ts
@@ -67,9 +67,50 @@ const injectRelationWidgetsIntoLayout = (
...layout,
tabs: layout.tabs.map((tab) => {
if (tab.id === firstTab.id) {
+ const firstFieldsWidgetIndex = tab.widgets.findIndex(
+ (widget) => widget.type === WidgetType.FIELDS,
+ );
+
+ if (firstFieldsWidgetIndex === -1) {
+ return {
+ ...tab,
+ widgets: [...tab.widgets, ...relationWidgets],
+ };
+ }
+
+ // TODO: This note widget repositioning logic is temporary and will be deleted soon.
+ // We need this to ensure the note editor is displayed before record relations,
+ // matching the behavior of the old show page.
+ const noteWidgetIndex = tab.widgets.findIndex(
+ (widget) => widget.type === WidgetType.NOTES,
+ );
+
+ const widgetsBeforeRelation = tab.widgets.slice(
+ 0,
+ firstFieldsWidgetIndex + 1,
+ );
+ const widgetsAfterRelation =
+ noteWidgetIndex === -1
+ ? tab.widgets.slice(firstFieldsWidgetIndex + 1)
+ : [
+ ...tab.widgets.slice(
+ firstFieldsWidgetIndex + 1,
+ noteWidgetIndex,
+ ),
+ ...tab.widgets.slice(noteWidgetIndex + 1),
+ ];
+
+ const noteWidget =
+ noteWidgetIndex !== -1 ? [tab.widgets[noteWidgetIndex]] : [];
+
return {
...tab,
- widgets: [...tab.widgets, ...relationWidgets],
+ widgets: [
+ ...widgetsBeforeRelation,
+ ...relationWidgets,
+ ...noteWidget,
+ ...widgetsAfterRelation,
+ ],
};
}
return tab;