Improve mouse tracking (#1061)
* Improve mouse tracking * Fix lint * Fix regression on Filters * Fix according to review
This commit is contained in:
@ -14,9 +14,9 @@ import { PersonChip } from '@/people/components/PersonChip';
|
|||||||
import { useFilteredSearchPeopleQuery } from '@/people/queries';
|
import { useFilteredSearchPeopleQuery } from '@/people/queries';
|
||||||
import { MultipleEntitySelect } from '@/ui/input/relation-picker/components/MultipleEntitySelect';
|
import { MultipleEntitySelect } from '@/ui/input/relation-picker/components/MultipleEntitySelect';
|
||||||
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
|
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/click-outside/hooks/useListenClickOutside';
|
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||||
import { Activity, ActivityTarget, CommentableType } from '~/generated/graphql';
|
import { Activity, ActivityTarget, CommentableType } from '~/generated/graphql';
|
||||||
import { assertNotNull } from '~/utils/assert';
|
import { assertNotNull } from '~/utils/assert';
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { useSetRecoilState } from 'recoil';
|
|||||||
|
|
||||||
import { useSetEntityTableData } from '@/ui/table/hooks/useSetEntityTableData';
|
import { useSetEntityTableData } from '@/ui/table/hooks/useSetEntityTableData';
|
||||||
import { entityTableDimensionsState } from '@/ui/table/states/entityTableDimensionsState';
|
import { entityTableDimensionsState } from '@/ui/table/states/entityTableDimensionsState';
|
||||||
import { viewFieldsFamilyState } from '@/ui/table/states/viewFieldsState';
|
import { viewFieldsState } from '@/ui/table/states/viewFieldsState';
|
||||||
|
|
||||||
import { companyViewFields } from '../../constants/companyViewFields';
|
import { companyViewFields } from '../../constants/companyViewFields';
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ export function CompanyTableMockData() {
|
|||||||
const setEntityTableDimensions = useSetRecoilState(
|
const setEntityTableDimensions = useSetRecoilState(
|
||||||
entityTableDimensionsState,
|
entityTableDimensionsState,
|
||||||
);
|
);
|
||||||
const setViewFields = useSetRecoilState(viewFieldsFamilyState);
|
const setViewFields = useSetRecoilState(viewFieldsState);
|
||||||
const setEntityTableData = useSetEntityTableData();
|
const setEntityTableData = useSetEntityTableData();
|
||||||
|
|
||||||
setEntityTableData(mockedCompaniesData, []);
|
setEntityTableData(mockedCompaniesData, []);
|
||||||
|
|||||||
@ -2,8 +2,8 @@ import { ReactElement, useRef } from 'react';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { overlayBackground } from '@/ui/theme/constants/effects';
|
import { overlayBackground } from '@/ui/theme/constants/effects';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/click-outside/hooks/useListenClickOutside';
|
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
|
|
||||||
import { BoardCardFieldHotkeyScope } from '../types/BoardCardFieldHotkeyScope';
|
import { BoardCardFieldHotkeyScope } from '../types/BoardCardFieldHotkeyScope';
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMen
|
|||||||
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
|
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
|
||||||
import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton';
|
import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton';
|
||||||
import { icon } from '@/ui/theme/constants/icon';
|
import { icon } from '@/ui/theme/constants/icon';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/click-outside/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
|
|
||||||
import { BoardColumnEditTitleMenu } from './BoardColumnEditTitleMenu';
|
import { BoardColumnEditTitleMenu } from './BoardColumnEditTitleMenu';
|
||||||
|
|
||||||
|
|||||||
@ -63,18 +63,20 @@ export function BoardHeader<SortField>({
|
|||||||
{viewName}
|
{viewName}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
rightComponents={[
|
rightComponent={
|
||||||
<FilterDropdownButton
|
<>
|
||||||
context={context}
|
<FilterDropdownButton
|
||||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
context={context}
|
||||||
/>,
|
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||||
<SortDropdownButton<SortField>
|
/>
|
||||||
isSortSelected={sorts.length > 0}
|
<SortDropdownButton<SortField>
|
||||||
availableSorts={availableSorts || []}
|
isSortSelected={sorts.length > 0}
|
||||||
onSortSelect={sortSelect}
|
availableSorts={availableSorts || []}
|
||||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
onSortSelect={sortSelect}
|
||||||
/>,
|
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||||
]}
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
bottomComponent={
|
bottomComponent={
|
||||||
<SortAndFilterBar
|
<SortAndFilterBar
|
||||||
context={context}
|
context={context}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useListenClickOutside } from '@/ui/utilities/click-outside/hooks/useListenClickOutside';
|
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
|
|
||||||
import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope';
|
import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope';
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { useListenClickOutside } from '@/ui/utilities/click-outside/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
|
|
||||||
import { DropdownMenu } from '../../dropdown/components/DropdownMenu';
|
import { DropdownMenu } from '../../dropdown/components/DropdownMenu';
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
|
|||||||
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { DropdownMenuSearch } from '@/ui/dropdown/components/DropdownMenuSearch';
|
import { DropdownMenuSearch } from '@/ui/dropdown/components/DropdownMenuSearch';
|
||||||
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/click-outside/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { Avatar } from '@/users/components/Avatar';
|
import { Avatar } from '@/users/components/Avatar';
|
||||||
import { isNonEmptyString } from '~/utils/isNonEmptyString';
|
import { isNonEmptyString } from '~/utils/isNonEmptyString';
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMen
|
|||||||
import { DropdownMenuSearch } from '@/ui/dropdown/components/DropdownMenuSearch';
|
import { DropdownMenuSearch } from '@/ui/dropdown/components/DropdownMenuSearch';
|
||||||
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
|
||||||
import { IconPlus } from '@/ui/icon';
|
import { IconPlus } from '@/ui/icon';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/click-outside/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
import { useEntitySelectSearch } from '../hooks/useEntitySelectSearch';
|
import { useEntitySelectSearch } from '../hooks/useEntitySelectSearch';
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { motion } from 'framer-motion';
|
|||||||
import {
|
import {
|
||||||
ClickOutsideMode,
|
ClickOutsideMode,
|
||||||
useListenClickOutside,
|
useListenClickOutside,
|
||||||
} from '@/ui/utilities/click-outside/hooks/useListenClickOutside';
|
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -5,11 +5,11 @@ import { motion } from 'framer-motion';
|
|||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import {
|
import {
|
||||||
ClickOutsideMode,
|
ClickOutsideMode,
|
||||||
useListenClickOutside,
|
useListenClickOutside,
|
||||||
} from '@/ui/utilities/click-outside/hooks/useListenClickOutside';
|
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilValue, useSetRecoilState } 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/pointer-event/hooks/useListenClickOutside';
|
||||||
import { useUpdateViewFieldMutation } from '~/generated/graphql';
|
|
||||||
|
|
||||||
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';
|
||||||
@ -102,11 +99,6 @@ export function EntityTable<SortField>({
|
|||||||
onSortsUpdate,
|
onSortsUpdate,
|
||||||
useUpdateEntityMutation,
|
useUpdateEntityMutation,
|
||||||
}: OwnProps<SortField>) {
|
}: OwnProps<SortField>) {
|
||||||
const viewFields = useRecoilValue(viewFieldsFamilyState);
|
|
||||||
const setViewFields = useSetRecoilState(viewFieldsFamilyState);
|
|
||||||
|
|
||||||
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
|
|
||||||
|
|
||||||
const tableBodyRef = useRef<HTMLDivElement>(null);
|
const tableBodyRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useMapKeyboardToSoftFocus();
|
useMapKeyboardToSoftFocus();
|
||||||
@ -120,25 +112,6 @@ export function EntityTable<SortField>({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleColumnResize = useCallback(
|
|
||||||
(resizedFieldId: string, width: number) => {
|
|
||||||
setViewFields((previousViewFields) =>
|
|
||||||
previousViewFields.map((viewField) =>
|
|
||||||
viewField.id === resizedFieldId
|
|
||||||
? { ...viewField, columnSize: width }
|
|
||||||
: viewField,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
updateViewFieldMutation({
|
|
||||||
variables: {
|
|
||||||
data: { sizeInPx: width },
|
|
||||||
where: { id: resizedFieldId },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[setViewFields, updateViewFieldMutation],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntityUpdateMutationHookContext.Provider value={useUpdateEntityMutation}>
|
<EntityUpdateMutationHookContext.Provider value={useUpdateEntityMutation}>
|
||||||
<StyledTableWithHeader>
|
<StyledTableWithHeader>
|
||||||
@ -150,15 +123,10 @@ export function EntityTable<SortField>({
|
|||||||
onSortsUpdate={onSortsUpdate}
|
onSortsUpdate={onSortsUpdate}
|
||||||
/>
|
/>
|
||||||
<StyledTableWrapper>
|
<StyledTableWrapper>
|
||||||
{viewFields.length > 0 && (
|
<StyledTable>
|
||||||
<StyledTable>
|
<EntityTableHeader />
|
||||||
<EntityTableHeader
|
<EntityTableBody />
|
||||||
onColumnResize={handleColumnResize}
|
</StyledTable>
|
||||||
viewFields={viewFields}
|
|
||||||
/>
|
|
||||||
<EntityTableBody />
|
|
||||||
</StyledTable>
|
|
||||||
)}
|
|
||||||
</StyledTableWrapper>
|
</StyledTableWrapper>
|
||||||
</StyledTableContainer>
|
</StyledTableContainer>
|
||||||
</StyledTableWithHeader>
|
</StyledTableWithHeader>
|
||||||
|
|||||||
@ -34,14 +34,7 @@ export function EntityTableCell({ cellIndex }: { cellIndex: number }) {
|
|||||||
return (
|
return (
|
||||||
<RecoilScope>
|
<RecoilScope>
|
||||||
<ColumnIndexContext.Provider value={cellIndex}>
|
<ColumnIndexContext.Provider value={cellIndex}>
|
||||||
<td
|
<td onContextMenu={(event) => handleContextMenu(event)}>
|
||||||
onContextMenu={(event) => handleContextMenu(event)}
|
|
||||||
style={{
|
|
||||||
width: viewField.columnSize,
|
|
||||||
minWidth: viewField.columnSize,
|
|
||||||
maxWidth: viewField.columnSize,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<GenericEditableCell viewField={viewField} />
|
<GenericEditableCell viewField={viewField} />
|
||||||
</td>
|
</td>
|
||||||
</ColumnIndexContext.Provider>
|
</ColumnIndexContext.Provider>
|
||||||
|
|||||||
@ -1,10 +1,17 @@
|
|||||||
import { PointerEvent, useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import {
|
||||||
|
useRecoilCallback,
|
||||||
|
useRecoilState,
|
||||||
|
useRecoilValue,
|
||||||
|
useSetRecoilState,
|
||||||
|
} from 'recoil';
|
||||||
|
|
||||||
import type {
|
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
|
||||||
ViewFieldDefinition,
|
import { useUpdateViewFieldMutation } from '~/generated/graphql';
|
||||||
ViewFieldMetadata,
|
|
||||||
} from '../types/ViewField';
|
import { resizeFieldOffsetState } from '../states/resizeFieldOffsetState';
|
||||||
|
import { viewFieldsState } from '../states/viewFieldsState';
|
||||||
|
|
||||||
import { ColumnHead } from './ColumnHead';
|
import { ColumnHead } from './ColumnHead';
|
||||||
import { SelectAllCheckbox } from './SelectAllCheckbox';
|
import { SelectAllCheckbox } from './SelectAllCheckbox';
|
||||||
@ -42,12 +49,11 @@ const StyledResizeHandler = styled.div`
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type OwnProps = {
|
export function EntityTableHeader() {
|
||||||
onColumnResize: (resizedFieldId: string, width: number) => void;
|
const viewFields = useRecoilValue(viewFieldsState);
|
||||||
viewFields: ViewFieldDefinition<ViewFieldMetadata>[];
|
const setViewFields = useSetRecoilState(viewFieldsState);
|
||||||
};
|
|
||||||
|
|
||||||
export function EntityTableHeader({ onColumnResize, viewFields }: OwnProps) {
|
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
|
||||||
const columnWidths = useMemo(
|
const columnWidths = useMemo(
|
||||||
() =>
|
() =>
|
||||||
viewFields.reduce<Record<string, number>>(
|
viewFields.reduce<Record<string, number>>(
|
||||||
@ -59,46 +65,75 @@ export function EntityTableHeader({ onColumnResize, viewFields }: OwnProps) {
|
|||||||
),
|
),
|
||||||
[viewFields],
|
[viewFields],
|
||||||
);
|
);
|
||||||
const [isResizing, setIsResizing] = useState(false);
|
|
||||||
const [initialPointerPositionX, setInitialPointerPositionX] = useState<
|
const [initialPointerPositionX, setInitialPointerPositionX] = useState<
|
||||||
number | null
|
number | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
const [resizedFieldId, setResizedFieldId] = useState<string | null>(null);
|
const [resizedFieldId, setResizedFieldId] = useState<string | null>(null);
|
||||||
const [offset, setOffset] = useState(0);
|
const [offset, setOffset] = useRecoilState(resizeFieldOffsetState);
|
||||||
|
|
||||||
const handleResizeHandlerDragStart = useCallback(
|
const handleColumnResize = useCallback(
|
||||||
(event: PointerEvent<HTMLDivElement>, fieldId: string) => {
|
(resizedFieldId: string, width: number) => {
|
||||||
setIsResizing(true);
|
setViewFields((previousViewFields) =>
|
||||||
setResizedFieldId(fieldId);
|
previousViewFields.map((viewField) =>
|
||||||
setInitialPointerPositionX(event.clientX);
|
viewField.id === resizedFieldId
|
||||||
|
? { ...viewField, columnSize: width }
|
||||||
|
: viewField,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
updateViewFieldMutation({
|
||||||
|
variables: {
|
||||||
|
data: { sizeInPx: width },
|
||||||
|
where: { id: resizedFieldId },
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[setIsResizing, setResizedFieldId, setInitialPointerPositionX],
|
[setViewFields, updateViewFieldMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleResizeHandlerDrag = useCallback(
|
const handleResizeHandlerStart = useCallback(
|
||||||
(event: PointerEvent<HTMLDivElement>) => {
|
(positionX: number, _: number) => {
|
||||||
if (!isResizing || initialPointerPositionX === null) return;
|
setInitialPointerPositionX(positionX);
|
||||||
|
|
||||||
setOffset(event.clientX - initialPointerPositionX);
|
|
||||||
},
|
},
|
||||||
[isResizing, initialPointerPositionX],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleResizeHandlerDragEnd = useCallback(() => {
|
const handleResizeHandlerMove = useCallback(
|
||||||
setIsResizing(false);
|
(positionX: number, _positionY: number) => {
|
||||||
if (!resizedFieldId) return;
|
if (!initialPointerPositionX) return;
|
||||||
|
setOffset(positionX - initialPointerPositionX);
|
||||||
|
},
|
||||||
|
[setOffset, initialPointerPositionX],
|
||||||
|
);
|
||||||
|
|
||||||
const nextWidth = Math.round(
|
const handleResizeHandlerEnd = useRecoilCallback(
|
||||||
Math.max(columnWidths[resizedFieldId] + offset, COLUMN_MIN_WIDTH),
|
({ snapshot, set }) =>
|
||||||
);
|
(_positionX: number, _positionY: number) => {
|
||||||
|
if (!resizedFieldId) return;
|
||||||
|
const nextWidth = Math.round(
|
||||||
|
Math.max(
|
||||||
|
columnWidths[resizedFieldId] +
|
||||||
|
snapshot.getLoadable(resizeFieldOffsetState).valueOrThrow(),
|
||||||
|
COLUMN_MIN_WIDTH,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (nextWidth !== columnWidths[resizedFieldId]) {
|
if (nextWidth !== columnWidths[resizedFieldId]) {
|
||||||
onColumnResize(resizedFieldId, nextWidth);
|
handleColumnResize(resizedFieldId, nextWidth);
|
||||||
}
|
}
|
||||||
|
set(resizeFieldOffsetState, 0);
|
||||||
|
setInitialPointerPositionX(null);
|
||||||
|
setResizedFieldId(null);
|
||||||
|
},
|
||||||
|
[resizedFieldId, columnWidths, setResizedFieldId, handleColumnResize],
|
||||||
|
);
|
||||||
|
|
||||||
setOffset(0);
|
useTrackPointer({
|
||||||
}, [resizedFieldId, columnWidths, offset, onColumnResize]);
|
shouldTrackPointer: resizedFieldId !== null,
|
||||||
|
onMouseDown: handleResizeHandlerStart,
|
||||||
|
onMouseMove: handleResizeHandlerMove,
|
||||||
|
onMouseUp: handleResizeHandlerEnd,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<thead>
|
<thead>
|
||||||
@ -116,7 +151,7 @@ export function EntityTableHeader({ onColumnResize, viewFields }: OwnProps) {
|
|||||||
{viewFields.map((viewField) => (
|
{viewFields.map((viewField) => (
|
||||||
<StyledColumnHeaderCell
|
<StyledColumnHeaderCell
|
||||||
key={viewField.columnOrder.toString()}
|
key={viewField.columnOrder.toString()}
|
||||||
isResizing={isResizing && resizedFieldId === viewField.id}
|
isResizing={resizedFieldId === viewField.id}
|
||||||
style={{
|
style={{
|
||||||
width: Math.max(
|
width: Math.max(
|
||||||
columnWidths[viewField.id] +
|
columnWidths[viewField.id] +
|
||||||
@ -132,12 +167,9 @@ export function EntityTableHeader({ onColumnResize, viewFields }: OwnProps) {
|
|||||||
<StyledResizeHandler
|
<StyledResizeHandler
|
||||||
className="cursor-col-resize"
|
className="cursor-col-resize"
|
||||||
role="separator"
|
role="separator"
|
||||||
onPointerDown={(event) =>
|
onPointerDown={() => {
|
||||||
handleResizeHandlerDragStart(event, viewField.id)
|
setResizedFieldId(viewField.id);
|
||||||
}
|
}}
|
||||||
onPointerMove={handleResizeHandlerDrag}
|
|
||||||
onPointerOut={handleResizeHandlerDragEnd}
|
|
||||||
onPointerUp={handleResizeHandlerDragEnd}
|
|
||||||
/>
|
/>
|
||||||
</StyledColumnHeaderCell>
|
</StyledColumnHeaderCell>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import styled from '@emotion/styled';
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { ViewFieldContext } from '../states/ViewFieldContext';
|
import { ViewFieldContext } from '../states/ViewFieldContext';
|
||||||
import { viewFieldsFamilyState } from '../states/viewFieldsState';
|
import { viewFieldsState } from '../states/viewFieldsState';
|
||||||
|
|
||||||
import { CheckboxCell } from './CheckboxCell';
|
import { CheckboxCell } from './CheckboxCell';
|
||||||
import { EntityTableCell } from './EntityTableCell';
|
import { EntityTableCell } from './EntityTableCell';
|
||||||
@ -13,7 +13,7 @@ const StyledRow = styled.tr<{ selected: boolean }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function EntityTableRow({ rowId }: { rowId: string }) {
|
export function EntityTableRow({ rowId }: { rowId: string }) {
|
||||||
const viewFields = useRecoilValue(viewFieldsFamilyState);
|
const viewFields = useRecoilValue(viewFieldsState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledRow data-testid={`row-id-${rowId}`} selected={false}>
|
<StyledRow data-testid={`row-id-${rowId}`} selected={false}>
|
||||||
|
|||||||
@ -33,7 +33,6 @@ export function GenericEntityTableData({
|
|||||||
variables: { orderBy, where: whereFilters },
|
variables: { orderBy, where: whereFilters },
|
||||||
onCompleted: (data: any) => {
|
onCompleted: (data: any) => {
|
||||||
const entities = data[getRequestResultKey] ?? [];
|
const entities = data[getRequestResultKey] ?? [];
|
||||||
|
|
||||||
setEntityTableData(entities, filterDefinitionArray);
|
setEntityTableData(entities, filterDefinitionArray);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useListenClickOutside } from '@/ui/utilities/click-outside/hooks/useListenClickOutside';
|
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
|
|
||||||
import { useMoveSoftFocus } from '../../hooks/useMoveSoftFocus';
|
import { useMoveSoftFocus } from '../../hooks/useMoveSoftFocus';
|
||||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import { Key } from 'ts-key-enum';
|
|||||||
|
|
||||||
import { DateInputEdit } from '@/ui/input/date/components/DateInputEdit';
|
import { DateInputEdit } from '@/ui/input/date/components/DateInputEdit';
|
||||||
import { TableHotkeyScope } from '@/ui/table/types/TableHotkeyScope';
|
import { TableHotkeyScope } from '@/ui/table/types/TableHotkeyScope';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/click-outside/hooks/useListenClickOutside';
|
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
|
|
||||||
import { useEditableCell } from '../../hooks/useEditableCell';
|
import { useEditableCell } from '../../hooks/useEditableCell';
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
|
|
||||||
import { entityTableDimensionsState } from '../states/entityTableDimensionsState';
|
import { entityTableDimensionsState } from '../states/entityTableDimensionsState';
|
||||||
import { viewFieldsFamilyState } from '../states/viewFieldsState';
|
import { viewFieldsState } from '../states/viewFieldsState';
|
||||||
import {
|
import {
|
||||||
ViewFieldDefinition,
|
ViewFieldDefinition,
|
||||||
ViewFieldMetadata,
|
ViewFieldMetadata,
|
||||||
@ -32,7 +32,7 @@ export const useLoadView = ({
|
|||||||
const setEntityTableDimensions = useSetRecoilState(
|
const setEntityTableDimensions = useSetRecoilState(
|
||||||
entityTableDimensionsState,
|
entityTableDimensionsState,
|
||||||
);
|
);
|
||||||
const setViewFields = useSetRecoilState(viewFieldsFamilyState);
|
const setViewFields = useSetRecoilState(viewFieldsState);
|
||||||
|
|
||||||
const [createViewFieldsMutation] = useCreateViewFieldsMutation();
|
const [createViewFieldsMutation] = useCreateViewFieldsMutation();
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
export const resizeFieldOffsetState = atom<number>({
|
||||||
|
key: 'resizeFieldOffsetState',
|
||||||
|
default: 0,
|
||||||
|
});
|
||||||
@ -2,9 +2,7 @@ import { atom } from 'recoil';
|
|||||||
|
|
||||||
import { ViewFieldDefinition, ViewFieldMetadata } from '../types/ViewField';
|
import { ViewFieldDefinition, ViewFieldMetadata } from '../types/ViewField';
|
||||||
|
|
||||||
export const viewFieldsFamilyState = atom<
|
export const viewFieldsState = atom<ViewFieldDefinition<ViewFieldMetadata>[]>({
|
||||||
ViewFieldDefinition<ViewFieldMetadata>[]
|
key: 'viewFieldsState',
|
||||||
>({
|
|
||||||
key: 'viewFieldsFamilyState',
|
|
||||||
default: [],
|
default: [],
|
||||||
});
|
});
|
||||||
|
|||||||
@ -64,18 +64,20 @@ export function TableHeader<SortField>({
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
displayBottomBorder={false}
|
displayBottomBorder={false}
|
||||||
rightComponents={[
|
rightComponent={
|
||||||
<FilterDropdownButton
|
<>
|
||||||
context={TableContext}
|
<FilterDropdownButton
|
||||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
context={TableContext}
|
||||||
/>,
|
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||||
<SortDropdownButton<SortField>
|
/>
|
||||||
isSortSelected={sorts.length > 0}
|
<SortDropdownButton<SortField>
|
||||||
availableSorts={availableSorts || []}
|
isSortSelected={sorts.length > 0}
|
||||||
onSortSelect={sortSelect}
|
availableSorts={availableSorts || []}
|
||||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
onSortSelect={sortSelect}
|
||||||
/>,
|
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||||
]}
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
bottomComponent={
|
bottomComponent={
|
||||||
<SortAndFilterBar
|
<SortAndFilterBar
|
||||||
context={TableContext}
|
context={TableContext}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import styled from '@emotion/styled';
|
|||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
leftComponent?: ReactNode;
|
leftComponent?: ReactNode;
|
||||||
rightComponents?: ReactNode[];
|
rightComponent?: ReactNode;
|
||||||
bottomComponent?: ReactNode;
|
bottomComponent?: ReactNode;
|
||||||
displayBottomBorder?: boolean;
|
displayBottomBorder?: boolean;
|
||||||
};
|
};
|
||||||
@ -40,7 +40,7 @@ const StyledRightSection = styled.div`
|
|||||||
|
|
||||||
export function TopBar({
|
export function TopBar({
|
||||||
leftComponent,
|
leftComponent,
|
||||||
rightComponents,
|
rightComponent,
|
||||||
bottomComponent,
|
bottomComponent,
|
||||||
displayBottomBorder = true,
|
displayBottomBorder = true,
|
||||||
}: OwnProps) {
|
}: OwnProps) {
|
||||||
@ -48,7 +48,7 @@ export function TopBar({
|
|||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<StyledTopBar displayBottomBorder={displayBottomBorder}>
|
<StyledTopBar displayBottomBorder={displayBottomBorder}>
|
||||||
<StyledLeftSection>{leftComponent}</StyledLeftSection>
|
<StyledLeftSection>{leftComponent}</StyledLeftSection>
|
||||||
<StyledRightSection>{rightComponents}</StyledRightSection>
|
<StyledRightSection>{rightComponent}</StyledRightSection>
|
||||||
</StyledTopBar>
|
</StyledTopBar>
|
||||||
{bottomComponent}
|
{bottomComponent}
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
|
|||||||
@ -0,0 +1,67 @@
|
|||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
type MouseListener = (positionX: number, positionY: number) => void;
|
||||||
|
|
||||||
|
export function useTrackPointer({
|
||||||
|
shouldTrackPointer = true,
|
||||||
|
onMouseMove,
|
||||||
|
onMouseDown,
|
||||||
|
onMouseUp,
|
||||||
|
}: {
|
||||||
|
shouldTrackPointer?: boolean;
|
||||||
|
onMouseMove?: MouseListener;
|
||||||
|
onMouseDown?: MouseListener;
|
||||||
|
onMouseUp?: MouseListener;
|
||||||
|
}) {
|
||||||
|
const extractPosition = useCallback((event: MouseEvent | TouchEvent) => {
|
||||||
|
const clientX =
|
||||||
|
'clientX' in event ? event.clientX : event.changedTouches[0].clientX;
|
||||||
|
const clientY =
|
||||||
|
'clientY' in event ? event.clientY : event.changedTouches[0].clientY;
|
||||||
|
|
||||||
|
return { clientX, clientY };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onInternalMouseMove = useCallback(
|
||||||
|
(event: MouseEvent | TouchEvent) => {
|
||||||
|
const { clientX, clientY } = extractPosition(event);
|
||||||
|
onMouseMove?.(clientX, clientY);
|
||||||
|
},
|
||||||
|
[onMouseMove, extractPosition],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onInternalMouseDown = useCallback(
|
||||||
|
(event: MouseEvent | TouchEvent) => {
|
||||||
|
const { clientX, clientY } = extractPosition(event);
|
||||||
|
onMouseDown?.(clientX, clientY);
|
||||||
|
},
|
||||||
|
[onMouseDown, extractPosition],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onInternalMouseUp = useCallback(
|
||||||
|
(event: MouseEvent | TouchEvent) => {
|
||||||
|
const { clientX, clientY } = extractPosition(event);
|
||||||
|
onMouseUp?.(clientX, clientY);
|
||||||
|
},
|
||||||
|
[onMouseUp, extractPosition],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldTrackPointer) {
|
||||||
|
document.addEventListener('mousemove', onInternalMouseMove);
|
||||||
|
document.addEventListener('mousedown', onInternalMouseDown);
|
||||||
|
document.addEventListener('mouseup', onInternalMouseUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousemove', onInternalMouseMove);
|
||||||
|
document.removeEventListener('mousedown', onInternalMouseDown);
|
||||||
|
document.removeEventListener('mouseup', onInternalMouseUp);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
shouldTrackPointer,
|
||||||
|
onInternalMouseMove,
|
||||||
|
onInternalMouseDown,
|
||||||
|
onInternalMouseUp,
|
||||||
|
]);
|
||||||
|
}
|
||||||
@ -54,13 +54,13 @@ export function Tasks() {
|
|||||||
<TabList context={TasksContext} tabs={TASK_TABS} />
|
<TabList context={TasksContext} tabs={TASK_TABS} />
|
||||||
</StyledTabListContainer>
|
</StyledTabListContainer>
|
||||||
}
|
}
|
||||||
rightComponents={[
|
rightComponent={
|
||||||
<FilterDropdownButton
|
<FilterDropdownButton
|
||||||
key="tasks-filter-dropdown-button"
|
key="tasks-filter-dropdown-button"
|
||||||
context={TasksContext}
|
context={TasksContext}
|
||||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||||
/>,
|
/>
|
||||||
]}
|
}
|
||||||
/>
|
/>
|
||||||
<TaskGroups />
|
<TaskGroups />
|
||||||
</RecoilScope>
|
</RecoilScope>
|
||||||
|
|||||||
Reference in New Issue
Block a user