mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-18 09:02:11 +03:00
feat: add column resizing (#975)
* feat: add column resizing Closes #817 * Use mouse up and down instead of dragging --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
parent
ade5e52e55
commit
58e5d24261
@ -14,6 +14,7 @@ const StyledTitle = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: ${({ theme }) => theme.spacing(8)};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledIcon = styled.div`
|
||||
@ -25,11 +26,17 @@ const StyledIcon = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledText = styled.span`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export function ColumnHead({ viewName, viewIcon }: OwnProps) {
|
||||
return (
|
||||
<StyledTitle>
|
||||
<StyledIcon>{viewIcon}</StyledIcon>
|
||||
{viewName}
|
||||
<StyledText>{viewName}</StyledText>
|
||||
</StyledTitle>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
|
||||
import { useListenClickOutside } from '@/ui/utilities/click-outside/hooks/useListenClickOutside';
|
||||
@ -7,6 +8,7 @@ import { useListenClickOutside } from '@/ui/utilities/click-outside/hooks/useLis
|
||||
import { useLeaveTableFocus } from '../hooks/useLeaveTableFocus';
|
||||
import { useMapKeyboardToSoftFocus } from '../hooks/useMapKeyboardToSoftFocus';
|
||||
import { EntityUpdateMutationHookContext } from '../states/EntityUpdateMutationHookContext';
|
||||
import { viewFieldsFamilyState } from '../states/viewFieldsState';
|
||||
import { TableHeader } from '../table-header/components/TableHeader';
|
||||
|
||||
import { EntityTableBody } from './EntityTableBody';
|
||||
@ -99,6 +101,8 @@ export function EntityTable<SortField>({
|
||||
onSortsUpdate,
|
||||
useUpdateEntityMutation,
|
||||
}: OwnProps<SortField>) {
|
||||
const viewFields = useRecoilValue(viewFieldsFamilyState);
|
||||
|
||||
const tableBodyRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
useMapKeyboardToSoftFocus();
|
||||
@ -123,10 +127,12 @@ export function EntityTable<SortField>({
|
||||
onSortsUpdate={onSortsUpdate}
|
||||
/>
|
||||
<StyledTableWrapper>
|
||||
<StyledTable>
|
||||
<EntityTableHeader />
|
||||
<EntityTableBody />
|
||||
</StyledTable>
|
||||
{viewFields.length && (
|
||||
<StyledTable>
|
||||
<EntityTableHeader viewFields={viewFields} />
|
||||
<EntityTableBody />
|
||||
</StyledTable>
|
||||
)}
|
||||
</StyledTableWrapper>
|
||||
</StyledTableContainer>
|
||||
</StyledTableWithHeader>
|
||||
|
@ -1,12 +1,97 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { PointerEvent, useCallback, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { viewFieldsFamilyState } from '../states/viewFieldsState';
|
||||
import { ViewFieldDefinition, ViewFieldMetadata } from '../types/ViewField';
|
||||
|
||||
import { ColumnHead } from './ColumnHead';
|
||||
import { SelectAllCheckbox } from './SelectAllCheckbox';
|
||||
|
||||
export function EntityTableHeader() {
|
||||
const viewFields = useRecoilValue(viewFieldsFamilyState);
|
||||
const COLUMN_MIN_WIDTH = 75;
|
||||
|
||||
const StyledColumnHeaderCell = styled.th<{ isResizing?: boolean }>`
|
||||
min-width: ${COLUMN_MIN_WIDTH}px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
${({ isResizing, theme }) => {
|
||||
if (isResizing) {
|
||||
return `&:after {
|
||||
background-color: ${theme.color.blue};
|
||||
bottom: 0;
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
top: 0;
|
||||
width: 2px;
|
||||
}`;
|
||||
}
|
||||
}};
|
||||
`;
|
||||
|
||||
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;
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
viewFields: ViewFieldDefinition<ViewFieldMetadata>[];
|
||||
};
|
||||
|
||||
export function EntityTableHeader({ viewFields }: OwnProps) {
|
||||
const initialColumnWidths = viewFields.reduce<Record<string, number>>(
|
||||
(result, viewField) => ({
|
||||
...result,
|
||||
[viewField.id]: viewField.columnSize,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
const [columnWidths, setColumnWidths] = useState(initialColumnWidths);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [initialPointerPositionX, setInitialPointerPositionX] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const [resizedFieldId, setResizedFieldId] = useState<string | null>(null);
|
||||
const [offset, setOffset] = useState(0);
|
||||
|
||||
const handleResizeHandlerDragStart = useCallback(
|
||||
(event: PointerEvent<HTMLDivElement>, fieldId: string) => {
|
||||
setIsResizing(true);
|
||||
setResizedFieldId(fieldId);
|
||||
setInitialPointerPositionX(event.clientX);
|
||||
},
|
||||
[setIsResizing, setResizedFieldId, setInitialPointerPositionX],
|
||||
);
|
||||
|
||||
const handleResizeHandlerDrag = useCallback(
|
||||
(event: PointerEvent<HTMLDivElement>) => {
|
||||
if (!isResizing || initialPointerPositionX === null) return;
|
||||
|
||||
setOffset(event.clientX - initialPointerPositionX);
|
||||
},
|
||||
[isResizing, initialPointerPositionX],
|
||||
);
|
||||
|
||||
const handleResizeHandlerDragEnd = useCallback(() => {
|
||||
setIsResizing(false);
|
||||
if (!resizedFieldId) return;
|
||||
|
||||
const newColumnWidths = {
|
||||
...columnWidths,
|
||||
[resizedFieldId]: Math.max(
|
||||
columnWidths[resizedFieldId] + offset,
|
||||
COLUMN_MIN_WIDTH,
|
||||
),
|
||||
};
|
||||
setColumnWidths(newColumnWidths);
|
||||
setOffset(0);
|
||||
}, [offset, setIsResizing, columnWidths, resizedFieldId]);
|
||||
|
||||
return (
|
||||
<thead>
|
||||
@ -20,20 +105,34 @@ export function EntityTableHeader() {
|
||||
>
|
||||
<SelectAllCheckbox />
|
||||
</th>
|
||||
|
||||
{viewFields.map((viewField) => (
|
||||
<th
|
||||
<StyledColumnHeaderCell
|
||||
key={viewField.columnOrder.toString()}
|
||||
isResizing={isResizing && resizedFieldId === viewField.id}
|
||||
style={{
|
||||
width: viewField.columnSize,
|
||||
minWidth: viewField.columnSize,
|
||||
maxWidth: viewField.columnSize,
|
||||
width: Math.max(
|
||||
columnWidths[viewField.id] +
|
||||
(resizedFieldId === viewField.id ? offset : 0),
|
||||
COLUMN_MIN_WIDTH,
|
||||
),
|
||||
}}
|
||||
>
|
||||
<ColumnHead
|
||||
viewName={viewField.columnLabel}
|
||||
viewIcon={viewField.columnIcon}
|
||||
/>
|
||||
</th>
|
||||
<StyledResizeHandler
|
||||
className="cursor-col-resize"
|
||||
role="separator"
|
||||
onPointerDown={(event) =>
|
||||
handleResizeHandlerDragStart(event, viewField.id)
|
||||
}
|
||||
onPointerMove={handleResizeHandlerDrag}
|
||||
onPointerOut={handleResizeHandlerDragEnd}
|
||||
onPointerUp={handleResizeHandlerDragEnd}
|
||||
/>
|
||||
</StyledColumnHeaderCell>
|
||||
))}
|
||||
<th></th>
|
||||
</tr>
|
||||
|
Loading…
Reference in New Issue
Block a user