From d2852df976487493cc67fa5a085a629b8026c5b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=93=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D1=86=D0=BA=D0=B8=D0=B9?= Date: Mon, 19 Oct 2026 10:07:06 +0500 Subject: [PATCH] refactor: remove drag and drop from the objects tree Decided during manual testing: the legacy drop-inside zone had zero height, so drops only worked before/after a node and moves happened by accident. Sections are re-parented via Edit, as before. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../stroyBot/pages/objects/DropZone.tsx | 32 --------- .../stroyBot/pages/objects/ObjectTreeNode.tsx | 33 ++------- .../pages/objects/ObjectsPage.module.scss | 30 -------- .../pages/objects/ObjectsPageContent.tsx | 12 +--- .../stroyBot/pages/objects/ObjectsTree.tsx | 15 +--- .../pages/objects/objectsRepository.test.ts | 11 --- .../pages/objects/objectsRepository.ts | 7 -- .../pages/objects/objectsRules.test.ts | 24 +------ .../stroyBot/pages/objects/objectsRules.ts | 29 +------- .../stroyBot/pages/objects/objectsTypes.ts | 3 - src/webparts/stroyBot/pages/objects/texts.ts | 1 - .../pages/objects/useObjectActions.ts | 24 ++----- .../stroyBot/pages/objects/useObjectDrag.ts | 72 ------------------- 13 files changed, 17 insertions(+), 276 deletions(-) delete mode 100644 src/webparts/stroyBot/pages/objects/DropZone.tsx delete mode 100644 src/webparts/stroyBot/pages/objects/useObjectDrag.ts diff --git a/src/webparts/stroyBot/pages/objects/DropZone.tsx b/src/webparts/stroyBot/pages/objects/DropZone.tsx deleted file mode 100644 index 416385d..0000000 --- a/src/webparts/stroyBot/pages/objects/DropZone.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import * as React from 'react'; -import { DropPosition } from './objectsTypes'; -import { IObjectDrag } from './useObjectDrag'; -import styles from './ObjectsPage.module.scss'; - -interface Props { - targetId: number; - position: DropPosition; - drag: IObjectDrag; -} - -const POSITION_CLASS: Record = { - before: styles.dropZoneEdge, - inside: styles.dropZoneInside, - after: styles.dropZoneEdge -}; - -/** Тонкая полоса до / внутри / после узла, куда можно бросить перетаскиваемый объект. */ -export const DropZone: React.FC = ({ targetId, position, drag }) => { - const isActive = drag.activeZone?.targetId === targetId && drag.activeZone.position === position; - const className = [styles.dropZone, POSITION_CLASS[position], isActive ? styles.dropZoneActive : ''] - .filter(Boolean) - .join(' '); - - return ( -
drag.onZoneDragOver({ targetId, position }, event)} - onDragLeave={drag.onZoneDragLeave} - /> - ); -}; diff --git a/src/webparts/stroyBot/pages/objects/ObjectTreeNode.tsx b/src/webparts/stroyBot/pages/objects/ObjectTreeNode.tsx index 8c76af6..42a6036 100644 --- a/src/webparts/stroyBot/pages/objects/ObjectTreeNode.tsx +++ b/src/webparts/stroyBot/pages/objects/ObjectTreeNode.tsx @@ -3,8 +3,6 @@ import { Icon } from '@tnsu/ui-kit'; import { IMenuAnchor } from '../_tree/ContextMenu'; import { ObjectNode } from './objectsTypes'; import { IObjectTreeView } from './useObjectTreeView'; -import { IObjectDrag } from './useObjectDrag'; -import { DropZone } from './DropZone'; import { OBJECTS_TEXTS } from './texts'; import styles from './ObjectsPage.module.scss'; @@ -12,7 +10,6 @@ interface Props { node: ObjectNode; canEdit: boolean; view: IObjectTreeView; - drag: IObjectDrag; onOpenMenu: (node: ObjectNode, anchor: IMenuAnchor) => void; } @@ -20,19 +17,14 @@ interface Props { const ROW_BASE_INDENT_PX = 12; const ROW_LEVEL_INDENT_PX = 22; -function rowClassName(hasChildren: boolean, isSelected: boolean, isDragging: boolean): string { - return [ - styles.nodeRow, - hasChildren ? styles.nodeRowParent : '', - isSelected ? styles.nodeRowSelected : '', - isDragging ? styles.nodeRowDragging : '' - ] +function rowClassName(hasChildren: boolean, isSelected: boolean): string { + return [styles.nodeRow, hasChildren ? styles.nodeRowParent : '', isSelected ? styles.nodeRowSelected : ''] .filter(Boolean) .join(' '); } -/** Узел дерева объектов с зонами сброса и (если раскрыт) дочерними узлами. */ -export const ObjectTreeNode: React.FC = ({ node, canEdit, view, drag, onOpenMenu }) => { +/** Узел дерева объектов и (если раскрыт) его дочерние узлы. */ +export const ObjectTreeNode: React.FC = ({ node, canEdit, view, onOpenMenu }) => { const hasChildren = node.children.length > 0; const isExpanded = view.expandedIds.has(node.id); @@ -58,13 +50,9 @@ export const ObjectTreeNode: React.FC = ({ node, canEdit, view, drag, onO return (
-
drag.onDragStart(node.id, event)} - onDragEnd={drag.onDragEnd} onClick={() => view.clickRow(node)} onContextMenu={onContextMenu} > @@ -93,22 +81,13 @@ export const ObjectTreeNode: React.FC = ({ node, canEdit, view, drag, onO )}
- {hasChildren && isExpanded && (
{node.children.map((child) => ( - + ))}
)} -
); }; diff --git a/src/webparts/stroyBot/pages/objects/ObjectsPage.module.scss b/src/webparts/stroyBot/pages/objects/ObjectsPage.module.scss index 9ed51a8..c0aad92 100644 --- a/src/webparts/stroyBot/pages/objects/ObjectsPage.module.scss +++ b/src/webparts/stroyBot/pages/objects/ObjectsPage.module.scss @@ -91,10 +91,6 @@ $menu-button-size: 28px; } } -.nodeRowDragging { - opacity: 0.45; -} - .nodeToggle { display: flex; align-items: center; @@ -182,32 +178,6 @@ $menu-button-size: 28px; } } -// ── Зоны сброса при перетаскивании ────────────────────────────────────────── -// Высоты 1:1 со старым web part: зона «внутрь» нулевой высоты (см. открытые вопросы в чек-листе). -.dropZone { - margin: 0 $sp-2; - border-radius: 1px; - transition: background 0.12s, height 0.12s; -} - -.dropZoneEdge { - height: 2px; - - &.dropZoneActive { - height: 3px; - background: $gold-400; - } -} - -.dropZoneInside { - height: 0; - - &.dropZoneActive { - height: 2px; - background: rgba($gold-400, 0.3); - } -} - // ── Модалка ───────────────────────────────────────────────────────────────── .modalBody { display: flex; diff --git a/src/webparts/stroyBot/pages/objects/ObjectsPageContent.tsx b/src/webparts/stroyBot/pages/objects/ObjectsPageContent.tsx index f91da5a..98844f1 100644 --- a/src/webparts/stroyBot/pages/objects/ObjectsPageContent.tsx +++ b/src/webparts/stroyBot/pages/objects/ObjectsPageContent.tsx @@ -10,7 +10,6 @@ import { useObjectsData } from './useObjectsData'; import { useObjectActions } from './useObjectActions'; import { useObjectsExport } from './useObjectsExport'; import { useObjectTreeView } from './useObjectTreeView'; -import { useObjectDrag } from './useObjectDrag'; import { ObjectsHeader } from './ObjectsHeader'; import { ObjectsTree } from './ObjectsTree'; import { ObjectModal } from './ObjectModal'; @@ -42,17 +41,9 @@ export const ObjectsPageContent: React.FC = ({ projectId }) => { ); const data = useObjectsData(repository, projectId); - const actions = useObjectActions({ - repository, - projectTitle: data.projectTitle, - tree: data.tree, - reload: data.reload - }); + const actions = useObjectActions({ repository, projectTitle: data.projectTitle, reload: data.reload }); const exporter = useObjectsExport(repository, projectId); const view = useObjectTreeView(data.tree); - const drag = useObjectDrag(data.tree, (dragId, targetId, position) => { - actions.moveObject(dragId, targetId, position).catch(() => undefined); - }); const [menu, setMenu] = React.useState(undefined); const [modalTarget, setModalTarget] = React.useState(undefined); @@ -105,7 +96,6 @@ export const ObjectsPageContent: React.FC = ({ projectId }) => { tree={data.tree} canEdit={data.canEdit} view={view} - drag={drag} onOpenMenu={(node, anchor) => setMenu({ node, anchor })} onAddRoot={openAddRoot} /> diff --git a/src/webparts/stroyBot/pages/objects/ObjectsTree.tsx b/src/webparts/stroyBot/pages/objects/ObjectsTree.tsx index 0d1821f..6c0a56b 100644 --- a/src/webparts/stroyBot/pages/objects/ObjectsTree.tsx +++ b/src/webparts/stroyBot/pages/objects/ObjectsTree.tsx @@ -5,7 +5,6 @@ import { IMenuAnchor } from '../_tree/ContextMenu'; import { countNodes } from '../_tree/treeRules'; import { ObjectNode, ObjectTree } from './objectsTypes'; import { IObjectTreeView } from './useObjectTreeView'; -import { IObjectDrag } from './useObjectDrag'; import { ObjectTreeNode } from './ObjectTreeNode'; import { OBJECTS_TEXTS } from './texts'; import styles from './ObjectsPage.module.scss'; @@ -14,13 +13,12 @@ interface Props { tree: ObjectTree; canEdit: boolean; view: IObjectTreeView; - drag: IObjectDrag; onOpenMenu: (node: ObjectNode, anchor: IMenuAnchor) => void; onAddRoot: () => void; } /** Панель «N объектов / Развернуть / Свернуть» и дерево объектов (или «Объекты не найдены»). */ -export const ObjectsTree: React.FC = ({ tree, canEdit, view, drag, onOpenMenu, onAddRoot }) => ( +export const ObjectsTree: React.FC = ({ tree, canEdit, view, onOpenMenu, onAddRoot }) => ( <>
{OBJECTS_TEXTS.toolbar.total(countNodes(tree.roots))} @@ -48,16 +46,9 @@ export const ObjectsTree: React.FC = ({ tree, canEdit, view, drag, onOpen /> )} -
+
{tree.roots.map((node) => ( - + ))}
diff --git a/src/webparts/stroyBot/pages/objects/objectsRepository.test.ts b/src/webparts/stroyBot/pages/objects/objectsRepository.test.ts index ee97ee9..e062ab5 100644 --- a/src/webparts/stroyBot/pages/objects/objectsRepository.test.ts +++ b/src/webparts/stroyBot/pages/objects/objectsRepository.test.ts @@ -174,17 +174,6 @@ describe('запись', () => { expect(bodyOf(sharePoint, 2)).toEqual({ __metadata: { type: ENTITY_TYPE }, Title: 'Новое', parentId: 3 }); }); - it('перенос: меняется только parentId, в корень — null', async () => { - const { repository, sharePoint } = setup([ENTITY_TYPE_ROUTE, { match: '/items(', response: { status: 204 } }]); - - await repository.moveObject(7, 2); - await repository.moveObject(7, undefined); - - expect(sharePoint.requests[1].request.headers).toEqual(MERGE_HEADERS); - expect(bodyOf(sharePoint, 1)).toEqual({ __metadata: { type: ENTITY_TYPE }, parentId: 2 }); - expect(bodyOf(sharePoint, 2)).toEqual({ __metadata: { type: ENTITY_TYPE }, parentId: null }); - }); - it('ошибка записи: error.message.value → error.message → statusText', async () => { const verbose = setup([ ENTITY_TYPE_ROUTE, diff --git a/src/webparts/stroyBot/pages/objects/objectsRepository.ts b/src/webparts/stroyBot/pages/objects/objectsRepository.ts index 6e34ea0..8fc61fb 100644 --- a/src/webparts/stroyBot/pages/objects/objectsRepository.ts +++ b/src/webparts/stroyBot/pages/objects/objectsRepository.ts @@ -165,8 +165,6 @@ export interface IObjectsRepository { createObject(projectTitle: string, title: string, parentId: number | undefined): Promise; /** newParentId — только при смене родителя Раздела; undefined — поле не трогаем. */ updateObject(itemId: number, title: string, newParentId: number | undefined): Promise; - /** Перенос (drag & drop): меняется только родитель; undefined — в корень. */ - moveObject(itemId: number, newParentId: number | undefined): Promise; deleteObject(itemId: number): Promise; downloadObjectsExcel(projectId: string): Promise; } @@ -211,11 +209,6 @@ export function createObjectsRepository(deps: IObjectsRepositoryDeps): IObjectsR if (newParentId !== undefined) body[OBJECT_FIELDS.parentWrite] = newParentId; await postVerbose(deps, objectItemUrl(deps.siteUrl, itemId), MERGE_HEADERS, body); }, - moveObject: async (itemId, newParentId) => { - // Перенос в корень сбрасывает lookup явным null. - const body = { ...(await metadata()), [OBJECT_FIELDS.parentWrite]: newParentId ?? null }; - await postVerbose(deps, objectItemUrl(deps.siteUrl, itemId), MERGE_HEADERS, body); - }, deleteObject: (itemId) => deleteObject(deps, itemId), downloadObjectsExcel: (projectId) => downloadObjectsExcel(deps, projectId) }; diff --git a/src/webparts/stroyBot/pages/objects/objectsRules.test.ts b/src/webparts/stroyBot/pages/objects/objectsRules.test.ts index 4483052..c006bf3 100644 --- a/src/webparts/stroyBot/pages/objects/objectsRules.test.ts +++ b/src/webparts/stroyBot/pages/objects/objectsRules.test.ts @@ -1,7 +1,6 @@ import { buildObjectTree, canAddChild, - canDrop, getChangedParentId, getDeleteConfirmMessage, getErrorText, @@ -9,8 +8,7 @@ import { getParentDisplay, getParentOptions, getSubmitLabel, - isParentSelectable, - resolveMove + isParentSelectable } from './objectsRules'; import { buildObjectMenuItems } from './objectMenu'; import { IProjectObject, ObjectModalMode, ObjectNode, ObjectTree } from './objectsTypes'; @@ -100,26 +98,6 @@ describe('модалка', () => { }); }); -describe('перетаскивание', () => { - it('нельзя бросить в себя и в своих потомков', () => { - expect(canDrop(tree, 1, 1)).toBe(false); - expect(canDrop(tree, 1, 4)).toBe(false); - expect(canDrop(tree, 4, 1)).toBe(true); - }); - - it('«внутрь» — родитель = цель, «до/после» — родитель цели', () => { - expect(resolveMove(tree, 4, 3, 'inside')).toEqual({ parentId: 3 }); - expect(resolveMove(tree, 4, 3, 'before')).toEqual({ parentId: 1 }); - expect(resolveMove(tree, 2, 5, 'after')).toEqual({ parentId: undefined }); - }); - - it('родитель не меняется или перенос запрещён — ничего не делаем', () => { - expect(resolveMove(tree, 2, 3, 'after')).toBeUndefined(); - expect(resolveMove(tree, 1, 5, 'before')).toBeUndefined(); - expect(resolveMove(tree, 1, 4, 'inside')).toBeUndefined(); - }); -}); - describe('удаление и ошибки', () => { it('подтверждение с числом дочерних', () => { expect(getDeleteConfirmMessage(node(1))).toEqual( diff --git a/src/webparts/stroyBot/pages/objects/objectsRules.ts b/src/webparts/stroyBot/pages/objects/objectsRules.ts index e60857e..121eb4c 100644 --- a/src/webparts/stroyBot/pages/objects/objectsRules.ts +++ b/src/webparts/stroyBot/pages/objects/objectsRules.ts @@ -1,6 +1,6 @@ import { ISelectOption } from '@tnsu/ui-kit'; import { buildTree, isDescendant } from '../_tree/treeRules'; -import { DropPosition, IObjectModalTarget, IProjectObject, ObjectNode, ObjectTree } from './objectsTypes'; +import { IObjectModalTarget, IProjectObject, ObjectNode, ObjectTree } from './objectsTypes'; import { OBJECTS_TEXTS } from './texts'; // Правила страницы «Объекты» (stroy-bot-objects), вынесенные из DOM-кода в чистые функции. @@ -72,33 +72,6 @@ export function getChangedParentId(node: ObjectNode | undefined, selectValue: st return !isNaN(parentId) && parentId !== node.item.parentId ? parentId : undefined; } -/** Бросать узел можно куда угодно, кроме себя и своих потомков. */ -export function canDrop(tree: ObjectTree, dragId: number, targetId: number): boolean { - return dragId !== targetId && !isDescendant(tree.byId, dragId, targetId); -} - -export interface IMoveResult { - /** undefined — перенос в корень. */ - parentId: number | undefined; -} - -/** - * Новый родитель после перетаскивания: «внутрь» — целевой узел, «до/после» — родитель целевого. - * undefined — переносить нечего (родитель не меняется или перенос запрещён). - */ -export function resolveMove( - tree: ObjectTree, - dragId: number, - targetId: number, - position: DropPosition -): IMoveResult | undefined { - const dragNode = tree.byId.get(dragId); - if (!dragNode || !canDrop(tree, dragId, targetId)) return undefined; - - const parentId = position === 'inside' ? targetId : tree.byId.get(targetId)?.item.parentId; - return dragNode.item.parentId === parentId ? undefined : { parentId }; -} - export function getDeleteConfirmMessage(node: ObjectNode): string { return OBJECTS_TEXTS.deleteConfirm( node.item.title || OBJECTS_TEXTS.deleteFallbackTitle(node.id), diff --git a/src/webparts/stroyBot/pages/objects/objectsTypes.ts b/src/webparts/stroyBot/pages/objects/objectsTypes.ts index dfc6f9c..ae9318d 100644 --- a/src/webparts/stroyBot/pages/objects/objectsTypes.ts +++ b/src/webparts/stroyBot/pages/objects/objectsTypes.ts @@ -22,9 +22,6 @@ export interface IObjectModalTarget { node: ObjectNode | undefined; } -/** Куда бросили узел относительно целевого: до, внутрь, после. */ -export type DropPosition = 'before' | 'inside' | 'after'; - /** * Строка «Объекты» в ответе SharePoint ($select=Id,Title,parent/Id&$expand=parent). * Пустой lookup приходит как null — читается через `?.`. diff --git a/src/webparts/stroyBot/pages/objects/texts.ts b/src/webparts/stroyBot/pages/objects/texts.ts index 8917434..ed2bfa2 100644 --- a/src/webparts/stroyBot/pages/objects/texts.ts +++ b/src/webparts/stroyBot/pages/objects/texts.ts @@ -58,7 +58,6 @@ export const OBJECTS_TEXTS = { deleteFallbackTitle: (id: number): string => `ID ${id}`, serverError: (status: number): string => `Ошибка сервера: ${status}`, saveError: (message: string): string => `Ошибка сохранения: ${message}`, - moveError: (message: string): string => `Не удалось переместить: ${message}`, deleteError: (message: string): string => `Не удалось удалить: ${message}`, exportError: (message: string): string => `Не удалось скачать файл: ${message}` } as const; diff --git a/src/webparts/stroyBot/pages/objects/useObjectActions.ts b/src/webparts/stroyBot/pages/objects/useObjectActions.ts index 040fc63..b036cf9 100644 --- a/src/webparts/stroyBot/pages/objects/useObjectActions.ts +++ b/src/webparts/stroyBot/pages/objects/useObjectActions.ts @@ -1,28 +1,26 @@ import * as React from 'react'; import { confirmModal } from '@tnsu/ui-kit'; import { IObjectsRepository } from './objectsRepository'; -import { getChangedParentId, getDeleteConfirmMessage, getErrorText, resolveMove } from './objectsRules'; -import { DropPosition, IObjectModalTarget, ObjectNode, ObjectTree } from './objectsTypes'; +import { getChangedParentId, getDeleteConfirmMessage, getErrorText } from './objectsRules'; +import { IObjectModalTarget, ObjectNode } from './objectsTypes'; import { OBJECTS_TEXTS } from './texts'; interface IActionsDeps { repository: IObjectsRepository; projectTitle: string | undefined; - tree: ObjectTree; reload: () => void; } export interface IObjectActions { - /** Ошибка переноса или удаления (раньше — alert). */ + /** Ошибка удаления (раньше — alert). */ actionError: string | undefined; /** Создание / правка из модалки. Ошибку бросает дальше — модалка покажет её у себя. */ saveObject: (target: IObjectModalTarget, title: string, parentValue: string) => Promise; - moveObject: (dragId: number, targetId: number, position: DropPosition) => Promise; deleteObject: (node: ObjectNode) => Promise; } /** Пишущие действия над деревом объектов. После каждого успешного действия дерево перечитывается. */ -export function useObjectActions({ repository, projectTitle, tree, reload }: IActionsDeps): IObjectActions { +export function useObjectActions({ repository, projectTitle, reload }: IActionsDeps): IObjectActions { const [actionError, setActionError] = React.useState(undefined); const saveObject = async (target: IObjectModalTarget, title: string, parentValue: string): Promise => { @@ -39,18 +37,6 @@ export function useObjectActions({ repository, projectTitle, tree, reload }: IAc reload(); }; - const moveObject = async (dragId: number, targetId: number, position: DropPosition): Promise => { - const move = resolveMove(tree, dragId, targetId, position); - if (!move) return; - setActionError(undefined); - try { - await repository.moveObject(dragId, move.parentId); - reload(); - } catch (error) { - setActionError(OBJECTS_TEXTS.moveError(getErrorText(error))); - } - }; - const deleteObject = async (node: ObjectNode): Promise => { const isConfirmed = await confirmModal({ message: getDeleteConfirmMessage(node), @@ -67,5 +53,5 @@ export function useObjectActions({ repository, projectTitle, tree, reload }: IAc } }; - return { actionError, saveObject, moveObject, deleteObject }; + return { actionError, saveObject, deleteObject }; } diff --git a/src/webparts/stroyBot/pages/objects/useObjectDrag.ts b/src/webparts/stroyBot/pages/objects/useObjectDrag.ts deleted file mode 100644 index 6b69980..0000000 --- a/src/webparts/stroyBot/pages/objects/useObjectDrag.ts +++ /dev/null @@ -1,72 +0,0 @@ -import * as React from 'react'; -import { canDrop } from './objectsRules'; -import { DropPosition, ObjectTree } from './objectsTypes'; - -export interface IDropZone { - targetId: number; - position: DropPosition; -} - -export interface IObjectDrag { - draggingId: number | undefined; - /** Подсвеченная зона сброса. */ - activeZone: IDropZone | undefined; - onDragStart: (nodeId: number, event: React.DragEvent) => void; - onDragEnd: () => void; - onZoneDragOver: (zone: IDropZone, event: React.DragEvent) => void; - onZoneDragLeave: () => void; - onDrop: (event: React.DragEvent) => void; -} - -/** - * Перетаскивание узлов — как в stroy-bot-objects: узел бросается на зону до / внутрь / после - * другого узла. Бросок засчитывается в последнюю «разрешённую» зону, над которой был курсор. - */ -export function useObjectDrag( - tree: ObjectTree, - onMove: (dragId: number, targetId: number, position: DropPosition) => void -): IObjectDrag { - const [draggingId, setDraggingId] = React.useState(undefined); - const [activeZone, setActiveZone] = React.useState(undefined); - // Последняя разрешённая зона: в старом коде dragleave снимал только подсветку, а цель запоминалась. - const dropTargetRef = React.useRef(undefined); - - const reset = (): void => { - setDraggingId(undefined); - setActiveZone(undefined); - dropTargetRef.current = undefined; - }; - - const onDragStart = (nodeId: number, event: React.DragEvent): void => { - setDraggingId(nodeId); - event.dataTransfer.effectAllowed = 'move'; - event.dataTransfer.setData('text/plain', String(nodeId)); - }; - - const onZoneDragOver = (zone: IDropZone, event: React.DragEvent): void => { - if (draggingId === undefined || !canDrop(tree, draggingId, zone.targetId)) return; - event.preventDefault(); - event.dataTransfer.dropEffect = 'move'; - setActiveZone(zone); - dropTargetRef.current = zone; - }; - - const onDrop = (event: React.DragEvent): void => { - event.preventDefault(); - const dragId = draggingId; - const target = dropTargetRef.current; - reset(); - if (dragId === undefined || !target) return; - onMove(dragId, target.targetId, target.position); - }; - - return { - draggingId, - activeZone, - onDragStart, - onDragEnd: reset, - onZoneDragOver, - onZoneDragLeave: () => setActiveZone(undefined), - onDrop - }; -}