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)
This commit is contained in:
+116
-3
@@ -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 (
|
||||
<StyledTableContainer ref={containerRef}>
|
||||
<StyledTableWithPointerEvents ref={tableBodyRef} isDragging={isDragging}>
|
||||
<StyledTableWithPointerEvents
|
||||
ref={tableBodyRef}
|
||||
isDragging={isDragging}
|
||||
stickyColumnZIndex={computedStickyColumnZIndex}
|
||||
normalColumnZIndex={computedNormalColumnZIndex}
|
||||
>
|
||||
<RecordTableHeader />
|
||||
{hasRecordGroups ? (
|
||||
<RecordTableRecordGroupsBody />
|
||||
|
||||
-9
@@ -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 = () => {
|
||||
<RecordTableBodyFetchMoreLoader />
|
||||
<RecordTableBodyDroppablePlaceholder />
|
||||
<RecordTableAddNew />
|
||||
{!isRecordTableInitialLoading && allRecordIds.length > 0 && (
|
||||
<RecordTableAggregateFooter />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
-27
@@ -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 <></>;
|
||||
};
|
||||
|
||||
-32
@@ -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 <></>;
|
||||
|
||||
+1
-9
@@ -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``;
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ export const TABLE_Z_INDEX = {
|
||||
editMode: 20,
|
||||
},
|
||||
footer: {
|
||||
default: 12,
|
||||
default: 18,
|
||||
stickyColumn: 20,
|
||||
},
|
||||
noScrollAtAll: {
|
||||
|
||||
+8
-7
@@ -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;
|
||||
|
||||
+12
-23
@@ -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) => (
|
||||
<StyledTable>
|
||||
<RecordTableBody
|
||||
id={recordTableBodyId}
|
||||
ref={provided.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
onMouseLeave={() => setRecordTableHoverPosition(null)}
|
||||
<RecordTableBody
|
||||
id={recordTableBodyId}
|
||||
ref={provided.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
onMouseLeave={() => setRecordTableHoverPosition(null)}
|
||||
>
|
||||
<RecordTableBodyDroppableContextProvider
|
||||
value={{ droppablePlaceholder: provided.placeholder }}
|
||||
>
|
||||
<RecordTableBodyDroppableContextProvider
|
||||
value={{ droppablePlaceholder: provided.placeholder }}
|
||||
>
|
||||
{children}
|
||||
</RecordTableBodyDroppableContextProvider>
|
||||
</RecordTableBody>
|
||||
</StyledTable>
|
||||
{children}
|
||||
</RecordTableBodyDroppableContextProvider>
|
||||
</RecordTableBody>
|
||||
)}
|
||||
</Droppable>
|
||||
);
|
||||
|
||||
+6
-5
@@ -56,13 +56,14 @@ export const RecordTableBodyFetchMoreLoader = () => {
|
||||
if (!showLoadingMoreRow) {
|
||||
return <></>;
|
||||
}
|
||||
// TODO: fix here styling
|
||||
|
||||
return (
|
||||
<tr ref={tbodyRef}>
|
||||
<td colSpan={7}>
|
||||
<div ref={tbodyRef}>
|
||||
<div>
|
||||
<StyledText>Loading more...</StyledText>
|
||||
</td>
|
||||
<td colSpan={7} />
|
||||
</tr>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+4
@@ -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 = () => {
|
||||
<RecordTableNoRecordGroupRows />
|
||||
<RecordTableCellPortals />
|
||||
</RecordTableBodyDroppable>
|
||||
{!isRecordTableInitialLoading && allRecordIds.length > 0 && (
|
||||
<RecordTableAggregateFooter />
|
||||
)}
|
||||
</RecordTableBodyDragDropContextProvider>
|
||||
</RecordTableNoRecordGroupBodyContextProvider>
|
||||
);
|
||||
|
||||
+2
-1
@@ -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}
|
||||
</StyledTd>
|
||||
|
||||
+12
-90
@@ -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 (
|
||||
<StyledTableRow
|
||||
id={`record-table-footer${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`}
|
||||
data-select-disable
|
||||
hasHorizontalOverflow={
|
||||
hasHorizontalOverflow && isUndefined(currentRecordGroupId)
|
||||
}
|
||||
>
|
||||
<StyledTd />
|
||||
<>
|
||||
<StyledPlaceholderFirstCell />
|
||||
{visibleRecordFields.map((recordField, index) => {
|
||||
return (
|
||||
<RecordTableColumnAggregateFooterCellContext.Provider
|
||||
@@ -119,9 +40,10 @@ export const RecordTableAggregateFooter = ({
|
||||
</RecordTableColumnAggregateFooterCellContext.Provider>
|
||||
);
|
||||
})}
|
||||
<td colSpan={visibleRecordFields.length - 1} />
|
||||
<td />
|
||||
<td />
|
||||
</StyledTableRow>
|
||||
{/* TODO: fix span for divs styling here colSpan={visibleRecordFields.length - 1}*/}
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+12
-7
@@ -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 (
|
||||
<StyledColumnFooterCell
|
||||
columnWidth={Math.max(recordField.size + 24, COLUMN_MIN_WIDTH)}
|
||||
colSpan={isFirstCell ? 2 : undefined}
|
||||
columnWidth={recordField.size + 1}
|
||||
// TODO: fix colspan
|
||||
// colSpan={isFirstCell ? 2 : undefined}
|
||||
isFirstCell={isFirstCell}
|
||||
className={isFirstCell ? '' : 'footer-cell'}
|
||||
>
|
||||
<StyledColumnFootContainer>
|
||||
<RecordTableColumnFooterWithDropdown
|
||||
|
||||
+2
-116
@@ -1,131 +1,17 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { RecordTableHeaderAddColumnButton } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderAddColumnButton';
|
||||
import { RecordTableHeaderCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCell';
|
||||
import { RecordTableHeaderCheckboxColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn';
|
||||
import { RecordTableHeaderDragDropColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn';
|
||||
import { RecordTableHeaderLastColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn';
|
||||
import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState';
|
||||
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
|
||||
|
||||
export const FIRST_TH_WIDTH = '10px';
|
||||
|
||||
const StyledTableHead = styled.div<{
|
||||
stickyColumnZIndex: number;
|
||||
normalColumnZIndex: number;
|
||||
entireRowZIndex: number;
|
||||
}>`
|
||||
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 (
|
||||
<StyledTableHead
|
||||
id="record-table-header"
|
||||
data-select-disable
|
||||
stickyColumnZIndex={computedStickyColumnZIndex}
|
||||
normalColumnZIndex={computedNormalColumnZIndex}
|
||||
entireRowZIndex={computedHeaderRowZIndex}
|
||||
>
|
||||
<>
|
||||
<RecordTableHeaderDragDropColumn />
|
||||
<RecordTableHeaderCheckboxColumn />
|
||||
{visibleRecordFields.map((recordField) => (
|
||||
@@ -136,6 +22,6 @@ export const RecordTableHeader = () => {
|
||||
))}
|
||||
<RecordTableHeaderAddColumnButton />
|
||||
<RecordTableHeaderLastColumn />
|
||||
</StyledTableHead>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+4
@@ -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 = () => {
|
||||
<StyledPlusIconHeaderCell
|
||||
isTableWiderThanScreen={isTableWiderThanScreen}
|
||||
isFirstRowActiveOrFocused={isFirstRowActiveOrFocused}
|
||||
className="header-cell"
|
||||
>
|
||||
<StyledDropdownContainer>
|
||||
<Dropdown
|
||||
|
||||
+4
-1
@@ -25,7 +25,7 @@ import { throwIfNotDefined } from 'twenty-shared/utils';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
|
||||
const COLUMN_MIN_WIDTH = 104;
|
||||
const COLUMN_MIN_WIDTH = 48;
|
||||
|
||||
const StyledColumnHeaderCell = styled.div<{
|
||||
columnWidth: number;
|
||||
@@ -36,6 +36,9 @@ const StyledColumnHeaderCell = styled.div<{
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
|
||||
height: 32px;
|
||||
max-height: 32px;
|
||||
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
border-right: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
|
||||
|
||||
+2
@@ -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 = () => {
|
||||
|
||||
+8
-5
@@ -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 (
|
||||
<StyledTh
|
||||
<StyledDragDropHeaderCell
|
||||
className="header-cell"
|
||||
backgroundColor={theme.background.primary}
|
||||
></StyledTh>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+5
-2
@@ -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 <StyledLastColumnHeader></StyledLastColumnHeader>;
|
||||
return (
|
||||
<StyledLastColumnHeader className="header-cell"></StyledLastColumnHeader>
|
||||
);
|
||||
};
|
||||
|
||||
+86
-15
@@ -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<HTMLTableRowElement>) => void;
|
||||
onClick?: (event?: React.MouseEvent<HTMLDivElement>) => 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 (
|
||||
<StyledRecordTableDraggableTr onClick={onClick}>
|
||||
<td aria-hidden />
|
||||
<StyledDragDropPlaceholderCell />
|
||||
<StyledIconContainer>
|
||||
<LeftIcon
|
||||
stroke={theme.icon.stroke.sm}
|
||||
@@ -82,12 +147,18 @@ export const RecordTableActionRow = ({
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
</StyledIconContainer>
|
||||
<StyledRecordTableTdTextContainer className="disable-shadow">
|
||||
<StyledRecordTableTdTextContainer
|
||||
width={labelIdentifierRecordField?.size ?? 104}
|
||||
>
|
||||
<StyledText>{text}</StyledText>
|
||||
</StyledRecordTableTdTextContainer>
|
||||
<td colSpan={visibleRecordFields.length - 1} aria-hidden />
|
||||
<td aria-hidden />
|
||||
<td aria-hidden />
|
||||
<StyledFieldPlaceholderCell
|
||||
widthOfFields={
|
||||
sumOfWidthOfVisibleRecordFieldsAfterLabelIdentifierField +
|
||||
sumOfBorderWidthForFields
|
||||
}
|
||||
/>
|
||||
<StyledPlusButtonPlaceholderCell />
|
||||
</StyledRecordTableDraggableTr>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-2
@@ -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}`}
|
||||
|
||||
+9
-10
@@ -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<
|
||||
}}
|
||||
>
|
||||
<StyledTr
|
||||
className="table-row"
|
||||
data-virtualized-id={recordId}
|
||||
isDragging={isDragging}
|
||||
ref={ref}
|
||||
|
||||
+92
-20
@@ -3,30 +3,53 @@ import styled from '@emotion/styled';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { RecordBoardColumnHeaderAggregateDropdown } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown';
|
||||
import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
|
||||
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
|
||||
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
|
||||
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
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 { RecordTableRecordGroupStickyEffect } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupStickyEffect';
|
||||
import { useAggregateRecordsForRecordTableSection } from '@/object-record/record-table/record-table-section/hooks/useAggregateRecordsForRecordTableSection';
|
||||
import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState';
|
||||
import { useRecoilComponentFamilyState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
filterOutByProperty,
|
||||
findByProperty,
|
||||
isDefined,
|
||||
sumByProperty,
|
||||
} from 'twenty-shared/utils';
|
||||
import { Tag } from 'twenty-ui/components';
|
||||
import { IconChevronDown } from 'twenty-ui/display';
|
||||
import { AnimatedLightIconButton } from 'twenty-ui/input';
|
||||
|
||||
const StyledTrContainer = styled.tr`
|
||||
cursor: pointer;
|
||||
const StyledDragDropHeaderPlaceholder = styled.div`
|
||||
min-width: 16px;
|
||||
width: 16px;
|
||||
position: sticky;
|
||||
left: 0;
|
||||
`;
|
||||
|
||||
const StyledChevronContainer = styled(RecordTableTd)`
|
||||
const StyledTrContainer = styled.div`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const StyledChevronContainer = styled.div`
|
||||
border-bottom: 1px solid ${({ theme }) => 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 (
|
||||
<StyledTrContainer onClick={handleDropdownToggle}>
|
||||
<td aria-hidden />
|
||||
<StyledDragDropHeaderPlaceholder />
|
||||
<StyledChevronContainer>
|
||||
<StyledAnimatedLightIconButton
|
||||
Icon={IconChevronDown}
|
||||
@@ -93,7 +163,10 @@ export const RecordTableRecordGroupSection = () => {
|
||||
transition={{ duration: theme.animation.duration.normal }}
|
||||
/>
|
||||
</StyledChevronContainer>
|
||||
<StyledRecordGroupSection className="disable-shadow">
|
||||
<StyledRecordGroupSection
|
||||
className="disable-shadow"
|
||||
width={widthOfLabelIdentifierRecordField ?? 104}
|
||||
>
|
||||
<StyledTag
|
||||
variant={
|
||||
recordGroup.type !== RecordGroupDefinitionType.NoValue
|
||||
@@ -116,9 +189,8 @@ export const RecordTableRecordGroupSection = () => {
|
||||
/>
|
||||
<RecordTableRecordGroupStickyEffect />
|
||||
</StyledRecordGroupSection>
|
||||
<StyledEmptyTd colSpan={visibleRecordFields.length - 1} />
|
||||
<StyledEmptyTd />
|
||||
<StyledEmptyTd />
|
||||
<StyledFieldPlaceholderCell widthOfFields={fieldsPlaceholderWidth} />
|
||||
<StyledPlusButtonPlaceholderCell />
|
||||
</StyledTrContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export const filterOutByProperty = <T, K extends keyof T>(property: K, valueToExclude: T[K] | null | undefined) => {
|
||||
return (itemToFilter: T) => {
|
||||
return itemToFilter[property] !== valueToExclude
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export const findByProperty = <T, K extends keyof T>(property: K, valueToMatch: T[K]) => {
|
||||
export const findByProperty = <T, K extends keyof T>(property: K, valueToMatch: T[K] | null | undefined) => {
|
||||
return (itemToFind: T) => {
|
||||
return itemToFind[property] === valueToMatch
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { isNumberOrNaN } from "@sniptt/guards";
|
||||
|
||||
export const sumByProperty = <T, K extends keyof T>(property: K) => {
|
||||
return (accumulator: number, nextItem: T) => {
|
||||
if(typeof accumulator !== "number") {
|
||||
accumulator = 0;
|
||||
}
|
||||
|
||||
if(!isNumberOrNaN(nextItem[property])) {
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
accumulator += nextItem[property];
|
||||
|
||||
return accumulator
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user