Skip to content

Commit adc2839

Browse files
BonaparaBredo
andauthored
Improve record page layout editing experience (#25087)
## Summary - Insert widgets at a chosen position using hover controls and the existing picker. - Show the expanded Add widget chooser at the bottom when possible, or above a lone full-height widget. - Click a different tab to navigate; click the current tab again to open its settings, preserving the blue outline. - Use distinct Fields group / Field icons and clearer picker labels. ## Before / After **Before:** Widget insertion used a fixed chooser, and clicking a tab also opened its settings. **After:** Insertion controls mark the intended position, and tab navigation stays separate from configuration. **Between widgets** ![Hover insertion control between widgets](https://github.com/user-attachments/assets/3b3fd707-549f-4f66-8824-f73c70810c14) **Above a lone full-height widget** ![Add widget chooser above a full-height widget](https://github.com/user-attachments/assets/f9a176a1-824c-432f-b54b-14d1c27768b4) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/25087?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Jonathan Bredo <Bredo@users.noreply.github.com>
1 parent 17e9b96 commit adc2839

38 files changed

Lines changed: 1975 additions & 236 deletions

File tree

packages/twenty-front/src/modules/page-layout/components/PageLayoutContent.tsx

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutCo
44
import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow';
55
import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
66
import { usePageLayoutTabWithVisibleWidgetsOrThrow } from '@/page-layout/hooks/usePageLayoutTabWithVisibleWidgetsOrThrow';
7-
import { StandaloneWidgetPlaceholder } from '@/page-layout/widgets/components/StandaloneWidgetPlaceholder';
87
import { RecordPageAddWidgetSection } from '@/page-layout/widgets/components/RecordPageAddWidgetSection';
8+
import { RecordPageWidgetInsertionSeparator } from '@/page-layout/widgets/components/RecordPageWidgetInsertionSeparator';
9+
import { StandaloneWidgetPlaceholder } from '@/page-layout/widgets/components/StandaloneWidgetPlaceholder';
10+
import { isViewportFillingWidgetType } from '@/page-layout/widgets/utils/isViewportFillingWidgetType';
911
import { styled } from '@linaria/react';
12+
import { isDefined } from 'twenty-shared/utils';
1013
import {
1114
PageLayoutTabLayoutMode,
1215
PageLayoutType,
@@ -23,6 +26,7 @@ export const PageLayoutContent = () => {
2326
const { layoutMode, tabId } = usePageLayoutContentContext();
2427

2528
const activeTab = usePageLayoutTabWithVisibleWidgetsOrThrow(tabId);
29+
const firstWidget = activeTab.widgets[0];
2630

2731
const { currentPageLayout } = useCurrentPageLayoutOrThrow();
2832

@@ -47,14 +51,29 @@ export const PageLayoutContent = () => {
4751
return <PageLayoutGridLayout tabId={tabId} />;
4852
}
4953

50-
const isVerticalListInEditMode = isPageLayoutInEditMode && isRecordPageLayout;
51-
5254
return (
5355
<PageLayoutVerticalList
54-
isInEditMode={isVerticalListInEditMode}
56+
isInEditMode={isPageLayoutInEditMode && isRecordPageLayout}
5557
widgets={activeTab.widgets}
58+
leadingElement={
59+
isRecordPageLayout &&
60+
isDefined(firstWidget) &&
61+
isViewportFillingWidgetType(firstWidget.type) ? (
62+
<RecordPageAddWidgetSection
63+
insertionContext={{
64+
targetWidgetId: firstWidget.id,
65+
direction: 'above',
66+
}}
67+
/>
68+
) : undefined
69+
}
5670
trailingElement={
57-
isVerticalListInEditMode ? <RecordPageAddWidgetSection /> : undefined
71+
isRecordPageLayout ? <RecordPageAddWidgetSection /> : undefined
72+
}
73+
renderWidgetSeparator={
74+
isRecordPageLayout
75+
? (widget) => <RecordPageWidgetInsertionSeparator widget={widget} />
76+
: undefined
5877
}
5978
/>
6079
);

packages/twenty-front/src/modules/page-layout/components/PageLayoutTabList.tsx

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@ import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
4040
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
4141
import { type PageLayoutWidgetDndData } from '@/page-layout/types/PageLayoutWidgetDndData';
4242
import { shouldEnableTabEditingFeatures } from '@/page-layout/utils/shouldEnableTabEditingFeatures';
43+
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
4344
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
4445
import { TabListDropdown } from '@/ui/layout/tab-list/components/TabListDropdown';
4546
import { TabListFromUrlOptionalEffect } from '@/ui/layout/tab-list/components/TabListFromUrlOptionalEffect';
4647
import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
4748
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
48-
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
4949
import { isDefined } from 'twenty-shared/utils';
5050
import { themeCssVariables } from 'twenty-ui/theme-constants';
5151
import {
@@ -267,11 +267,13 @@ export const PageLayoutTabList = ({
267267
});
268268

269269
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
270-
const pageLayoutTabSettingsOpenTabId = useAtomComponentStateValue(
271-
pageLayoutTabSettingsOpenTabIdComponentState,
272-
pageLayoutId,
273-
);
270+
const [pageLayoutTabSettingsOpenTabId, setPageLayoutTabSettingsOpenTabId] =
271+
useAtomComponentState(
272+
pageLayoutTabSettingsOpenTabIdComponentState,
273+
pageLayoutId,
274+
);
274275
const { openTabSettings } = useOpenPageLayoutTabSettings(pageLayoutId);
276+
const { closeSidePanelMenu } = useSidePanelMenu();
275277

276278
const isTabSettingsOpen = isDefined(pageLayoutTabSettingsOpenTabId);
277279

@@ -295,7 +297,13 @@ export const PageLayoutTabList = ({
295297
);
296298

297299
const handleSelectTab = useCallback(
298-
(tabId: string) => {
300+
({
301+
tabId,
302+
select = selectTab,
303+
}: {
304+
tabId: string;
305+
select?: (tabId: string) => void;
306+
}) => {
299307
const shouldOpenSettings =
300308
isPageLayoutInEditMode &&
301309
shouldEnableTabEditingFeatures(pageLayoutType);
@@ -306,48 +314,34 @@ export const PageLayoutTabList = ({
306314
}
307315

308316
if (shouldOpenSettings && isTabSettingsOpen) {
309-
openTabSettings(tabId);
317+
if (pageLayoutType === PageLayoutType.RECORD_PAGE) {
318+
closeSidePanelMenu();
319+
setPageLayoutTabSettingsOpenTabId(null);
320+
} else {
321+
openTabSettings(tabId);
322+
}
310323
}
311324

312-
selectTab(tabId);
325+
select(tabId);
313326
},
314327
[
315328
isPageLayoutInEditMode,
316329
pageLayoutType,
317330
activeTabId,
318331
isTabSettingsOpen,
332+
closeSidePanelMenu,
333+
setPageLayoutTabSettingsOpenTabId,
319334
openTabSettings,
320335
selectTab,
321336
],
322337
);
323338

324339
const handleSelectTabFromDropdown = useCallback(
325340
(tabId: string) => {
326-
const shouldOpenSettings =
327-
isPageLayoutInEditMode &&
328-
shouldEnableTabEditingFeatures(pageLayoutType);
329-
330-
if (shouldOpenSettings && activeTabId === tabId) {
331-
openTabSettings(tabId);
332-
closeOverflowDropdown();
333-
return;
334-
}
335-
336-
if (shouldOpenSettings && isTabSettingsOpen) {
337-
openTabSettings(tabId);
338-
}
339-
340-
selectTabFromDropdown(tabId);
341+
handleSelectTab({ tabId, select: selectTabFromDropdown });
342+
closeOverflowDropdown();
341343
},
342-
[
343-
isPageLayoutInEditMode,
344-
pageLayoutType,
345-
activeTabId,
346-
isTabSettingsOpen,
347-
openTabSettings,
348-
closeOverflowDropdown,
349-
selectTabFromDropdown,
350-
],
344+
[handleSelectTab, closeOverflowDropdown, selectTabFromDropdown],
351345
);
352346

353347
if (tabsWithIcons.length === 0) {
@@ -435,7 +429,7 @@ export const PageLayoutTabList = ({
435429
behaveAsLinks={behaveAsLinks}
436430
loading={loading}
437431
onChangeTab={onChangeTab}
438-
onSelectTab={handleSelectTab}
432+
onSelectTab={(tabId) => handleSelectTab({ tabId })}
439433
canReorder={canReorderTabs}
440434
widgetDropTargetWidgetsByTabId={widgetDropTargetWidgetsByTabId}
441435
firstHiddenTabId={

packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListNewTabDropdownContent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,11 @@ export const PageLayoutTabListNewTabDropdownContent = ({
5454
(tabId: string) => {
5555
updatePageLayoutTab(tabId, { isActive: true });
5656
setActiveTabId(tabId);
57-
setPageLayoutTabSettingsOpenTabId(tabId);
5857
navigatePageLayoutSidePanel({
5958
sidePanelPage: SidePanelPages.PageLayoutTabSettings,
6059
resetNavigationStack: true,
6160
});
61+
setPageLayoutTabSettingsOpenTabId(tabId);
6262
closeDropdown(dropdownId);
6363
},
6464
[

packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListReorderableOverflowDropdown.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,10 @@ export const PageLayoutTabListReorderableOverflowDropdown = ({
103103
};
104104

105105
const handleEditClick = (tabId: string) => {
106-
setPageLayoutTabSettingsOpenTabId(tabId);
107106
navigatePageLayoutSidePanel({
108107
sidePanelPage: SidePanelPages.PageLayoutTabSettings,
109108
});
109+
setPageLayoutTabSettingsOpenTabId(tabId);
110110
onClose();
111111
};
112112

packages/twenty-front/src/modules/page-layout/components/PageLayoutTabListReorderableTab.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import { PAGE_LAYOUT_TAB_DND_TYPE } from '@/page-layout/constants/PageLayoutTabD
33
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
44
import { type PageLayoutTabDragData } from '@/page-layout/types/PageLayoutTabDragData';
55
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
6+
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
67
import { type SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
78
import { DragDropItemSortableCell } from '@/ui/utilities/drag-and-drop/components/DragDropItemSortableCell';
89
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
910
import { styled } from '@linaria/react';
1011
import { isDefined } from 'twenty-shared/utils';
1112
import { StyledTabContainer, TabContent } from 'twenty-ui/input';
1213
import { themeCssVariables } from 'twenty-ui/theme-constants';
14+
import { PageLayoutType } from '~/generated-metadata/graphql';
1315

1416
type PageLayoutTabListReorderableTabProps = {
1517
tab: SingleTabProps;
@@ -22,10 +24,10 @@ type PageLayoutTabListReorderableTabProps = {
2224
onSelect: () => void;
2325
};
2426

25-
const StyledTabContentWrapper = styled.div<{ isBeingEdited: boolean }>`
27+
const StyledTabContentWrapper = styled.div<{ isHighlighted: boolean }>`
2628
border-radius: ${themeCssVariables.border.radius.sm};
27-
outline: ${({ isBeingEdited }) =>
28-
isBeingEdited ? `1px solid ${themeCssVariables.color.blue}` : 'none'};
29+
outline: ${({ isHighlighted }) =>
30+
isHighlighted ? `1px solid ${themeCssVariables.color.blue}` : 'none'};
2931
outline-offset: -1px;
3032
`;
3133

@@ -39,11 +41,15 @@ export const PageLayoutTabListReorderableTab = ({
3941
widgetDropTargetWidgets,
4042
onSelect,
4143
}: PageLayoutTabListReorderableTabProps) => {
44+
const { layoutType } = useLayoutRenderingContext();
4245
const pageLayoutTabSettingsOpenTabId = useAtomComponentStateValue(
4346
pageLayoutTabSettingsOpenTabIdComponentState,
4447
);
4548

46-
const isSettingsOpenForThisTab = pageLayoutTabSettingsOpenTabId === tab.id;
49+
const isHighlighted =
50+
layoutType === PageLayoutType.RECORD_PAGE
51+
? isActive
52+
: pageLayoutTabSettingsOpenTabId === tab.id;
4753

4854
const tabDragData: PageLayoutTabDragData = {
4955
type: 'tab',
@@ -69,7 +75,7 @@ export const PageLayoutTabListReorderableTab = ({
6975
active={isActive}
7076
disabled={disabled}
7177
>
72-
<StyledTabContentWrapper isBeingEdited={isSettingsOpenForThisTab}>
78+
<StyledTabContentWrapper isHighlighted={isHighlighted}>
7379
<TabContent
7480
id={tab.id}
7581
active={isActive}

packages/twenty-front/src/modules/page-layout/components/PageLayoutTabMenuItemSelectAvatar.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,10 @@ export const PageLayoutTabMenuItemSelectAvatar = ({
8080
Icon={IconPencil}
8181
size="small"
8282
accent="tertiary"
83-
onClick={() => onEditClick?.(tab.id)}
83+
onClick={(event) => {
84+
event.stopPropagation();
85+
onEditClick?.(tab.id);
86+
}}
8487
/>
8588
</div>
8689
)}

packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalList.tsx

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
55
import { type PageLayoutWidgetListDropData } from '@/page-layout/types/PageLayoutWidgetListDropData';
66
import { canVerticalListAcceptWidgetDrag } from '@/page-layout/utils/canVerticalListAcceptWidgetDrag';
77
import { getIsSingleWidgetTab } from '@/page-layout/utils/getIsSingleWidgetTab';
8+
import { isViewportFillingWidgetType } from '@/page-layout/widgets/utils/isViewportFillingWidgetType';
89
import { DragDropItemDropTarget } from '@/ui/utilities/drag-and-drop/components/DragDropItemDropTarget';
910
import { WorkflowDiagramAllowPageScrollContext } from '@/workflow/workflow-diagram/contexts/WorkflowDiagramAllowPageScrollContext';
1011
import { type Draggable } from '@dnd-kit/abstract';
1112
import { pointerIntersection } from '@dnd-kit/collision';
1213
import { useDroppable } from '@dnd-kit/react';
1314
import { styled } from '@linaria/react';
14-
import { type ReactNode, useCallback } from 'react';
15+
import { Fragment, type ReactNode, useCallback } from 'react';
16+
import { isDefined } from 'twenty-shared/utils';
1517
import { themeCssVariables } from 'twenty-ui/theme-constants';
1618
import { PageLayoutTabLayoutMode } from '~/generated-metadata/graphql';
1719

@@ -41,8 +43,6 @@ const StyledVerticalListContainer = styled.div<{
4143
background: var(--record-card-background-color);
4244
display: flex;
4345
flex-direction: column;
44-
gap: ${({ isInEditMode }) =>
45-
isInEditMode ? themeCssVariables.spacing[4] : '0'};
4646
min-height: ${({ isInEditMode }) => (isInEditMode ? '0' : '100%')};
4747
// The pinned tab sits next to the main tab area, so while editing it takes
4848
// that area's vertical padding to line their widgets up, and keeps the
@@ -58,25 +58,37 @@ const StyledVerticalListContainer = styled.div<{
5858
: '0'};
5959
`;
6060

61+
const StyledHeader = styled.div`
62+
flex-shrink: 0;
63+
margin-bottom: ${themeCssVariables.spacing[4]};
64+
`;
65+
6166
const StyledDropTarget = styled.div`
6267
display: flex;
6368
flex: 1;
6469
flex-direction: column;
65-
gap: ${themeCssVariables.spacing[4]};
6670
min-height: ${themeCssVariables.spacing[6]};
6771
position: relative;
72+
73+
&:not(:first-child) {
74+
margin-top: ${themeCssVariables.spacing[4]};
75+
}
6876
`;
6977

7078
type PageLayoutVerticalListProps = {
7179
isInEditMode: boolean;
7280
widgets: PageLayoutWidget[];
81+
leadingElement?: ReactNode;
7382
trailingElement?: ReactNode;
83+
renderWidgetSeparator?: (widget: PageLayoutWidget) => ReactNode;
7484
};
7585

7686
export const PageLayoutVerticalList = ({
7787
isInEditMode,
7888
widgets,
89+
leadingElement,
7990
trailingElement,
91+
renderWidgetSeparator,
8092
}: PageLayoutVerticalListProps) => {
8193
const { layoutMode, tabId } = usePageLayoutContentContext();
8294

@@ -99,6 +111,11 @@ export const PageLayoutVerticalList = ({
99111
// (workflow canvases) must keep it when there is no page scroll to reach.
100112
const hasPageScroll = isInEditMode || widgets.length > 1;
101113

114+
const firstViewportFillingWidgetIndex = widgets.findIndex((widget) =>
115+
isViewportFillingWidgetType(widget.type),
116+
);
117+
const hasViewportFillingWidget = firstViewportFillingWidgetIndex !== -1;
118+
102119
const endDropData: PageLayoutWidgetListDropData = {
103120
type: 'widget-list',
104121
tabId,
@@ -119,7 +136,7 @@ export const PageLayoutVerticalList = ({
119136
accept: canAcceptWidgetDrag,
120137
collisionDetector: pointerIntersection,
121138
data: endDropData,
122-
disabled: !isInEditMode,
139+
disabled: !isInEditMode || hasViewportFillingWidget,
123140
});
124141

125142
return (
@@ -130,20 +147,29 @@ export const PageLayoutVerticalList = ({
130147
shouldUseWhiteBackground={!isInPinnedTab || isMobile}
131148
>
132149
<WorkflowDiagramAllowPageScrollContext.Provider value={hasPageScroll}>
150+
{isInEditMode && isDefined(leadingElement) && (
151+
<StyledHeader>{leadingElement}</StyledHeader>
152+
)}
133153
{widgets.map((widget, index) => (
134-
<PageLayoutVerticalListWidgetSlot
135-
canAcceptWidgetDrag={canAcceptWidgetDrag}
136-
index={index}
137-
isInEditMode={isInEditMode}
138-
isSoloCanvasPresentation={shouldUseSoloCanvasPresentation}
139-
key={widget.id}
140-
layoutMode={layoutMode}
141-
shouldShowDivider={isSideColumnContext}
142-
tabId={tabId}
143-
widget={widget}
144-
/>
154+
<Fragment key={widget.id}>
155+
{isInEditMode &&
156+
index > 0 &&
157+
(!hasViewportFillingWidget ||
158+
index <= firstViewportFillingWidgetIndex) &&
159+
renderWidgetSeparator?.(widget)}
160+
<PageLayoutVerticalListWidgetSlot
161+
canAcceptWidgetDrag={canAcceptWidgetDrag}
162+
index={index}
163+
isInEditMode={isInEditMode}
164+
isSoloCanvasPresentation={shouldUseSoloCanvasPresentation}
165+
layoutMode={layoutMode}
166+
shouldShowDivider={isSideColumnContext}
167+
tabId={tabId}
168+
widget={widget}
169+
/>
170+
</Fragment>
145171
))}
146-
{isInEditMode && (
172+
{isInEditMode && !hasViewportFillingWidget && (
147173
<StyledDropTarget ref={endDropZoneRef}>
148174
<DragDropItemDropTarget
149175
index={widgets.length}

0 commit comments

Comments
 (0)