From 8fa1821e1afdb60c6ecda7ed22e914779b06af48 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Mon, 8 Sep 2025 12:13:50 +0200 Subject: [PATCH] Refactored table body and footer with divs (#14346) This PR refactors what is remaining of HTML table API to divs. It is mainly about the body and aggregate footer. Because a `position: sticky` creates a stacking context, and because a div wrapping other divs prevents those children divs from being sticky, it has been found that removing the wrapping container of both header and footer allows us to have all z-index in the same stacking context and create the right experience. Though the fine-tuning of z-index will be done in another PR. There are many fixes left that will be addressed very soon in subsequent PRs. This PR focuses on bringing a functional table both with and without RecordGroups. (Check Task views for that) --- .../components/RecordTableContent.tsx | 119 +++++++++++++++++- .../RecordTableNoRecordGroupRows.tsx | 9 -- .../RecordTableStickyBottomEffect.tsx | 27 ---- .../components/RecordTableStickyEffect.tsx | 32 ----- .../components/RecordTableStyles.tsx | 10 +- .../record-table/constants/TableZIndex.ts | 2 +- .../components/RecordTableBody.tsx | 15 +-- .../components/RecordTableBodyDroppable.tsx | 35 ++---- .../RecordTableBodyFetchMoreLoader.tsx | 11 +- .../RecordTableNoRecordGroupBody.tsx | 4 + .../components/RecordTableTd.tsx | 3 +- .../components/RecordTableAggregateFooter.tsx | 102 ++------------- .../RecordTableAggregateFooterCell.tsx | 19 +-- .../components/RecordTableHeader.tsx | 118 +---------------- .../RecordTableHeaderAddColumnButton.tsx | 4 + .../components/RecordTableHeaderCell.tsx | 5 +- .../RecordTableHeaderCheckboxColumn.tsx | 2 + .../RecordTableHeaderDragDropColumn.tsx | 13 +- .../RecordTableHeaderLastColumn.tsx | 7 +- .../components/RecordTableActionRow.tsx | 101 ++++++++++++--- .../components/RecordTableDraggableTr.tsx | 4 +- .../components/RecordTableTr.tsx | 19 ++- .../RecordTableRecordGroupSection.tsx | 112 ++++++++++++++--- .../src/utils/array/filterOutByProperty.ts | 5 + .../src/utils/array/findByProperty.ts | 2 +- .../src/utils/array/sumByProperty.ts | 17 +++ packages/twenty-shared/src/utils/index.ts | 2 + 27 files changed, 413 insertions(+), 386 deletions(-) create mode 100644 packages/twenty-shared/src/utils/array/filterOutByProperty.ts create mode 100644 packages/twenty-shared/src/utils/array/sumByProperty.ts diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContent.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContent.tsx index 55d2dfdc893..9d40f650019 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContent.tsx @@ -1,23 +1,105 @@ import { RecordTableStickyBottomEffect } from '@/object-record/record-table/components/RecordTableStickyBottomEffect'; import { RecordTableStickyEffect } from '@/object-record/record-table/components/RecordTableStickyEffect'; -import { StyledTableDiv } from '@/object-record/record-table/components/RecordTableStyles'; +import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex'; import { RecordTableNoRecordGroupBody } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody'; import { RecordTableRecordGroupsBody } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody'; import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader'; import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; +import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState'; +import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; import { RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS } from '@/ui/utilities/drag-select/constants/RecordIndecDragSelectBoundaryClass'; import { useRecoilComponentFamilyCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyCallbackState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import styled from '@emotion/styled'; import { useRef, useState } from 'react'; import { useRecoilCallback } from 'recoil'; +import { MOBILE_VIEWPORT } from 'twenty-ui/theme'; -const StyledTableWithPointerEvents = styled(StyledTableDiv)<{ +const StyledTableWithPointerEvents = styled.div<{ isDragging: boolean; + stickyColumnZIndex: number; + normalColumnZIndex: number; }>` & > * { pointer-events: ${({ isDragging }) => (isDragging ? 'none' : 'auto')}; } + + display: flex; + flex-wrap: wrap; + + div.header-cell { + position: sticky; + top: 0; + } + + div.header-cell:nth-of-type(n + 3) { + z-index: ${({ normalColumnZIndex }) => normalColumnZIndex}; + } + + div.header-cell:nth-of-type(1) { + // position: sticky; + left: 0px; + z-index: ${({ stickyColumnZIndex }) => stickyColumnZIndex}; + transition: 0.3s ease; + background-color: ${({ theme }) => theme.background.primary}; + } + + div.header-cell:nth-of-type(2) { + // position: sticky; + left: 16px; + top: 0; + z-index: ${({ stickyColumnZIndex }) => stickyColumnZIndex}; + transition: 0.3s ease; + background-color: ${({ theme }) => theme.background.primary}; + } + + div.header-cell:nth-of-type(3) { + // position: sticky; + left: 48px; + right: 0; + z-index: ${({ stickyColumnZIndex }) => stickyColumnZIndex}; + transition: 0.3s ease; + background-color: ${({ theme }) => theme.background.primary}; + + // &::after { + // content: ''; + // position: absolute; + // top: -1px; + // height: calc(100% + 2px); + // width: 4px; + // right: 0px; + // box-shadow: ${({ theme }) => theme.boxShadow.light}; + // clip-path: inset(0px -4px 0px 0px); + // } + + @media (max-width: ${MOBILE_VIEWPORT}px) { + width: 38px; + max-width: 38px; + min-width: 38px; + } + } + + div.footer-cell:nth-of-type(n + 3) { + z-index: ${TABLE_Z_INDEX.footer.default}; + + position: sticky; + bottom: 0; + } + + div.footer-cell:nth-of-type(1) { + z-index: ${TABLE_Z_INDEX.footer.stickyColumn}; + left: 0px; + bottom: 0; + position: sticky; + } + + div.footer-cell:nth-of-type(2) { + z-index: ${TABLE_Z_INDEX.footer.stickyColumn}; + left: 48px; + bottom: 0; + position: sticky; + } `; const StyledTableContainer = styled.div` @@ -66,9 +148,40 @@ export const RecordTableContent = ({ [isRowSelectedCallbackFamilyState], ); + const isRecordTableScrolledHorizontally = useRecoilComponentValue( + isRecordTableScrolledHorizontallyComponentState, + ); + + const isRecordTableScrolledVertically = useRecoilComponentValue( + isRecordTableScrolledVerticallyComponentState, + ); + + const computedStickyColumnZIndex = + isRecordTableScrolledHorizontally && isRecordTableScrolledVertically + ? TABLE_Z_INDEX.scrolledBothVerticallyAndHorizontally.headerColumnsSticky + : isRecordTableScrolledHorizontally + ? TABLE_Z_INDEX.scrolledHorizontallyOnly.headerColumnsSticky + : isRecordTableScrolledVertically + ? TABLE_Z_INDEX.scrolledVerticallyOnly.headerColumnsSticky + : TABLE_Z_INDEX.noScrollAtAll.headerColumnsSticky; + + const computedNormalColumnZIndex = + isRecordTableScrolledHorizontally && isRecordTableScrolledVertically + ? TABLE_Z_INDEX.scrolledBothVerticallyAndHorizontally.headerColumnsNormal + : isRecordTableScrolledHorizontally + ? TABLE_Z_INDEX.scrolledHorizontallyOnly.headerColumnsNormal + : isRecordTableScrolledVertically + ? TABLE_Z_INDEX.scrolledVerticallyOnly.headerColumnsNormal + : TABLE_Z_INDEX.noScrollAtAll.headerColumnsNormal; + return ( - + {hasRecordGroups ? ( diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupRows.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupRows.tsx index aede2642108..c293724af22 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupRows.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupRows.tsx @@ -2,9 +2,7 @@ import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record import { RecordTableAddNew } from '@/object-record/record-table/components/RecordTableAddNew'; import { RecordTableBodyDroppablePlaceholder } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppablePlaceholder'; import { RecordTableBodyFetchMoreLoader } from '@/object-record/record-table/record-table-body/components/RecordTableBodyFetchMoreLoader'; -import { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter'; import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow'; -import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; export const RecordTableNoRecordGroupRows = () => { @@ -12,10 +10,6 @@ export const RecordTableNoRecordGroupRows = () => { recordIndexAllRecordIdsComponentSelector, ); - const isRecordTableInitialLoading = useRecoilComponentValue( - isRecordTableInitialLoadingComponentState, - ); - return ( <> {allRecordIds.map((recordId, rowIndex) => { @@ -31,9 +25,6 @@ export const RecordTableNoRecordGroupRows = () => { - {!isRecordTableInitialLoading && allRecordIds.length > 0 && ( - - )} ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyBottomEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyBottomEffect.tsx index 325e9a43388..a0f744beac4 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyBottomEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyBottomEffect.tsx @@ -1,30 +1,3 @@ -import { useEffect } from 'react'; - -import { scrollWrapperScrollBottomComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollBottomComponentState'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; - export const RecordTableStickyBottomEffect = () => { - const scrollBottom = useRecoilComponentValue( - scrollWrapperScrollBottomComponentState, - ); - - useEffect(() => { - if (scrollBottom > 1) { - document - .getElementById('record-table-body') - ?.classList.add('footer-sticky'); - document - .getElementById('record-table-footer') - ?.classList.add('footer-sticky'); - } else { - document - .getElementById('record-table-body') - ?.classList.remove('footer-sticky'); - document - .getElementById('record-table-footer') - ?.classList.remove('footer-sticky'); - } - }, [scrollBottom]); - return <>; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyEffect.tsx index 5f51863498e..a06a9378851 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyEffect.tsx @@ -18,16 +18,6 @@ export const RecordTableStickyEffect = () => { useEffect(() => { setIsRecordTableScrolledVertically(scrollTop > 0); - - if (scrollTop > 0) { - document - .getElementById('record-table-header') - ?.classList.add('header-sticky'); - } else { - document - .getElementById('record-table-header') - ?.classList.remove('header-sticky'); - } }, [scrollTop, setIsRecordTableScrolledVertically]); const scrollLeft = useRecoilComponentValue( @@ -40,28 +30,6 @@ export const RecordTableStickyEffect = () => { useEffect(() => { setIsRecordTableScrolledHorizontally(scrollLeft > 0); - - if (scrollLeft > 0) { - document - .getElementById('record-table-body') - ?.classList.add('first-columns-sticky'); - document - .getElementById('record-table-header') - ?.classList.add('first-columns-sticky'); - document - .getElementById('record-table-footer') - ?.classList.add('first-columns-sticky'); - } else { - document - .getElementById('record-table-body') - ?.classList.remove('first-columns-sticky'); - document - .getElementById('record-table-header') - ?.classList.remove('first-columns-sticky'); - document - .getElementById('record-table-footer') - ?.classList.remove('first-columns-sticky'); - } }, [scrollLeft, setIsRecordTableScrolledHorizontally]); return <>; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStyles.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStyles.tsx index 4d67f8eb864..996447597c4 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStyles.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStyles.tsx @@ -1,11 +1,3 @@ import styled from '@emotion/styled'; -export const StyledTableDiv = styled.div` - border-radius: ${({ theme }) => theme.border.radius.sm}; - border-spacing: 0; - width: 100%; - - .footer-sticky tr:nth-last-of-type(2) td { - border-bottom-color: ${({ theme }) => theme.background.transparent}; - } -`; +export const StyledTableDiv = styled.div``; diff --git a/packages/twenty-front/src/modules/object-record/record-table/constants/TableZIndex.ts b/packages/twenty-front/src/modules/object-record/record-table/constants/TableZIndex.ts index c7179596116..ce735819d0c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/constants/TableZIndex.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/constants/TableZIndex.ts @@ -6,7 +6,7 @@ export const TABLE_Z_INDEX = { editMode: 20, }, footer: { - default: 12, + default: 18, stickyColumn: 20, }, noScrollAtAll: { diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx index e7d8ecc7a7f..43356d772f4 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx @@ -3,32 +3,33 @@ import styled from '@emotion/styled'; import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex'; import { MOBILE_VIEWPORT } from 'twenty-ui/theme'; -const StyledTbody = styled.tbody` +const StyledTbody = styled.div` // TODO: re-implement horizontal scroll here after table have been refactored to divs - td:nth-of-type(1) { + div.table-cell:nth-of-type(1) { position: sticky; left: 0px; z-index: ${TABLE_Z_INDEX.cell.sticky}; } - td:nth-of-type(2) { + div.table-cell:nth-of-type(2) { position: sticky; left: 16px; z-index: ${TABLE_Z_INDEX.cell.sticky}; } - tr:not(:last-child) td:nth-of-type(3) { + div.table-cell:nth-of-type(3) { position: sticky; - left: 49px; + left: 48px; z-index: ${TABLE_Z_INDEX.cell.sticky}; - } - td:nth-of-type(3) { @media (max-width: ${MOBILE_VIEWPORT}px) { width: ${38}px; max-width: ${38}px; } } + + display: flex; + flex-wrap: wrap; `; export const RecordTableBody = StyledTbody; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDroppable.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDroppable.tsx index bbadbab21a8..0593f5e6f3e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDroppable.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDroppable.tsx @@ -2,19 +2,10 @@ import { RecordTableBody } from '@/object-record/record-table/record-table-body/ import { RecordTableBodyDroppableContextProvider } from '@/object-record/record-table/record-table-body/contexts/RecordTableBodyDroppableContext'; import { recordTableHoverPositionComponentState } from '@/object-record/record-table/states/recordTableHoverPositionComponentState'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; -import styled from '@emotion/styled'; import { Droppable } from '@hello-pangea/dnd'; import { type ReactNode, useState } from 'react'; import { v4 } from 'uuid'; -const StyledTable = styled.table` - table-layout: fixed; - - border-radius: ${({ theme }) => theme.border.radius.sm}; - border-spacing: 0; - width: 100%; -`; - type RecordTableBodyDroppableProps = { children: ReactNode; recordGroupId?: string; @@ -39,21 +30,19 @@ export const RecordTableBodyDroppable = ({ isDropDisabled={isDropDisabled} > {(provided) => ( - - setRecordTableHoverPosition(null)} + setRecordTableHoverPosition(null)} + > + - - {children} - - - + {children} + + )} ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFetchMoreLoader.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFetchMoreLoader.tsx index 428856610fe..75418e5e88c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFetchMoreLoader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFetchMoreLoader.tsx @@ -56,13 +56,14 @@ export const RecordTableBodyFetchMoreLoader = () => { if (!showLoadingMoreRow) { return <>; } + // TODO: fix here styling return ( - - +
+
Loading more... - - - +
+
+
); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody.tsx index 469ad11008f..31a0511bd7d 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody.tsx @@ -5,6 +5,7 @@ import { RecordTableBodyDragDropContextProvider } from '@/object-record/record-t import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable'; import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading'; import { RecordTableCellPortals } from '@/object-record/record-table/record-table-cell/components/RecordTableCellPortals'; +import { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; @@ -28,6 +29,9 @@ export const RecordTableNoRecordGroupBody = () => { + {!isRecordTableInitialLoading && allRecordIds.length > 0 && ( + + )} ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx index b20e29c745c..034227018d3 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableTd.tsx @@ -5,7 +5,7 @@ import { ThemeContext } from 'twenty-ui/theme'; export const DEFAULT_RECORD_TABLE_TD_WIDTH = 32; -const StyledTd = styled.td<{ +const StyledTd = styled.div<{ backgroundColor: string; borderColor: string; isDragging?: boolean; @@ -74,6 +74,7 @@ export const RecordTableTd = ({ width={width} // eslint-disable-next-line react/jsx-props-no-spreading {...dragHandleProps} + className="table-cell" > {children} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx index 36159d60427..737dee31b7d 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx @@ -4,81 +4,14 @@ import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZInde import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableAggregateFooterCell } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell'; import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext'; -import { FIRST_TH_WIDTH } from '@/object-record/record-table/record-table-header/components/RecordTableHeader'; -import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement'; -import { isUndefined } from '@sniptt/guards'; -import { MOBILE_VIEWPORT } from 'twenty-ui/theme'; -const StyledTd = styled.td` +const StyledPlaceholderFirstCell = styled.div` background-color: ${({ theme }) => theme.background.primary}; -`; - -const StyledTableRow = styled.tr<{ - hasHorizontalOverflow?: boolean; -}>` - z-index: ${TABLE_Z_INDEX.footer.default}; + width: 48px; position: sticky; - border: none; - - // TODO: see how we reimplement horizontal scrolling after all table has been refactored - td { - border-top: ${({ theme }) => `1px solid ${theme.border.color.light}`}; - z-index: ${TABLE_Z_INDEX.footer.default}; - position: sticky; - bottom: 0; - } - - cursor: pointer; - td:nth-of-type(1) { - width: ${FIRST_TH_WIDTH}; - left: 0; - border-top: none; - } - - td:nth-of-type(1) { - position: sticky; - z-index: ${TABLE_Z_INDEX.footer.stickyColumn}; - } - td:nth-of-type(2) { - position: sticky; - z-index: ${TABLE_Z_INDEX.footer.stickyColumn}; - transition: 0.3s ease; - &::after { - content: ''; - position: absolute; - top: -1px; - height: calc(100% + 2px); - width: 4px; - right: 0px; - box-shadow: ${({ theme }) => theme.boxShadow.light}; - clip-path: inset(0px -4px 0px 0px); - } - @media (max-width: ${MOBILE_VIEWPORT}px) { - width: 38px; - max-width: 38px; - } - } - - background: ${({ theme }) => theme.background.primary}; - ${({ hasHorizontalOverflow }) => - `.footer-sticky { - bottom: ${hasHorizontalOverflow ? '10px' : '0'}; - ${ - hasHorizontalOverflow && - ` - &::after { - content: ''; - position: absolute; - bottom: -10px; - left: 0; - right: 0; - height: 10px; - background: inherit; - } - } - ` - } - `} + left: 0px; + bottom: 0; + z-index: ${TABLE_Z_INDEX.footer.stickyColumn}; `; export const RecordTableAggregateFooter = ({ @@ -88,21 +21,9 @@ export const RecordTableAggregateFooter = ({ }) => { const { visibleRecordFields } = useRecordTableContextOrThrow(); - const { scrollWrapperHTMLElement } = useScrollWrapperElement(); - - const hasHorizontalOverflow = - (scrollWrapperHTMLElement?.scrollWidth ?? 0) > - (scrollWrapperHTMLElement?.clientWidth ?? 0); - return ( - - + <> + {visibleRecordFields.map((recordField, index) => { return ( ); })} - - - - + {/* TODO: fix span for divs styling here colSpan={visibleRecordFields.length - 1}*/} +
+
+
+ ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx index fb14e6b2333..b1092e72b19 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx @@ -1,23 +1,21 @@ import styled from '@emotion/styled'; import { useContext } from 'react'; +import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext'; import { RecordTableColumnFooterWithDropdown } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterWithDropdown'; import { findByProperty, isDefined } from 'twenty-shared/utils'; -const COLUMN_MIN_WIDTH = 104; - -const StyledColumnFooterCell = styled.td<{ +const StyledColumnFooterCell = styled.div<{ columnWidth: number; isFirstCell?: boolean; }>` background-color: ${({ theme }) => theme.background.primary}; color: ${({ theme }) => theme.font.color.tertiary}; - overflow: hidden; + padding: 0; - position: relative; ${({ columnWidth }) => ` min-width: ${columnWidth}px; width: ${columnWidth}px; @@ -36,6 +34,11 @@ const StyledColumnFooterCell = styled.td<{ }}; height: 32px; + position: sticky; + left: 48px; + bottom: 0; + z-index: ${TABLE_Z_INDEX.footer.stickyColumn}; + user-select: none; overflow: auto; scrollbar-width: none; @@ -74,9 +77,11 @@ export const RecordTableAggregateFooterCell = ({ return ( ` - cursor: pointer; - - display: flex; - flex-direction: row; - align-items: center; - - height: 32px; - background-color: ${({ theme }) => theme.background.primary}; - - div.header-cell:nth-of-type(n + 3) { - z-index: ${({ normalColumnZIndex }) => normalColumnZIndex}; - } - - div.header-cell:nth-of-type(1) { - position: sticky; - left: 0px; - z-index: ${({ stickyColumnZIndex }) => stickyColumnZIndex}; - transition: 0.3s ease; - background-color: ${({ theme }) => theme.background.primary}; - } - - div.header-cell:nth-of-type(2) { - position: sticky; - left: 17px; - top: 0; - z-index: ${({ stickyColumnZIndex }) => stickyColumnZIndex}; - transition: 0.3s ease; - background-color: ${({ theme }) => theme.background.primary}; - } - - div.header-cell:nth-of-type(3) { - position: sticky; - left: 49px; - right: 0; - z-index: ${({ stickyColumnZIndex }) => stickyColumnZIndex}; - transition: 0.3s ease; - background-color: ${({ theme }) => theme.background.primary}; - - // &::after { - // content: ''; - // position: absolute; - // top: -1px; - // height: calc(100% + 2px); - // width: 4px; - // right: 0px; - // box-shadow: ${({ theme }) => theme.boxShadow.light}; - // clip-path: inset(0px -4px 0px 0px); - // } - - @media (max-width: ${MOBILE_VIEWPORT}px) { - width: 38px; - max-width: 38px; - min-width: 38px; - } - } - - position: sticky; - top: 0px; - z-index: ${({ entireRowZIndex }) => entireRowZIndex}; -`; - export const RecordTableHeader = () => { const { visibleRecordFields } = useRecordTableContextOrThrow(); - const isRecordTableScrolledHorizontally = useRecoilComponentValue( - isRecordTableScrolledHorizontallyComponentState, - ); - - const isRecordTableScrolledVertically = useRecoilComponentValue( - isRecordTableScrolledVerticallyComponentState, - ); - - const computedStickyColumnZIndex = - isRecordTableScrolledHorizontally && isRecordTableScrolledVertically - ? TABLE_Z_INDEX.scrolledBothVerticallyAndHorizontally.headerColumnsSticky - : isRecordTableScrolledHorizontally - ? TABLE_Z_INDEX.scrolledHorizontallyOnly.headerColumnsSticky - : isRecordTableScrolledVertically - ? TABLE_Z_INDEX.scrolledVerticallyOnly.headerColumnsSticky - : TABLE_Z_INDEX.noScrollAtAll.headerColumnsSticky; - - const computedNormalColumnZIndex = - isRecordTableScrolledHorizontally && isRecordTableScrolledVertically - ? TABLE_Z_INDEX.scrolledBothVerticallyAndHorizontally.headerColumnsNormal - : isRecordTableScrolledHorizontally - ? TABLE_Z_INDEX.scrolledHorizontallyOnly.headerColumnsNormal - : isRecordTableScrolledVertically - ? TABLE_Z_INDEX.scrolledVerticallyOnly.headerColumnsNormal - : TABLE_Z_INDEX.noScrollAtAll.headerColumnsNormal; - - const computedHeaderRowZIndex = - isRecordTableScrolledHorizontally && isRecordTableScrolledVertically - ? TABLE_Z_INDEX.scrolledBothVerticallyAndHorizontally.headerRow - : isRecordTableScrolledHorizontally - ? TABLE_Z_INDEX.scrolledHorizontallyOnly.headerRow - : isRecordTableScrolledVertically - ? TABLE_Z_INDEX.scrolledVerticallyOnly.headerRow - : TABLE_Z_INDEX.noScrollAtAll.headerRow; - return ( - + <> {visibleRecordFields.map((recordField) => ( @@ -136,6 +22,6 @@ export const RecordTableHeader = () => { ))} - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderAddColumnButton.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderAddColumnButton.tsx index 6e0dc4242bf..34e45222687 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderAddColumnButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderAddColumnButton.tsx @@ -29,6 +29,9 @@ const StyledPlusIconHeaderCell = styled.div<{ z-index: 1; + height: 32px; + max-height: 32px; + &:hover { background: ${({ theme }) => theme.background.transparent.secondary}; } @@ -72,6 +75,7 @@ export const RecordTableHeaderAddColumnButton = () => { theme.background.primary}; border-right: 1px solid ${({ theme }) => theme.border.color.light}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx index c9385bd8af8..e1c947919a5 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx @@ -20,6 +20,7 @@ const StyledContainer = styled.div` min-width: 24px; padding-right: ${({ theme }) => theme.spacing(1)}; background-color: ${({ theme }) => theme.background.primary}; + border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; `; const StyledColumnHeaderCell = styled.div<{ @@ -31,6 +32,7 @@ const StyledColumnHeaderCell = styled.div<{ box-sizing: border-box; border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; + max-height: 32px; `; export const RecordTableHeaderCheckboxColumn = () => { diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn.tsx index bbe9159e3c1..5c22249462e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn.tsx @@ -2,10 +2,13 @@ import { styled } from '@linaria/react'; import { useContext } from 'react'; import { ThemeContext } from 'twenty-ui/theme'; -const StyledTh = styled.div<{ backgroundColor: string }>` +const StyledDragDropHeaderCell = styled.div<{ backgroundColor: string }>` background-color: ${({ backgroundColor }) => backgroundColor}; - min-width: 17px; - min-height: 100%; + min-width: 16px; + width: 16px; + max-width: 16px; + min-height: 32px; + max-height: 32px; border-bottom: 1px solid ${({ backgroundColor }) => backgroundColor}; `; @@ -14,9 +17,9 @@ export const RecordTableHeaderDragDropColumn = () => { const { theme } = useContext(ThemeContext); return ( - + /> ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn.tsx index 269e8ee11d4..6ded68f2c96 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn.tsx @@ -7,10 +7,13 @@ const StyledLastColumnHeader = styled.div` border-left: none !important; color: ${({ theme }) => theme.font.color.tertiary}; - width: 100%; + width: fit-content; height: 32px; + max-height: 32px; `; export const RecordTableHeaderLastColumn = () => { - return ; + return ( + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableActionRow.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableActionRow.tsx index b0174ee882e..895544132ee 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableActionRow.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableActionRow.tsx @@ -1,27 +1,60 @@ import styled from '@emotion/styled'; -import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; -import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd'; import { useTheme } from '@emotion/react'; +import { + filterOutByProperty, + findByProperty, + sumByProperty, +} from 'twenty-shared/utils'; import { type IconComponent } from 'twenty-ui/display'; -const StyledRecordTableDraggableTr = styled.tr` +const StyledDragDropPlaceholderCell = styled.div` + min-width: 16px; + width: 16px; + + position: sticky; + left: 0; +`; + +const StyledPlusButtonPlaceholderCell = styled.div` + height: 32px; + min-width: 32px; + width: 32px; + &:hover { + background-color: ${({ theme }) => theme.background.transparent.light}; + } +`; + +const StyledFieldPlaceholderCell = styled.div<{ widthOfFields: number }>` + height: 32px; + min-width: ${({ widthOfFields }) => widthOfFields}px; + width: ${({ widthOfFields }) => widthOfFields}px; + + &:hover { + background-color: ${({ theme }) => theme.background.transparent.light}; + } +`; + +const StyledRecordTableDraggableTr = styled.div` cursor: pointer; transition: background-color ${({ theme }) => theme.animation.duration.fast} ease-in-out; border: none; background: ${({ theme }) => theme.background.primary}; - position: relative; - z-index: ${TABLE_Z_INDEX.base}; + + display: flex; + flex-direction: row; + align-items: center; &:hover { - td:not(:first-of-type) { + div:not(:first-of-type) { background-color: ${({ theme }) => theme.background.transparent.light}; } } - td { + div { border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; background-color: ${({ theme }) => theme.background.primary}; transition: background-color ${({ theme }) => theme.animation.duration.fast} @@ -31,9 +64,11 @@ const StyledRecordTableDraggableTr = styled.tr` border-bottom: 1px solid ${({ theme }) => theme.background.primary}; } } + + width: 100%; `; -const StyledIconContainer = styled(RecordTableTd)` +const StyledIconContainer = styled.div` align-items: center; background-color: transparent; border-right: none; @@ -41,12 +76,24 @@ const StyledIconContainer = styled(RecordTableTd)` display: flex; height: 32px; justify-content: center; + width: 32px; + + position: sticky; + left: 16px; `; -const StyledRecordTableTdTextContainer = styled(RecordTableTd)` +const StyledRecordTableTdTextContainer = styled.div<{ width: number }>` + align-items: center; background-color: transparent; border-right: none; + display: flex; + height: 32px; + justify-content: start; + + left: 48px; + position: sticky; + width: ${({ width }) => width}px; `; const StyledText = styled.span` @@ -60,7 +107,7 @@ const StyledText = styled.span` type RecordTableActionRowProps = { LeftIcon: IconComponent; text: string; - onClick?: (event?: React.MouseEvent) => void; + onClick?: (event?: React.MouseEvent) => void; }; export const RecordTableActionRow = ({ @@ -71,10 +118,28 @@ export const RecordTableActionRow = ({ const theme = useTheme(); const { visibleRecordFields } = useRecordTableContextOrThrow(); + const { labelIdentifierFieldMetadataItem } = useRecordIndexContextOrThrow(); + + const visibleRecordFieldsWithoutLabelIdentifier = visibleRecordFields.filter( + filterOutByProperty( + 'fieldMetadataItemId', + labelIdentifierFieldMetadataItem?.id, + ), + ); + + const labelIdentifierRecordField = visibleRecordFields.find( + findByProperty('fieldMetadataItemId', labelIdentifierFieldMetadataItem?.id), + ); + + const sumOfWidthOfVisibleRecordFieldsAfterLabelIdentifierField = + visibleRecordFieldsWithoutLabelIdentifier.reduce(sumByProperty('size'), 0); + + const sumOfBorderWidthForFields = + visibleRecordFieldsWithoutLabelIdentifier.length; return ( - + - + {text} - - - + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableDraggableTr.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableDraggableTr.tsx index 00325fef891..af9fddc7a99 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableDraggableTr.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableDraggableTr.tsx @@ -2,9 +2,9 @@ import { useTheme } from '@emotion/react'; import { Draggable } from '@hello-pangea/dnd'; import { type ReactNode } from 'react'; +import { useRecordDragState } from '@/object-record/record-drag/shared/hooks/useRecordDragState'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableRowDraggableContextProvider } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext'; -import { useRecordDragState } from '@/object-record/record-drag/shared/hooks/useRecordDragState'; import { RecordTableRowMultiDragPreview } from '@/object-record/record-table/record-table-row/components/RecordTableRowMultiDragPreview'; import { RecordTableTr } from '@/object-record/record-table/record-table-row/components/RecordTableTr'; import { RecordTableTrEffect } from '@/object-record/record-table/record-table-row/components/RecordTableTrEffect'; @@ -61,7 +61,7 @@ export const RecordTableDraggableTr = ({ borderColor: draggableSnapshot.isDragging ? `${theme.border.color.medium}` : 'transparent', - opacity: isSecondaryDragged ? 0.3 : 1, + opacity: isSecondaryDragged ? 0.3 : undefined, }} isDragging={draggableSnapshot.isDragging} data-testid={`row-id-${recordId}`} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx index 7beb14df944..4c2467f88a2 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx @@ -12,25 +12,23 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho import styled from '@emotion/styled'; import { forwardRef, type ReactNode } from 'react'; -const StyledTr = styled.tr<{ +const StyledTr = styled.div<{ isDragging: boolean; }>` - border: ${({ isDragging, theme }) => - isDragging - ? `1px solid ${theme.border.color.medium}` - : '1px solid transparent'}; + border-top: ${({ isDragging, theme }) => + isDragging ? `1px solid ${theme.border.color.medium}` : 'none'}; - border-left: none; - position: relative; + display: flex; + flex-direction: row; &[data-next-row-active-or-focused='true'] { - td { + div.table-cell { border-bottom: none; } } &[data-focused='true'] { - td { + div.table-cell { &:not(:first-of-type) { border-bottom: 1px solid ${({ theme }) => theme.border.color.medium}; border-top: 1px solid ${({ theme }) => theme.border.color.medium}; @@ -51,7 +49,7 @@ const StyledTr = styled.tr<{ } &[data-active='true'] { - td { + div.table-cell { &:not(:first-of-type) { border-bottom: 1px solid ${({ theme }) => theme.adaptiveColors.blue3}; border-top: 1px solid ${({ theme }) => theme.adaptiveColors.blue3}; @@ -145,6 +143,7 @@ export const RecordTableTr = forwardRef< }} > theme.border.color.light}; border-right: none; color: ${({ theme }) => theme.font.color.secondary}; + display: flex; text-align: center; vertical-align: middle; + width: 32px; + min-width: 32px; + + position: sticky; + left: 16px; `; const StyledAnimatedLightIconButton = styled(AnimatedLightIconButton)` @@ -34,33 +57,63 @@ const StyledAnimatedLightIconButton = styled(AnimatedLightIconButton)` margin: auto; `; -const StyledRecordGroupSection = styled(RecordTableTd)` - border-right: none; - height: 32px; - display: flex; +const StyledRecordGroupSection = styled.div<{ width: number }>` align-items: center; - gap: ${({ theme }) => theme.spacing(1)}; -`; - -const StyledEmptyTd = styled.td` border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; + border-right: none; + display: flex; + flex-direction: row; + gap: ${({ theme }) => theme.spacing(1)}; + height: 32px; + width: ${({ width }) => width}px; + min-width: ${({ width }) => width}px; + + position: sticky; + left: 48px; `; const StyledTag = styled(Tag)` flex-shrink: 0; `; +const StyledPlusButtonPlaceholderCell = styled.div` + border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; + height: 32px; + min-width: 32px; + width: 32px; +`; + +const StyledFieldPlaceholderCell = styled.div<{ widthOfFields: number }>` + border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; + height: 32px; + min-width: ${({ widthOfFields }) => widthOfFields}px; + width: ${({ widthOfFields }) => widthOfFields}px; +`; + export const RecordTableRecordGroupSection = () => { const theme = useTheme(); const currentRecordGroupId = useCurrentRecordGroupId(); - const { visibleRecordFields, objectMetadataItem } = - useRecordTableContextOrThrow(); + const { objectMetadataItem } = useRecordTableContextOrThrow(); const { aggregateValue, aggregateLabel } = useAggregateRecordsForRecordTableSection(); + const { labelIdentifierFieldMetadataItem } = useRecordIndexContextOrThrow(); + + const visibleRecordFields = useRecoilComponentValue( + visibleRecordFieldsComponentSelector, + ); + + const widthOfLabelIdentifierRecordField = + visibleRecordFields.find( + findByProperty( + 'fieldMetadataItemId', + labelIdentifierFieldMetadataItem?.id ?? '', + ), + )?.size ?? null; + const [ isRecordGroupTableSectionToggled, setIsRecordGroupTableSectionToggled, @@ -77,13 +130,30 @@ export const RecordTableRecordGroupSection = () => { setIsRecordGroupTableSectionToggled((prevState) => !prevState); }, [setIsRecordGroupTableSectionToggled]); + const visibleRecordFieldsWithoutLabelIdentifier = visibleRecordFields.filter( + filterOutByProperty( + 'fieldMetadataItemId', + labelIdentifierFieldMetadataItem?.id, + ), + ); + + const sumOfWidthOfVisibleRecordFieldsAfterLabelIdentifierField = + visibleRecordFieldsWithoutLabelIdentifier.reduce(sumByProperty('size'), 0); + + const sumOfBorderWidthForFields = + visibleRecordFieldsWithoutLabelIdentifier.length; + + const fieldsPlaceholderWidth = + sumOfWidthOfVisibleRecordFieldsAfterLabelIdentifierField + + sumOfBorderWidthForFields; + if (!isDefined(recordGroup)) { return null; } return ( - + { transition={{ duration: theme.animation.duration.normal }} /> - + { /> - - - + + ); }; diff --git a/packages/twenty-shared/src/utils/array/filterOutByProperty.ts b/packages/twenty-shared/src/utils/array/filterOutByProperty.ts new file mode 100644 index 00000000000..387c6d969ac --- /dev/null +++ b/packages/twenty-shared/src/utils/array/filterOutByProperty.ts @@ -0,0 +1,5 @@ +export const filterOutByProperty = (property: K, valueToExclude: T[K] | null | undefined) => { + return (itemToFilter: T) => { + return itemToFilter[property] !== valueToExclude + } +} \ No newline at end of file diff --git a/packages/twenty-shared/src/utils/array/findByProperty.ts b/packages/twenty-shared/src/utils/array/findByProperty.ts index 046d52d19c1..6788b704ce1 100644 --- a/packages/twenty-shared/src/utils/array/findByProperty.ts +++ b/packages/twenty-shared/src/utils/array/findByProperty.ts @@ -1,4 +1,4 @@ -export const findByProperty = (property: K, valueToMatch: T[K]) => { +export const findByProperty = (property: K, valueToMatch: T[K] | null | undefined) => { return (itemToFind: T) => { return itemToFind[property] === valueToMatch } diff --git a/packages/twenty-shared/src/utils/array/sumByProperty.ts b/packages/twenty-shared/src/utils/array/sumByProperty.ts new file mode 100644 index 00000000000..1bcadd53d07 --- /dev/null +++ b/packages/twenty-shared/src/utils/array/sumByProperty.ts @@ -0,0 +1,17 @@ +import { isNumberOrNaN } from "@sniptt/guards"; + +export const sumByProperty = (property: K) => { + return (accumulator: number, nextItem: T) => { + if(typeof accumulator !== "number") { + accumulator = 0; + } + + if(!isNumberOrNaN(nextItem[property])) { + return accumulator; + } + + accumulator += nextItem[property]; + + return accumulator + } +} \ No newline at end of file diff --git a/packages/twenty-shared/src/utils/index.ts b/packages/twenty-shared/src/utils/index.ts index 19f1ae9dc50..2a629c9cf0e 100644 --- a/packages/twenty-shared/src/utils/index.ts +++ b/packages/twenty-shared/src/utils/index.ts @@ -7,8 +7,10 @@ * |___/ */ +export { filterOutByProperty } from './array/filterOutByProperty'; export { findById } from './array/findById'; export { findByProperty } from './array/findByProperty'; +export { sumByProperty } from './array/sumByProperty'; export { assertUnreachable } from './assertUnreachable'; export { deepMerge } from './deepMerge'; export { extractAndSanitizeObjectStringFields } from './extractAndSanitizeObjectStringFields';