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)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
height: ${({ theme }) => theme.spacing(8)};
|
height: ${({ theme }) => theme.spacing(8)};
|
||||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledIcon = styled.div`
|
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) {
|
export function ColumnHead({ viewName, viewIcon }: OwnProps) {
|
||||||
return (
|
return (
|
||||||
<StyledTitle>
|
<StyledTitle>
|
||||||
<StyledIcon>{viewIcon}</StyledIcon>
|
<StyledIcon>{viewIcon}</StyledIcon>
|
||||||
{viewName}
|
<StyledText>{viewName}</StyledText>
|
||||||
</StyledTitle>
|
</StyledTitle>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
|
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/click-outside/hooks/useListenClickOutside';
|
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 { useLeaveTableFocus } from '../hooks/useLeaveTableFocus';
|
||||||
import { useMapKeyboardToSoftFocus } from '../hooks/useMapKeyboardToSoftFocus';
|
import { useMapKeyboardToSoftFocus } from '../hooks/useMapKeyboardToSoftFocus';
|
||||||
import { EntityUpdateMutationHookContext } from '../states/EntityUpdateMutationHookContext';
|
import { EntityUpdateMutationHookContext } from '../states/EntityUpdateMutationHookContext';
|
||||||
|
import { viewFieldsFamilyState } from '../states/viewFieldsState';
|
||||||
import { TableHeader } from '../table-header/components/TableHeader';
|
import { TableHeader } from '../table-header/components/TableHeader';
|
||||||
|
|
||||||
import { EntityTableBody } from './EntityTableBody';
|
import { EntityTableBody } from './EntityTableBody';
|
||||||
@ -99,6 +101,8 @@ export function EntityTable<SortField>({
|
|||||||
onSortsUpdate,
|
onSortsUpdate,
|
||||||
useUpdateEntityMutation,
|
useUpdateEntityMutation,
|
||||||
}: OwnProps<SortField>) {
|
}: OwnProps<SortField>) {
|
||||||
|
const viewFields = useRecoilValue(viewFieldsFamilyState);
|
||||||
|
|
||||||
const tableBodyRef = React.useRef<HTMLDivElement>(null);
|
const tableBodyRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useMapKeyboardToSoftFocus();
|
useMapKeyboardToSoftFocus();
|
||||||
@ -123,10 +127,12 @@ export function EntityTable<SortField>({
|
|||||||
onSortsUpdate={onSortsUpdate}
|
onSortsUpdate={onSortsUpdate}
|
||||||
/>
|
/>
|
||||||
<StyledTableWrapper>
|
<StyledTableWrapper>
|
||||||
<StyledTable>
|
{viewFields.length && (
|
||||||
<EntityTableHeader />
|
<StyledTable>
|
||||||
<EntityTableBody />
|
<EntityTableHeader viewFields={viewFields} />
|
||||||
</StyledTable>
|
<EntityTableBody />
|
||||||
|
</StyledTable>
|
||||||
|
)}
|
||||||
</StyledTableWrapper>
|
</StyledTableWrapper>
|
||||||
</StyledTableContainer>
|
</StyledTableContainer>
|
||||||
</StyledTableWithHeader>
|
</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 { ColumnHead } from './ColumnHead';
|
||||||
import { SelectAllCheckbox } from './SelectAllCheckbox';
|
import { SelectAllCheckbox } from './SelectAllCheckbox';
|
||||||
|
|
||||||
export function EntityTableHeader() {
|
const COLUMN_MIN_WIDTH = 75;
|
||||||
const viewFields = useRecoilValue(viewFieldsFamilyState);
|
|
||||||
|
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 (
|
return (
|
||||||
<thead>
|
<thead>
|
||||||
@ -20,20 +105,34 @@ export function EntityTableHeader() {
|
|||||||
>
|
>
|
||||||
<SelectAllCheckbox />
|
<SelectAllCheckbox />
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
{viewFields.map((viewField) => (
|
{viewFields.map((viewField) => (
|
||||||
<th
|
<StyledColumnHeaderCell
|
||||||
key={viewField.columnOrder.toString()}
|
key={viewField.columnOrder.toString()}
|
||||||
|
isResizing={isResizing && resizedFieldId === viewField.id}
|
||||||
style={{
|
style={{
|
||||||
width: viewField.columnSize,
|
width: Math.max(
|
||||||
minWidth: viewField.columnSize,
|
columnWidths[viewField.id] +
|
||||||
maxWidth: viewField.columnSize,
|
(resizedFieldId === viewField.id ? offset : 0),
|
||||||
|
COLUMN_MIN_WIDTH,
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ColumnHead
|
<ColumnHead
|
||||||
viewName={viewField.columnLabel}
|
viewName={viewField.columnLabel}
|
||||||
viewIcon={viewField.columnIcon}
|
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>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
Loading…
Reference in New Issue
Block a user