Fix bug reoder on table in view groups mode (#8894)

Fixes #8403 

Added comments on the PR to explain changes
This commit is contained in:
Charles Bochet 2024-12-06 19:19:27 +01:00 committed by GitHub
parent f36555bdc0
commit ab8ad46685
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 150 additions and 174 deletions

View File

@ -15,7 +15,8 @@ export const RecordTableNoRecordGroupRows = () => {
<RecordTableRow <RecordTableRow
key={recordId} key={recordId}
recordId={recordId} recordId={recordId}
rowIndex={rowIndex} rowIndexForFocus={rowIndex}
rowIndexForDrag={rowIndex}
/> />
); );
})} })}

View File

@ -32,7 +32,7 @@ export const RecordTableRecordGroupRows = () => {
return ( return (
isRecordGroupTableSectionToggled && isRecordGroupTableSectionToggled &&
recordIdsByGroup.map((recordId) => { recordIdsByGroup.map((recordId, rowIndexInGroup) => {
const rowIndex = rowIndexMap.get(recordId); const rowIndex = rowIndexMap.get(recordId);
if (!isDefined(rowIndex)) { if (!isDefined(rowIndex)) {
@ -43,7 +43,8 @@ export const RecordTableRecordGroupRows = () => {
<RecordTableRow <RecordTableRow
key={recordId} key={recordId}
recordId={recordId} recordId={recordId}
rowIndex={rowIndex} rowIndexForFocus={rowIndex}
rowIndexForDrag={rowIndexInGroup}
isPendingRow={!isRecordGroupTableSectionToggled} isPendingRow={!isRecordGroupTableSectionToggled}
/> />
); );

View File

@ -1,90 +0,0 @@
import { DropResult } from '@hello-pangea/dnd';
import { useRecoilCallback } from 'recoil';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { isDefined } from '~/utils/isDefined';
export const useComputeNewRowPosition = () => {
return useRecoilCallback(
({ snapshot }) =>
(result: DropResult, tableRowIds: string[]) => {
if (!isDefined(result.destination)) {
return;
}
const draggedRecordId = result.draggableId;
const destinationIndex = result.destination.index;
const sourceIndex = result.source.index;
const recordBeforeId = tableRowIds[destinationIndex - 1];
const recordDestinationId = tableRowIds[destinationIndex];
const recordAfterDestinationId = tableRowIds[destinationIndex + 1];
const recordBefore = recordBeforeId
? snapshot
.getLoadable(recordStoreFamilyState(recordBeforeId))
.getValue()
: null;
const recordDestination = recordDestinationId
? snapshot
.getLoadable(recordStoreFamilyState(recordDestinationId))
.getValue()
: null;
const recordAfterDestination = recordAfterDestinationId
? snapshot
.getLoadable(recordStoreFamilyState(recordAfterDestinationId))
.getValue()
: null;
const computeNewPosition = (destIndex: number, sourceIndex: number) => {
const moveToFirstPosition = destIndex === 0;
const moveToLastPosition = destIndex === tableRowIds.length - 1;
const moveAfterSource = destIndex > sourceIndex;
const firstRecord = tableRowIds[0]
? snapshot
.getLoadable(recordStoreFamilyState(tableRowIds[0]))
.getValue()
: null;
const lastRecord = tableRowIds[tableRowIds.length - 1]
? snapshot
.getLoadable(
recordStoreFamilyState(tableRowIds[tableRowIds.length - 1]),
)
.getValue()
: null;
const firstRecordPosition = firstRecord?.position ?? 0;
if (moveToFirstPosition) {
if (firstRecordPosition <= 0) {
return firstRecordPosition - 1;
} else {
return firstRecordPosition / 2;
}
} else if (moveToLastPosition) {
return lastRecord?.position + 1;
} else if (moveAfterSource) {
const recordAfterDestinationPosition =
recordAfterDestination?.position ?? 0;
const recordDestinationPosition = recordDestination?.position ?? 0;
return (
(recordAfterDestinationPosition + recordDestinationPosition) / 2
);
} else {
return (
recordDestination?.position -
(recordDestination?.position - recordBefore?.position) / 2
);
}
};
const newPosition = computeNewPosition(destinationIndex, sourceIndex);
return { draggedRecordId, newPosition };
},
[],
);
};

View File

@ -1,13 +1,15 @@
import { DragDropContext, DropResult } from '@hello-pangea/dnd'; import { DragDropContext, DropResult } from '@hello-pangea/dnd';
import { ReactNode, useContext } from 'react'; import { ReactNode, useContext } from 'react';
import { useSetRecoilState } from 'recoil'; import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { getDraggedRecordPosition } from '@/object-record/record-board/utils/getDraggedRecordPosition';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useComputeNewRowPosition } from '@/object-record/record-table/hooks/useComputeNewRowPosition';
import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState'; import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
@ -22,7 +24,7 @@ export const RecordTableBodyDragDropContextProvider = ({
objectNameSingular, objectNameSingular,
}); });
const allRecordIds = useRecoilComponentValueV2( const recordIndexAllRecordIdsSelector = useRecoilComponentCallbackStateV2(
recordIndexAllRecordIdsComponentSelector, recordIndexAllRecordIdsComponentSelector,
); );
@ -35,27 +37,75 @@ export const RecordTableBodyDragDropContextProvider = ({
isRemoveSortingModalOpenState, isRemoveSortingModalOpenState,
); );
const computeNewRowPosition = useComputeNewRowPosition(); const handleDragEnd = useRecoilCallback(
({ snapshot }) =>
const handleDragEnd = (result: DropResult) => { (result: DropResult) => {
if (viewSorts.length > 0) { if (viewSorts.length > 0) {
setIsRemoveSortingModalOpenState(true); setIsRemoveSortingModalOpenState(true);
return; return;
} }
const computeResult = computeNewRowPosition(result, allRecordIds); if (!isDefined(result.destination)) {
throw new Error('Drop Destination is not defined');
}
if (!isDefined(computeResult)) { const allRecordIds = getSnapshotValue(
snapshot,
recordIndexAllRecordIdsSelector,
);
const isSourceIndexBeforeDestinationIndex =
result.source.index < result.destination.index;
const recordBeforeDestinationId =
allRecordIds[
isSourceIndexBeforeDestinationIndex
? result.destination.index
: result.destination.index - 1
];
const recordBeforeDestination = recordBeforeDestinationId
? snapshot
.getLoadable(recordStoreFamilyState(recordBeforeDestinationId))
.getValue()
: null;
const recordAfterDestinationId =
allRecordIds[
isSourceIndexBeforeDestinationIndex
? result.destination.index + 1
: result.destination.index
];
const recordAfterDestination = recordAfterDestinationId
? snapshot
.getLoadable(recordStoreFamilyState(recordAfterDestinationId))
.getValue()
: null;
const newPosition = getDraggedRecordPosition(
recordBeforeDestination?.position,
recordAfterDestination?.position,
);
if (!isDefined(newPosition)) {
return; return;
} }
updateOneRow({ updateOneRow({
idToUpdate: computeResult.draggedRecordId, idToUpdate: result.draggableId,
updateOneRecordInput: { updateOneRecordInput: {
position: computeResult.newPosition, position: newPosition,
}, },
}); });
}; },
[
recordIndexAllRecordIdsSelector,
setIsRemoveSortingModalOpenState,
updateOneRow,
viewSorts.length,
],
);
return ( return (
<DragDropContext onDragEnd={handleDragEnd}>{children}</DragDropContext> <DragDropContext onDragEnd={handleDragEnd}>{children}</DragDropContext>

View File

@ -3,10 +3,11 @@ import { ReactNode, useContext } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { getDraggedRecordPosition } from '@/object-record/record-board/utils/getDraggedRecordPosition';
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { useComputeNewRowPosition } from '@/object-record/record-table/hooks/useComputeNewRowPosition';
import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState'; import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
@ -25,10 +26,6 @@ export const RecordTableBodyRecordGroupDragDropContextProvider = ({
objectNameSingular, objectNameSingular,
}); });
const recordIndexAllRecordIdsSelector = useRecoilComponentCallbackStateV2(
recordIndexAllRecordIdsComponentSelector,
);
const { currentViewWithCombinedFiltersAndSorts } = const { currentViewWithCombinedFiltersAndSorts } =
useGetCurrentView(recordTableId); useGetCurrentView(recordTableId);
@ -38,33 +35,34 @@ export const RecordTableBodyRecordGroupDragDropContextProvider = ({
isRemoveSortingModalOpenState, isRemoveSortingModalOpenState,
); );
const computeNewRowPosition = useComputeNewRowPosition(); const recordIdsByGroupFamilyState = useRecoilComponentCallbackStateV2(
recordIndexRecordIdsByGroupComponentFamilyState,
);
const handleDragEnd = useRecoilCallback( const handleDragEnd = useRecoilCallback(
({ snapshot }) => ({ snapshot }) =>
(result: DropResult) => { (result: DropResult) => {
const tableAllRecordIds = getSnapshotValue( const destinationRecordGroupId = result.destination?.droppableId;
snapshot,
recordIndexAllRecordIdsSelector,
);
const recordGroupId = result.destination?.droppableId; if (!isDefined(result.destination)) {
throw new Error('Drop Destination is not defined');
}
if (!isDefined(recordGroupId)) { if (!isDefined(destinationRecordGroupId)) {
throw new Error('Record group id is not defined'); throw new Error('Record group id is not defined');
} }
const recordGroup = getSnapshotValue( const destinationRecordGroup = getSnapshotValue(
snapshot, snapshot,
recordGroupDefinitionFamilyState(recordGroupId), recordGroupDefinitionFamilyState(destinationRecordGroupId),
); );
if (!isDefined(recordGroup)) { if (!isDefined(destinationRecordGroup)) {
throw new Error('Record group is not defined'); throw new Error('Record group is not defined');
} }
const fieldMetadata = objectMetadataItem.fields.find( const fieldMetadata = objectMetadataItem.fields.find(
(field) => field.id === recordGroup.fieldMetadataId, (field) => field.id === destinationRecordGroup.fieldMetadataId,
); );
if (!isDefined(fieldMetadata)) { if (!isDefined(fieldMetadata)) {
@ -76,25 +74,62 @@ export const RecordTableBodyRecordGroupDragDropContextProvider = ({
return; return;
} }
const computeResult = computeNewRowPosition(result, tableAllRecordIds); const isSourceIndexBeforeDestinationIndexInSameGroup =
result.source.index < result.destination.index &&
result.source.droppableId === result.destination.droppableId;
if (!isDefined(computeResult)) { const destinationGroupRecordIds = getSnapshotValue(
snapshot,
recordIdsByGroupFamilyState(destinationRecordGroupId),
);
const recordBeforeDestinationId =
destinationGroupRecordIds[
isSourceIndexBeforeDestinationIndexInSameGroup
? result.destination.index
: result.destination.index - 1
];
const recordBeforeDestination = recordBeforeDestinationId
? snapshot
.getLoadable(recordStoreFamilyState(recordBeforeDestinationId))
.getValue()
: null;
const recordAfterDestinationId =
destinationGroupRecordIds[
isSourceIndexBeforeDestinationIndexInSameGroup
? result.destination.index + 1
: result.destination.index
];
const recordAfterDestination = recordAfterDestinationId
? snapshot
.getLoadable(recordStoreFamilyState(recordAfterDestinationId))
.getValue()
: null;
const newPosition = getDraggedRecordPosition(
recordBeforeDestination?.position,
recordAfterDestination?.position,
);
if (!isDefined(newPosition)) {
return; return;
} }
updateOneRow({ updateOneRow({
idToUpdate: computeResult.draggedRecordId, idToUpdate: result.draggableId,
updateOneRecordInput: { updateOneRecordInput: {
position: computeResult.newPosition, position: newPosition,
[fieldMetadata.name]: recordGroup.value, [fieldMetadata.name]: destinationRecordGroup.value,
}, },
}); });
}, },
[ [
recordIndexAllRecordIdsSelector,
objectMetadataItem.fields, objectMetadataItem.fields,
viewSorts.length, viewSorts.length,
computeNewRowPosition, recordIdsByGroupFamilyState,
updateOneRow, updateOneRow,
setIsRemoveSortingModalOpenState, setIsRemoveSortingModalOpenState,
], ],

View File

@ -13,7 +13,8 @@ export const RecordTablePendingRow = () => {
<RecordTableRow <RecordTableRow
key={pendingRecordId} key={pendingRecordId}
recordId={pendingRecordId} recordId={pendingRecordId}
rowIndex={-1} rowIndexForDrag={-1}
rowIndexForFocus={-1}
isPendingRow isPendingRow
/> />
); );

View File

@ -7,19 +7,22 @@ import { RecordTableRowWrapper } from '@/object-record/record-table/record-table
type RecordTableRowProps = { type RecordTableRowProps = {
recordId: string; recordId: string;
rowIndex: number; rowIndexForFocus: number;
rowIndexForDrag: number;
isPendingRow?: boolean; isPendingRow?: boolean;
}; };
export const RecordTableRow = ({ export const RecordTableRow = ({
recordId, recordId,
rowIndex, rowIndexForFocus,
rowIndexForDrag,
isPendingRow, isPendingRow,
}: RecordTableRowProps) => { }: RecordTableRowProps) => {
return ( return (
<RecordTableRowWrapper <RecordTableRowWrapper
recordId={recordId} recordId={recordId}
rowIndex={rowIndex} rowIndexForFocus={rowIndexForFocus}
rowIndexForDrag={rowIndexForDrag}
isPendingRow={isPendingRow} isPendingRow={isPendingRow}
> >
<RecordTableCellGrip /> <RecordTableCellGrip />

View File

@ -16,14 +16,16 @@ import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-sta
type RecordTableRowWrapperProps = { type RecordTableRowWrapperProps = {
recordId: string; recordId: string;
rowIndex: number; rowIndexForFocus: number;
rowIndexForDrag: number;
isPendingRow?: boolean; isPendingRow?: boolean;
children: ReactNode; children: ReactNode;
}; };
export const RecordTableRowWrapper = ({ export const RecordTableRowWrapper = ({
recordId, recordId,
rowIndex, rowIndexForFocus,
rowIndexForDrag,
isPendingRow, isPendingRow,
children, children,
}: RecordTableRowWrapperProps) => { }: RecordTableRowWrapperProps) => {
@ -55,7 +57,7 @@ export const RecordTableRowWrapper = ({
); );
useEffect(() => { useEffect(() => {
if (rowIndex === 0) { if (rowIndexForFocus === 0) {
const tdArray = Array.from( const tdArray = Array.from(
trRef.current?.getElementsByTagName('td') ?? [], trRef.current?.getElementsByTagName('td') ?? [],
); );
@ -66,7 +68,7 @@ export const RecordTableRowWrapper = ({
setTableCellWidths(tdWidths); setTableCellWidths(tdWidths);
} }
}, [trRef, rowIndex, setTableCellWidths]); }, [trRef, rowIndexForFocus, setTableCellWidths]);
// TODO: find a better way to emit this event // TODO: find a better way to emit this event
useEffect(() => { useEffect(() => {
@ -76,7 +78,7 @@ export const RecordTableRowWrapper = ({
}, [inView, onIndexRecordsLoaded]); }, [inView, onIndexRecordsLoaded]);
return ( return (
<Draggable key={recordId} draggableId={recordId} index={rowIndex}> <Draggable key={recordId} draggableId={recordId} index={rowIndexForDrag}>
{(draggableProvided, draggableSnapshot) => ( {(draggableProvided, draggableSnapshot) => (
<RecordTableTr <RecordTableTr
ref={(node) => { ref={(node) => {
@ -103,7 +105,7 @@ export const RecordTableRowWrapper = ({
<RecordTableRowContext.Provider <RecordTableRowContext.Provider
value={{ value={{
recordId, recordId,
rowIndex, rowIndex: rowIndexForFocus,
pathToShowPage: pathToShowPage:
getBasePathToShowPage({ getBasePathToShowPage({
objectNameSingular: objectMetadataItem.nameSingular, objectNameSingular: objectMetadataItem.nameSingular,

View File

@ -1,27 +0,0 @@
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
import { RecoilComponentState } from '@/ui/utilities/state/component-state/types/RecoilComponentState';
export const useRecoilCallbackState = <Value>(
componentState: RecoilComponentState<Value>,
componentId?: string,
) => {
const componentContext = (window as any).componentContextStateMap?.get(
componentState.key,
);
if (!componentContext) {
throw new Error(
`Component context for key "${componentState.key}" is not defined`,
);
}
const internalScopeId = useAvailableScopeIdOrThrow(
componentContext,
getScopeIdOrUndefinedFromComponentId(componentId),
);
return componentState.atomFamily({
scopeId: internalScopeId,
});
};