Fixed table header resize bugs (#14401)

This PR fixes the main resize bugs in
https://github.com/twentyhq/core-team-issues/issues/1453

Changes :

- A new `RecordTableResizeEffect` has been created to modify the last
column width to compensate for the resize width dynamically, because of
flew-wrap on the table elements, we cannot use width: 100% everywhere
and we have to set the width of everything.
- CSS transitions have been removed because they were degrading both the
performance and the UX when resizing
- Created a common `RecordTableHeaderCellContainer`, this was needed to
factorize the various table header components
- Created a `RecordTableHeaderLabelIdentifierCellPlusButton` component
to ease the reading of `RecordTableHeaderLabelIdentifierCell`
- Put the CSS of the blue line while resizing in the
`RecordTableHeaderResizeHandler` component to avoid duplicating it
everywhere.
- Extracted `COLUMN_MIN_WIDTH` in a constant file

Fixes https://github.com/twentyhq/core-team-issues/issues/1453
This commit is contained in:
Lucas Bordeau
2025-09-10 19:04:47 +02:00
committed by GitHub
parent fb43827031
commit 8d10e738ac
23 changed files with 364 additions and 316 deletions
@@ -24,7 +24,7 @@ export const RecordTable = () => {
objectMetadataItem.id, objectMetadataItem.id,
); );
const tableBodyRef = useRef<HTMLTableElement>(null); const tableBodyRef = useRef<HTMLDivElement>(null);
const { toggleClickOutside } = useClickOutsideListener( const { toggleClickOutside } = useClickOutsideListener(
RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID, RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID,
@@ -7,7 +7,7 @@ import { RecordTableRecordGroupBodyEffects } from '@/object-record/record-table/
export interface RecordTableBodyEffectsWrapperProps { export interface RecordTableBodyEffectsWrapperProps {
hasRecordGroups: boolean; hasRecordGroups: boolean;
tableBodyRef: React.RefObject<HTMLTableElement>; tableBodyRef: React.RefObject<HTMLDivElement>;
} }
export const RecordTableBodyEffectsWrapper = ({ export const RecordTableBodyEffectsWrapper = ({
@@ -1,3 +1,4 @@
import { RecordTableResizeEffect } from '@/object-record/record-table/components/RecordTableResizeEffect';
import { RecordTableScrollAndZIndexEffect } from '@/object-record/record-table/components/RecordTableScrollAndZIndexEffect'; import { RecordTableScrollAndZIndexEffect } from '@/object-record/record-table/components/RecordTableScrollAndZIndexEffect';
import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex'; import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex';
import { RecordTableNoRecordGroupBody } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody'; import { RecordTableNoRecordGroupBody } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody';
@@ -23,6 +24,7 @@ const StyledTable = styled.div<{
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
width: 100%;
div.header-cell { div.header-cell {
position: sticky; position: sticky;
@@ -36,7 +38,6 @@ const StyledTable = styled.div<{
div.header-cell:nth-of-type(1) { div.header-cell:nth-of-type(1) {
left: 0px; left: 0px;
transition: 0.3s ease;
background-color: ${({ theme }) => theme.background.primary}; background-color: ${({ theme }) => theme.background.primary};
z-index: ${TABLE_Z_INDEX.headerColumnsSticky}; z-index: ${TABLE_Z_INDEX.headerColumnsSticky};
@@ -46,7 +47,6 @@ const StyledTable = styled.div<{
left: 16px; left: 16px;
top: 0; top: 0;
transition: 0.3s ease;
background-color: ${({ theme }) => theme.background.primary}; background-color: ${({ theme }) => theme.background.primary};
z-index: ${TABLE_Z_INDEX.headerColumnsSticky}; z-index: ${TABLE_Z_INDEX.headerColumnsSticky};
@@ -56,7 +56,6 @@ const StyledTable = styled.div<{
left: 48px; left: 48px;
right: 0; right: 0;
transition: 0.3s ease;
background-color: ${({ theme }) => theme.background.primary}; background-color: ${({ theme }) => theme.background.primary};
z-index: ${TABLE_Z_INDEX.headerColumnsSticky}; z-index: ${TABLE_Z_INDEX.headerColumnsSticky};
@@ -143,7 +142,7 @@ const StyledTableContainer = styled.div`
`; `;
export interface RecordTableContentProps { export interface RecordTableContentProps {
tableBodyRef: React.RefObject<HTMLTableElement>; tableBodyRef: React.RefObject<HTMLDivElement>;
handleDragSelectionStart: () => void; handleDragSelectionStart: () => void;
handleDragSelectionEnd: () => void; handleDragSelectionEnd: () => void;
hasRecordGroups: boolean; hasRecordGroups: boolean;
@@ -197,6 +196,7 @@ export const RecordTableContent = ({
<RecordTableNoRecordGroupBody /> <RecordTableNoRecordGroupBody />
)} )}
<RecordTableScrollAndZIndexEffect /> <RecordTableScrollAndZIndexEffect />
<RecordTableResizeEffect />
</StyledTable> </StyledTable>
<DragSelect <DragSelect
selectableItemsContainerRef={containerRef} selectableItemsContainerRef={containerRef}
@@ -9,7 +9,7 @@ const StyledEmptyStateContainer = styled.div`
`; `;
export interface RecordTableEmptyProps { export interface RecordTableEmptyProps {
tableBodyRef: React.RefObject<HTMLTableElement>; tableBodyRef: React.RefObject<HTMLDivElement>;
} }
export const RecordTableEmpty = ({ tableBodyRef }: RecordTableEmptyProps) => ( export const RecordTableEmpty = ({ tableBodyRef }: RecordTableEmptyProps) => (
@@ -0,0 +1,40 @@
import { recordTableWidthComponentState } from '@/object-record/record-table/states/recordTableWidthComponentState';
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const RecordTableResizeEffect = () => {
const setRecordTableWidth = useSetRecoilComponentState(
recordTableWidthComponentState,
);
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
useEffect(() => {
const tableWidth = scrollWrapperHTMLElement?.clientWidth ?? 0;
setRecordTableWidth(tableWidth);
const tableResizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.target === scrollWrapperHTMLElement) {
const newWidth = scrollWrapperHTMLElement.clientWidth;
setRecordTableWidth(newWidth);
}
}
});
if (isDefined(scrollWrapperHTMLElement)) {
tableResizeObserver.observe(scrollWrapperHTMLElement);
}
return () => {
tableResizeObserver.disconnect();
};
}, [setRecordTableWidth, scrollWrapperHTMLElement]);
return null;
};
@@ -0,0 +1 @@
export const COLUMN_MIN_WIDTH = 48;
@@ -0,0 +1,41 @@
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { recordTableWidthComponentState } from '@/object-record/record-table/states/recordTableWidthComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { sumByProperty } from 'twenty-shared/utils';
// TODO: use this everywhere and extract in files
export const RECORD_TABLE_DRAG_DROP_COLUMN_WIDTH = 16;
export const RECORD_TABLE_CHECKBOX_COLUMN_WIDTH = 32;
export const RECORD_TABLE_PLUS_BUTTON_COLUMN_WIDTH = 32;
export const useRecordTableLastColumnWidthToFill = () => {
const { visibleRecordFields } = useRecordTableContextOrThrow();
const recordTableWidth = useRecoilComponentValue(
recordTableWidthComponentState,
);
const totalColumnsWidth = visibleRecordFields.reduce(
sumByProperty('size'),
0,
);
const widthOfBorders = visibleRecordFields.length;
const fixedColumnsWidth =
RECORD_TABLE_DRAG_DROP_COLUMN_WIDTH +
RECORD_TABLE_CHECKBOX_COLUMN_WIDTH +
RECORD_TABLE_PLUS_BUTTON_COLUMN_WIDTH +
widthOfBorders;
const remainingWidthToFill = Math.max(
0,
recordTableWidth - fixedColumnsWidth - totalColumnsWidth,
);
const width = remainingWidthToFill > 0 ? remainingWidthToFill : 0;
return {
lastColumnWidth: width,
};
};
@@ -20,7 +20,6 @@ export const RecordTableBodyDroppable = ({
isDropDisabled, isDropDisabled,
}: RecordTableBodyDroppableProps) => { }: RecordTableBodyDroppableProps) => {
const [v4Persistable] = useState(v4()); const [v4Persistable] = useState(v4());
const recordTableBodyId = `record-table-body${recordGroupId ? '-' + recordGroupId : ''}`;
const setRecordTableHoverPosition = useSetRecoilComponentState( const setRecordTableHoverPosition = useSetRecoilComponentState(
recordTableHoverPositionComponentState, recordTableHoverPositionComponentState,
@@ -42,19 +41,20 @@ export const RecordTableBodyDroppable = ({
isDropDisabled={isDropDisabled} isDropDisabled={isDropDisabled}
> >
{(provided) => ( {(provided) => (
<RecordTableBody <>
id={recordTableBodyId} <RecordTableBody
ref={provided.innerRef} ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps} {...provided.droppableProps}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
>
<RecordTableBodyDroppableContextProvider
value={{ droppablePlaceholder: provided.placeholder }}
> >
{children} <RecordTableBodyDroppableContextProvider
</RecordTableBodyDroppableContextProvider> value={{ droppablePlaceholder: provided.placeholder }}
</RecordTableBody> >
{children}
</RecordTableBodyDroppableContextProvider>
</RecordTableBody>
</>
)} )}
</Droppable> </Droppable>
); );
@@ -1,8 +1,27 @@
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableLastColumnWidthToFill } from '@/object-record/record-table/hooks/useRecordTableLastColumnWidthToFill';
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd'; import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
export const RecordTableLastEmptyCell = () => { export const RecordTableLastEmptyCell = () => {
const { isSelected } = useRecordTableRowContextOrThrow(); const { isSelected } = useRecordTableRowContextOrThrow();
const { lastColumnWidth } = useRecordTableLastColumnWidthToFill();
return <RecordTableTd isSelected={isSelected} hasRightBorder={false} />; const resizeFieldOffset = useRecoilComponentValue(
resizeFieldOffsetComponentState,
);
const width =
resizeFieldOffset > 0
? lastColumnWidth + resizeFieldOffset
: lastColumnWidth;
return (
<RecordTableTd
isSelected={isSelected}
hasRightBorder={false}
width={width}
/>
);
}; };
@@ -0,0 +1,15 @@
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RECORD_TABLE_PLUS_BUTTON_COLUMN_WIDTH } from '@/object-record/record-table/hooks/useRecordTableLastColumnWidthToFill';
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
export const RecordTablePlusButtonCellPlaceholder = () => {
const { isSelected } = useRecordTableRowContextOrThrow();
return (
<RecordTableTd
isSelected={isSelected}
hasRightBorder={false}
width={RECORD_TABLE_PLUS_BUTTON_COLUMN_WIDTH}
/>
);
};
@@ -21,7 +21,6 @@ const StyledColumnFooterCell = styled.div<{
width: ${columnWidth}px; width: ${columnWidth}px;
`} `}
text-align: left; text-align: left;
transition: 0.3s ease;
${({ theme }) => { ${({ theme }) => {
return ` return `
&:hover { &:hover {
@@ -5,13 +5,11 @@ import { RecordTableHeaderPlusButtonContent } from '@/object-record/record-table
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState'; import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState'; import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import { IconPlus } from 'twenty-ui/display'; import { IconPlus } from 'twenty-ui/display';
const StyledPlusIconHeaderCell = styled.div<{ const StyledPlusIconHeaderCell = styled.div<{
isTableWiderThanScreen: boolean;
isFirstRowActiveOrFocused: boolean; isFirstRowActiveOrFocused: boolean;
}>` }>`
border-bottom: ${({ isFirstRowActiveOrFocused, theme }) => border-bottom: ${({ isFirstRowActiveOrFocused, theme }) =>
@@ -53,12 +51,6 @@ const StyledDropdownContainer = styled.div`
export const RecordTableHeaderAddColumnButton = () => { export const RecordTableHeaderAddColumnButton = () => {
const theme = useTheme(); const theme = useTheme();
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
const isTableWiderThanScreen =
(scrollWrapperHTMLElement?.clientWidth ?? 0) <
(scrollWrapperHTMLElement?.scrollWidth ?? 0);
const isFirstRowActive = useRecoilComponentFamilyValue( const isFirstRowActive = useRecoilComponentFamilyValue(
isRecordTableRowActiveComponentFamilyState, isRecordTableRowActiveComponentFamilyState,
0, 0,
@@ -73,7 +65,6 @@ export const RecordTableHeaderAddColumnButton = () => {
return ( return (
<StyledPlusIconHeaderCell <StyledPlusIconHeaderCell
isTableWiderThanScreen={isTableWiderThanScreen}
isFirstRowActiveOrFocused={isFirstRowActiveOrFocused} isFirstRowActiveOrFocused={isFirstRowActiveOrFocused}
className="header-cell" className="header-cell"
> >
@@ -1,8 +1,8 @@
import styled from '@emotion/styled';
import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { type RecordField } from '@/object-record/record-field/types/RecordField';
import { COLUMN_MIN_WIDTH } from '@/object-record/record-table/constants/ColumnMinWidth';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown'; import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown';
import { RecordTableHeaderCellContainer } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCellContainer';
import { RecordTableHeaderResizeHandler } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderResizeHandler'; import { RecordTableHeaderResizeHandler } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderResizeHandler';
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState'; import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState'; import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
@@ -11,71 +11,6 @@ import { resizeFieldOffsetComponentState } from '@/object-record/record-table/st
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
const COLUMN_MIN_WIDTH = 48;
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;
}
`;
type RecordTableHeaderCellProps = { type RecordTableHeaderCellProps = {
recordField: RecordField; recordField: RecordField;
}; };
@@ -117,7 +52,7 @@ export const RecordTableHeaderCell = ({
const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused; const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused;
return ( return (
<StyledColumnHeaderCell <RecordTableHeaderCellContainer
className="header-cell" className="header-cell"
key={recordField.fieldMetadataItemId} key={recordField.fieldMetadataItemId}
isResizing={ isResizing={
@@ -126,13 +61,11 @@ export const RecordTableHeaderCell = ({
columnWidth={columnWidth} columnWidth={columnWidth}
isFirstRowActiveOrFocused={isFirstRowActiveOrFocused} isFirstRowActiveOrFocused={isFirstRowActiveOrFocused}
> >
<StyledColumnHeadContainer> <RecordTableColumnHeadWithDropdown
<RecordTableColumnHeadWithDropdown recordField={recordField}
recordField={recordField} objectMetadataId={objectMetadataItem.id}
objectMetadataId={objectMetadataItem.id} />
/>
</StyledColumnHeadContainer>
<RecordTableHeaderResizeHandler recordField={recordField} /> <RecordTableHeaderResizeHandler recordField={recordField} />
</StyledColumnHeaderCell> </RecordTableHeaderCellContainer>
); );
}; };
@@ -0,0 +1,54 @@
import styled from '@emotion/styled';
const StyledHeaderCell = 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;
}`;
}
}};
z-index: ${({ zIndex }) => zIndex ?? 'auto'};
`;
export const RecordTableHeaderCellContainer = StyledHeaderCell;
@@ -1,12 +1,13 @@
import styled from '@emotion/styled';
import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { type RecordField } from '@/object-record/record-field/types/RecordField';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex'; import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown'; 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 { 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 { RecordTableHeaderCellContainer } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCellContainer';
import { COLUMN_MIN_WIDTH } from '@/object-record/record-table/constants/ColumnMinWidth';
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState'; import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState'; import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState'; import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState';
@@ -17,73 +18,6 @@ import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-st
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { filterOutByProperty } from 'twenty-shared/utils'; 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 = () => { export const RecordTableHeaderFirstScrollableCell = () => {
const { objectMetadataItem, visibleRecordFields } = const { objectMetadataItem, visibleRecordFields } =
useRecordTableContextOrThrow(); useRecordTableContextOrThrow();
@@ -124,7 +58,7 @@ export const RecordTableHeaderFirstScrollableCell = () => {
const computedDynamicWidth = baseWidth + widthOffsetWhileResizing; const computedDynamicWidth = baseWidth + widthOffsetWhileResizing;
const columnWidth = Math.max(computedDynamicWidth, COLUMN_RESIZE_MIN_WIDTH); const columnWidth = Math.max(computedDynamicWidth, COLUMN_MIN_WIDTH);
const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused; const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused;
@@ -151,7 +85,7 @@ export const RecordTableHeaderFirstScrollableCell = () => {
} }
return ( return (
<StyledColumnHeaderCell <RecordTableHeaderCellContainer
className="header-cell" className="header-cell"
key={recordField.fieldMetadataItemId} key={recordField.fieldMetadataItemId}
isResizing={ isResizing={
@@ -161,13 +95,11 @@ export const RecordTableHeaderFirstScrollableCell = () => {
isFirstRowActiveOrFocused={isFirstRowActiveOrFocused} isFirstRowActiveOrFocused={isFirstRowActiveOrFocused}
zIndex={zIndex} zIndex={zIndex}
> >
<StyledColumnHeadContainer> <RecordTableColumnHeadWithDropdown
<RecordTableColumnHeadWithDropdown recordField={recordField}
recordField={recordField} objectMetadataId={objectMetadataItem.id}
objectMetadataId={objectMetadataItem.id} />
/>
</StyledColumnHeadContainer>
<RecordTableHeaderResizeHandler recordField={recordField} /> <RecordTableHeaderResizeHandler recordField={recordField} />
</StyledColumnHeaderCell> </RecordTableHeaderCellContainer>
); );
}; };
@@ -1,102 +1,40 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useState } from 'react';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; 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 { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown';
import { RecordTableHeaderResizeHandler } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderResizeHandler'; 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 { RecordTableHeaderCellContainer } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCellContainer';
import { COLUMN_MIN_WIDTH } from '@/object-record/record-table/constants/ColumnMinWidth';
import { RecordTableHeaderLabelIdentifierCellPlusButton } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCellPlusButton';
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState'; import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState'; import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState'; import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState';
import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState'; 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 { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useState } from 'react';
import { findByProperty } from 'twenty-shared/utils'; 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` const StyledColumnHeadContainer = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
overflow: hidden;
& > :first-of-type { & > :first-of-type {
flex: 1; flex: 1;
} }
`; `;
const StyledHeaderIcon = styled.div`
margin: ${({ theme }) => theme.spacing(1, 1, 1, 1.5)};
`;
export const RecordTableHeaderLabelIdentifierCell = () => { export const RecordTableHeaderLabelIdentifierCell = () => {
const { objectMetadataItem, objectPermissions, visibleRecordFields } = const { objectMetadataItem, visibleRecordFields } =
useRecordTableContextOrThrow(); useRecordTableContextOrThrow();
const [iconIsVisible, setIconIsVisible] = useState(false); const [iconIsVisible, setIconIsVisible] = useState(false);
const isMobile = useIsMobile();
const isFirstRowActive = useRecoilComponentFamilyValue( const isFirstRowActive = useRecoilComponentFamilyValue(
isRecordTableRowActiveComponentFamilyState, isRecordTableRowActiveComponentFamilyState,
0, 0,
@@ -113,10 +51,6 @@ export const RecordTableHeaderLabelIdentifierCell = () => {
findByProperty('fieldMetadataItemId', labelIdentifierFieldMetadataItem?.id), findByProperty('fieldMetadataItemId', labelIdentifierFieldMetadataItem?.id),
); );
const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem,
});
const resizeFieldOffset = useRecoilComponentValue( const resizeFieldOffset = useRecoilComponentValue(
resizeFieldOffsetComponentState, resizeFieldOffsetComponentState,
); );
@@ -125,25 +59,10 @@ export const RecordTableHeaderLabelIdentifierCell = () => {
resizedFieldMetadataIdComponentState, resizedFieldMetadataIdComponentState,
); );
const hasAnySoftDeleteFilterOnView = useRecoilComponentValue(
hasAnySoftDeleteFilterOnViewComponentSelector,
);
if (!recordField) { if (!recordField) {
return <></>; return <></>;
} }
const handlePlusButtonClick = () => {
createNewIndexRecord();
};
const isReadOnly = isObjectMetadataReadOnly({
objectPermissions,
objectMetadataItem,
});
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
const widthOffsetWhileResizing = const widthOffsetWhileResizing =
resizedFieldMetadataItemId === recordField.fieldMetadataItemId resizedFieldMetadataItemId === recordField.fieldMetadataItemId
? resizeFieldOffset ? resizeFieldOffset
@@ -153,12 +72,12 @@ export const RecordTableHeaderLabelIdentifierCell = () => {
const computedDynamicWidth = baseWidth + widthOffsetWhileResizing; const computedDynamicWidth = baseWidth + widthOffsetWhileResizing;
const columnWidth = Math.max(computedDynamicWidth, COLUMN_RESIZE_MIN_WIDTH); const columnWidth = Math.max(computedDynamicWidth, COLUMN_MIN_WIDTH);
const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused; const isFirstRowActiveOrFocused = isFirstRowActive || isFirstRowFocused;
return ( return (
<StyledColumnHeaderCell <RecordTableHeaderCellContainer
className="header-cell" className="header-cell"
key={recordField.fieldMetadataItemId} key={recordField.fieldMetadataItemId}
isResizing={ isResizing={
@@ -174,21 +93,9 @@ export const RecordTableHeaderLabelIdentifierCell = () => {
recordField={recordField} recordField={recordField}
objectMetadataId={objectMetadataItem.id} objectMetadataId={objectMetadataItem.id}
/> />
{(isMobile || iconIsVisible) && {iconIsVisible && <RecordTableHeaderLabelIdentifierCellPlusButton />}
!isReadOnly &&
hasObjectUpdatePermissions &&
!hasAnySoftDeleteFilterOnView && (
<StyledHeaderIcon>
<LightIconButton
Icon={IconPlus}
size="small"
accent="tertiary"
onClick={handlePlusButtonClick}
/>
</StyledHeaderIcon>
)}
</StyledColumnHeadContainer> </StyledColumnHeadContainer>
<RecordTableHeaderResizeHandler recordField={recordField} /> <RecordTableHeaderResizeHandler recordField={recordField} />
</StyledColumnHeaderCell> </RecordTableHeaderCellContainer>
); );
}; };
@@ -0,0 +1,55 @@
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
import { IconPlus } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { useIsMobile } from 'twenty-ui/utilities';
const StyledHeaderIcon = styled.div`
margin: ${({ theme }) => theme.spacing(1, 1, 1, 1.5)};
`;
export const RecordTableHeaderLabelIdentifierCellPlusButton = () => {
const { objectMetadataItem, objectPermissions } =
useRecordTableContextOrThrow();
const isMobile = useIsMobile();
const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem,
});
const handlePlusButtonClick = () => {
createNewIndexRecord();
};
const isReadOnly = isObjectMetadataReadOnly({
objectPermissions,
objectMetadataItem,
});
const hasAnySoftDeleteFilterOnView = useRecoilComponentValue(
hasAnySoftDeleteFilterOnViewComponentSelector,
);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
return (
!isMobile &&
!isReadOnly &&
hasObjectUpdatePermissions &&
!hasAnySoftDeleteFilterOnView && (
<StyledHeaderIcon>
<LightIconButton
Icon={IconPlus}
size="small"
accent="tertiary"
onClick={handlePlusButtonClick}
/>
</StyledHeaderIcon>
)
);
};
@@ -1,19 +1,29 @@
import { useRecordTableLastColumnWidthToFill } from '@/object-record/record-table/hooks/useRecordTableLastColumnWidthToFill';
import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
const StyledLastColumnHeader = styled.div` const StyledLastColumnHeader = styled.div<{ width: number }>`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
background-color: ${({ theme }) => theme.background.primary}; background-color: ${({ theme }) => theme.background.primary};
border-left: none !important; border-left: none !important;
color: ${({ theme }) => theme.font.color.tertiary}; color: ${({ theme }) => theme.font.color.tertiary};
width: fit-content;
height: 32px; height: 32px;
max-height: 32px; max-height: 32px;
width: ${({ width }) => width}px;
`; `;
export const RecordTableHeaderLastColumn = () => { export const RecordTableHeaderLastColumn = () => {
return ( const { lastColumnWidth } = useRecordTableLastColumnWidthToFill();
<StyledLastColumnHeader className="header-cell"></StyledLastColumnHeader>
const resizeFieldOffset = useRecoilComponentValue(
resizeFieldOffsetComponentState,
); );
const width = lastColumnWidth - resizeFieldOffset;
return <StyledLastColumnHeader className="header-cell" width={width} />;
}; };
@@ -1,10 +1,11 @@
import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { type RecordField } from '@/object-record/record-field/types/RecordField';
import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState'; import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useIsMobile } from 'twenty-ui/utilities'; import { useIsMobile } from 'twenty-ui/utilities';
const StyledResizeHandler = styled.div` const StyledResizeHandler = styled.div<{ isResizing: boolean }>`
bottom: 0; bottom: 0;
cursor: col-resize; cursor: col-resize;
padding: 0 ${({ theme }) => theme.spacing(2)}; padding: 0 ${({ theme }) => theme.spacing(2)};
@@ -13,6 +14,21 @@ const StyledResizeHandler = styled.div`
top: 0; top: 0;
width: 3px; width: 3px;
z-index: 1; z-index: 1;
${({ isResizing, theme }) => {
if (isResizing === true) {
return `&:after {
background-color: ${theme.color.blue};
bottom: 0;
content: '';
display: block;
position: absolute;
right: 8px;
top: 0;
width: 2px;
}`;
}
}};
`; `;
export const RecordTableHeaderResizeHandler = ({ export const RecordTableHeaderResizeHandler = ({
@@ -24,18 +40,26 @@ export const RecordTableHeaderResizeHandler = ({
const columnResizeDisabled = isMobile; const columnResizeDisabled = isMobile;
const setResizedFieldMetadataItemId = useSetRecoilComponentState( const [resizedFieldMetadataItemId, setResizedFieldMetadataItemId] =
resizedFieldMetadataIdComponentState, useRecoilComponentState(resizedFieldMetadataIdComponentState);
);
const isResizing =
recordField.fieldMetadataItemId === resizedFieldMetadataItemId;
const { setDragSelectionStartEnabled } = useDragSelect();
const handlePointerDown = () => {
setDragSelectionStartEnabled(false);
setResizedFieldMetadataItemId(recordField.fieldMetadataItemId);
};
return ( return (
!columnResizeDisabled && ( !columnResizeDisabled && (
<StyledResizeHandler <StyledResizeHandler
className="cursor-col-resize" className="cursor-col-resize"
role="separator" role="separator"
onPointerDown={() => { onPointerDown={handlePointerDown}
setResizedFieldMetadataItemId(recordField.fieldMetadataItemId); isResizing={isResizing}
}}
/> />
) )
); );
@@ -1,7 +1,11 @@
import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField'; import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField';
import { COLUMN_MIN_WIDTH } from '@/object-record/record-table/constants/ColumnMinWidth';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState'; import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState';
import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState'; import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer'; import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
import { type PointerEventListener } from '@/ui/utilities/pointer-event/types/PointerEventListener'; import { type PointerEventListener } from '@/ui/utilities/pointer-event/types/PointerEventListener';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
@@ -13,8 +17,6 @@ import { useCallback, useState } from 'react';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { findByProperty, throwIfNotDefined } from 'twenty-shared/utils'; import { findByProperty, throwIfNotDefined } from 'twenty-shared/utils';
export const COLUMN_RESIZE_MIN_WIDTH = 48;
export const useResizeTableHeader = () => { export const useResizeTableHeader = () => {
const { visibleRecordFields } = useRecordTableContextOrThrow(); const { visibleRecordFields } = useRecordTableContextOrThrow();
@@ -33,33 +35,48 @@ export const useResizeTableHeader = () => {
const [resizedFieldMetadataItemId, setResizedFieldMetadataItemId] = const [resizedFieldMetadataItemId, setResizedFieldMetadataItemId] =
useRecoilComponentState(resizedFieldMetadataIdComponentState); useRecoilComponentState(resizedFieldMetadataIdComponentState);
const recordField = visibleRecordFields.find(
findByProperty('fieldMetadataItemId', resizedFieldMetadataItemId),
);
const { resetTableRowSelection } = useResetTableRowSelection();
const { saveRecordFields } = useSaveRecordFields(); const { saveRecordFields } = useSaveRecordFields();
const { updateRecordField } = useUpdateRecordField(); const { updateRecordField } = useUpdateRecordField();
const handleResizeHandlerStart = useCallback<PointerEventListener>( const handleResizeHandlerStart = useCallback<PointerEventListener>(
({ x }) => { ({ x }) => {
resetTableRowSelection();
setInitialPointerPositionX(x); setInitialPointerPositionX(x);
}, },
[], [resetTableRowSelection],
); );
const handleResizeHandlerMove = useCallback<PointerEventListener>( const handleResizeHandlerMove = useCallback<PointerEventListener>(
({ x }) => { ({ x }) => {
if (!initialPointerPositionX) return; if (!initialPointerPositionX) return;
throwIfNotDefined(recordField, 'recordField');
const newResizeOffset = x - initialPointerPositionX;
const newRecordFieldSizeWithOffset = recordField.size + newResizeOffset;
if (newRecordFieldSizeWithOffset < COLUMN_MIN_WIDTH) {
return;
}
setResizeFieldOffset(x - initialPointerPositionX); setResizeFieldOffset(x - initialPointerPositionX);
}, },
[setResizeFieldOffset, initialPointerPositionX], [setResizeFieldOffset, initialPointerPositionX, recordField],
); );
const { setDragSelectionStartEnabled } = useDragSelect();
const handleResizeHandlerEnd = useRecoilCallback( const handleResizeHandlerEnd = useRecoilCallback(
({ snapshot, set }) => ({ snapshot, set }) =>
async () => { async () => {
const recordField = visibleRecordFields.find(
findByProperty('fieldMetadataItemId', resizedFieldMetadataItemId),
);
throwIfNotDefined(recordField, 'recordField'); throwIfNotDefined(recordField, 'recordField');
if (!resizedFieldMetadataItemId) return; if (!resizedFieldMetadataItemId) return;
@@ -70,10 +87,7 @@ export const useResizeTableHeader = () => {
); );
const nextWidth = Math.round( const nextWidth = Math.round(
Math.max( Math.max(recordField.size + resizeFieldOffset, COLUMN_MIN_WIDTH),
recordField.size + resizeFieldOffset,
COLUMN_RESIZE_MIN_WIDTH,
),
); );
set(resizeFieldOffsetCallbackState, 0); set(resizeFieldOffsetCallbackState, 0);
@@ -90,6 +104,8 @@ export const useResizeTableHeader = () => {
saveRecordFields([updatedRecordField]); saveRecordFields([updatedRecordField]);
} }
setDragSelectionStartEnabled(true);
}, },
[ [
saveRecordFields, saveRecordFields,
@@ -97,7 +113,8 @@ export const useResizeTableHeader = () => {
resizeFieldOffsetCallbackState, resizeFieldOffsetCallbackState,
setResizedFieldMetadataItemId, setResizedFieldMetadataItemId,
updateRecordField, updateRecordField,
visibleRecordFields, setDragSelectionStartEnabled,
recordField,
], ],
); );
@@ -2,6 +2,7 @@ import { useRecordIndexContextOrThrow } from '@/object-record/record-index/conte
import { RecordTableCellCheckbox } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox'; 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 { RecordTableCellGrip } from '@/object-record/record-table/record-table-cell/components/RecordTableCellGrip';
import { RecordTableLastEmptyCell } from '@/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell'; import { RecordTableLastEmptyCell } from '@/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell';
import { RecordTablePlusButtonCellPlaceholder } from '@/object-record/record-table/record-table-cell/components/RecordTablePlusButtonCellPlaceholder';
import { RecordTableCells } from '@/object-record/record-table/record-table-row/components/RecordTableCells'; import { RecordTableCells } from '@/object-record/record-table/record-table-row/components/RecordTableCells';
import { RecordTableDraggableTr } from '@/object-record/record-table/record-table-row/components/RecordTableDraggableTr'; import { RecordTableDraggableTr } from '@/object-record/record-table/record-table-row/components/RecordTableDraggableTr';
import { RecordTableRowArrowKeysEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowArrowKeysEffect'; import { RecordTableRowArrowKeysEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowArrowKeysEffect';
@@ -51,6 +52,7 @@ export const RecordTableRow = ({
<RecordTableCellGrip /> <RecordTableCellGrip />
<RecordTableCellCheckbox /> <RecordTableCellCheckbox />
<RecordTableCells /> <RecordTableCells />
<RecordTablePlusButtonCellPlaceholder />
<RecordTableLastEmptyCell /> <RecordTableLastEmptyCell />
<ListenRecordUpdatesEffect <ListenRecordUpdatesEffect
objectNameSingular={objectNameSingular} objectNameSingular={objectNameSingular}
@@ -0,0 +1,8 @@
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const recordTableWidthComponentState = createComponentState<number>({
key: 'recordTableWidthComponentState',
defaultValue: 0,
componentInstanceContext: RecordTableComponentInstanceContext,
});
@@ -1,11 +1,11 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { type RefObject, useCallback, useState } from 'react'; import { type RefObject, useCallback, useState } from 'react';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useDragSelectWithAutoScroll } from '@/ui/utilities/drag-select/hooks/useDragSelectWithAutoScroll'; import { useDragSelectWithAutoScroll } from '@/ui/utilities/drag-select/hooks/useDragSelectWithAutoScroll';
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer'; import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { useDragSelect } from '../hooks/useDragSelect';
import { type SelectionBox } from '../types/SelectionBox'; import { type SelectionBox } from '../types/SelectionBox';
import { isValidSelectionStart } from '../utils/selectionBoxValidation'; import { isValidSelectionStart } from '../utils/selectionBoxValidation';