payload-nested-docs-page-tree 1.2.4 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +356 -222
  2. package/dist/components/PageTreeContext.d.ts +13 -1
  3. package/dist/components/PageTreeContext.js +11 -1
  4. package/dist/components/PageTreeContext.js.map +1 -1
  5. package/dist/components/PageTreeListView.client.d.ts +4 -5
  6. package/dist/components/PageTreeListView.client.js +668 -128
  7. package/dist/components/PageTreeListView.client.js.map +1 -1
  8. package/dist/components/PageTreeListView.module.css +399 -376
  9. package/dist/components/PageTreeListView.server.js +18 -28
  10. package/dist/components/PageTreeListView.server.js.map +1 -1
  11. package/dist/components/PageTreeTitleCell.js +80 -18
  12. package/dist/components/PageTreeTitleCell.js.map +1 -1
  13. package/dist/endpoints/createMovePageEndpoint.d.ts +3 -0
  14. package/dist/endpoints/createMovePageEndpoint.js +186 -125
  15. package/dist/endpoints/createMovePageEndpoint.js.map +1 -1
  16. package/dist/endpoints/createReorderPageEndpoint.d.ts +8 -0
  17. package/dist/endpoints/createReorderPageEndpoint.js +706 -0
  18. package/dist/endpoints/createReorderPageEndpoint.js.map +1 -0
  19. package/dist/endpoints/shared.d.ts +40 -0
  20. package/dist/endpoints/shared.js +93 -0
  21. package/dist/endpoints/shared.js.map +1 -0
  22. package/dist/index.js +110 -27
  23. package/dist/index.js.map +1 -1
  24. package/dist/types.d.ts +47 -2
  25. package/dist/types.js +22 -3
  26. package/dist/types.js.map +1 -1
  27. package/dist/utilities/diagnostics.d.ts +42 -0
  28. package/dist/utilities/diagnostics.js +83 -0
  29. package/dist/utilities/diagnostics.js.map +1 -0
  30. package/dist/utilities/dropTargets.d.ts +2 -2
  31. package/dist/utilities/dropTargets.js +9 -7
  32. package/dist/utilities/dropTargets.js.map +1 -1
  33. package/dist/utilities/moveValidation.js.map +1 -1
  34. package/dist/utilities/pageTree.js +4 -6
  35. package/dist/utilities/pageTree.js.map +1 -1
  36. package/dist/utilities/pageTreeCollision.d.ts +6 -0
  37. package/dist/utilities/pageTreeCollision.js +39 -4
  38. package/dist/utilities/pageTreeCollision.js.map +1 -1
  39. package/dist/utilities/pageTreeConfig.js.map +1 -1
  40. package/dist/utilities/status.d.ts +1 -1
  41. package/dist/utilities/status.js +7 -3
  42. package/dist/utilities/status.js.map +1 -1
  43. package/package.json +54 -32
@@ -1,27 +1,142 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { DndContext, DragOverlay, PointerSensor, useDroppable, useSensor, useSensors } from '@dnd-kit/core';
4
- import { DefaultListView, ListQueryProvider, SelectAll, SelectRow, toast, useConfig, useLocale, useTranslation } from '@payloadcms/ui';
4
+ import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';
5
+ import { DefaultListView, ListQueryProvider, Pill, SelectAll, SelectRow, SortHeader, toast, useConfig, useLocale, usePreferences, useTranslation } from '@payloadcms/ui';
5
6
  import { useRouter, useSearchParams } from 'next/navigation';
6
7
  import React from 'react';
7
- import { pageTreeMoveRequestHeader, pageTreeMoveRequestHeaderValue } from '../types.js';
8
8
  import { buildInsertDropTargets, getDropTargetParentDoc } from '../utilities/dropTargets.js';
9
9
  import { CANCEL_DRAG_MESSAGE, getDropValidation } from '../utilities/moveValidation.js';
10
10
  import { buildDocSlugPath, buildPageTreeDocs, buildProspectiveDocSlugPath, getDocDisplayLabel, getVisibleTreeDocs } from '../utilities/pageTree.js';
11
11
  import { pageTreeCollisionDetectionStrategy } from '../utilities/pageTreeCollision.js';
12
12
  import { getPageTreeBadgeColor, getPageTreeBadgeLabel, getPageTreeDisplayStatus } from '../utilities/status.js';
13
- import { PageTreeProvider } from './PageTreeContext.js';
13
+ import { PageTreeProvider, PageTreeRowDndProvider } from './PageTreeContext.js';
14
14
  import styles from './PageTreeListView.module.css';
15
15
  import { PageTreeTitleCell } from './PageTreeTitleCell.js';
16
- const MANUAL_ORDER_COLUMN_ACCESSOR = '_dragHandle';
17
16
  const SILENT_MOVE_MESSAGES = new Set([
18
17
  CANCEL_DRAG_MESSAGE
19
18
  ]);
19
+ const ALREADY_ROOT_MESSAGE = 'This page is already in root level.';
20
+ function getParentMovePreferenceKey(collectionSlug) {
21
+ return `payload-nested-docs-page-tree:${collectionSlug}:parentMoveHandle`;
22
+ }
20
23
  function getRowDropID(rowID) {
21
24
  return `page-drop:${rowID}`;
22
25
  }
26
+ function getSortableRowID(rowID) {
27
+ return `page-sort:${rowID}`;
28
+ }
29
+ function getDiagnosticString(value) {
30
+ return typeof value === 'string' && value.length > 0 ? value : null;
31
+ }
23
32
  function getPageTreeDragData(value) {
24
- return value && typeof value === 'object' ? value : {};
33
+ if (!value || typeof value !== 'object') {
34
+ return {};
35
+ }
36
+ const data = value;
37
+ const dragType = data.dragType === 'move' || data.dragType === 'order' ? data.dragType : undefined;
38
+ const parentID = data.parentID === null || typeof data.parentID === 'string' ? data.parentID : undefined;
39
+ const rowID = typeof data.rowID === 'string' ? data.rowID : undefined;
40
+ return {
41
+ dragType,
42
+ parentID,
43
+ rowID
44
+ };
45
+ }
46
+ function getSortableTransform(transform) {
47
+ if (!transform) {
48
+ return undefined;
49
+ }
50
+ const scaleX = transform.scaleX ?? 1;
51
+ const scaleY = transform.scaleY ?? 1;
52
+ return `translate3d(${transform.x}px, ${transform.y}px, 0) scaleX(${scaleX}) scaleY(${scaleY})`;
53
+ }
54
+ function getVisibleSubtreeIndexRange(docs, rootIndex) {
55
+ const rootDoc = docs[rootIndex];
56
+ if (!rootDoc) {
57
+ return null;
58
+ }
59
+ let end = rootIndex;
60
+ for(let index = rootIndex + 1; index < docs.length; index += 1){
61
+ if (!docs[index]?.__pageTreeAncestorIDs.includes(rootDoc.__pageTreeID)) {
62
+ break;
63
+ }
64
+ end = index;
65
+ }
66
+ return {
67
+ end,
68
+ start: rootIndex
69
+ };
70
+ }
71
+ function getRangeRect(args) {
72
+ const { activeNodeRect, activeRange, range, rects } = args;
73
+ const firstRect = rects[range.start] ?? (range.start === activeRange.start ? activeNodeRect : undefined);
74
+ const lastRect = rects[range.end] ?? (range.end === activeRange.end ? activeNodeRect : undefined);
75
+ if (!firstRect || !lastRect) {
76
+ return null;
77
+ }
78
+ return {
79
+ bottom: lastRect.top + lastRect.height,
80
+ height: lastRect.top + lastRect.height - firstRect.top,
81
+ top: firstRect.top
82
+ };
83
+ }
84
+ function isIndexInRange(index, range) {
85
+ return index >= range.start && index <= range.end;
86
+ }
87
+ function buildPageTreeSortingStrategy(docs) {
88
+ return ({ activeIndex, activeNodeRect, index, overIndex, rects })=>{
89
+ const activeRange = getVisibleSubtreeIndexRange(docs, activeIndex);
90
+ const overRange = getVisibleSubtreeIndexRange(docs, overIndex);
91
+ if (!activeRange || !overRange || activeRange.start === overRange.start) {
92
+ return null;
93
+ }
94
+ const activeRect = getRangeRect({
95
+ activeNodeRect,
96
+ activeRange,
97
+ range: activeRange,
98
+ rects
99
+ });
100
+ const overRect = getRangeRect({
101
+ activeNodeRect,
102
+ activeRange,
103
+ range: overRange,
104
+ rects
105
+ });
106
+ if (!activeRect || !overRect) {
107
+ return null;
108
+ }
109
+ if (isIndexInRange(index, activeRange)) {
110
+ return {
111
+ scaleX: 1,
112
+ scaleY: 1,
113
+ x: 0,
114
+ y: activeRange.start < overRange.start ? overRect.bottom - activeRect.bottom : overRect.top - activeRect.top
115
+ };
116
+ }
117
+ if (activeRange.start < overRange.start && index > activeRange.end && index <= overRange.end) {
118
+ return {
119
+ scaleX: 1,
120
+ scaleY: 1,
121
+ x: 0,
122
+ y: -activeRect.height
123
+ };
124
+ }
125
+ if (activeRange.start > overRange.start && index >= overRange.start && index < activeRange.start) {
126
+ return {
127
+ scaleX: 1,
128
+ scaleY: 1,
129
+ x: 0,
130
+ y: activeRect.height
131
+ };
132
+ }
133
+ return {
134
+ scaleX: 1,
135
+ scaleY: 1,
136
+ x: 0,
137
+ y: 0
138
+ };
139
+ };
25
140
  }
26
141
  function buildPaginatedData(docs, limit, requestedPage) {
27
142
  const totalDocs = docs.length;
@@ -56,8 +171,12 @@ function normalizePositiveInt(value, fallback) {
56
171
  const parsedValue = Number.parseInt(value, 10);
57
172
  return Number.isNaN(parsedValue) || parsedValue <= 0 ? fallback : parsedValue;
58
173
  }
59
- function normalizePageTreeColumnState(columnState, useAsTitle) {
60
- return columnState.filter((column)=>column.accessor !== MANUAL_ORDER_COLUMN_ACCESSOR).map((column)=>column.accessor === useAsTitle ? {
174
+ function isOrderableSortActive(args) {
175
+ const { orderableFieldName, sort } = args;
176
+ return Boolean(orderableFieldName && (sort === orderableFieldName || sort === `-${orderableFieldName}`));
177
+ }
178
+ function ensureUseAsTitleColumn(columnState, useAsTitle) {
179
+ return columnState.map((column)=>column.accessor === useAsTitle ? {
61
180
  ...column,
62
181
  active: true
63
182
  } : column);
@@ -82,6 +201,191 @@ function getSelectableRowData(doc) {
82
201
  function shouldSilenceMoveMessage(message) {
83
202
  return typeof message === 'string' && SILENT_MOVE_MESSAGES.has(message);
84
203
  }
204
+ function getVisibleSubtreeRowIDs(docs, rowID) {
205
+ const rootIndex = docs.findIndex((doc)=>doc.__pageTreeID === rowID);
206
+ if (rootIndex < 0) {
207
+ return [];
208
+ }
209
+ const rowIDs = [
210
+ rowID
211
+ ];
212
+ for (const doc of docs.slice(rootIndex + 1)){
213
+ if (!doc.__pageTreeAncestorIDs.includes(rowID)) {
214
+ break;
215
+ }
216
+ rowIDs.push(doc.__pageTreeID);
217
+ }
218
+ return rowIDs;
219
+ }
220
+ function buildOptimisticOrderRowIDs(args) {
221
+ const { activeRowID, docs, targetRowID } = args;
222
+ if (activeRowID === targetRowID) {
223
+ return null;
224
+ }
225
+ const activeDoc = docs.find((doc)=>doc.__pageTreeID === activeRowID);
226
+ const targetDoc = docs.find((doc)=>doc.__pageTreeID === targetRowID);
227
+ if (!activeDoc || !targetDoc || activeDoc.__pageTreeParentID !== targetDoc.__pageTreeParentID) {
228
+ return null;
229
+ }
230
+ const activeBlockIDs = getVisibleSubtreeRowIDs(docs, activeRowID);
231
+ const targetBlockIDs = getVisibleSubtreeRowIDs(docs, targetRowID);
232
+ if (activeBlockIDs.length === 0 || targetBlockIDs.length === 0 || targetBlockIDs.includes(activeRowID)) {
233
+ return null;
234
+ }
235
+ const currentRowIDs = docs.map((doc)=>doc.__pageTreeID);
236
+ if (activeBlockIDs.length === 1 && targetBlockIDs.length === 1) {
237
+ const activeIndex = currentRowIDs.indexOf(activeRowID);
238
+ const targetIndex = currentRowIDs.indexOf(targetRowID);
239
+ return activeIndex < 0 || targetIndex < 0 ? null : arrayMove(currentRowIDs, activeIndex, targetIndex);
240
+ }
241
+ const remainingRowIDs = currentRowIDs.filter((rowID)=>!activeBlockIDs.includes(rowID));
242
+ const activeIndex = currentRowIDs.indexOf(activeRowID);
243
+ const targetIndex = currentRowIDs.indexOf(targetRowID);
244
+ const targetAnchorID = activeIndex < targetIndex ? targetBlockIDs[targetBlockIDs.length - 1] : targetBlockIDs[0];
245
+ if (!targetAnchorID) {
246
+ return null;
247
+ }
248
+ const targetAnchorIndex = remainingRowIDs.indexOf(targetAnchorID);
249
+ if (targetAnchorIndex < 0) {
250
+ return null;
251
+ }
252
+ const insertIndex = activeIndex < targetIndex ? targetAnchorIndex + 1 : targetAnchorIndex;
253
+ return [
254
+ ...remainingRowIDs.slice(0, insertIndex),
255
+ ...activeBlockIDs,
256
+ ...remainingRowIDs.slice(insertIndex)
257
+ ];
258
+ }
259
+ function buildReorderRequestBody(args) {
260
+ const { activeRowID, docs, orderableFieldName, sort, targetRowID } = args;
261
+ if (!orderableFieldName || sort !== orderableFieldName && sort !== `-${orderableFieldName}`) {
262
+ return null;
263
+ }
264
+ if (activeRowID === targetRowID) {
265
+ return null;
266
+ }
267
+ const activeDoc = docs.find((doc)=>doc.__pageTreeID === activeRowID);
268
+ const targetDoc = docs.find((doc)=>doc.__pageTreeID === targetRowID);
269
+ if (!activeDoc || !targetDoc || activeDoc.__pageTreeParentID !== targetDoc.__pageTreeParentID) {
270
+ return null;
271
+ }
272
+ const siblingDocs = docs.filter((doc)=>doc.__pageTreeParentID === activeDoc.__pageTreeParentID);
273
+ const moveFromIndex = siblingDocs.findIndex((doc)=>doc.__pageTreeID === activeRowID);
274
+ const moveToIndex = siblingDocs.findIndex((doc)=>doc.__pageTreeID === targetRowID);
275
+ if (moveFromIndex < 0 || moveToIndex < 0 || moveFromIndex === moveToIndex) {
276
+ return null;
277
+ }
278
+ const newBeforeRow = moveToIndex > moveFromIndex ? siblingDocs[moveToIndex] : siblingDocs[moveToIndex - 1];
279
+ const newAfterRow = moveToIndex > moveFromIndex ? siblingDocs[moveToIndex + 1] : siblingDocs[moveToIndex];
280
+ const target = newBeforeRow ?? newAfterRow;
281
+ if (!target) {
282
+ return null;
283
+ }
284
+ const getFrontendOrderEntry = (doc, index)=>{
285
+ const orderKey = doc[orderableFieldName];
286
+ return {
287
+ slug: getDiagnosticString(doc.slug),
288
+ index,
289
+ orderKey: typeof orderKey === 'string' && orderKey.length > 0 ? orderKey : null
290
+ };
291
+ };
292
+ const siblingDocsAfter = [
293
+ ...siblingDocs
294
+ ];
295
+ const [movedSiblingDoc] = siblingDocsAfter.splice(moveFromIndex, 1);
296
+ if (!movedSiblingDoc) {
297
+ return null;
298
+ }
299
+ siblingDocsAfter.splice(moveToIndex, 0, movedSiblingDoc);
300
+ const targetKey = target[orderableFieldName];
301
+ const newKeyWillBe = newBeforeRow && sort === orderableFieldName || !newBeforeRow && sort === `-${orderableFieldName}` ? 'greater' : 'less';
302
+ return {
303
+ docsToMove: [
304
+ activeRowID
305
+ ],
306
+ frontendOrder: {
307
+ activeSlug: getDiagnosticString(activeDoc.slug),
308
+ after: siblingDocsAfter.map(getFrontendOrderEntry),
309
+ before: siblingDocs.map(getFrontendOrderEntry),
310
+ moveFromIndex,
311
+ moveToIndex,
312
+ newAfterRowSlug: newAfterRow ? getDiagnosticString(newAfterRow.slug) : null,
313
+ newBeforeRowSlug: newBeforeRow ? getDiagnosticString(newBeforeRow.slug) : null,
314
+ sort: sort ?? null,
315
+ targetSlug: getDiagnosticString(targetDoc.slug)
316
+ },
317
+ newKeyWillBe,
318
+ orderableFieldName,
319
+ target: {
320
+ id: target.__pageTreeID,
321
+ key: typeof targetKey === 'string' && targetKey.length > 0 ? targetKey : null
322
+ }
323
+ };
324
+ }
325
+ function orderDocsByRowIDs(docs, orderRowIDs) {
326
+ if (!orderRowIDs) {
327
+ return docs;
328
+ }
329
+ const docsByID = new Map(docs.map((doc)=>[
330
+ doc.__pageTreeID,
331
+ doc
332
+ ]));
333
+ const orderedDocs = orderRowIDs.flatMap((rowID)=>{
334
+ const doc = docsByID.get(rowID);
335
+ return doc ? [
336
+ doc
337
+ ] : [];
338
+ });
339
+ return orderedDocs.length === docs.length ? orderedDocs : docs;
340
+ }
341
+ function getDropTargetValidation(args) {
342
+ const { activeDoc, docsByID, dropTarget } = args;
343
+ if (dropTarget.dropType === 'row') {
344
+ return getDropValidation({
345
+ activeDoc,
346
+ targetDoc: docsByID.get(dropTarget.rowID) ?? undefined
347
+ });
348
+ }
349
+ if (dropTarget.parentID === null) {
350
+ if (activeDoc.__pageTreeParentID === null) {
351
+ return {
352
+ isValid: false,
353
+ message: ALREADY_ROOT_MESSAGE,
354
+ parentID: null
355
+ };
356
+ }
357
+ return {
358
+ isValid: true,
359
+ parentID: null
360
+ };
361
+ }
362
+ const targetDoc = docsByID.get(dropTarget.parentID);
363
+ if (!targetDoc) {
364
+ return {
365
+ isValid: false,
366
+ message: 'Could not resolve drop target.',
367
+ parentID: dropTarget.parentID
368
+ };
369
+ }
370
+ if (targetDoc.__pageTreeID === activeDoc.__pageTreeID) {
371
+ return {
372
+ isValid: false,
373
+ message: CANCEL_DRAG_MESSAGE,
374
+ parentID: targetDoc.__pageTreeID
375
+ };
376
+ }
377
+ if (targetDoc.__pageTreeAncestorIDs.includes(activeDoc.__pageTreeID)) {
378
+ return {
379
+ isValid: false,
380
+ message: 'A document cannot be moved under one of its descendants.',
381
+ parentID: targetDoc.__pageTreeID
382
+ };
383
+ }
384
+ return {
385
+ isValid: true,
386
+ parentID: targetDoc.__pageTreeID
387
+ };
388
+ }
85
389
  function getStatusClassName(status) {
86
390
  if (status === 'changed' || status === 'draft' || status === 'published') {
87
391
  return status;
@@ -113,8 +417,20 @@ function renderStatusBadge(args) {
113
417
  })
114
418
  }, doc.__pageTreeID ?? index);
115
419
  }
420
+ function ParentMoveToggle({ collectionSlug, enabled, onToggle }) {
421
+ return /*#__PURE__*/ _jsx(Pill, {
422
+ "aria-checked": enabled,
423
+ "aria-label": `${enabled ? 'Hide' : 'Show'} edit hierarchy handles`,
424
+ className: "pages-hierarchy-parent-move-toggle",
425
+ id: `pages-hierarchy-parent-move-toggle-${collectionSlug}`,
426
+ onClick: onToggle,
427
+ pillStyle: enabled ? 'dark' : 'light',
428
+ size: "small",
429
+ children: "Edit Hierarchy"
430
+ });
431
+ }
116
432
  function buildTableColumns(args) {
117
- const { badgeConfig, columnState, docs, enableRowSelections, homeIndicatorEnabled, parentFieldSlug, t, useAsTitle } = args;
433
+ const { badgeConfig, columnState, docs, enableRowSelections, homeIndicatorEnabled, orderableFieldName, parentFieldSlug, t, useAsTitle } = args;
118
434
  const columnsToUse = columnState.map((column)=>{
119
435
  if (column.accessor === useAsTitle) {
120
436
  return {
@@ -162,18 +478,35 @@ function buildTableColumns(args) {
162
478
  }, doc.__pageTreeID ?? index))
163
479
  });
164
480
  }
481
+ if (orderableFieldName) {
482
+ const orderableColumn = {
483
+ accessor: '_pageTreeOrderSort',
484
+ active: true,
485
+ field: {
486
+ hidden: true
487
+ },
488
+ Heading: /*#__PURE__*/ _jsx(SortHeader, {}),
489
+ renderedCells: docs.map((doc, index)=>/*#__PURE__*/ _jsx("span", {
490
+ "aria-hidden": "true",
491
+ className: "pages-hierarchy-order-sort-spacer"
492
+ }, doc.__pageTreeID ?? index))
493
+ };
494
+ const selectColumnIndex = columnsToUse.findIndex((column)=>column.accessor === '_select');
495
+ columnsToUse.splice(selectColumnIndex >= 0 ? selectColumnIndex + 1 : 0, 0, orderableColumn);
496
+ }
165
497
  return columnsToUse;
166
498
  }
167
- function HierarchyInsertRow({ activeColumnsCount, activeDragRowID, dropTarget, dropValidation, isMovePending }) {
499
+ function HierarchyInsertRow({ activeColumnsCount, activeDragRowID, activeDropTargetID, dropTarget, dropValidation, isMovePending }) {
168
500
  const { isOver, setNodeRef } = useDroppable({
169
501
  id: dropTarget.dropID,
170
502
  data: dropTarget,
171
503
  disabled: isMovePending
172
504
  });
173
505
  const hasActiveDrag = Boolean(activeDragRowID);
506
+ const showInsertLine = isOver && activeDropTargetID === dropTarget.dropID;
174
507
  return /*#__PURE__*/ _jsx("tr", {
175
508
  className: "pages-hierarchy-insert-row",
176
- "data-drag-over": isOver ? 'true' : 'false',
509
+ "data-drag-over": showInsertLine ? 'true' : 'false',
177
510
  "data-drop-valid": hasActiveDrag ? dropValidation?.isValid ? 'true' : 'false' : undefined,
178
511
  "data-page-tree-insert": "true",
179
512
  children: /*#__PURE__*/ _jsx("td", {
@@ -186,7 +519,7 @@ function HierarchyInsertRow({ activeColumnsCount, activeDragRowID, dropTarget, d
186
519
  })
187
520
  });
188
521
  }
189
- function HierarchyTableRow({ activeColumns, activeDragRowID, doc, dropValidation, insertAfterDropID, insertBeforeDropID, isMovePending, rowIndex, titleCellAccessor }) {
522
+ function HierarchyTableRow({ activeColumns, activeDragRowID, activeDragType, canReorderRows, doc, dropValidation, insertAfterDropID, insertBeforeDropID, isMovePending, rowIndex, titleCellAccessor }) {
190
523
  const { isOver, setNodeRef } = useDroppable({
191
524
  id: getRowDropID(doc.__pageTreeID),
192
525
  data: {
@@ -197,51 +530,110 @@ function HierarchyTableRow({ activeColumns, activeDragRowID, doc, dropValidation
197
530
  },
198
531
  disabled: isMovePending
199
532
  });
200
- const hasActiveDrag = Boolean(activeDragRowID);
533
+ const sortable = useSortable({
534
+ id: getSortableRowID(doc.__pageTreeID),
535
+ data: {
536
+ dragType: 'order',
537
+ parentID: doc.__pageTreeParentID,
538
+ rowID: doc.__pageTreeID
539
+ },
540
+ disabled: !canReorderRows || isMovePending || activeDragType === 'move'
541
+ });
542
+ const hasActiveMoveDrag = activeDragType === 'move' && Boolean(activeDragRowID);
201
543
  const isActiveDragRow = activeDragRowID === doc.__pageTreeID;
544
+ const setRowRef = React.useCallback((element)=>{
545
+ setNodeRef(element);
546
+ sortable.setNodeRef(element);
547
+ }, [
548
+ setNodeRef,
549
+ sortable
550
+ ]);
551
+ const rowDndValue = React.useMemo(()=>({
552
+ isOrderDragging: sortable.isDragging,
553
+ orderHandleAttributes: sortable.attributes,
554
+ orderHandleListeners: sortable.listeners,
555
+ orderHandleRef: sortable.setActivatorNodeRef
556
+ }), [
557
+ sortable.attributes,
558
+ sortable.isDragging,
559
+ sortable.listeners,
560
+ sortable.setActivatorNodeRef
561
+ ]);
562
+ const sortableStyle = React.useMemo(()=>({
563
+ position: sortable.isDragging ? 'relative' : undefined,
564
+ transform: getSortableTransform(sortable.transform),
565
+ transition: activeDragType === 'order' ? sortable.transition : undefined,
566
+ zIndex: sortable.isDragging ? 1 : undefined
567
+ }), [
568
+ activeDragType,
569
+ sortable.isDragging,
570
+ sortable.transform,
571
+ sortable.transition
572
+ ]);
202
573
  return /*#__PURE__*/ _jsx("tr", {
203
574
  className: `row-${rowIndex + 1}`,
204
- "data-drag-over": isOver ? 'true' : 'false',
205
- "data-drop-valid": hasActiveDrag ? dropValidation?.isValid ? 'true' : 'false' : undefined,
575
+ "data-drag-over": activeDragType === 'move' && isOver ? 'true' : 'false',
576
+ "data-drop-valid": hasActiveMoveDrag ? dropValidation?.isValid ? 'true' : 'false' : undefined,
206
577
  "data-id": doc.id,
207
- "data-is-drag-source": isActiveDragRow ? 'true' : 'false',
578
+ "data-is-drag-source": activeDragType === 'move' && isActiveDragRow ? 'true' : 'false',
579
+ "data-is-order-drag-source": sortable.isDragging ? 'true' : 'false',
208
580
  "data-page-tree-row": "true",
209
- ref: setNodeRef,
210
- children: activeColumns.map((column, columnIndex)=>{
211
- const { accessor } = column;
212
- return /*#__PURE__*/ _jsx("td", {
213
- className: `cell-${accessor.replace(/\./g, '__')}`,
214
- "data-page-tree-manual-order-cell": accessor === MANUAL_ORDER_COLUMN_ACCESSOR ? 'true' : undefined,
215
- "data-page-tree-title-cell": accessor === titleCellAccessor ? 'true' : undefined,
216
- children: column.renderedCells?.[rowIndex] ?? null
217
- }, columnIndex);
581
+ ref: setRowRef,
582
+ style: sortableStyle,
583
+ children: /*#__PURE__*/ _jsx(PageTreeRowDndProvider, {
584
+ value: rowDndValue,
585
+ children: activeColumns.map((column, columnIndex)=>{
586
+ const { accessor } = column;
587
+ return /*#__PURE__*/ _jsx("td", {
588
+ className: `cell-${accessor.replace(/\./g, '__')}`,
589
+ "data-page-tree-title-cell": accessor === titleCellAccessor ? 'true' : undefined,
590
+ children: column.renderedCells?.[rowIndex] ?? null
591
+ }, columnIndex);
592
+ })
218
593
  })
219
594
  });
220
595
  }
221
- function HierarchyTable({ activeDragRowID, allDocsByID, columns, data, isMovePending, titleCellAccessor }) {
596
+ function HierarchyTable({ activeDragRowID, activeDragType, activeDropTarget, allDocsByID, canReorderRows, columns, data, isMovePending, titleCellAccessor }) {
222
597
  const activeColumns = React.useMemo(()=>columns.filter((column)=>column?.active), [
223
598
  columns
224
599
  ]);
225
- const hasManualOrderColumn = React.useMemo(()=>activeColumns.some((column)=>column.accessor === MANUAL_ORDER_COLUMN_ACCESSOR), [
226
- activeColumns
600
+ const sortableItems = React.useMemo(()=>data.map((doc)=>getSortableRowID(doc.__pageTreeID)), [
601
+ data
602
+ ]);
603
+ const sortingStrategy = React.useMemo(()=>buildPageTreeSortingStrategy(data), [
604
+ data
227
605
  ]);
228
606
  const insertDropTargets = React.useMemo(()=>buildInsertDropTargets(data), [
229
607
  data
230
608
  ]);
231
- const activeDoc = activeDragRowID ? allDocsByID.get(activeDragRowID) ?? null : null;
609
+ const insertDropTargetsByReferenceRowID = React.useMemo(()=>new Map(insertDropTargets.map((dropTarget)=>[
610
+ dropTarget.referenceRowID,
611
+ dropTarget
612
+ ])), [
613
+ insertDropTargets
614
+ ]);
615
+ const activeDropTargetID = activeDropTarget?.dropType === 'insert' ? activeDropTarget.dropID : null;
616
+ const activeDoc = activeDragType === 'move' && activeDragRowID ? allDocsByID.get(activeDragRowID) ?? null : null;
232
617
  const rowDropValidationByID = React.useMemo(()=>{
233
618
  if (!activeDoc) {
234
619
  return new Map();
235
620
  }
236
621
  return new Map(data.map((doc)=>[
237
622
  doc.__pageTreeID,
238
- getDropValidation({
623
+ getDropTargetValidation({
239
624
  activeDoc,
240
- targetDoc: doc
625
+ docsByID: allDocsByID,
626
+ dropTarget: {
627
+ dropType: 'row',
628
+ insertAfterDropID: null,
629
+ insertBeforeDropID: null,
630
+ rowID: doc.__pageTreeID
631
+ }
241
632
  })
242
633
  ]));
243
634
  }, [
244
635
  activeDoc,
636
+ allDocsByID,
245
637
  data
246
638
  ]);
247
639
  const insertDropValidationByID = React.useMemo(()=>{
@@ -250,9 +642,10 @@ function HierarchyTable({ activeDragRowID, allDocsByID, columns, data, isMovePen
250
642
  }
251
643
  return new Map(insertDropTargets.map((dropTarget)=>[
252
644
  dropTarget.dropID,
253
- getDropValidation({
645
+ getDropTargetValidation({
254
646
  activeDoc,
255
- targetDoc: dropTarget.parentID === null ? undefined : allDocsByID.get(dropTarget.parentID) ?? undefined
647
+ docsByID: allDocsByID,
648
+ dropTarget
256
649
  })
257
650
  ]));
258
651
  }, [
@@ -270,7 +663,6 @@ function HierarchyTable({ activeDragRowID, allDocsByID, columns, data, isMovePen
270
663
  'table-wrap pages-hierarchy-table',
271
664
  activeDoc ? 'pages-hierarchy-table--dragging' : ''
272
665
  ].filter(Boolean).join(' '),
273
- "data-page-tree-orderable": hasManualOrderColumn ? 'true' : undefined,
274
666
  children: /*#__PURE__*/ _jsx("div", {
275
667
  className: "table table--appearance-default",
276
668
  children: /*#__PURE__*/ _jsxs("table", {
@@ -280,66 +672,72 @@ function HierarchyTable({ activeDragRowID, allDocsByID, columns, data, isMovePen
280
672
  /*#__PURE__*/ _jsx("thead", {
281
673
  children: /*#__PURE__*/ _jsx("tr", {
282
674
  children: activeColumns.map((column, index)=>/*#__PURE__*/ _jsx("th", {
283
- "data-page-tree-manual-order-cell": column.accessor === MANUAL_ORDER_COLUMN_ACCESSOR ? 'true' : undefined,
284
675
  "data-page-tree-title-cell": column.accessor === titleCellAccessor ? 'true' : undefined,
285
676
  id: `heading-${column.accessor.replace(/\./g, '__')}`,
286
677
  children: column.Heading
287
678
  }, index))
288
679
  })
289
680
  }),
290
- /*#__PURE__*/ _jsxs("tbody", {
291
- children: [
292
- insertDropTargets[0] ? /*#__PURE__*/ _jsx(HierarchyInsertRow, {
293
- activeColumnsCount: activeColumns.length,
294
- activeDragRowID: activeDragRowID,
295
- dropTarget: insertDropTargets[0],
296
- dropValidation: insertDropValidationByID.get(insertDropTargets[0].dropID),
297
- isMovePending: isMovePending
298
- }, insertDropTargets[0].dropID) : null,
299
- data.map((doc, rowIndex)=>/*#__PURE__*/ _jsxs(React.Fragment, {
681
+ /*#__PURE__*/ _jsx("tbody", {
682
+ children: /*#__PURE__*/ _jsx(SortableContext, {
683
+ items: sortableItems,
684
+ strategy: sortingStrategy,
685
+ children: data.map((doc, rowIndex)=>{
686
+ const rootInsertBeforeDropTarget = doc.__pageTreeParentID === null ? insertDropTargetsByReferenceRowID.get(doc.__pageTreeID) : undefined;
687
+ const rootInsertTargetIndex = rootInsertBeforeDropTarget !== undefined ? insertDropTargets.indexOf(rootInsertBeforeDropTarget) : -1;
688
+ const rootInsertAfterDropTarget = rootInsertTargetIndex >= 0 ? insertDropTargets[rootInsertTargetIndex + 1] : undefined;
689
+ const isTrailingRootInsert = rootInsertAfterDropTarget === insertDropTargets[insertDropTargets.length - 1];
690
+ const nextVisibleDoc = data[rowIndex + 1];
691
+ const hasVisibleDescendantAfter = nextVisibleDoc?.__pageTreeAncestorIDs.includes(doc.__pageTreeID) ?? false;
692
+ return /*#__PURE__*/ _jsxs(React.Fragment, {
300
693
  children: [
694
+ rootInsertBeforeDropTarget ? /*#__PURE__*/ _jsx(HierarchyInsertRow, {
695
+ activeColumnsCount: activeColumns.length,
696
+ activeDragRowID: activeDoc ? activeDragRowID : null,
697
+ activeDropTargetID: activeDropTargetID,
698
+ dropTarget: rootInsertBeforeDropTarget,
699
+ dropValidation: insertDropValidationByID.get(rootInsertBeforeDropTarget.dropID),
700
+ isMovePending: isMovePending
701
+ }, rootInsertBeforeDropTarget.dropID) : null,
301
702
  /*#__PURE__*/ _jsx(HierarchyTableRow, {
302
703
  activeColumns: activeColumns,
303
704
  activeDragRowID: activeDragRowID,
705
+ activeDragType: activeDragType,
706
+ canReorderRows: canReorderRows,
304
707
  doc: doc,
305
708
  dropValidation: rowDropValidationByID.get(doc.__pageTreeID),
306
- insertAfterDropID: insertDropTargets[rowIndex + 1].dropID,
307
- insertBeforeDropID: insertDropTargets[rowIndex].dropID,
709
+ insertAfterDropID: hasVisibleDescendantAfter || isTrailingRootInsert ? null : rootInsertAfterDropTarget?.dropID ?? null,
710
+ insertBeforeDropID: rootInsertBeforeDropTarget?.dropID ?? null,
308
711
  isMovePending: isMovePending,
309
712
  rowIndex: rowIndex,
310
713
  titleCellAccessor: titleCellAccessor
311
- }),
312
- insertDropTargets[rowIndex + 1] ? /*#__PURE__*/ _jsx(HierarchyInsertRow, {
313
- activeColumnsCount: activeColumns.length,
314
- activeDragRowID: activeDragRowID,
315
- dropTarget: insertDropTargets[rowIndex + 1],
316
- dropValidation: insertDropValidationByID.get(insertDropTargets[rowIndex + 1].dropID),
317
- isMovePending: isMovePending
318
- }, insertDropTargets[rowIndex + 1].dropID) : null
714
+ })
319
715
  ]
320
- }, doc.__pageTreeID))
321
- ]
716
+ }, doc.__pageTreeID);
717
+ })
718
+ })
322
719
  })
323
720
  ]
324
721
  })
325
722
  })
326
723
  });
327
724
  }
328
- export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDocs, columnState, homeIndicatorEnabled, orderableFieldName: _orderableFieldName, parentFieldSlug, query, sourceDocs, useAsTitle, ...props }) {
725
+ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDocs, columnState, homeIndicatorEnabled, orderableFieldName, parentFieldSlug, query, sourceDocs, useAsTitle, ...props }) {
329
726
  const router = useRouter();
330
727
  const searchParams = useSearchParams();
331
728
  const { config } = useConfig();
332
729
  const locale = useLocale();
333
730
  const { i18n } = useTranslation();
334
731
  const [activeDragRowID, setActiveDragRowID] = React.useState(null);
732
+ const [activeDragType, setActiveDragType] = React.useState(null);
335
733
  const [activeDropTarget, setActiveDropTarget] = React.useState(null);
336
734
  const [collapsedIDs, setCollapsedIDs] = React.useState(()=>new Set());
337
- const [localSourceDocs, setLocalSourceDocs] = React.useState(sourceDocs);
735
+ const [optimisticOrderRowIDs, setOptimisticOrderRowIDs] = React.useState(null);
736
+ const [parentMoveEnabled, setParentMoveEnabled] = React.useState(false);
338
737
  const [pendingMoveRowID, setPendingMoveRowID] = React.useState(null);
339
- React.useEffect(()=>{
340
- setLocalSourceDocs(sourceDocs);
341
- }, [
342
- sourceDocs
738
+ const { getPreference, setPreference } = usePreferences();
739
+ const parentMovePreferenceKey = React.useMemo(()=>getParentMovePreferenceKey(props.collectionSlug), [
740
+ props.collectionSlug
343
741
  ]);
344
742
  const toggleRow = React.useCallback((rowID)=>{
345
743
  setCollapsedIDs((currentState)=>{
@@ -352,6 +750,16 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
352
750
  return nextState;
353
751
  });
354
752
  }, []);
753
+ const handleParentMoveToggle = React.useCallback(()=>{
754
+ setParentMoveEnabled((currentValue)=>{
755
+ const nextValue = !currentValue;
756
+ void setPreference(parentMovePreferenceKey, nextValue);
757
+ return nextValue;
758
+ });
759
+ }, [
760
+ parentMovePreferenceKey,
761
+ setPreference
762
+ ]);
355
763
  const currentSort = React.useMemo(()=>{
356
764
  const searchParamSort = searchParams.getAll('sort');
357
765
  if (searchParamSort.length > 0) {
@@ -362,6 +770,10 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
362
770
  query.sort,
363
771
  searchParams
364
772
  ]);
773
+ const canReorderRows = canMoveDocs && isOrderableSortActive({
774
+ orderableFieldName,
775
+ sort: currentSort
776
+ });
365
777
  const currentLimit = React.useMemo(()=>normalizePositiveInt(searchParams.get('limit'), typeof query.limit === 'number' && query.limit > 0 ? query.limit : 10), [
366
778
  query.limit,
367
779
  searchParams
@@ -370,13 +782,13 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
370
782
  query.page,
371
783
  searchParams
372
784
  ]);
373
- const liveAllDocs = React.useMemo(()=>buildPageTreeDocs(localSourceDocs, {
785
+ const liveAllDocs = React.useMemo(()=>buildPageTreeDocs(sourceDocs, {
374
786
  parentFieldSlug,
375
787
  sort: currentSort
376
788
  }), [
377
789
  currentSort,
378
- localSourceDocs,
379
- parentFieldSlug
790
+ parentFieldSlug,
791
+ sourceDocs
380
792
  ]);
381
793
  const collapseResetKey = React.useMemo(()=>JSON.stringify([
382
794
  props.collectionSlug,
@@ -391,19 +803,37 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
391
803
  activeDragRowID,
392
804
  canMoveDocs,
393
805
  collapsedIDs,
806
+ parentMoveEnabled,
394
807
  pendingMoveRowID,
395
808
  toggleRow
396
809
  }), [
397
810
  activeDragRowID,
398
811
  canMoveDocs,
399
812
  collapsedIDs,
813
+ parentMoveEnabled,
400
814
  pendingMoveRowID,
401
815
  toggleRow
402
816
  ]);
817
+ React.useEffect(()=>{
818
+ let isMounted = true;
819
+ void getPreference(parentMovePreferenceKey).then((storedValue)=>{
820
+ if (isMounted) {
821
+ setParentMoveEnabled(storedValue === true);
822
+ }
823
+ });
824
+ return ()=>{
825
+ isMounted = false;
826
+ };
827
+ }, [
828
+ getPreference,
829
+ parentMovePreferenceKey
830
+ ]);
403
831
  React.useEffect(()=>{
404
832
  setActiveDragRowID(null);
833
+ setActiveDragType(null);
405
834
  setActiveDropTarget(null);
406
835
  setCollapsedIDs(new Set());
836
+ setOptimisticOrderRowIDs(null);
407
837
  }, [
408
838
  collapseResetKey
409
839
  ]);
@@ -417,17 +847,36 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
417
847
  visibleDocs
418
848
  ]);
419
849
  const paginatedDocs = paginatedData.docs;
850
+ const paginatedServerOrderKey = React.useMemo(()=>paginatedDocs.map((doc)=>doc.__pageTreeID).join('\0'), [
851
+ paginatedDocs
852
+ ]);
853
+ const displayedPaginatedDocs = React.useMemo(()=>orderDocsByRowIDs(paginatedDocs, optimisticOrderRowIDs), [
854
+ optimisticOrderRowIDs,
855
+ paginatedDocs
856
+ ]);
857
+ const displayedPaginatedData = React.useMemo(()=>({
858
+ ...paginatedData,
859
+ docs: displayedPaginatedDocs
860
+ }), [
861
+ displayedPaginatedDocs,
862
+ paginatedData
863
+ ]);
864
+ React.useEffect(()=>{
865
+ setOptimisticOrderRowIDs(null);
866
+ }, [
867
+ paginatedServerOrderKey
868
+ ]);
420
869
  const allDocsByID = React.useMemo(()=>new Map(liveAllDocs.map((doc)=>[
421
870
  doc.__pageTreeID,
422
871
  doc
423
872
  ])), [
424
873
  liveAllDocs
425
874
  ]);
426
- const paginatedDocsByID = React.useMemo(()=>new Map(paginatedDocs.map((doc)=>[
875
+ const paginatedDocsByID = React.useMemo(()=>new Map(displayedPaginatedDocs.map((doc)=>[
427
876
  doc.__pageTreeID,
428
877
  doc
429
878
  ])), [
430
- paginatedDocs
879
+ displayedPaginatedDocs
431
880
  ]);
432
881
  const renderedCellIndexByDocID = React.useMemo(()=>new Map(allDocs.map((doc, index)=>[
433
882
  doc.__pageTreeID,
@@ -435,29 +884,31 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
435
884
  ])), [
436
885
  allDocs
437
886
  ]);
438
- const normalizedColumnState = React.useMemo(()=>normalizePageTreeColumnState(columnState, useAsTitle), [
887
+ const normalizedColumnState = React.useMemo(()=>ensureUseAsTitleColumn(columnState, useAsTitle), [
439
888
  columnState,
440
889
  useAsTitle
441
890
  ]);
442
- const paginatedColumnState = React.useMemo(()=>sliceColumnState(normalizedColumnState, paginatedDocs, renderedCellIndexByDocID), [
891
+ const paginatedColumnState = React.useMemo(()=>sliceColumnState(normalizedColumnState, displayedPaginatedDocs, renderedCellIndexByDocID), [
892
+ displayedPaginatedDocs,
443
893
  normalizedColumnState,
444
- paginatedDocs,
445
894
  renderedCellIndexByDocID
446
895
  ]);
447
896
  const tableColumns = React.useMemo(()=>buildTableColumns({
448
897
  badgeConfig,
449
898
  columnState: paginatedColumnState,
450
- docs: paginatedDocs,
899
+ docs: displayedPaginatedDocs,
451
900
  enableRowSelections: props.enableRowSelections,
452
901
  homeIndicatorEnabled,
902
+ orderableFieldName,
453
903
  parentFieldSlug,
454
904
  t: i18n.t,
455
905
  useAsTitle
456
906
  }), [
457
907
  paginatedColumnState,
458
- paginatedDocs,
908
+ displayedPaginatedDocs,
459
909
  badgeConfig,
460
910
  homeIndicatorEnabled,
911
+ orderableFieldName,
461
912
  parentFieldSlug,
462
913
  props.enableRowSelections,
463
914
  i18n.t,
@@ -465,7 +916,7 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
465
916
  ]);
466
917
  const activeDragDoc = activeDragRowID ? paginatedDocsByID.get(activeDragRowID) ?? null : null;
467
918
  const activeDragPreviewPath = React.useMemo(()=>{
468
- if (!activeDragDoc) {
919
+ if (!activeDragDoc || activeDragType !== 'move') {
469
920
  return null;
470
921
  }
471
922
  if (!activeDropTarget) {
@@ -478,9 +929,10 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
478
929
  docsByID: allDocsByID,
479
930
  dropTarget: activeDropTarget
480
931
  });
481
- const dropValidation = getDropValidation({
932
+ const dropValidation = getDropTargetValidation({
482
933
  activeDoc: activeDragDoc,
483
- targetDoc: targetDoc ?? undefined
934
+ docsByID: allDocsByID,
935
+ dropTarget: activeDropTarget
484
936
  });
485
937
  if (!dropValidation.isValid) {
486
938
  return buildDocSlugPath({
@@ -495,66 +947,46 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
495
947
  });
496
948
  }, [
497
949
  activeDragDoc,
950
+ activeDragType,
498
951
  activeDropTarget,
499
952
  allDocsByID
500
953
  ]);
501
954
  const isMovePending = pendingMoveRowID !== null;
502
955
  const sensors = useSensors(useSensor(PointerSensor, {
503
956
  activationConstraint: {
504
- distance: 4
957
+ distance: 2
505
958
  }
506
959
  }));
507
- const moveDocument = React.useCallback(async (args)=>{
508
- const { parentID, rowID } = args;
509
- const params = new URLSearchParams();
510
- if (locale?.code) {
511
- params.set('locale', locale.code);
512
- }
513
- const response = await fetch(`${config.routes.api}/${props.collectionSlug}/${encodeURIComponent(rowID)}/move${params.size > 0 ? `?${params.toString()}` : ''}`, {
514
- body: JSON.stringify({
515
- parentID
516
- }),
517
- credentials: 'include',
518
- headers: {
519
- 'Accept-Language': i18n.language,
520
- 'Content-Type': 'application/json',
521
- [pageTreeMoveRequestHeader]: pageTreeMoveRequestHeaderValue
522
- },
523
- method: 'POST'
524
- });
525
- const result = await response.json().catch(()=>null);
526
- if (!response.ok) {
527
- throw new Error(result?.message ?? 'Could not move document.');
528
- }
529
- return result;
530
- }, [
531
- config.routes.api,
532
- i18n.language,
533
- locale?.code,
534
- props.collectionSlug
535
- ]);
536
960
  const handleDragCancel = React.useCallback(()=>{
537
961
  setActiveDragRowID(null);
962
+ setActiveDragType(null);
538
963
  setActiveDropTarget(null);
539
964
  }, []);
540
965
  const handleDragStart = React.useCallback((event)=>{
541
- const dragData = getPageTreeDragData(event.active.data.current);
542
966
  if (!canMoveDocs || isMovePending) {
543
967
  return;
544
968
  }
545
- const rowID = dragData.rowID;
546
- if (typeof rowID === 'string' && paginatedDocsByID.has(rowID)) {
969
+ const dragData = getPageTreeDragData(event.active.data.current);
970
+ const { dragType, rowID } = dragData;
971
+ if (typeof rowID === 'string' && dragType && paginatedDocsByID.has(rowID) && (dragType !== 'order' || canReorderRows)) {
547
972
  setActiveDragRowID(rowID);
973
+ setActiveDragType(dragType);
548
974
  setActiveDropTarget(null);
549
975
  }
550
976
  }, [
551
977
  canMoveDocs,
978
+ canReorderRows,
552
979
  isMovePending,
553
980
  paginatedDocsByID
554
981
  ]);
555
982
  const handleDragOver = React.useCallback((event)=>{
983
+ const dragData = getPageTreeDragData(event.active.data.current);
556
984
  const overData = event.over?.data.current;
557
- if (!overData) {
985
+ if (dragData.dragType === 'order') {
986
+ setActiveDropTarget(null);
987
+ return;
988
+ }
989
+ if (dragData.dragType !== 'move' || !overData) {
558
990
  setActiveDropTarget(null);
559
991
  return;
560
992
  }
@@ -570,51 +1002,132 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
570
1002
  }, []);
571
1003
  const handleDragEnd = React.useCallback(async (event)=>{
572
1004
  const dragData = getPageTreeDragData(event.active.data.current);
573
- const rowID = dragData.rowID;
1005
+ const { dragType, rowID } = dragData;
574
1006
  const activeDoc = typeof rowID === 'string' ? paginatedDocsByID.get(rowID) ?? null : null;
575
1007
  const overData = event.over?.data.current;
1008
+ const overDragData = getPageTreeDragData(event.over?.data.current);
576
1009
  setActiveDragRowID(null);
1010
+ setActiveDragType(null);
577
1011
  setActiveDropTarget(null);
578
- if (!rowID || !activeDoc || !overData) {
1012
+ if (dragType === 'order') {
1013
+ if (typeof rowID === 'string' && typeof overDragData.rowID === 'string') {
1014
+ const nextOrderRowIDs = buildOptimisticOrderRowIDs({
1015
+ activeRowID: rowID,
1016
+ docs: displayedPaginatedDocs,
1017
+ targetRowID: overDragData.rowID
1018
+ });
1019
+ const reorderRequestBody = buildReorderRequestBody({
1020
+ activeRowID: rowID,
1021
+ docs: displayedPaginatedDocs,
1022
+ orderableFieldName,
1023
+ sort: currentSort,
1024
+ targetRowID: overDragData.rowID
1025
+ });
1026
+ if (!nextOrderRowIDs || !reorderRequestBody) {
1027
+ return;
1028
+ }
1029
+ const apiRoute = config.routes.api;
1030
+ const params = new URLSearchParams();
1031
+ if (locale?.code) {
1032
+ params.set('locale', locale.code);
1033
+ }
1034
+ setOptimisticOrderRowIDs(nextOrderRowIDs);
1035
+ setPendingMoveRowID(rowID);
1036
+ try {
1037
+ const response = await fetch(`${apiRoute}/${props.collectionSlug}/${encodeURIComponent(rowID)}/reorder${params.size > 0 ? `?${params.toString()}` : ''}`, {
1038
+ body: JSON.stringify(reorderRequestBody),
1039
+ credentials: 'include',
1040
+ headers: {
1041
+ 'Accept-Language': i18n.language,
1042
+ 'Content-Type': 'application/json'
1043
+ },
1044
+ method: 'POST'
1045
+ });
1046
+ const result = await response.json().catch(()=>null);
1047
+ if (!response.ok) {
1048
+ setOptimisticOrderRowIDs(null);
1049
+ toast.error(result?.message ?? 'Could not reorder document.');
1050
+ return;
1051
+ }
1052
+ if (result?.message === 'initial migration') {
1053
+ setOptimisticOrderRowIDs(null);
1054
+ }
1055
+ React.startTransition(()=>{
1056
+ router.refresh();
1057
+ });
1058
+ } catch (error) {
1059
+ const message = error instanceof Error && error.message ? error.message : 'Could not reorder document.';
1060
+ setOptimisticOrderRowIDs(null);
1061
+ toast.error(message);
1062
+ } finally{
1063
+ setPendingMoveRowID(null);
1064
+ }
1065
+ }
579
1066
  return;
580
1067
  }
581
- const targetDoc = getDropTargetParentDoc({
1068
+ if (dragType !== 'move' || typeof rowID !== 'string' || !activeDoc || !overData) {
1069
+ return;
1070
+ }
1071
+ const dropValidation = getDropTargetValidation({
1072
+ activeDoc,
582
1073
  docsByID: allDocsByID,
583
1074
  dropTarget: overData
584
1075
  });
585
- const dropValidation = getDropValidation({
586
- activeDoc,
587
- targetDoc: targetDoc ?? undefined
588
- });
589
1076
  if (!dropValidation.isValid) {
590
1077
  if (!shouldSilenceMoveMessage(dropValidation.message)) {
591
1078
  toast.error(dropValidation.message ?? 'Could not move document.');
592
1079
  }
593
1080
  return;
594
1081
  }
1082
+ if (overData.dropType === 'insert' && activeDoc.__pageTreeParentID === dropValidation.parentID) {
1083
+ return;
1084
+ }
1085
+ const apiRoute = config.routes.api;
1086
+ const params = new URLSearchParams();
1087
+ if (locale?.code) {
1088
+ params.set('locale', locale.code);
1089
+ }
595
1090
  setPendingMoveRowID(rowID);
596
1091
  try {
597
- await moveDocument({
598
- parentID: dropValidation.parentID,
599
- rowID
1092
+ const response = await fetch(`${apiRoute}/${props.collectionSlug}/${encodeURIComponent(rowID)}/move${params.size > 0 ? `?${params.toString()}` : ''}`, {
1093
+ body: JSON.stringify({
1094
+ parentID: dropValidation.parentID
1095
+ }),
1096
+ credentials: 'include',
1097
+ headers: {
1098
+ 'Accept-Language': i18n.language,
1099
+ 'Content-Type': 'application/json'
1100
+ },
1101
+ method: 'POST'
600
1102
  });
1103
+ const result = await response.json().catch(()=>null);
1104
+ if (!response.ok) {
1105
+ if (shouldSilenceMoveMessage(result?.message)) {
1106
+ return;
1107
+ }
1108
+ toast.error(result?.message ?? 'Could not move document.');
1109
+ return;
1110
+ }
601
1111
  toast.success(`Moved "${getDocDisplayLabel(activeDoc)}".`);
602
1112
  React.startTransition(()=>{
603
1113
  router.refresh();
604
1114
  });
605
1115
  } catch (error) {
606
1116
  const message = error instanceof Error && error.message ? error.message : 'Could not move document.';
607
- if (shouldSilenceMoveMessage(message)) {
608
- return;
609
- }
610
1117
  toast.error(message);
611
1118
  } finally{
612
1119
  setPendingMoveRowID(null);
613
1120
  }
614
1121
  }, [
615
1122
  allDocsByID,
616
- moveDocument,
1123
+ config.routes.api,
1124
+ currentSort,
1125
+ displayedPaginatedDocs,
1126
+ i18n.language,
1127
+ locale?.code,
1128
+ orderableFieldName,
617
1129
  paginatedDocsByID,
1130
+ props.collectionSlug,
618
1131
  router
619
1132
  ]);
620
1133
  const handleDragEndSync = React.useCallback((event)=>{
@@ -632,9 +1145,12 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
632
1145
  children: [
633
1146
  /*#__PURE__*/ _jsx(HierarchyTable, {
634
1147
  activeDragRowID: activeDragRowID,
1148
+ activeDragType: activeDragType,
1149
+ activeDropTarget: activeDropTarget,
635
1150
  allDocsByID: allDocsByID,
1151
+ canReorderRows: canReorderRows,
636
1152
  columns: tableColumns,
637
- data: paginatedDocs,
1153
+ data: displayedPaginatedDocs,
638
1154
  isMovePending: isMovePending,
639
1155
  titleCellAccessor: useAsTitle
640
1156
  }),
@@ -652,33 +1168,57 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
652
1168
  }), [
653
1169
  activeDragPreviewPath,
654
1170
  activeDragRowID,
1171
+ activeDragType,
1172
+ activeDropTarget,
1173
+ allDocsByID,
1174
+ canReorderRows,
1175
+ displayedPaginatedDocs,
655
1176
  handleDragCancel,
656
1177
  handleDragEndSync,
657
1178
  handleDragOver,
658
1179
  handleDragStart,
659
1180
  isMovePending,
660
- allDocsByID,
661
- paginatedDocs,
662
1181
  sensors,
663
1182
  tableColumns,
664
1183
  useAsTitle
665
1184
  ]);
1185
+ const beforeActions = React.useMemo(()=>{
1186
+ const actions = props.beforeActions ? [
1187
+ ...props.beforeActions
1188
+ ] : [];
1189
+ if (canMoveDocs) {
1190
+ actions.unshift(/*#__PURE__*/ _jsx(ParentMoveToggle, {
1191
+ collectionSlug: props.collectionSlug,
1192
+ enabled: parentMoveEnabled,
1193
+ onToggle: handleParentMoveToggle
1194
+ }, "parent-move-toggle"));
1195
+ }
1196
+ return actions.length > 0 ? actions : undefined;
1197
+ }, [
1198
+ canMoveDocs,
1199
+ handleParentMoveToggle,
1200
+ parentMoveEnabled,
1201
+ props.beforeActions,
1202
+ props.collectionSlug
1203
+ ]);
666
1204
  return /*#__PURE__*/ _jsx("div", {
667
1205
  className: styles.root,
668
1206
  children: /*#__PURE__*/ _jsx(PageTreeProvider, {
669
1207
  value: hierarchyValue,
670
1208
  children: /*#__PURE__*/ _jsx(ListQueryProvider, {
671
1209
  collectionSlug: props.collectionSlug,
672
- data: paginatedData,
1210
+ data: displayedPaginatedData,
673
1211
  modifySearchParams: true,
1212
+ orderableFieldName: orderableFieldName,
674
1213
  query: {
675
1214
  ...query,
676
1215
  limit: currentLimit,
677
- page: paginatedData.page,
1216
+ page: displayedPaginatedData.page,
678
1217
  sort: currentSort
679
1218
  },
680
1219
  children: /*#__PURE__*/ _jsx(DefaultListView, {
681
1220
  ...props,
1221
+ beforeActions: beforeActions,
682
1222
  columnState: paginatedColumnState,
683
1223
  Table: tableNode
684
1224
  })