Refactored z-index handling for hovered portal with a better UX (#14384)
This PR closes the first step of https://github.com/twentyhq/core-team-issues/issues/1451, about z-index fine-tuning on the table. The core part was to succeed in having the hovered portal and the surrounding cells switch their z-index to have the borders overlap nicely in all scrolling cases. See associated video for the new behavior. This PR fixes two majors UX issues we had : - The sticky column and row were sliding a bit on the beginning of the scroll - Too many components were re-rendering while we only needed to isolate those who presented a complex use case In the end, we isolated some very specific components like the cell 0-0 and the first scrollable header cell, which were giving the most problems with the hovered portal. This allows to leave the rest of the cells and header cells alone with those concerns because they weren't involved at all in z-index update, once we isolated those components. ## Demo https://github.com/user-attachments/assets/cbe630ed-63c3-4e86-a22e-a11662c6082c https://github.com/user-attachments/assets/36e6947d-9f89-4ed2-ba6c-cbb13016d7d1
This commit is contained in:
+55
-48
@@ -1,25 +1,21 @@
|
||||
import { RecordTableStickyBottomEffect } from '@/object-record/record-table/components/RecordTableStickyBottomEffect';
|
||||
import { RecordTableStickyEffect } from '@/object-record/record-table/components/RecordTableStickyEffect';
|
||||
import { RecordTableScrollAndZIndexEffect } from '@/object-record/record-table/components/RecordTableScrollAndZIndexEffect';
|
||||
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.div<{
|
||||
export const RECORD_TABLE_HTML_ID = 'record-table';
|
||||
|
||||
const StyledTable = styled.div<{
|
||||
isDragging: boolean;
|
||||
stickyColumnZIndex: number;
|
||||
normalColumnZIndex: number;
|
||||
}>`
|
||||
& > * {
|
||||
pointer-events: ${({ isDragging }) => (isDragging ? 'none' : 'auto')};
|
||||
@@ -33,35 +29,38 @@ const StyledTableWithPointerEvents = styled.div<{
|
||||
top: 0;
|
||||
}
|
||||
|
||||
div.header-cell:nth-of-type(n + 3) {
|
||||
z-index: ${({ normalColumnZIndex }) => normalColumnZIndex};
|
||||
div.header-cell:nth-of-type(n + 5) {
|
||||
z-index: ${TABLE_Z_INDEX.headerColumnsNormal};
|
||||
}
|
||||
|
||||
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};
|
||||
|
||||
z-index: ${TABLE_Z_INDEX.headerColumnsSticky};
|
||||
}
|
||||
|
||||
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};
|
||||
|
||||
z-index: ${TABLE_Z_INDEX.headerColumnsSticky};
|
||||
}
|
||||
|
||||
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};
|
||||
|
||||
z-index: ${TABLE_Z_INDEX.headerColumnsSticky};
|
||||
|
||||
// &::after {
|
||||
// content: '';
|
||||
// position: absolute;
|
||||
@@ -80,6 +79,40 @@ const StyledTableWithPointerEvents = styled.div<{
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: re-implement horizontal scroll here after table have been refactored to divs
|
||||
div.table-cell:nth-of-type(1) {
|
||||
position: sticky;
|
||||
left: 0px;
|
||||
z-index: ${TABLE_Z_INDEX.cell.sticky};
|
||||
}
|
||||
|
||||
div.table-cell:nth-of-type(2) {
|
||||
position: sticky;
|
||||
left: 16px;
|
||||
z-index: ${TABLE_Z_INDEX.cell.sticky};
|
||||
}
|
||||
|
||||
div.table-cell-0-0 {
|
||||
position: sticky;
|
||||
left: 48px;
|
||||
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
width: ${38}px;
|
||||
max-width: ${38}px;
|
||||
}
|
||||
}
|
||||
|
||||
div.table-cell:nth-of-type(3) {
|
||||
position: sticky;
|
||||
left: 48px;
|
||||
z-index: ${TABLE_Z_INDEX.cell.sticky};
|
||||
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
width: ${38}px;
|
||||
max-width: ${38}px;
|
||||
}
|
||||
}
|
||||
|
||||
div.footer-cell:nth-of-type(n + 3) {
|
||||
z-index: ${TABLE_Z_INDEX.footer.default};
|
||||
|
||||
@@ -148,39 +181,14 @@ 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;
|
||||
const recordTableScrollWrapperId = `record-table-scroll-${recordTableId}`;
|
||||
|
||||
return (
|
||||
<StyledTableContainer ref={containerRef}>
|
||||
<StyledTableWithPointerEvents
|
||||
<StyledTable
|
||||
ref={tableBodyRef}
|
||||
isDragging={isDragging}
|
||||
stickyColumnZIndex={computedStickyColumnZIndex}
|
||||
normalColumnZIndex={computedNormalColumnZIndex}
|
||||
id={RECORD_TABLE_HTML_ID}
|
||||
>
|
||||
<RecordTableHeader />
|
||||
{hasRecordGroups ? (
|
||||
@@ -188,15 +196,14 @@ export const RecordTableContent = ({
|
||||
) : (
|
||||
<RecordTableNoRecordGroupBody />
|
||||
)}
|
||||
<RecordTableStickyEffect />
|
||||
<RecordTableStickyBottomEffect />
|
||||
</StyledTableWithPointerEvents>
|
||||
<RecordTableScrollAndZIndexEffect />
|
||||
</StyledTable>
|
||||
<DragSelect
|
||||
selectableItemsContainerRef={containerRef}
|
||||
onDragSelectionStart={handleDragStart}
|
||||
onDragSelectionChange={handleDragSelectionChange}
|
||||
onDragSelectionEnd={handleDragEnd}
|
||||
scrollWrapperComponentInstanceId={`record-table-scroll-${recordTableId}`}
|
||||
scrollWrapperComponentInstanceId={recordTableScrollWrapperId}
|
||||
selectionBoundaryClass={RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS}
|
||||
/>
|
||||
</StyledTableContainer>
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState';
|
||||
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
|
||||
|
||||
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const RecordTableScrollAndZIndexEffect = () => {
|
||||
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
|
||||
|
||||
const setIsRecordTableScrolledHorizontally = useSetRecoilComponentState(
|
||||
isRecordTableScrolledHorizontallyComponentState,
|
||||
);
|
||||
|
||||
const setIsRecordTableScrolledVertically = useSetRecoilComponentState(
|
||||
isRecordTableScrolledVerticallyComponentState,
|
||||
);
|
||||
|
||||
const [isScrolledVertically, setIsScrolledVertically] = useState(false);
|
||||
const [isScrolledHorizontally, setIsScrolledHorizontally] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefined(scrollWrapperHTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleScroll = (event: any) => {
|
||||
const target = event.currentTarget;
|
||||
|
||||
let somethingHasChanged = false;
|
||||
|
||||
const newIsScrolledVertically = target?.scrollTop > 0;
|
||||
|
||||
if (newIsScrolledVertically !== isScrolledVertically) {
|
||||
setIsScrolledVertically(newIsScrolledVertically);
|
||||
setIsRecordTableScrolledVertically(newIsScrolledVertically);
|
||||
somethingHasChanged = true;
|
||||
}
|
||||
|
||||
const newIsScrolledHorizontally = target?.scrollLeft > 0;
|
||||
|
||||
if (newIsScrolledHorizontally !== isScrolledHorizontally) {
|
||||
setIsScrolledHorizontally(newIsScrolledHorizontally);
|
||||
setIsRecordTableScrolledHorizontally(newIsScrolledHorizontally);
|
||||
somethingHasChanged = true;
|
||||
}
|
||||
|
||||
if (!somethingHasChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: insert imperative CSS update here
|
||||
};
|
||||
|
||||
scrollWrapperHTMLElement?.addEventListener('scroll', handleScroll);
|
||||
|
||||
return () => {
|
||||
scrollWrapperHTMLElement?.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [
|
||||
scrollWrapperHTMLElement,
|
||||
isScrolledVertically,
|
||||
isScrolledHorizontally,
|
||||
setIsRecordTableScrolledVertically,
|
||||
setIsRecordTableScrolledHorizontally,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
export const RecordTableStickyBottomEffect = () => {
|
||||
return <></>;
|
||||
};
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState';
|
||||
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
|
||||
import { scrollWrapperScrollLeftComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollLeftComponentState';
|
||||
import { scrollWrapperScrollTopComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollTopComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
|
||||
export const RecordTableStickyEffect = () => {
|
||||
const scrollTop = useRecoilComponentValue(
|
||||
scrollWrapperScrollTopComponentState,
|
||||
);
|
||||
|
||||
const setIsRecordTableScrolledVertically = useSetRecoilComponentState(
|
||||
isRecordTableScrolledVerticallyComponentState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsRecordTableScrolledVertically(scrollTop > 0);
|
||||
}, [scrollTop, setIsRecordTableScrolledVertically]);
|
||||
|
||||
const scrollLeft = useRecoilComponentValue(
|
||||
scrollWrapperScrollLeftComponentState,
|
||||
);
|
||||
|
||||
const setIsRecordTableScrolledHorizontally = useSetRecoilComponentState(
|
||||
isRecordTableScrolledHorizontallyComponentState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsRecordTableScrolledHorizontally(scrollLeft > 0);
|
||||
}, [scrollLeft, setIsRecordTableScrolledHorizontally]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
+32
-28
@@ -1,37 +1,41 @@
|
||||
export const TABLE_Z_INDEX = {
|
||||
base: 1,
|
||||
cell: {
|
||||
default: 3,
|
||||
sticky: 10,
|
||||
editMode: 20,
|
||||
default: 8,
|
||||
sticky: 12,
|
||||
editMode: 30,
|
||||
},
|
||||
headerColumnsSticky: 14,
|
||||
headerColumnsNormal: 10,
|
||||
firstCellWithoutVerticalScroll: 15,
|
||||
firstCellWithVerticalScroll: 12,
|
||||
noScrollAtAll: {
|
||||
hoverPortalCellOnFirstScrollableColumn: 17,
|
||||
hoverPortalCellOnNormalColumn: 17,
|
||||
hoverPortalCellOnLabelIdentifierColumn: 17,
|
||||
firstScrollableHeaderCell: 12,
|
||||
},
|
||||
scrolledBothVerticallyAndHorizontally: {
|
||||
hoverPortalCellOnNormalColumn: 2,
|
||||
hoverPortalCellOnFirstScrollableColumn: 2,
|
||||
hoverPortalCellOnLabelIdentifierColumn: 2,
|
||||
firstScrollableHeaderCell: 12,
|
||||
},
|
||||
scrolledHorizontallyOnly: {
|
||||
hoverPortalCellOnLabelIdentifierColumn: 17,
|
||||
hoverPortalCellOnNormalColumn: 11,
|
||||
hoverPortalCellOnFirstScrollableColumn: 11,
|
||||
firstScrollableHeaderCell: 10,
|
||||
},
|
||||
scrolledVerticallyOnly: {
|
||||
hoverPortalCellOnNormalColumn: 9,
|
||||
hoverPortalCellOnFirstScrollableColumn: 13,
|
||||
hoverPortalCellOnLabelIdentifierColumn: 13,
|
||||
firstScrollableHeaderCell: 14,
|
||||
},
|
||||
columnGrip: 30,
|
||||
footer: {
|
||||
default: 18,
|
||||
stickyColumn: 20,
|
||||
},
|
||||
noScrollAtAll: {
|
||||
hoverPortalCell: 17,
|
||||
headerRow: 13,
|
||||
headerColumnsSticky: 16,
|
||||
headerColumnsNormal: 15,
|
||||
},
|
||||
scrolledBothVerticallyAndHorizontally: {
|
||||
headerColumnsSticky: 13,
|
||||
headerColumnsNormal: 12,
|
||||
headerRow: 11,
|
||||
hoverPortalCell: 2,
|
||||
},
|
||||
scrolledHorizontallyOnly: {
|
||||
headerColumnsSticky: 13,
|
||||
headerColumnsNormal: 12,
|
||||
headerRow: 11,
|
||||
hoverPortalCell: 9,
|
||||
},
|
||||
scrolledVerticallyOnly: {
|
||||
headerColumnsSticky: 14,
|
||||
headerColumnsNormal: 13,
|
||||
headerRow: 12,
|
||||
hoverPortalCell: 11,
|
||||
},
|
||||
columnGrip: 30,
|
||||
};
|
||||
|
||||
+2
-29
@@ -1,35 +1,8 @@
|
||||
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.div`
|
||||
// TODO: re-implement horizontal scroll here after table have been refactored to divs
|
||||
div.table-cell:nth-of-type(1) {
|
||||
position: sticky;
|
||||
left: 0px;
|
||||
z-index: ${TABLE_Z_INDEX.cell.sticky};
|
||||
}
|
||||
|
||||
div.table-cell:nth-of-type(2) {
|
||||
position: sticky;
|
||||
left: 16px;
|
||||
z-index: ${TABLE_Z_INDEX.cell.sticky};
|
||||
}
|
||||
|
||||
div.table-cell:nth-of-type(3) {
|
||||
position: sticky;
|
||||
left: 48px;
|
||||
z-index: ${TABLE_Z_INDEX.cell.sticky};
|
||||
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
width: ${38}px;
|
||||
max-width: ${38}px;
|
||||
}
|
||||
}
|
||||
|
||||
const StyledTableBody = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
export const RecordTableBody = StyledTbody;
|
||||
export const RecordTableBody = StyledTableBody;
|
||||
|
||||
+13
-1
@@ -1,6 +1,8 @@
|
||||
import { RecordTableBody } from '@/object-record/record-table/record-table-body/components/RecordTableBody';
|
||||
import { RecordTableBodyDroppableContextProvider } from '@/object-record/record-table/record-table-body/contexts/RecordTableBodyDroppableContext';
|
||||
import { recordTableHoverPositionComponentState } from '@/object-record/record-table/states/recordTableHoverPositionComponentState';
|
||||
import { isSomeCellInEditModeComponentSelector } from '@/object-record/record-table/states/selectors/isSomeCellInEditModeComponentSelector';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
@@ -24,6 +26,16 @@ export const RecordTableBodyDroppable = ({
|
||||
recordTableHoverPositionComponentState,
|
||||
);
|
||||
|
||||
const isSomeCellInEditMode = useRecoilComponentValue(
|
||||
isSomeCellInEditModeComponentSelector,
|
||||
);
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (!isSomeCellInEditMode) {
|
||||
setRecordTableHoverPosition(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Droppable
|
||||
droppableId={recordGroupId ?? v4Persistable}
|
||||
@@ -35,7 +47,7 @@ export const RecordTableBodyDroppable = ({
|
||||
ref={provided.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
onMouseLeave={() => setRecordTableHoverPosition(null)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<RecordTableBodyDroppableContextProvider
|
||||
value={{ droppablePlaceholder: provided.placeholder }}
|
||||
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { RecordTableRowDraggableContextProvider } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
|
||||
import { RecordTableBody } from '@/object-record/record-table/record-table-body/components/RecordTableBody';
|
||||
import { RecordTableCellCheckbox } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox';
|
||||
import { RecordTableCellGrip } from '@/object-record/record-table/record-table-cell/components/RecordTableCellGrip';
|
||||
import { RecordTableCellLoading } from '@/object-record/record-table/record-table-cell/components/RecordTableCellLoading';
|
||||
@@ -10,7 +11,7 @@ export const RecordTableBodyLoading = () => {
|
||||
const { visibleRecordFields } = useRecordTableContextOrThrow();
|
||||
|
||||
return (
|
||||
<tbody>
|
||||
<RecordTableBody>
|
||||
{Array.from({ length: 8 }).map((_, rowIndex) => (
|
||||
<RecordTableRowContextProvider
|
||||
key={rowIndex}
|
||||
@@ -45,6 +46,6 @@ export const RecordTableBodyLoading = () => {
|
||||
</RecordTableRowDraggableContextProvider>
|
||||
</RecordTableRowContextProvider>
|
||||
))}
|
||||
</tbody>
|
||||
</RecordTableBody>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-1
@@ -24,9 +24,10 @@ const StyledBaseContainer = styled.div<{
|
||||
cursor: ${({ isReadOnly }) => (isReadOnly ? 'default' : 'pointer')};
|
||||
display: flex;
|
||||
height: 32px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
|
||||
position: relative;
|
||||
|
||||
&.focus-active {
|
||||
border-radius: ${BORDER_COMMON.radius.sm};
|
||||
outline: 1px solid ${({ borderColorBlue }) => borderColorBlue};
|
||||
|
||||
-2
@@ -3,7 +3,6 @@ import { RecordFieldComponentInstanceContext } from '@/object-record/record-fiel
|
||||
import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/ui/states/recordFieldInputIsFieldInErrorComponentState';
|
||||
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionComponentState';
|
||||
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionLoadingComponentState';
|
||||
import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex';
|
||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||
import { useFocusRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useFocusRecordTableCell';
|
||||
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
||||
@@ -28,7 +27,6 @@ const StyledEditableCellEditModeContainer = styled.div<{
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: calc(100% + 2px);
|
||||
z-index: ${TABLE_Z_INDEX.cell.editMode};
|
||||
`;
|
||||
|
||||
const StyledInputModeOnlyContainer = styled.div`
|
||||
|
||||
+6
-11
@@ -1,20 +1,13 @@
|
||||
import { RecordTableCellPortalWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellPortalWrapper';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
|
||||
import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex';
|
||||
import { RecordTableCellEditMode } from '@/object-record/record-table/record-table-cell/components/RecordTableCellEditMode';
|
||||
import { RecordTableCellFieldInput } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput';
|
||||
import { RecordTableCellHotkeysEffect } from '@/object-record/record-table/record-table-cell/components/RecordTableCellHotkeysEffect';
|
||||
import { RecordTableCellPortalRootContainer } from '@/object-record/record-table/record-table-cell/components/RecordTableCellPortalRootContainer';
|
||||
import { recordTableCellEditModePositionComponentState } from '@/object-record/record-table/states/recordTableCellEditModePositionComponentState';
|
||||
import { recordTableFocusPositionComponentState } from '@/object-record/record-table/states/recordTableFocusPositionComponentState';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledRecordTableCellHoveredPortal = styled.div`
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const RecordTableCellEditModePortal = () => {
|
||||
const focusedCellPosition = useRecoilComponentValue(
|
||||
@@ -32,11 +25,13 @@ export const RecordTableCellEditModePortal = () => {
|
||||
return (
|
||||
<RecordTableCellPortalWrapper position={focusedCellPosition}>
|
||||
{currentTableCellInEditModePosition && (
|
||||
<StyledRecordTableCellHoveredPortal>
|
||||
<RecordTableCellPortalRootContainer
|
||||
zIndex={TABLE_Z_INDEX.cell.editMode}
|
||||
>
|
||||
<RecordTableCellEditMode>
|
||||
<RecordTableCellFieldInput />
|
||||
</RecordTableCellEditMode>
|
||||
</StyledRecordTableCellHoveredPortal>
|
||||
</RecordTableCellPortalRootContainer>
|
||||
)}
|
||||
<RecordTableCellHotkeysEffect />
|
||||
</RecordTableCellPortalWrapper>
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex';
|
||||
import {
|
||||
DEFAULT_RECORD_TABLE_TD_WIDTH,
|
||||
StyledTd,
|
||||
} from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
|
||||
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import styled from '@emotion/styled';
|
||||
import { type DraggableProvidedDragHandleProps } from '@hello-pangea/dnd';
|
||||
import { useContext, type ReactNode } from 'react';
|
||||
import { ThemeContext } from 'twenty-ui/theme';
|
||||
|
||||
const StyledRecordTableTd = styled(StyledTd)<{ zIndex: number }>`
|
||||
z-index: ${({ zIndex }) => zIndex};
|
||||
`;
|
||||
|
||||
export const RecordTableCellFirstRowFirstColumn = ({
|
||||
children,
|
||||
isSelected,
|
||||
isDragging,
|
||||
hasRightBorder = true,
|
||||
hasBottomBorder = true,
|
||||
width = DEFAULT_RECORD_TABLE_TD_WIDTH,
|
||||
...dragHandleProps
|
||||
}: {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
isSelected?: boolean;
|
||||
isDragging?: boolean;
|
||||
hasRightBorder?: boolean;
|
||||
hasBottomBorder?: boolean;
|
||||
width?: number;
|
||||
} & (Partial<DraggableProvidedDragHandleProps> | null)) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
const [isRecordTableScrolledVertically] = useRecoilComponentState(
|
||||
isRecordTableScrolledVerticallyComponentState,
|
||||
);
|
||||
|
||||
const zIndex = isRecordTableScrolledVertically
|
||||
? TABLE_Z_INDEX.firstCellWithVerticalScroll
|
||||
: TABLE_Z_INDEX.firstCellWithoutVerticalScroll;
|
||||
|
||||
const tdBackgroundColor = isSelected
|
||||
? theme.accent.quaternary
|
||||
: theme.background.primary;
|
||||
|
||||
const borderColor = theme.border.color.light;
|
||||
|
||||
const fontColor = theme.font.color.primary;
|
||||
|
||||
return (
|
||||
<StyledRecordTableTd
|
||||
isDragging={isDragging}
|
||||
backgroundColor={tdBackgroundColor}
|
||||
borderColor={borderColor}
|
||||
fontColor={fontColor}
|
||||
hasRightBorder={hasRightBorder}
|
||||
hasBottomBorder={hasBottomBorder}
|
||||
width={width}
|
||||
zIndex={zIndex}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...dragHandleProps}
|
||||
className="table-cell-0-0"
|
||||
>
|
||||
{children}
|
||||
</StyledRecordTableTd>
|
||||
);
|
||||
};
|
||||
+55
-114
@@ -1,137 +1,78 @@
|
||||
import { RecordTableCellPortalWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellPortalWrapper';
|
||||
import { recordTableHoverPositionComponentState } from '@/object-record/record-table/states/recordTableHoverPositionComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { FieldDisplay } from '@/object-record/record-field/ui/components/FieldDisplay';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { useIsFieldInputOnly } from '@/object-record/record-field/ui/hooks/useIsFieldInputOnly';
|
||||
import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex';
|
||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { RecordTableCellDisplayMode } from '@/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode';
|
||||
import { RecordTableCellEditButton } from '@/object-record/record-table/record-table-cell/components/RecordTableCellEditButton';
|
||||
import { RecordTableCellEditMode } from '@/object-record/record-table/record-table-cell/components/RecordTableCellEditMode';
|
||||
import { RecordTableCellFieldInput } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput';
|
||||
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
|
||||
import { RecordTableCellHoveredPortalContent } from '@/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortalContent';
|
||||
import { RecordTableCellPortalRootContainer } from '@/object-record/record-table/record-table-cell/components/RecordTableCellPortalRootContainer';
|
||||
import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState';
|
||||
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
import { useContext } from 'react';
|
||||
import { BORDER_COMMON } from 'twenty-ui/theme';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
const StyledRecordTableCellHoveredPortalContent = styled.div<{
|
||||
isReadOnly: boolean;
|
||||
isRowActive: boolean;
|
||||
zIndex: number;
|
||||
}>`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.transparent.secondary};
|
||||
background-color: ${({ theme, isRowActive }) =>
|
||||
isRowActive ? theme.accent.quaternary : theme.background.primary};
|
||||
border-radius: ${({ isReadOnly }) =>
|
||||
!isReadOnly ? BORDER_COMMON.radius.sm : 'none'};
|
||||
box-sizing: border-box;
|
||||
cursor: ${({ isReadOnly }) => (isReadOnly ? 'default' : 'pointer')};
|
||||
display: flex;
|
||||
|
||||
height: 32px;
|
||||
|
||||
outline: ${({ theme, isReadOnly, isRowActive }) =>
|
||||
isRowActive
|
||||
? 'none'
|
||||
: isReadOnly
|
||||
? `1px solid ${theme.border.color.medium}`
|
||||
: `1px solid ${theme.font.color.extraLight}`};
|
||||
|
||||
position: relative;
|
||||
user-select: none;
|
||||
|
||||
z-index: ${({ zIndex }) => zIndex};
|
||||
`;
|
||||
|
||||
const RecordTableCellHoveredPortalContent = () => {
|
||||
const hoverPosition = useRecoilComponentValue(
|
||||
recordTableHoverPositionComponentState,
|
||||
);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const isFirstColumn = hoverPosition?.column === 0;
|
||||
|
||||
const { isRecordFieldReadOnly: isReadOnly } = useContext(FieldContext);
|
||||
|
||||
const isFieldInputOnly = useIsFieldInputOnly();
|
||||
|
||||
const showButton =
|
||||
!isFieldInputOnly && !isReadOnly && !(isMobile && isFirstColumn);
|
||||
|
||||
const { rowIndex } = useRecordTableRowContextOrThrow();
|
||||
|
||||
const isRowActive = useRecoilComponentFamilyValue(
|
||||
isRecordTableRowActiveComponentFamilyState,
|
||||
rowIndex,
|
||||
);
|
||||
|
||||
const isRecordTableScrolledHorizontally = useRecoilComponentValue(
|
||||
isRecordTableScrolledHorizontallyComponentState,
|
||||
);
|
||||
|
||||
const isRecordTableScrolledVertically = useRecoilComponentValue(
|
||||
isRecordTableScrolledVerticallyComponentState,
|
||||
);
|
||||
|
||||
const computedZIndex =
|
||||
isRecordTableScrolledHorizontally && isRecordTableScrolledVertically
|
||||
? TABLE_Z_INDEX.scrolledBothVerticallyAndHorizontally.hoverPortalCell
|
||||
: isRecordTableScrolledHorizontally
|
||||
? TABLE_Z_INDEX.scrolledHorizontallyOnly.hoverPortalCell
|
||||
: isRecordTableScrolledVertically
|
||||
? TABLE_Z_INDEX.scrolledVerticallyOnly.hoverPortalCell
|
||||
: TABLE_Z_INDEX.noScrollAtAll.hoverPortalCell;
|
||||
|
||||
return (
|
||||
<StyledRecordTableCellHoveredPortalContent
|
||||
isReadOnly={isReadOnly}
|
||||
isRowActive={isRowActive}
|
||||
zIndex={computedZIndex}
|
||||
>
|
||||
{isFieldInputOnly ? (
|
||||
<RecordTableCellEditMode>
|
||||
<RecordTableCellFieldInput />
|
||||
</RecordTableCellEditMode>
|
||||
) : (
|
||||
<RecordTableCellDisplayMode>
|
||||
<FieldDisplay />
|
||||
</RecordTableCellDisplayMode>
|
||||
)}
|
||||
{showButton && <RecordTableCellEditButton />}
|
||||
</StyledRecordTableCellHoveredPortalContent>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledRecordTableCellHoveredPortal = styled.div`
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const RecordTableCellHoveredPortal = () => {
|
||||
const hoverPosition = useRecoilComponentValue(
|
||||
recordTableHoverPositionComponentState,
|
||||
);
|
||||
|
||||
const isRecordTableScrolledVertically = useRecoilComponentValue(
|
||||
isRecordTableScrolledVerticallyComponentState,
|
||||
);
|
||||
|
||||
const isRecordTableScrolledHorizontally = useRecoilComponentValue(
|
||||
isRecordTableScrolledHorizontallyComponentState,
|
||||
);
|
||||
|
||||
if (!hoverPosition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isOnFirstScrollableColumn = hoverPosition.column === 1;
|
||||
const isOnLabelIdentifierStickyColumn = hoverPosition.column === 0;
|
||||
|
||||
const zIndexForFirstScrollableColumn =
|
||||
isRecordTableScrolledHorizontally && isRecordTableScrolledVertically
|
||||
? TABLE_Z_INDEX.scrolledBothVerticallyAndHorizontally
|
||||
.hoverPortalCellOnFirstScrollableColumn
|
||||
: isRecordTableScrolledHorizontally
|
||||
? TABLE_Z_INDEX.scrolledHorizontallyOnly
|
||||
.hoverPortalCellOnFirstScrollableColumn
|
||||
: isRecordTableScrolledVertically
|
||||
? TABLE_Z_INDEX.scrolledVerticallyOnly
|
||||
.hoverPortalCellOnFirstScrollableColumn
|
||||
: TABLE_Z_INDEX.noScrollAtAll.hoverPortalCellOnFirstScrollableColumn;
|
||||
|
||||
const zIndexForLabelIdentifierStickyColumn =
|
||||
isRecordTableScrolledHorizontally && isRecordTableScrolledVertically
|
||||
? TABLE_Z_INDEX.scrolledBothVerticallyAndHorizontally
|
||||
.hoverPortalCellOnLabelIdentifierColumn
|
||||
: isRecordTableScrolledHorizontally
|
||||
? TABLE_Z_INDEX.scrolledHorizontallyOnly
|
||||
.hoverPortalCellOnLabelIdentifierColumn
|
||||
: isRecordTableScrolledVertically
|
||||
? TABLE_Z_INDEX.scrolledVerticallyOnly
|
||||
.hoverPortalCellOnLabelIdentifierColumn
|
||||
: TABLE_Z_INDEX.noScrollAtAll.hoverPortalCellOnLabelIdentifierColumn;
|
||||
|
||||
const zIndexForNormalColumn =
|
||||
isRecordTableScrolledHorizontally && isRecordTableScrolledVertically
|
||||
? TABLE_Z_INDEX.scrolledBothVerticallyAndHorizontally
|
||||
.hoverPortalCellOnNormalColumn
|
||||
: isRecordTableScrolledHorizontally
|
||||
? TABLE_Z_INDEX.scrolledHorizontallyOnly.hoverPortalCellOnNormalColumn
|
||||
: isRecordTableScrolledVertically
|
||||
? TABLE_Z_INDEX.scrolledVerticallyOnly.hoverPortalCellOnNormalColumn
|
||||
: TABLE_Z_INDEX.noScrollAtAll.hoverPortalCellOnNormalColumn;
|
||||
|
||||
const zIndex = isOnFirstScrollableColumn
|
||||
? zIndexForFirstScrollableColumn
|
||||
: isOnLabelIdentifierStickyColumn
|
||||
? zIndexForLabelIdentifierStickyColumn
|
||||
: zIndexForNormalColumn;
|
||||
|
||||
return (
|
||||
<RecordTableCellPortalWrapper position={hoverPosition}>
|
||||
<StyledRecordTableCellHoveredPortal>
|
||||
<RecordTableCellPortalRootContainer zIndex={zIndex}>
|
||||
<RecordTableCellHoveredPortalContent />
|
||||
</StyledRecordTableCellHoveredPortal>
|
||||
</RecordTableCellPortalRootContainer>
|
||||
</RecordTableCellPortalWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { FieldDisplay } from '@/object-record/record-field/ui/components/FieldDisplay';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { useIsFieldInputOnly } from '@/object-record/record-field/ui/hooks/useIsFieldInputOnly';
|
||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { RecordTableCellDisplayMode } from '@/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode';
|
||||
import { RecordTableCellEditButton } from '@/object-record/record-table/record-table-cell/components/RecordTableCellEditButton';
|
||||
import { RecordTableCellEditMode } from '@/object-record/record-table/record-table-cell/components/RecordTableCellEditMode';
|
||||
import { RecordTableCellFieldInput } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput';
|
||||
|
||||
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
|
||||
import { recordTableHoverPositionComponentState } from '@/object-record/record-table/states/recordTableHoverPositionComponentState';
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import styled from '@emotion/styled';
|
||||
import { useContext } from 'react';
|
||||
import { BORDER_COMMON } from 'twenty-ui/theme';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
const StyledRecordTableCellHoveredPortalContent = styled.div<{
|
||||
isReadOnly: boolean;
|
||||
isRowActive: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.transparent.secondary};
|
||||
background-color: ${({ theme, isRowActive }) =>
|
||||
isRowActive ? theme.accent.quaternary : theme.background.primary};
|
||||
border-radius: ${({ isReadOnly }) =>
|
||||
!isReadOnly ? BORDER_COMMON.radius.sm : 'none'};
|
||||
box-sizing: border-box;
|
||||
cursor: ${({ isReadOnly }) => (isReadOnly ? 'default' : 'pointer')};
|
||||
display: flex;
|
||||
|
||||
height: 32px;
|
||||
|
||||
outline: ${({ theme, isReadOnly, isRowActive }) =>
|
||||
isRowActive
|
||||
? 'none'
|
||||
: isReadOnly
|
||||
? `1px solid ${theme.border.color.medium}`
|
||||
: `1px solid ${theme.font.color.extraLight}`};
|
||||
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
export const RecordTableCellHoveredPortalContent = () => {
|
||||
const hoverPosition = useRecoilComponentValue(
|
||||
recordTableHoverPositionComponentState,
|
||||
);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const isFirstColumn = hoverPosition?.column === 0;
|
||||
|
||||
const { isRecordFieldReadOnly: isReadOnly } = useContext(FieldContext);
|
||||
|
||||
const isFieldInputOnly = useIsFieldInputOnly();
|
||||
|
||||
const showButton =
|
||||
!isFieldInputOnly && !isReadOnly && !(isMobile && isFirstColumn);
|
||||
|
||||
const { rowIndex } = useRecordTableRowContextOrThrow();
|
||||
|
||||
const isRowActive = useRecoilComponentFamilyValue(
|
||||
isRecordTableRowActiveComponentFamilyState,
|
||||
rowIndex,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledRecordTableCellHoveredPortalContent
|
||||
isReadOnly={isReadOnly}
|
||||
isRowActive={isRowActive}
|
||||
>
|
||||
{isFieldInputOnly ? (
|
||||
<RecordTableCellEditMode>
|
||||
<RecordTableCellFieldInput />
|
||||
</RecordTableCellEditMode>
|
||||
) : (
|
||||
<RecordTableCellDisplayMode>
|
||||
<FieldDisplay />
|
||||
</RecordTableCellDisplayMode>
|
||||
)}
|
||||
{showButton && <RecordTableCellEditButton />}
|
||||
</StyledRecordTableCellHoveredPortalContent>
|
||||
);
|
||||
};
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
|
||||
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
|
||||
import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper';
|
||||
import { recordTableHoverPositionComponentState } from '@/object-record/record-table/states/recordTableHoverPositionComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const RecordTableCellPortalContexts = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const hoverPosition = useRecoilComponentValue(
|
||||
recordTableHoverPositionComponentState,
|
||||
);
|
||||
|
||||
const allRecordIds = useRecoilComponentValue(
|
||||
recordIndexAllRecordIdsComponentSelector,
|
||||
);
|
||||
|
||||
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
|
||||
|
||||
const visibleRecordFields = useRecoilComponentValue(
|
||||
visibleRecordFieldsComponentSelector,
|
||||
);
|
||||
|
||||
const recordId = isDefined(hoverPosition)
|
||||
? allRecordIds.at(hoverPosition.row)
|
||||
: null;
|
||||
|
||||
const isRecordReadOnly = useIsRecordReadOnly({
|
||||
recordId: recordId ?? '',
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
});
|
||||
|
||||
if (!hoverPosition || !isDefined(recordId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RecordTableRowContextProvider
|
||||
value={{
|
||||
recordId,
|
||||
rowIndex: hoverPosition.row,
|
||||
isSelected: false,
|
||||
inView: true,
|
||||
pathToShowPage:
|
||||
getBasePathToShowPage({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
}) + recordId,
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
isRecordReadOnly,
|
||||
}}
|
||||
>
|
||||
<RecordTableCellContext.Provider
|
||||
value={{
|
||||
recordField: visibleRecordFields[hoverPosition.column],
|
||||
cellPosition: hoverPosition,
|
||||
}}
|
||||
>
|
||||
<RecordTableCellFieldContextWrapper
|
||||
recordField={visibleRecordFields[hoverPosition.column]}
|
||||
>
|
||||
{children}
|
||||
</RecordTableCellFieldContextWrapper>
|
||||
</RecordTableCellContext.Provider>
|
||||
</RecordTableRowContextProvider>
|
||||
);
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledRecordTableCellPortalRootContainer = styled.div<{
|
||||
zIndex?: number;
|
||||
}>`
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: ${({ zIndex }) => zIndex ?? 'auto'};
|
||||
`;
|
||||
|
||||
export const RecordTableCellPortalRootContainer =
|
||||
StyledRecordTableCellPortalRootContainer;
|
||||
+7
-56
@@ -1,13 +1,5 @@
|
||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
|
||||
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
|
||||
import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper';
|
||||
import { RecordTableCellPortalContexts } from '@/object-record/record-table/record-table-cell/components/RecordTableCellPortalContexts';
|
||||
import { type TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -18,62 +10,21 @@ export const RecordTableCellPortalWrapper = ({
|
||||
position: TableCellPosition;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const anchorElement = document.body.querySelector<HTMLAnchorElement>(
|
||||
const tableCellAnchorElement = document.body.querySelector<HTMLAnchorElement>(
|
||||
`#record-table-cell-${position.column}-${position.row}`,
|
||||
);
|
||||
|
||||
const allRecordIds = useRecoilComponentValue(
|
||||
recordIndexAllRecordIdsComponentSelector,
|
||||
);
|
||||
|
||||
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
|
||||
|
||||
const visibleRecordFields = useRecoilComponentValue(
|
||||
visibleRecordFieldsComponentSelector,
|
||||
);
|
||||
|
||||
const recordId = allRecordIds.at(position.row);
|
||||
|
||||
const isRecordReadOnly = useIsRecordReadOnly({
|
||||
recordId: recordId ?? '',
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
});
|
||||
|
||||
if (!isDefined(anchorElement) || !isDefined(recordId)) {
|
||||
if (!isDefined(tableCellAnchorElement)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{createPortal(
|
||||
<RecordTableRowContextProvider
|
||||
value={{
|
||||
recordId,
|
||||
rowIndex: position.row,
|
||||
isSelected: false,
|
||||
inView: true,
|
||||
pathToShowPage:
|
||||
getBasePathToShowPage({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
}) + recordId,
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
isRecordReadOnly,
|
||||
}}
|
||||
>
|
||||
<RecordTableCellContext.Provider
|
||||
value={{
|
||||
recordField: visibleRecordFields[position.column],
|
||||
cellPosition: position,
|
||||
}}
|
||||
>
|
||||
<RecordTableCellFieldContextWrapper
|
||||
recordField={visibleRecordFields[position.column]}
|
||||
>
|
||||
{children}
|
||||
</RecordTableCellFieldContextWrapper>
|
||||
</RecordTableCellContext.Provider>
|
||||
</RecordTableRowContextProvider>,
|
||||
anchorElement,
|
||||
<RecordTableCellPortalContexts>
|
||||
{children}
|
||||
</RecordTableCellPortalContexts>,
|
||||
tableCellAnchorElement,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
-1
@@ -16,7 +16,6 @@ export const RecordTableCellPortals = () => {
|
||||
return (
|
||||
<>
|
||||
<RecordTableCellHoveredPortal />
|
||||
|
||||
{isRecordTableFocusActive && (
|
||||
<>
|
||||
<RecordTableCellEditModePortal />
|
||||
|
||||
+1
-3
@@ -5,7 +5,7 @@ import { ThemeContext } from 'twenty-ui/theme';
|
||||
|
||||
export const DEFAULT_RECORD_TABLE_TD_WIDTH = 32;
|
||||
|
||||
const StyledTd = styled.div<{
|
||||
export const StyledTd = styled.div<{
|
||||
backgroundColor: string;
|
||||
borderColor: string;
|
||||
isDragging?: boolean;
|
||||
@@ -32,8 +32,6 @@ const StyledTd = styled.div<{
|
||||
|
||||
background: ${({ backgroundColor, isDragging }) =>
|
||||
isDragging ? 'transparent' : backgroundColor};
|
||||
|
||||
// TODO: reimplement horizontal scroll here once we have refactored body with divs
|
||||
`;
|
||||
|
||||
export const RecordTableTd = ({
|
||||
|
||||
+3
-10
@@ -1,12 +1,9 @@
|
||||
import { css, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState';
|
||||
|
||||
import { fieldMetadataItemByIdSelector } from '@/object-metadata/states/fieldMetadataItemByIdSelector';
|
||||
import { isFieldMetadataItemLabelIdentifierSelector } from '@/object-metadata/states/isFieldMetadataItemLabelIdentifierSelector';
|
||||
import { type RecordField } from '@/object-record/record-field/types/RecordField';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
|
||||
@@ -61,10 +58,6 @@ export const RecordTableColumnHead = ({
|
||||
}),
|
||||
);
|
||||
|
||||
const isRecordTableScrolledHorizontally = useRecoilComponentValue(
|
||||
isRecordTableScrolledHorizontallyComponentState,
|
||||
);
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
const Icon = getIcon(
|
||||
correspondingFieldMetadataItem.foundFieldMetadataItem?.icon,
|
||||
@@ -76,10 +69,10 @@ export const RecordTableColumnHead = ({
|
||||
}),
|
||||
);
|
||||
|
||||
// TODO: fix previous behavior with isRecordTableScrolledHorizontally
|
||||
|
||||
return (
|
||||
<StyledTitle
|
||||
hideTitle={isLabelIdentifier && isRecordTableScrolledHorizontally}
|
||||
>
|
||||
<StyledTitle hideTitle={isLabelIdentifier}>
|
||||
<StyledIcon>
|
||||
<Icon size={theme.icon.size.md} />
|
||||
</StyledIcon>
|
||||
|
||||
+20
-1
@@ -1,20 +1,39 @@
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
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 { RecordTableHeaderFirstScrollableCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderFirstScrollableCell';
|
||||
import { RecordTableHeaderLabelIdentifierCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCell';
|
||||
import { RecordTableHeaderLastColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn';
|
||||
import { useResizeTableHeader } from '@/object-record/record-table/record-table-header/hooks/useResizeTableHeader';
|
||||
import { filterOutByProperty } from 'twenty-shared/utils';
|
||||
|
||||
export const FIRST_TH_WIDTH = '10px';
|
||||
|
||||
export const RecordTableHeader = () => {
|
||||
const { visibleRecordFields } = useRecordTableContextOrThrow();
|
||||
const { labelIdentifierFieldMetadataItem } = useRecordIndexContextOrThrow();
|
||||
|
||||
const recordFieldsWithoutLabelIdentifierAndFirstOne = visibleRecordFields
|
||||
.filter(
|
||||
filterOutByProperty(
|
||||
'fieldMetadataItemId',
|
||||
labelIdentifierFieldMetadataItem?.id,
|
||||
),
|
||||
)
|
||||
.slice(1);
|
||||
|
||||
useResizeTableHeader();
|
||||
|
||||
return (
|
||||
<>
|
||||
<RecordTableHeaderDragDropColumn />
|
||||
<RecordTableHeaderCheckboxColumn />
|
||||
{visibleRecordFields.map((recordField) => (
|
||||
<RecordTableHeaderLabelIdentifierCell />
|
||||
<RecordTableHeaderFirstScrollableCell />
|
||||
{recordFieldsWithoutLabelIdentifierAndFirstOne.map((recordField) => (
|
||||
<RecordTableHeaderCell
|
||||
key={recordField.fieldMetadataItemId}
|
||||
recordField={recordField}
|
||||
|
||||
+13
-166
@@ -1,29 +1,15 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
|
||||
import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField';
|
||||
|
||||
import { type RecordField } from '@/object-record/record-field/types/RecordField';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
|
||||
import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown';
|
||||
import { RecordTableHeaderResizeHandler } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderResizeHandler';
|
||||
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
|
||||
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
|
||||
import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState';
|
||||
import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState';
|
||||
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
|
||||
import { type PointerEventListener } from '@/ui/utilities/pointer-event/types/PointerEventListener';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { useSaveRecordFields } from '@/views/hooks/useSaveRecordFields';
|
||||
import { throwIfNotDefined } from 'twenty-shared/utils';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
|
||||
const COLUMN_MIN_WIDTH = 48;
|
||||
|
||||
@@ -80,17 +66,6 @@ const StyledColumnHeaderCell = styled.div<{
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledResizeHandler = styled.div`
|
||||
bottom: 0;
|
||||
cursor: col-resize;
|
||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||
position: absolute;
|
||||
right: -9px;
|
||||
top: 0;
|
||||
width: 3px;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const StyledColumnHeadContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -101,10 +76,6 @@ const StyledColumnHeadContainer = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledHeaderIcon = styled.div`
|
||||
margin: ${({ theme }) => theme.spacing(1, 1, 1, 1.5)};
|
||||
`;
|
||||
|
||||
type RecordTableHeaderCellProps = {
|
||||
recordField: RecordField;
|
||||
};
|
||||
@@ -112,116 +83,7 @@ type RecordTableHeaderCellProps = {
|
||||
export const RecordTableHeaderCell = ({
|
||||
recordField,
|
||||
}: RecordTableHeaderCellProps) => {
|
||||
const { objectMetadataItem, objectPermissions } =
|
||||
useRecordTableContextOrThrow();
|
||||
|
||||
const resizeFieldOffsetState = useRecoilComponentCallbackState(
|
||||
resizeFieldOffsetComponentState,
|
||||
);
|
||||
|
||||
const [resizeFieldOffset, setResizeFieldOffset] = useRecoilComponentState(
|
||||
resizeFieldOffsetComponentState,
|
||||
);
|
||||
|
||||
const [initialPointerPositionX, setInitialPointerPositionX] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
const [resizedFieldMetadataItemId, setResizedFieldMetadataItemId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
const { saveRecordFields } = useSaveRecordFields();
|
||||
|
||||
const { updateRecordField } = useUpdateRecordField();
|
||||
|
||||
const handleResizeHandlerStart = useCallback<PointerEventListener>(
|
||||
({ x }) => {
|
||||
setInitialPointerPositionX(x);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const [iconVisibility, setIconVisibility] = useState(false);
|
||||
|
||||
const handleResizeHandlerMove = useCallback<PointerEventListener>(
|
||||
({ x }) => {
|
||||
if (!initialPointerPositionX) return;
|
||||
setResizeFieldOffset(x - initialPointerPositionX);
|
||||
},
|
||||
[setResizeFieldOffset, initialPointerPositionX],
|
||||
);
|
||||
|
||||
const handleResizeHandlerEnd = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async () => {
|
||||
throwIfNotDefined(recordField, 'recordField');
|
||||
|
||||
if (!resizedFieldMetadataItemId) return;
|
||||
|
||||
const resizeFieldOffset = getSnapshotValue(
|
||||
snapshot,
|
||||
resizeFieldOffsetState,
|
||||
);
|
||||
|
||||
const nextWidth = Math.round(
|
||||
Math.max(recordField.size + resizeFieldOffset, COLUMN_MIN_WIDTH),
|
||||
);
|
||||
|
||||
set(resizeFieldOffsetState, 0);
|
||||
setInitialPointerPositionX(null);
|
||||
setResizedFieldMetadataItemId(null);
|
||||
|
||||
if (nextWidth !== recordField.size) {
|
||||
const updatedRecordField = updateRecordField(
|
||||
resizedFieldMetadataItemId,
|
||||
{
|
||||
size: nextWidth,
|
||||
},
|
||||
);
|
||||
|
||||
saveRecordFields([updatedRecordField]);
|
||||
}
|
||||
},
|
||||
[
|
||||
recordField,
|
||||
saveRecordFields,
|
||||
resizedFieldMetadataItemId,
|
||||
resizeFieldOffsetState,
|
||||
setResizedFieldMetadataItemId,
|
||||
updateRecordField,
|
||||
],
|
||||
);
|
||||
|
||||
useTrackPointer({
|
||||
shouldTrackPointer: resizedFieldMetadataItemId !== null,
|
||||
onMouseDown: handleResizeHandlerStart,
|
||||
onMouseMove: handleResizeHandlerMove,
|
||||
onMouseUp: handleResizeHandlerEnd,
|
||||
});
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const { labelIdentifierFieldMetadataItem } = useRecordIndexContextOrThrow();
|
||||
|
||||
const isLabelIdentifier =
|
||||
recordField.fieldMetadataItemId === labelIdentifierFieldMetadataItem?.id;
|
||||
|
||||
const disableColumnResize = isLabelIdentifier || isMobile;
|
||||
|
||||
const { createNewIndexRecord } = useCreateNewIndexRecord({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const handlePlusButtonClick = () => {
|
||||
createNewIndexRecord();
|
||||
};
|
||||
|
||||
const isReadOnly = isObjectMetadataReadOnly({
|
||||
objectPermissions,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
|
||||
const { objectMetadataItem } = useRecordTableContextOrThrow();
|
||||
|
||||
const isFirstRowActive = useRecoilComponentFamilyValue(
|
||||
isRecordTableRowActiveComponentFamilyState,
|
||||
@@ -233,6 +95,14 @@ export const RecordTableHeaderCell = ({
|
||||
0,
|
||||
);
|
||||
|
||||
const resizeFieldOffset = useRecoilComponentValue(
|
||||
resizeFieldOffsetComponentState,
|
||||
);
|
||||
|
||||
const resizedFieldMetadataItemId = useRecoilComponentValue(
|
||||
resizedFieldMetadataIdComponentState,
|
||||
);
|
||||
|
||||
const widthOffsetWhileResizing =
|
||||
resizedFieldMetadataItemId === recordField.fieldMetadataItemId
|
||||
? resizeFieldOffset
|
||||
@@ -254,8 +124,6 @@ export const RecordTableHeaderCell = ({
|
||||
resizedFieldMetadataItemId === recordField.fieldMetadataItemId
|
||||
}
|
||||
columnWidth={columnWidth}
|
||||
onMouseEnter={() => setIconVisibility(true)}
|
||||
onMouseLeave={() => setIconVisibility(false)}
|
||||
isFirstRowActiveOrFocused={isFirstRowActiveOrFocused}
|
||||
>
|
||||
<StyledColumnHeadContainer>
|
||||
@@ -263,29 +131,8 @@ export const RecordTableHeaderCell = ({
|
||||
recordField={recordField}
|
||||
objectMetadataId={objectMetadataItem.id}
|
||||
/>
|
||||
{(useIsMobile() || iconVisibility) &&
|
||||
!!isLabelIdentifier &&
|
||||
!isReadOnly &&
|
||||
hasObjectUpdatePermissions && (
|
||||
<StyledHeaderIcon>
|
||||
<LightIconButton
|
||||
Icon={IconPlus}
|
||||
size="small"
|
||||
accent="tertiary"
|
||||
onClick={handlePlusButtonClick}
|
||||
/>
|
||||
</StyledHeaderIcon>
|
||||
)}
|
||||
</StyledColumnHeadContainer>
|
||||
{!disableColumnResize && (
|
||||
<StyledResizeHandler
|
||||
className="cursor-col-resize"
|
||||
role="separator"
|
||||
onPointerDown={() => {
|
||||
setResizedFieldMetadataItemId(recordField.fieldMetadataItemId);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<RecordTableHeaderResizeHandler recordField={recordField} />
|
||||
</StyledColumnHeaderCell>
|
||||
);
|
||||
};
|
||||
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { type RecordField } from '@/object-record/record-field/types/RecordField';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown';
|
||||
import { RecordTableHeaderResizeHandler } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderResizeHandler';
|
||||
import { COLUMN_RESIZE_MIN_WIDTH } from '@/object-record/record-table/record-table-header/hooks/useResizeTableHeader';
|
||||
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
|
||||
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
|
||||
import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState';
|
||||
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
|
||||
import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState';
|
||||
import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState';
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { filterOutByProperty } from 'twenty-shared/utils';
|
||||
|
||||
// TODO: factorize duplicated code here
|
||||
const StyledColumnHeaderCell = styled.div<{
|
||||
columnWidth: number;
|
||||
isResizing?: boolean;
|
||||
isFirstRowActiveOrFocused: boolean;
|
||||
zIndex: number;
|
||||
}>`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
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};
|
||||
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
|
||||
${({ columnWidth }) => `
|
||||
min-width: ${columnWidth}px;
|
||||
width: ${columnWidth}px;
|
||||
`}
|
||||
user-select: none;
|
||||
${({ theme }) => {
|
||||
return `
|
||||
&:hover {
|
||||
background: ${theme.background.secondary};
|
||||
};
|
||||
&:active {
|
||||
background: ${theme.background.tertiary};
|
||||
};
|
||||
`;
|
||||
}};
|
||||
${({ isResizing, theme }) => {
|
||||
if (isResizing === true) {
|
||||
return `&:after {
|
||||
background-color: ${theme.color.blue};
|
||||
bottom: 0;
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
top: 0;
|
||||
width: 2px;
|
||||
}`;
|
||||
}
|
||||
}};
|
||||
|
||||
// TODO: refactor this, each component should own its CSS
|
||||
div {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
z-index: ${({ zIndex }) => zIndex};
|
||||
`;
|
||||
|
||||
const StyledColumnHeadContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
& > :first-of-type {
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export const RecordTableHeaderFirstScrollableCell = () => {
|
||||
const { objectMetadataItem, visibleRecordFields } =
|
||||
useRecordTableContextOrThrow();
|
||||
|
||||
const isFirstRowActive = useRecoilComponentFamilyValue(
|
||||
isRecordTableRowActiveComponentFamilyState,
|
||||
0,
|
||||
);
|
||||
|
||||
const isFirstRowFocused = useRecoilComponentFamilyValue(
|
||||
isRecordTableRowFocusedComponentFamilyState,
|
||||
0,
|
||||
);
|
||||
|
||||
const { labelIdentifierFieldMetadataItem } = useRecordIndexContextOrThrow();
|
||||
|
||||
const recordField = visibleRecordFields.filter(
|
||||
filterOutByProperty(
|
||||
'fieldMetadataItemId',
|
||||
labelIdentifierFieldMetadataItem?.id,
|
||||
),
|
||||
)[0] as RecordField | undefined;
|
||||
|
||||
const resizeFieldOffset = useRecoilComponentValue(
|
||||
resizeFieldOffsetComponentState,
|
||||
);
|
||||
|
||||
const resizedFieldMetadataItemId = useRecoilComponentValue(
|
||||
resizedFieldMetadataIdComponentState,
|
||||
);
|
||||
|
||||
const widthOffsetWhileResizing =
|
||||
resizedFieldMetadataItemId === recordField?.fieldMetadataItemId
|
||||
? resizeFieldOffset
|
||||
: 0;
|
||||
|
||||
const baseWidth = recordField?.size ?? 0;
|
||||
|
||||
const computedDynamicWidth = baseWidth + widthOffsetWhileResizing;
|
||||
|
||||
const columnWidth = Math.max(computedDynamicWidth, COLUMN_RESIZE_MIN_WIDTH);
|
||||
|
||||
const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused;
|
||||
|
||||
const isRecordTableScrolledVertically = useRecoilComponentValue(
|
||||
isRecordTableScrolledVerticallyComponentState,
|
||||
);
|
||||
|
||||
const isRecordTableScrolledHorizontally = useRecoilComponentValue(
|
||||
isRecordTableScrolledHorizontallyComponentState,
|
||||
);
|
||||
|
||||
const zIndex =
|
||||
isRecordTableScrolledHorizontally && isRecordTableScrolledVertically
|
||||
? TABLE_Z_INDEX.scrolledBothVerticallyAndHorizontally
|
||||
.firstScrollableHeaderCell
|
||||
: isRecordTableScrolledHorizontally
|
||||
? TABLE_Z_INDEX.scrolledHorizontallyOnly.firstScrollableHeaderCell
|
||||
: isRecordTableScrolledVertically
|
||||
? TABLE_Z_INDEX.scrolledVerticallyOnly.firstScrollableHeaderCell
|
||||
: TABLE_Z_INDEX.noScrollAtAll.firstScrollableHeaderCell;
|
||||
|
||||
if (!recordField) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledColumnHeaderCell
|
||||
className="header-cell"
|
||||
key={recordField.fieldMetadataItemId}
|
||||
isResizing={
|
||||
resizedFieldMetadataItemId === recordField.fieldMetadataItemId
|
||||
}
|
||||
columnWidth={columnWidth}
|
||||
isFirstRowActiveOrFocused={isFirstRowActiveOrFocused}
|
||||
zIndex={zIndex}
|
||||
>
|
||||
<StyledColumnHeadContainer>
|
||||
<RecordTableColumnHeadWithDropdown
|
||||
recordField={recordField}
|
||||
objectMetadataId={objectMetadataItem.id}
|
||||
/>
|
||||
</StyledColumnHeadContainer>
|
||||
<RecordTableHeaderResizeHandler recordField={recordField} />
|
||||
</StyledColumnHeaderCell>
|
||||
);
|
||||
};
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
|
||||
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
|
||||
import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown';
|
||||
import { RecordTableHeaderResizeHandler } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderResizeHandler';
|
||||
import { COLUMN_RESIZE_MIN_WIDTH } from '@/object-record/record-table/record-table-header/hooks/useResizeTableHeader';
|
||||
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
|
||||
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
|
||||
import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState';
|
||||
import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { findByProperty } from 'twenty-shared/utils';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
|
||||
// TODO: factorize duplicated code here
|
||||
const StyledColumnHeaderCell = styled.div<{
|
||||
columnWidth: number;
|
||||
isResizing?: boolean;
|
||||
isFirstRowActiveOrFocused: boolean;
|
||||
}>`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
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};
|
||||
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
|
||||
${({ columnWidth }) => `
|
||||
min-width: ${columnWidth}px;
|
||||
width: ${columnWidth}px;
|
||||
`}
|
||||
user-select: none;
|
||||
${({ theme }) => {
|
||||
return `
|
||||
&:hover {
|
||||
background: ${theme.background.secondary};
|
||||
};
|
||||
&:active {
|
||||
background: ${theme.background.tertiary};
|
||||
};
|
||||
`;
|
||||
}};
|
||||
${({ isResizing, theme }) => {
|
||||
if (isResizing === true) {
|
||||
return `&:after {
|
||||
background-color: ${theme.color.blue};
|
||||
bottom: 0;
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
top: 0;
|
||||
width: 2px;
|
||||
}`;
|
||||
}
|
||||
}};
|
||||
|
||||
// TODO: refactor this, each component should own its CSS
|
||||
div {
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledColumnHeadContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
& > :first-of-type {
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledHeaderIcon = styled.div`
|
||||
margin: ${({ theme }) => theme.spacing(1, 1, 1, 1.5)};
|
||||
`;
|
||||
|
||||
export const RecordTableHeaderLabelIdentifierCell = () => {
|
||||
const { objectMetadataItem, objectPermissions, visibleRecordFields } =
|
||||
useRecordTableContextOrThrow();
|
||||
|
||||
const [iconIsVisible, setIconIsVisible] = useState(false);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const isFirstRowActive = useRecoilComponentFamilyValue(
|
||||
isRecordTableRowActiveComponentFamilyState,
|
||||
0,
|
||||
);
|
||||
|
||||
const isFirstRowFocused = useRecoilComponentFamilyValue(
|
||||
isRecordTableRowFocusedComponentFamilyState,
|
||||
0,
|
||||
);
|
||||
|
||||
const { labelIdentifierFieldMetadataItem } = useRecordIndexContextOrThrow();
|
||||
|
||||
const recordField = visibleRecordFields.find(
|
||||
findByProperty('fieldMetadataItemId', labelIdentifierFieldMetadataItem?.id),
|
||||
);
|
||||
|
||||
const { createNewIndexRecord } = useCreateNewIndexRecord({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const resizeFieldOffset = useRecoilComponentValue(
|
||||
resizeFieldOffsetComponentState,
|
||||
);
|
||||
|
||||
const resizedFieldMetadataItemId = useRecoilComponentValue(
|
||||
resizedFieldMetadataIdComponentState,
|
||||
);
|
||||
|
||||
if (!recordField) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const handlePlusButtonClick = () => {
|
||||
createNewIndexRecord();
|
||||
};
|
||||
|
||||
const isReadOnly = isObjectMetadataReadOnly({
|
||||
objectPermissions,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
|
||||
|
||||
const widthOffsetWhileResizing =
|
||||
resizedFieldMetadataItemId === recordField.fieldMetadataItemId
|
||||
? resizeFieldOffset
|
||||
: 0;
|
||||
|
||||
const baseWidth = recordField?.size ?? 0;
|
||||
|
||||
const computedDynamicWidth = baseWidth + widthOffsetWhileResizing;
|
||||
|
||||
const columnWidth = Math.max(computedDynamicWidth, COLUMN_RESIZE_MIN_WIDTH);
|
||||
|
||||
const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused;
|
||||
|
||||
return (
|
||||
<StyledColumnHeaderCell
|
||||
className="header-cell"
|
||||
key={recordField.fieldMetadataItemId}
|
||||
isResizing={
|
||||
resizedFieldMetadataItemId === recordField.fieldMetadataItemId
|
||||
}
|
||||
columnWidth={columnWidth}
|
||||
onMouseEnter={() => setIconIsVisible(true)}
|
||||
onMouseLeave={() => setIconIsVisible(false)}
|
||||
isFirstRowActiveOrFocused={isFirstRowActiveOrFocused}
|
||||
>
|
||||
<StyledColumnHeadContainer>
|
||||
<RecordTableColumnHeadWithDropdown
|
||||
recordField={recordField}
|
||||
objectMetadataId={objectMetadataItem.id}
|
||||
/>
|
||||
{(isMobile || iconIsVisible) &&
|
||||
!isReadOnly &&
|
||||
hasObjectUpdatePermissions && (
|
||||
<StyledHeaderIcon>
|
||||
<LightIconButton
|
||||
Icon={IconPlus}
|
||||
size="small"
|
||||
accent="tertiary"
|
||||
onClick={handlePlusButtonClick}
|
||||
/>
|
||||
</StyledHeaderIcon>
|
||||
)}
|
||||
</StyledColumnHeadContainer>
|
||||
<RecordTableHeaderResizeHandler recordField={recordField} />
|
||||
</StyledColumnHeaderCell>
|
||||
);
|
||||
};
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { type RecordField } from '@/object-record/record-field/types/RecordField';
|
||||
import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import styled from '@emotion/styled';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
const StyledResizeHandler = styled.div`
|
||||
bottom: 0;
|
||||
cursor: col-resize;
|
||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||
position: absolute;
|
||||
right: -9px;
|
||||
top: 0;
|
||||
width: 3px;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
export const RecordTableHeaderResizeHandler = ({
|
||||
recordField,
|
||||
}: {
|
||||
recordField: RecordField;
|
||||
}) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const columnResizeDisabled = isMobile;
|
||||
|
||||
const setResizedFieldMetadataItemId = useSetRecoilComponentState(
|
||||
resizedFieldMetadataIdComponentState,
|
||||
);
|
||||
|
||||
return (
|
||||
!columnResizeDisabled && (
|
||||
<StyledResizeHandler
|
||||
className="cursor-col-resize"
|
||||
role="separator"
|
||||
onPointerDown={() => {
|
||||
setResizedFieldMetadataItemId(recordField.fieldMetadataItemId);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState';
|
||||
import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState';
|
||||
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
|
||||
import { type PointerEventListener } from '@/ui/utilities/pointer-event/types/PointerEventListener';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { useSaveRecordFields } from '@/views/hooks/useSaveRecordFields';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { findByProperty, throwIfNotDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const COLUMN_RESIZE_MIN_WIDTH = 48;
|
||||
|
||||
export const useResizeTableHeader = () => {
|
||||
const { visibleRecordFields } = useRecordTableContextOrThrow();
|
||||
|
||||
const resizeFieldOffsetCallbackState = useRecoilComponentCallbackState(
|
||||
resizeFieldOffsetComponentState,
|
||||
);
|
||||
|
||||
const setResizeFieldOffset = useSetRecoilComponentState(
|
||||
resizeFieldOffsetComponentState,
|
||||
);
|
||||
|
||||
const [initialPointerPositionX, setInitialPointerPositionX] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const [resizedFieldMetadataItemId, setResizedFieldMetadataItemId] =
|
||||
useRecoilComponentState(resizedFieldMetadataIdComponentState);
|
||||
|
||||
const { saveRecordFields } = useSaveRecordFields();
|
||||
|
||||
const { updateRecordField } = useUpdateRecordField();
|
||||
|
||||
const handleResizeHandlerStart = useCallback<PointerEventListener>(
|
||||
({ x }) => {
|
||||
setInitialPointerPositionX(x);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleResizeHandlerMove = useCallback<PointerEventListener>(
|
||||
({ x }) => {
|
||||
if (!initialPointerPositionX) return;
|
||||
|
||||
setResizeFieldOffset(x - initialPointerPositionX);
|
||||
},
|
||||
[setResizeFieldOffset, initialPointerPositionX],
|
||||
);
|
||||
|
||||
const handleResizeHandlerEnd = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async () => {
|
||||
const recordField = visibleRecordFields.find(
|
||||
findByProperty('fieldMetadataItemId', resizedFieldMetadataItemId),
|
||||
);
|
||||
|
||||
throwIfNotDefined(recordField, 'recordField');
|
||||
|
||||
if (!resizedFieldMetadataItemId) return;
|
||||
|
||||
const resizeFieldOffset = getSnapshotValue(
|
||||
snapshot,
|
||||
resizeFieldOffsetCallbackState,
|
||||
);
|
||||
|
||||
const nextWidth = Math.round(
|
||||
Math.max(
|
||||
recordField.size + resizeFieldOffset,
|
||||
COLUMN_RESIZE_MIN_WIDTH,
|
||||
),
|
||||
);
|
||||
|
||||
set(resizeFieldOffsetCallbackState, 0);
|
||||
setInitialPointerPositionX(null);
|
||||
setResizedFieldMetadataItemId(null);
|
||||
|
||||
if (nextWidth !== recordField.size) {
|
||||
const updatedRecordField = updateRecordField(
|
||||
resizedFieldMetadataItemId,
|
||||
{
|
||||
size: nextWidth,
|
||||
},
|
||||
);
|
||||
|
||||
saveRecordFields([updatedRecordField]);
|
||||
}
|
||||
},
|
||||
[
|
||||
saveRecordFields,
|
||||
resizedFieldMetadataItemId,
|
||||
resizeFieldOffsetCallbackState,
|
||||
setResizedFieldMetadataItemId,
|
||||
updateRecordField,
|
||||
visibleRecordFields,
|
||||
],
|
||||
);
|
||||
|
||||
useTrackPointer({
|
||||
shouldTrackPointer: resizedFieldMetadataItemId !== null,
|
||||
onMouseDown: handleResizeHandlerStart,
|
||||
onMouseMove: handleResizeHandlerMove,
|
||||
onMouseUp: handleResizeHandlerEnd,
|
||||
});
|
||||
};
|
||||
+21
-8
@@ -2,12 +2,13 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte
|
||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { useRecordTableRowDraggableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
|
||||
import { RecordTableCell } from '@/object-record/record-table/record-table-cell/components/RecordTableCell';
|
||||
import { RecordTableCellFirstRowFirstColumn } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFirstRowFirstColumn';
|
||||
import { RecordTableCellWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellWrapper';
|
||||
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
|
||||
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
|
||||
|
||||
export const RecordTableCellsVisible = () => {
|
||||
const { isSelected } = useRecordTableRowContextOrThrow();
|
||||
const { isSelected, rowIndex } = useRecordTableRowContextOrThrow();
|
||||
|
||||
const { isDragging } = useRecordTableRowDraggableContextOrThrow();
|
||||
|
||||
@@ -19,19 +20,31 @@ export const RecordTableCellsVisible = () => {
|
||||
|
||||
const recordFieldsAfterFirst = visibleRecordFields.slice(1);
|
||||
|
||||
const isFirstRow = rowIndex === 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<RecordTableCellWrapper
|
||||
recordField={visibleRecordFields[0]}
|
||||
recordFieldIndex={0}
|
||||
>
|
||||
<RecordTableTd
|
||||
isSelected={isSelected}
|
||||
isDragging={isDragging}
|
||||
width={visibleRecordFields[0].size}
|
||||
>
|
||||
<RecordTableCell />
|
||||
</RecordTableTd>
|
||||
{isFirstRow ? (
|
||||
<RecordTableCellFirstRowFirstColumn
|
||||
isSelected={isSelected}
|
||||
isDragging={isDragging}
|
||||
width={visibleRecordFields[0].size}
|
||||
>
|
||||
<RecordTableCell />
|
||||
</RecordTableCellFirstRowFirstColumn>
|
||||
) : (
|
||||
<RecordTableTd
|
||||
isSelected={isSelected}
|
||||
isDragging={isDragging}
|
||||
width={visibleRecordFields[0].size}
|
||||
>
|
||||
<RecordTableCell />
|
||||
</RecordTableTd>
|
||||
)}
|
||||
</RecordTableCellWrapper>
|
||||
{recordFieldsAfterFirst.map((recordField, recordFieldIndex) => (
|
||||
<RecordTableCellWrapper
|
||||
|
||||
+6
-3
@@ -22,13 +22,15 @@ const StyledTr = styled.div<{
|
||||
flex-direction: row;
|
||||
|
||||
&[data-next-row-active-or-focused='true'] {
|
||||
div.table-cell {
|
||||
div.table-cell,
|
||||
div.table-cell-0-0 {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-focused='true'] {
|
||||
div.table-cell {
|
||||
div.table-cell,
|
||||
div.table-cell-0-0 {
|
||||
&:not(:first-of-type) {
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
@@ -49,7 +51,8 @@ const StyledTr = styled.div<{
|
||||
}
|
||||
|
||||
&[data-active='true'] {
|
||||
div.table-cell {
|
||||
div.table-cell,
|
||||
div.table-cell-0-0 {
|
||||
&:not(:first-of-type) {
|
||||
border-bottom: 1px solid ${({ theme }) => theme.adaptiveColors.blue3};
|
||||
border-top: 1px solid ${({ theme }) => theme.adaptiveColors.blue3};
|
||||
|
||||
+1
-1
@@ -5,5 +5,5 @@ export const isRecordTableScrolledHorizontallyComponentState =
|
||||
createComponentState<boolean>({
|
||||
key: 'isRecordTableScrolledHorizontallyComponentState',
|
||||
componentInstanceContext: RecordTableComponentInstanceContext,
|
||||
defaultValue: true,
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
+1
-1
@@ -5,5 +5,5 @@ export const isRecordTableScrolledVerticallyComponentState =
|
||||
createComponentState<boolean>({
|
||||
key: 'isRecordTableScrolledVerticallyComponentState',
|
||||
componentInstanceContext: RecordTableComponentInstanceContext,
|
||||
defaultValue: true,
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
import { type Nullable } from 'twenty-shared/types';
|
||||
|
||||
export const resizedFieldMetadataIdComponentState = createComponentState<
|
||||
Nullable<string>
|
||||
>({
|
||||
key: 'resizedFieldMetadataIdComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: RecordTableComponentInstanceContext,
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export type Nullable<T> = T | null | undefined;
|
||||
@@ -18,6 +18,7 @@ export { FieldMetadataType } from './FieldMetadataType';
|
||||
export type { FromTo } from './FromToType';
|
||||
export type { IsExactly } from './IsExactly';
|
||||
export type { NonNullableRequired } from './NonNullableRequired';
|
||||
export type { Nullable } from './Nullable';
|
||||
export type { NullablePartial } from './NullablePartial';
|
||||
export type { ObjectPermissions } from './ObjectPermissions';
|
||||
export type { ObjectPermissionsDeprecated } from './ObjectPermissionsDeprecated';
|
||||
|
||||
Reference in New Issue
Block a user