From 62e496f65d0d53a3224e9ca870413baf5333a577 Mon Sep 17 00:00:00 2001 From: "Abdullah." <125115953+mabdullahabaid@users.noreply.github.com> Date: Wed, 31 Dec 2025 01:28:59 +0500 Subject: [PATCH] Do not display border bottom for last widget of tab (#16856) Closes [2030](https://github.com/twentyhq/core-team-issues/issues/2030). Determines if a widget is the last visible widget in its parent tab. The hook: - Finds the tab containing the widget - Filters widgets based on visibility rules (respects conditionalDisplay and edit mode) - Returns true if the current widget is the last in the filtered list - Updated `WidgetCard`: Added isLastWidget prop to conditionally apply border-bottom for the side-column variant - Updated `WidgetRenderer`: Integrates the hook and passes isLastWidget to WidgetCard --- .../widgets/components/WidgetRenderer.tsx | 4 ++ .../hooks/useIsCurrentWidgetLastOfTab.ts | 44 +++++++++++++++++++ .../widget-card/components/WidgetCard.tsx | 7 ++- 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 packages/twenty-front/src/modules/page-layout/widgets/hooks/useIsCurrentWidgetLastOfTab.ts diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx index 5766ad54d6..00dfa75930 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx @@ -9,6 +9,7 @@ import { pageLayoutResizingWidgetIdComponentState } from '@/page-layout/states/p import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { PageLayoutWidgetForbiddenDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetForbiddenDisplay'; import { WidgetContentRenderer } from '@/page-layout/widgets/components/WidgetContentRenderer'; +import { useIsCurrentWidgetLastOfTab } from '@/page-layout/widgets/hooks/useIsCurrentWidgetLastOfTab'; import { useIsInPinnedTab } from '@/page-layout/widgets/hooks/useIsInPinnedTab'; import { useWidgetActions } from '@/page-layout/widgets/hooks/useWidgetActions'; import { useWidgetPermissions } from '@/page-layout/widgets/hooks/useWidgetPermissions'; @@ -67,6 +68,8 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => { const { currentPageLayout } = useCurrentPageLayoutOrThrow(); + const isLastWidget = useIsCurrentWidgetLastOfTab(widget.id); + // TODO: when we have more widgets without headers, we should use a more generic approach to hide the header // each widget type could have metadata (e.g., hasHeader: boolean or headerMode: 'always' | 'editOnly' | 'never') const isRichTextWidget = widget.type === WidgetType.STANDALONE_RICH_TEXT; @@ -119,6 +122,7 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => { isEditing={isEditing} isDragging={isDragging} isResizing={isResizing} + isLastWidget={isLastWidget} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} data-widget-id={widget.id} diff --git a/packages/twenty-front/src/modules/page-layout/widgets/hooks/useIsCurrentWidgetLastOfTab.ts b/packages/twenty-front/src/modules/page-layout/widgets/hooks/useIsCurrentWidgetLastOfTab.ts new file mode 100644 index 0000000000..3d9b25e6e7 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/hooks/useIsCurrentWidgetLastOfTab.ts @@ -0,0 +1,44 @@ +import { useCurrentPageLayout } from '@/page-layout/hooks/useCurrentPageLayout'; +import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState'; +import { buildWidgetVisibilityContext } from '@/page-layout/utils/buildWidgetVisibilityContext'; +import { filterVisibleWidgets } from '@/page-layout/utils/filterVisibleWidgets'; +import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { isDefined } from 'twenty-shared/utils'; + +export const useIsCurrentWidgetLastOfTab = (widgetId: string): boolean => { + const { currentPageLayout } = useCurrentPageLayout(); + const isMobile = useIsMobile(); + const { isInRightDrawer } = useLayoutRenderingContext(); + const isPageLayoutInEditMode = useRecoilComponentValue( + isPageLayoutInEditModeComponentState, + ); + + if (!isDefined(currentPageLayout)) { + return false; + } + + const tab = currentPageLayout.tabs.find((tab) => + tab.widgets.some((widget) => widget.id === widgetId), + ); + + if (!isDefined(tab)) { + return false; + } + + const visibleWidgets = isPageLayoutInEditMode + ? tab.widgets + : filterVisibleWidgets({ + widgets: tab.widgets, + context: buildWidgetVisibilityContext({ isMobile, isInRightDrawer }), + }); + + if (visibleWidgets.length === 0) { + return false; + } + + const lastWidget = visibleWidgets.at(-1); + + return lastWidget?.id === widgetId; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetCard.tsx b/packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetCard.tsx index 7dee5c9b5b..364c980022 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetCard.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetCard.tsx @@ -11,6 +11,7 @@ const StyledWidgetCard = styled.div<{ isDragging: boolean; isResizing: boolean; headerLess?: boolean; + isLastWidget?: boolean; }>` box-sizing: border-box; display: flex; @@ -28,6 +29,7 @@ const StyledWidgetCard = styled.div<{ isResizing, onClick, headerLess, + isLastWidget, }) => { if (variant === 'dashboard' && !isEditable) { return css` @@ -80,7 +82,10 @@ const StyledWidgetCard = styled.div<{ return css` background: ${theme.background.secondary}; padding: ${theme.spacing(2)}; - border-bottom: 1px solid ${theme.border.color.light}; + ${isLastWidget !== true && + css` + border-bottom: 1px solid ${theme.border.color.light}; + `} `; }