payload-nested-docs-page-tree 1.2.3 → 1.4.0

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 (46) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +136 -47
  3. package/dist/components/PageTreeContext.d.ts +13 -1
  4. package/dist/components/PageTreeContext.js +11 -1
  5. package/dist/components/PageTreeContext.js.map +1 -1
  6. package/dist/components/PageTreeListView.client.d.ts +3 -4
  7. package/dist/components/PageTreeListView.client.js +645 -381
  8. package/dist/components/PageTreeListView.client.js.map +1 -1
  9. package/dist/components/PageTreeListView.module.css +90 -67
  10. package/dist/components/PageTreeListView.server.js +18 -28
  11. package/dist/components/PageTreeListView.server.js.map +1 -1
  12. package/dist/components/PageTreeTitleCell.js +80 -18
  13. package/dist/components/PageTreeTitleCell.js.map +1 -1
  14. package/dist/endpoints/createMovePageEndpoint.d.ts +2 -0
  15. package/dist/endpoints/createMovePageEndpoint.js +166 -123
  16. package/dist/endpoints/createMovePageEndpoint.js.map +1 -1
  17. package/dist/endpoints/createReorderPageEndpoint.d.ts +8 -0
  18. package/dist/endpoints/createReorderPageEndpoint.js +703 -0
  19. package/dist/endpoints/createReorderPageEndpoint.js.map +1 -0
  20. package/dist/endpoints/shared.d.ts +40 -0
  21. package/dist/endpoints/shared.js +93 -0
  22. package/dist/endpoints/shared.js.map +1 -0
  23. package/dist/exports/client.js.map +1 -1
  24. package/dist/exports/rsc.js.map +1 -1
  25. package/dist/index.js +105 -26
  26. package/dist/index.js.map +1 -1
  27. package/dist/types.d.ts +9 -2
  28. package/dist/types.js +0 -2
  29. package/dist/types.js.map +1 -1
  30. package/dist/utilities/badgeConfig.js.map +1 -1
  31. package/dist/utilities/diagnostics.d.ts +42 -0
  32. package/dist/utilities/diagnostics.js +83 -0
  33. package/dist/utilities/diagnostics.js.map +1 -0
  34. package/dist/utilities/dropTargets.d.ts +2 -2
  35. package/dist/utilities/dropTargets.js +9 -7
  36. package/dist/utilities/dropTargets.js.map +1 -1
  37. package/dist/utilities/pageTree.js +4 -6
  38. package/dist/utilities/pageTree.js.map +1 -1
  39. package/dist/utilities/pageTreeCollision.d.ts +6 -0
  40. package/dist/utilities/pageTreeCollision.js +39 -4
  41. package/dist/utilities/pageTreeCollision.js.map +1 -1
  42. package/dist/utilities/pageTreeConfig.js.map +1 -1
  43. package/dist/utilities/status.d.ts +1 -1
  44. package/dist/utilities/status.js +7 -3
  45. package/dist/utilities/status.js.map +1 -1
  46. package/package.json +13 -12
@@ -1,105 +1,141 @@
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, SortHeader, 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
- import { buildInsertDropTargets, getDropTargetParentDoc, getOrderPlacementFromDropTarget } from '../utilities/dropTargets.js';
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
  }
23
- function getPageTreeDragData(value) {
24
- return value && typeof value === 'object' ? value : {};
26
+ function getSortableRowID(rowID) {
27
+ return `page-sort:${rowID}`;
25
28
  }
26
- function getPayloadDocID(doc) {
27
- return typeof doc.id === 'number' || typeof doc.id === 'string' ? doc.id : undefined;
29
+ function getDiagnosticString(value) {
30
+ return typeof value === 'string' && value.length > 0 ? value : null;
28
31
  }
29
- function getManualOrderDropValidation(args) {
30
- const { activeDoc, targetDoc } = args;
31
- if (!targetDoc) {
32
- return {
33
- isValid: true,
34
- parentID: null
35
- };
36
- }
37
- if (targetDoc.__pageTreeID === activeDoc.__pageTreeID) {
38
- return {
39
- isValid: false,
40
- message: CANCEL_DRAG_MESSAGE,
41
- parentID: targetDoc.__pageTreeID
42
- };
43
- }
44
- if (targetDoc.__pageTreeAncestorIDs.includes(activeDoc.__pageTreeID)) {
45
- return {
46
- isValid: false,
47
- message: 'A document cannot be moved under one of its descendants.',
48
- parentID: targetDoc.__pageTreeID
49
- };
32
+ function getPageTreeDragData(value) {
33
+ if (!value || typeof value !== 'object') {
34
+ return {};
50
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;
51
40
  return {
52
- isValid: true,
53
- parentID: targetDoc.__pageTreeID
41
+ dragType,
42
+ parentID,
43
+ rowID
54
44
  };
55
45
  }
56
- function getDropValidationForMode(args) {
57
- const { activeDoc, allowSameParent, targetDoc } = args;
58
- if (allowSameParent) {
59
- return getManualOrderDropValidation({
60
- activeDoc,
61
- targetDoc
62
- });
46
+ function getSortableTransform(transform) {
47
+ if (!transform) {
48
+ return undefined;
63
49
  }
64
- return getDropValidation({
65
- activeDoc,
66
- targetDoc
67
- });
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})`;
68
53
  }
69
- function getCurrentOrderPlacement(args) {
70
- const { activeDoc, docs } = args;
71
- const activeIndex = docs.findIndex((doc)=>doc.__pageTreeID === activeDoc.__pageTreeID);
72
- const movingSubtreeIDs = new Set(docs.filter((doc)=>doc.__pageTreeID === activeDoc.__pageTreeID || doc.__pageTreeAncestorIDs.includes(activeDoc.__pageTreeID)).map((doc)=>doc.__pageTreeID));
73
- const siblingIsNotMoving = (doc)=>doc.__pageTreeParentID === activeDoc.__pageTreeParentID && !movingSubtreeIDs.has(doc.__pageTreeID);
74
- if (activeIndex < 0) {
75
- return {
76
- nextSiblingID: null,
77
- parentID: activeDoc.__pageTreeParentID,
78
- previousSiblingID: null
79
- };
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;
80
65
  }
81
66
  return {
82
- nextSiblingID: docs.slice(activeIndex + 1).find(siblingIsNotMoving)?.__pageTreeID ?? null,
83
- parentID: activeDoc.__pageTreeParentID,
84
- previousSiblingID: docs.slice(0, activeIndex).filter(siblingIsNotMoving).at(-1)?.__pageTreeID ?? null
67
+ end,
68
+ start: rootIndex
85
69
  };
86
70
  }
87
- function orderPlacementHasChanged(currentPlacement, nextPlacement) {
88
- return currentPlacement.parentID !== nextPlacement.parentID || currentPlacement.previousSiblingID !== nextPlacement.previousSiblingID || currentPlacement.nextSiblingID !== nextPlacement.nextSiblingID;
89
- }
90
- function getReorderTargetFromPlacement(args) {
91
- const { currentSort, docsByID, orderableFieldName, placement } = args;
92
- const targetDocID = placement.previousSiblingID ?? placement.nextSiblingID;
93
- if (!targetDocID) {
94
- return null;
95
- }
96
- const targetDoc = docsByID.get(targetDocID);
97
- if (!targetDoc) {
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) {
98
76
  return null;
99
77
  }
100
78
  return {
101
- newKeyWillBe: placement.previousSiblingID !== null && currentSort === orderableFieldName || placement.previousSiblingID === null && currentSort === `-${orderableFieldName}` ? 'greater' : 'less',
102
- targetDoc
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
+ };
103
139
  };
104
140
  }
105
141
  function buildPaginatedData(docs, limit, requestedPage) {
@@ -135,8 +171,12 @@ function normalizePositiveInt(value, fallback) {
135
171
  const parsedValue = Number.parseInt(value, 10);
136
172
  return Number.isNaN(parsedValue) || parsedValue <= 0 ? fallback : parsedValue;
137
173
  }
138
- function normalizePageTreeColumnState(columnState, useAsTitle) {
139
- 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 ? {
140
180
  ...column,
141
181
  active: true
142
182
  } : column);
@@ -161,6 +201,191 @@ function getSelectableRowData(doc) {
161
201
  function shouldSilenceMoveMessage(message) {
162
202
  return typeof message === 'string' && SILENT_MOVE_MESSAGES.has(message);
163
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
+ }
164
389
  function getStatusClassName(status) {
165
390
  if (status === 'changed' || status === 'draft' || status === 'published') {
166
391
  return status;
@@ -192,25 +417,17 @@ function renderStatusBadge(args) {
192
417
  })
193
418
  }, doc.__pageTreeID ?? index);
194
419
  }
195
- function createManualOrderColumn(docs) {
196
- return {
197
- accessor: MANUAL_ORDER_COLUMN_ACCESSOR,
198
- active: true,
199
- field: {
200
- hidden: true
201
- },
202
- Heading: /*#__PURE__*/ _jsx(SortHeader, {}),
203
- renderedCells: docs.map(()=>null)
204
- };
205
- }
206
- function insertManualOrderColumn(columns, orderColumn) {
207
- const selectColumnIndex = columns.findIndex((column)=>column.accessor === '_select');
208
- const insertIndex = selectColumnIndex >= 0 ? selectColumnIndex + 1 : 0;
209
- return [
210
- ...columns.slice(0, insertIndex),
211
- orderColumn,
212
- ...columns.slice(insertIndex)
213
- ];
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
+ });
214
431
  }
215
432
  function buildTableColumns(args) {
216
433
  const { badgeConfig, columnState, docs, enableRowSelections, homeIndicatorEnabled, orderableFieldName, parentFieldSlug, t, useAsTitle } = args;
@@ -261,21 +478,35 @@ function buildTableColumns(args) {
261
478
  }, doc.__pageTreeID ?? index))
262
479
  });
263
480
  }
264
- if (orderableFieldName && !columnsToUse.some((column)=>column.accessor === MANUAL_ORDER_COLUMN_ACCESSOR)) {
265
- return insertManualOrderColumn(columnsToUse, createManualOrderColumn(docs));
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);
266
496
  }
267
497
  return columnsToUse;
268
498
  }
269
- function HierarchyInsertRow({ activeColumnsCount, activeDragRowID, dropTarget, dropValidation, isMovePending }) {
499
+ function HierarchyInsertRow({ activeColumnsCount, activeDragRowID, activeDropTargetID, dropTarget, dropValidation, isMovePending }) {
270
500
  const { isOver, setNodeRef } = useDroppable({
271
501
  id: dropTarget.dropID,
272
502
  data: dropTarget,
273
503
  disabled: isMovePending
274
504
  });
275
505
  const hasActiveDrag = Boolean(activeDragRowID);
506
+ const showInsertLine = isOver && activeDropTargetID === dropTarget.dropID;
276
507
  return /*#__PURE__*/ _jsx("tr", {
277
508
  className: "pages-hierarchy-insert-row",
278
- "data-drag-over": isOver ? 'true' : 'false',
509
+ "data-drag-over": showInsertLine ? 'true' : 'false',
279
510
  "data-drop-valid": hasActiveDrag ? dropValidation?.isValid ? 'true' : 'false' : undefined,
280
511
  "data-page-tree-insert": "true",
281
512
  children: /*#__PURE__*/ _jsx("td", {
@@ -288,7 +519,7 @@ function HierarchyInsertRow({ activeColumnsCount, activeDragRowID, dropTarget, d
288
519
  })
289
520
  });
290
521
  }
291
- 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 }) {
292
523
  const { isOver, setNodeRef } = useDroppable({
293
524
  id: getRowDropID(doc.__pageTreeID),
294
525
  data: {
@@ -299,53 +530,110 @@ function HierarchyTableRow({ activeColumns, activeDragRowID, doc, dropValidation
299
530
  },
300
531
  disabled: isMovePending
301
532
  });
302
- 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);
303
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
+ ]);
304
573
  return /*#__PURE__*/ _jsx("tr", {
305
574
  className: `row-${rowIndex + 1}`,
306
- "data-drag-over": isOver ? 'true' : 'false',
307
- "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,
308
577
  "data-id": doc.id,
309
- "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',
310
580
  "data-page-tree-row": "true",
311
- ref: setNodeRef,
312
- children: activeColumns.map((column, columnIndex)=>{
313
- const { accessor } = column;
314
- return /*#__PURE__*/ _jsx("td", {
315
- className: `cell-${accessor.replace(/\./g, '__')}`,
316
- "data-page-tree-manual-order-cell": accessor === MANUAL_ORDER_COLUMN_ACCESSOR ? 'true' : undefined,
317
- "data-page-tree-title-cell": accessor === titleCellAccessor ? 'true' : undefined,
318
- children: column.renderedCells?.[rowIndex] ?? null
319
- }, 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
+ })
320
593
  })
321
594
  });
322
595
  }
323
- function HierarchyTable({ activeDragRowID, allDocsByID, allowSameParentDrops, columns, data, isMovePending, titleCellAccessor }) {
596
+ function HierarchyTable({ activeDragRowID, activeDragType, activeDropTarget, allDocsByID, canReorderRows, columns, data, isMovePending, titleCellAccessor }) {
324
597
  const activeColumns = React.useMemo(()=>columns.filter((column)=>column?.active), [
325
598
  columns
326
599
  ]);
327
- const hasManualOrderColumn = React.useMemo(()=>activeColumns.some((column)=>column.accessor === MANUAL_ORDER_COLUMN_ACCESSOR), [
328
- activeColumns
600
+ const sortableItems = React.useMemo(()=>data.map((doc)=>getSortableRowID(doc.__pageTreeID)), [
601
+ data
602
+ ]);
603
+ const sortingStrategy = React.useMemo(()=>buildPageTreeSortingStrategy(data), [
604
+ data
329
605
  ]);
330
606
  const insertDropTargets = React.useMemo(()=>buildInsertDropTargets(data), [
331
607
  data
332
608
  ]);
333
- 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;
334
617
  const rowDropValidationByID = React.useMemo(()=>{
335
618
  if (!activeDoc) {
336
619
  return new Map();
337
620
  }
338
621
  return new Map(data.map((doc)=>[
339
622
  doc.__pageTreeID,
340
- getDropValidationForMode({
623
+ getDropTargetValidation({
341
624
  activeDoc,
342
- allowSameParent: allowSameParentDrops,
343
- targetDoc: doc
625
+ docsByID: allDocsByID,
626
+ dropTarget: {
627
+ dropType: 'row',
628
+ insertAfterDropID: null,
629
+ insertBeforeDropID: null,
630
+ rowID: doc.__pageTreeID
631
+ }
344
632
  })
345
633
  ]));
346
634
  }, [
347
635
  activeDoc,
348
- allowSameParentDrops,
636
+ allDocsByID,
349
637
  data
350
638
  ]);
351
639
  const insertDropValidationByID = React.useMemo(()=>{
@@ -354,15 +642,14 @@ function HierarchyTable({ activeDragRowID, allDocsByID, allowSameParentDrops, co
354
642
  }
355
643
  return new Map(insertDropTargets.map((dropTarget)=>[
356
644
  dropTarget.dropID,
357
- getDropValidationForMode({
645
+ getDropTargetValidation({
358
646
  activeDoc,
359
- allowSameParent: allowSameParentDrops,
360
- targetDoc: dropTarget.parentID === null ? undefined : allDocsByID.get(dropTarget.parentID) ?? undefined
647
+ docsByID: allDocsByID,
648
+ dropTarget
361
649
  })
362
650
  ]));
363
651
  }, [
364
652
  activeDoc,
365
- allowSameParentDrops,
366
653
  allDocsByID,
367
654
  insertDropTargets
368
655
  ]);
@@ -376,7 +663,6 @@ function HierarchyTable({ activeDragRowID, allDocsByID, allowSameParentDrops, co
376
663
  'table-wrap pages-hierarchy-table',
377
664
  activeDoc ? 'pages-hierarchy-table--dragging' : ''
378
665
  ].filter(Boolean).join(' '),
379
- "data-page-tree-orderable": hasManualOrderColumn ? 'true' : undefined,
380
666
  children: /*#__PURE__*/ _jsx("div", {
381
667
  className: "table table--appearance-default",
382
668
  children: /*#__PURE__*/ _jsxs("table", {
@@ -386,45 +672,50 @@ function HierarchyTable({ activeDragRowID, allDocsByID, allowSameParentDrops, co
386
672
  /*#__PURE__*/ _jsx("thead", {
387
673
  children: /*#__PURE__*/ _jsx("tr", {
388
674
  children: activeColumns.map((column, index)=>/*#__PURE__*/ _jsx("th", {
389
- "data-page-tree-manual-order-cell": column.accessor === MANUAL_ORDER_COLUMN_ACCESSOR ? 'true' : undefined,
390
675
  "data-page-tree-title-cell": column.accessor === titleCellAccessor ? 'true' : undefined,
391
676
  id: `heading-${column.accessor.replace(/\./g, '__')}`,
392
677
  children: column.Heading
393
678
  }, index))
394
679
  })
395
680
  }),
396
- /*#__PURE__*/ _jsxs("tbody", {
397
- children: [
398
- insertDropTargets[0] ? /*#__PURE__*/ _jsx(HierarchyInsertRow, {
399
- activeColumnsCount: activeColumns.length,
400
- activeDragRowID: activeDragRowID,
401
- dropTarget: insertDropTargets[0],
402
- dropValidation: insertDropValidationByID.get(insertDropTargets[0].dropID),
403
- isMovePending: isMovePending
404
- }, insertDropTargets[0].dropID) : null,
405
- 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, {
406
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,
407
702
  /*#__PURE__*/ _jsx(HierarchyTableRow, {
408
703
  activeColumns: activeColumns,
409
704
  activeDragRowID: activeDragRowID,
705
+ activeDragType: activeDragType,
706
+ canReorderRows: canReorderRows,
410
707
  doc: doc,
411
708
  dropValidation: rowDropValidationByID.get(doc.__pageTreeID),
412
- insertAfterDropID: insertDropTargets[rowIndex + 1].dropID,
413
- insertBeforeDropID: insertDropTargets[rowIndex].dropID,
709
+ insertAfterDropID: hasVisibleDescendantAfter || isTrailingRootInsert ? null : rootInsertAfterDropTarget?.dropID ?? null,
710
+ insertBeforeDropID: rootInsertBeforeDropTarget?.dropID ?? null,
414
711
  isMovePending: isMovePending,
415
712
  rowIndex: rowIndex,
416
713
  titleCellAccessor: titleCellAccessor
417
- }),
418
- insertDropTargets[rowIndex + 1] ? /*#__PURE__*/ _jsx(HierarchyInsertRow, {
419
- activeColumnsCount: activeColumns.length,
420
- activeDragRowID: activeDragRowID,
421
- dropTarget: insertDropTargets[rowIndex + 1],
422
- dropValidation: insertDropValidationByID.get(insertDropTargets[rowIndex + 1].dropID),
423
- isMovePending: isMovePending
424
- }, insertDropTargets[rowIndex + 1].dropID) : null
714
+ })
425
715
  ]
426
- }, doc.__pageTreeID))
427
- ]
716
+ }, doc.__pageTreeID);
717
+ })
718
+ })
428
719
  })
429
720
  ]
430
721
  })
@@ -438,14 +729,15 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
438
729
  const locale = useLocale();
439
730
  const { i18n } = useTranslation();
440
731
  const [activeDragRowID, setActiveDragRowID] = React.useState(null);
732
+ const [activeDragType, setActiveDragType] = React.useState(null);
441
733
  const [activeDropTarget, setActiveDropTarget] = React.useState(null);
442
734
  const [collapsedIDs, setCollapsedIDs] = React.useState(()=>new Set());
443
- const [localSourceDocs, setLocalSourceDocs] = React.useState(sourceDocs);
735
+ const [optimisticOrderRowIDs, setOptimisticOrderRowIDs] = React.useState(null);
736
+ const [parentMoveEnabled, setParentMoveEnabled] = React.useState(false);
444
737
  const [pendingMoveRowID, setPendingMoveRowID] = React.useState(null);
445
- React.useEffect(()=>{
446
- setLocalSourceDocs(sourceDocs);
447
- }, [
448
- sourceDocs
738
+ const { getPreference, setPreference } = usePreferences();
739
+ const parentMovePreferenceKey = React.useMemo(()=>getParentMovePreferenceKey(props.collectionSlug), [
740
+ props.collectionSlug
449
741
  ]);
450
742
  const toggleRow = React.useCallback((rowID)=>{
451
743
  setCollapsedIDs((currentState)=>{
@@ -458,6 +750,16 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
458
750
  return nextState;
459
751
  });
460
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
+ ]);
461
763
  const currentSort = React.useMemo(()=>{
462
764
  const searchParamSort = searchParams.getAll('sort');
463
765
  if (searchParamSort.length > 0) {
@@ -468,7 +770,10 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
468
770
  query.sort,
469
771
  searchParams
470
772
  ]);
471
- const manualOrderIsActive = Boolean(orderableFieldName && (currentSort === orderableFieldName || currentSort === `-${orderableFieldName}`));
773
+ const canReorderRows = canMoveDocs && isOrderableSortActive({
774
+ orderableFieldName,
775
+ sort: currentSort
776
+ });
472
777
  const currentLimit = React.useMemo(()=>normalizePositiveInt(searchParams.get('limit'), typeof query.limit === 'number' && query.limit > 0 ? query.limit : 10), [
473
778
  query.limit,
474
779
  searchParams
@@ -477,13 +782,13 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
477
782
  query.page,
478
783
  searchParams
479
784
  ]);
480
- const liveAllDocs = React.useMemo(()=>buildPageTreeDocs(localSourceDocs, {
785
+ const liveAllDocs = React.useMemo(()=>buildPageTreeDocs(sourceDocs, {
481
786
  parentFieldSlug,
482
787
  sort: currentSort
483
788
  }), [
484
789
  currentSort,
485
- localSourceDocs,
486
- parentFieldSlug
790
+ parentFieldSlug,
791
+ sourceDocs
487
792
  ]);
488
793
  const collapseResetKey = React.useMemo(()=>JSON.stringify([
489
794
  props.collectionSlug,
@@ -498,19 +803,37 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
498
803
  activeDragRowID,
499
804
  canMoveDocs,
500
805
  collapsedIDs,
806
+ parentMoveEnabled,
501
807
  pendingMoveRowID,
502
808
  toggleRow
503
809
  }), [
504
810
  activeDragRowID,
505
811
  canMoveDocs,
506
812
  collapsedIDs,
813
+ parentMoveEnabled,
507
814
  pendingMoveRowID,
508
815
  toggleRow
509
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
+ ]);
510
831
  React.useEffect(()=>{
511
832
  setActiveDragRowID(null);
833
+ setActiveDragType(null);
512
834
  setActiveDropTarget(null);
513
835
  setCollapsedIDs(new Set());
836
+ setOptimisticOrderRowIDs(null);
514
837
  }, [
515
838
  collapseResetKey
516
839
  ]);
@@ -524,17 +847,36 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
524
847
  visibleDocs
525
848
  ]);
526
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
+ ]);
527
869
  const allDocsByID = React.useMemo(()=>new Map(liveAllDocs.map((doc)=>[
528
870
  doc.__pageTreeID,
529
871
  doc
530
872
  ])), [
531
873
  liveAllDocs
532
874
  ]);
533
- const paginatedDocsByID = React.useMemo(()=>new Map(paginatedDocs.map((doc)=>[
875
+ const paginatedDocsByID = React.useMemo(()=>new Map(displayedPaginatedDocs.map((doc)=>[
534
876
  doc.__pageTreeID,
535
877
  doc
536
878
  ])), [
537
- paginatedDocs
879
+ displayedPaginatedDocs
538
880
  ]);
539
881
  const renderedCellIndexByDocID = React.useMemo(()=>new Map(allDocs.map((doc, index)=>[
540
882
  doc.__pageTreeID,
@@ -542,19 +884,19 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
542
884
  ])), [
543
885
  allDocs
544
886
  ]);
545
- const normalizedColumnState = React.useMemo(()=>normalizePageTreeColumnState(columnState, useAsTitle), [
887
+ const normalizedColumnState = React.useMemo(()=>ensureUseAsTitleColumn(columnState, useAsTitle), [
546
888
  columnState,
547
889
  useAsTitle
548
890
  ]);
549
- const paginatedColumnState = React.useMemo(()=>sliceColumnState(normalizedColumnState, paginatedDocs, renderedCellIndexByDocID), [
891
+ const paginatedColumnState = React.useMemo(()=>sliceColumnState(normalizedColumnState, displayedPaginatedDocs, renderedCellIndexByDocID), [
892
+ displayedPaginatedDocs,
550
893
  normalizedColumnState,
551
- paginatedDocs,
552
894
  renderedCellIndexByDocID
553
895
  ]);
554
896
  const tableColumns = React.useMemo(()=>buildTableColumns({
555
897
  badgeConfig,
556
898
  columnState: paginatedColumnState,
557
- docs: paginatedDocs,
899
+ docs: displayedPaginatedDocs,
558
900
  enableRowSelections: props.enableRowSelections,
559
901
  homeIndicatorEnabled,
560
902
  orderableFieldName,
@@ -563,7 +905,7 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
563
905
  useAsTitle
564
906
  }), [
565
907
  paginatedColumnState,
566
- paginatedDocs,
908
+ displayedPaginatedDocs,
567
909
  badgeConfig,
568
910
  homeIndicatorEnabled,
569
911
  orderableFieldName,
@@ -574,7 +916,7 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
574
916
  ]);
575
917
  const activeDragDoc = activeDragRowID ? paginatedDocsByID.get(activeDragRowID) ?? null : null;
576
918
  const activeDragPreviewPath = React.useMemo(()=>{
577
- if (!activeDragDoc) {
919
+ if (!activeDragDoc || activeDragType !== 'move') {
578
920
  return null;
579
921
  }
580
922
  if (!activeDropTarget) {
@@ -587,10 +929,10 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
587
929
  docsByID: allDocsByID,
588
930
  dropTarget: activeDropTarget
589
931
  });
590
- const dropValidation = getDropValidationForMode({
932
+ const dropValidation = getDropTargetValidation({
591
933
  activeDoc: activeDragDoc,
592
- allowSameParent: manualOrderIsActive,
593
- targetDoc: targetDoc ?? undefined
934
+ docsByID: allDocsByID,
935
+ dropTarget: activeDropTarget
594
936
  });
595
937
  if (!dropValidation.isValid) {
596
938
  return buildDocSlugPath({
@@ -605,218 +947,46 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
605
947
  });
606
948
  }, [
607
949
  activeDragDoc,
950
+ activeDragType,
608
951
  activeDropTarget,
609
- allDocsByID,
610
- manualOrderIsActive
952
+ allDocsByID
611
953
  ]);
612
954
  const isMovePending = pendingMoveRowID !== null;
613
955
  const sensors = useSensors(useSensor(PointerSensor, {
614
956
  activationConstraint: {
615
- distance: 4
957
+ distance: 2
616
958
  }
617
959
  }));
618
- const moveDocument = React.useCallback(async (args)=>{
619
- const { parentID, rowID } = args;
620
- const params = new URLSearchParams();
621
- if (locale?.code) {
622
- params.set('locale', locale.code);
623
- }
624
- const response = await fetch(`${config.routes.api}/${props.collectionSlug}/${encodeURIComponent(rowID)}/move${params.size > 0 ? `?${params.toString()}` : ''}`, {
625
- body: JSON.stringify({
626
- parentID
627
- }),
628
- credentials: 'include',
629
- headers: {
630
- 'Accept-Language': i18n.language,
631
- 'Content-Type': 'application/json',
632
- [pageTreeMoveRequestHeader]: pageTreeMoveRequestHeaderValue
633
- },
634
- method: 'POST'
635
- });
636
- const result = await response.json().catch(()=>null);
637
- if (!response.ok) {
638
- throw new Error(result?.message ?? 'Could not move document.');
639
- }
640
- return result;
641
- }, [
642
- config.routes.api,
643
- i18n.language,
644
- locale?.code,
645
- props.collectionSlug
646
- ]);
647
- const reorderDocument = React.useCallback(async (args)=>{
648
- const { movedDocID, newKeyWillBe, targetDoc } = args;
649
- const targetID = getPayloadDocID(targetDoc);
650
- if (!orderableFieldName || targetID === undefined) {
651
- return undefined;
652
- }
653
- const params = new URLSearchParams();
654
- if (locale?.code) {
655
- params.set('locale', locale.code);
656
- }
657
- const response = await fetch(`${config.routes.api}/reorder${params.size > 0 ? `?${params.toString()}` : ''}`, {
658
- body: JSON.stringify({
659
- collectionSlug: props.collectionSlug,
660
- docsToMove: [
661
- movedDocID
662
- ],
663
- newKeyWillBe,
664
- orderableFieldName,
665
- target: {
666
- id: targetID,
667
- key: targetDoc[orderableFieldName]
668
- }
669
- }),
670
- credentials: 'include',
671
- headers: {
672
- 'Accept-Language': i18n.language,
673
- 'Content-Type': 'application/json',
674
- [pageTreeMoveRequestHeader]: pageTreeMoveRequestHeaderValue
675
- },
676
- method: 'POST'
677
- });
678
- const result = await response.json().catch(()=>null);
679
- if (response.status === 403) {
680
- throw new Error('You do not have permission to reorder these rows');
681
- }
682
- if (!response.ok) {
683
- throw new Error('Failed to reorder. This can happen if you reorder several rows too quickly. Please try again.');
684
- }
685
- if (result?.message === 'initial migration') {
686
- throw new Error('You have enabled "orderable" on a collection with existing documents and this is the first time you have sorted documents. We have run an automatic migration to add an initial order to the documents. Please refresh the page and try again.');
687
- }
688
- return result?.orderValues?.[0];
689
- }, [
690
- config.routes.api,
691
- i18n.language,
692
- locale?.code,
693
- orderableFieldName,
694
- props.collectionSlug
695
- ]);
696
- const handleManualOrderMoveEnd = React.useCallback(async (args)=>{
697
- const { activeDoc, dropTarget, rowID } = args;
698
- if (!orderableFieldName) {
699
- return;
700
- }
701
- const targetDoc = getDropTargetParentDoc({
702
- docsByID: allDocsByID,
703
- dropTarget
704
- });
705
- const dropValidation = getManualOrderDropValidation({
706
- activeDoc,
707
- targetDoc: targetDoc ?? undefined
708
- });
709
- if (!dropValidation.isValid) {
710
- if (!shouldSilenceMoveMessage(dropValidation.message)) {
711
- toast.error(dropValidation.message ?? 'Could not move document.');
712
- }
713
- return;
714
- }
715
- const placement = getOrderPlacementFromDropTarget({
716
- activeDoc,
717
- docs: paginatedDocs,
718
- docsByID: allDocsByID,
719
- dropTarget
720
- });
721
- if (!placement) {
722
- return;
723
- }
724
- const currentPlacement = getCurrentOrderPlacement({
725
- activeDoc,
726
- docs: paginatedDocs
727
- });
728
- const parentChanged = activeDoc.__pageTreeParentID !== placement.parentID;
729
- const orderChanged = orderPlacementHasChanged(currentPlacement, placement);
730
- const reorderTarget = orderChanged ? getReorderTargetFromPlacement({
731
- currentSort,
732
- docsByID: allDocsByID,
733
- orderableFieldName,
734
- placement
735
- }) : null;
736
- if (!parentChanged && !reorderTarget) {
737
- return;
738
- }
739
- const movedDocID = getPayloadDocID(activeDoc);
740
- if (movedDocID === undefined) {
741
- return;
742
- }
743
- const parentDoc = placement.parentID ? allDocsByID.get(placement.parentID) : undefined;
744
- const nextParentValue = placement.parentID === null ? null : parentDoc ? getPayloadDocID(parentDoc) ?? placement.parentID : placement.parentID;
745
- let parentMoveSucceeded = false;
746
- setPendingMoveRowID(rowID);
747
- try {
748
- if (parentChanged) {
749
- await moveDocument({
750
- parentID: placement.parentID,
751
- rowID
752
- });
753
- parentMoveSucceeded = true;
754
- }
755
- const nextOrderValue = reorderTarget ? await reorderDocument({
756
- movedDocID,
757
- newKeyWillBe: reorderTarget.newKeyWillBe,
758
- targetDoc: reorderTarget.targetDoc
759
- }) : undefined;
760
- if (parentChanged || typeof nextOrderValue === 'string') {
761
- setLocalSourceDocs((currentDocs)=>currentDocs.map((doc)=>String(doc.id ?? '') === String(movedDocID) ? {
762
- ...doc,
763
- ...parentChanged ? {
764
- [parentFieldSlug]: nextParentValue
765
- } : {},
766
- ...typeof nextOrderValue === 'string' ? {
767
- [orderableFieldName]: nextOrderValue
768
- } : {}
769
- } : doc));
770
- }
771
- if (parentChanged) {
772
- toast.success(`Moved "${getDocDisplayLabel(activeDoc)}".`);
773
- }
774
- React.startTransition(()=>{
775
- router.refresh();
776
- });
777
- } catch (error) {
778
- const message = error instanceof Error && error.message ? error.message : 'Could not move document.';
779
- toast.error(parentMoveSucceeded ? `Moved "${getDocDisplayLabel(activeDoc)}", but could not update its manual order. ${message}` : message);
780
- if (parentMoveSucceeded) {
781
- React.startTransition(()=>{
782
- router.refresh();
783
- });
784
- }
785
- } finally{
786
- setPendingMoveRowID(null);
787
- }
788
- }, [
789
- allDocsByID,
790
- currentSort,
791
- moveDocument,
792
- orderableFieldName,
793
- paginatedDocs,
794
- parentFieldSlug,
795
- reorderDocument,
796
- router
797
- ]);
798
960
  const handleDragCancel = React.useCallback(()=>{
799
961
  setActiveDragRowID(null);
962
+ setActiveDragType(null);
800
963
  setActiveDropTarget(null);
801
964
  }, []);
802
965
  const handleDragStart = React.useCallback((event)=>{
803
- const dragData = getPageTreeDragData(event.active.data.current);
804
966
  if (!canMoveDocs || isMovePending) {
805
967
  return;
806
968
  }
807
- const rowID = dragData.rowID;
808
- 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)) {
809
972
  setActiveDragRowID(rowID);
973
+ setActiveDragType(dragType);
810
974
  setActiveDropTarget(null);
811
975
  }
812
976
  }, [
813
977
  canMoveDocs,
978
+ canReorderRows,
814
979
  isMovePending,
815
980
  paginatedDocsByID
816
981
  ]);
817
982
  const handleDragOver = React.useCallback((event)=>{
983
+ const dragData = getPageTreeDragData(event.active.data.current);
818
984
  const overData = event.over?.data.current;
819
- if (!overData) {
985
+ if (dragData.dragType === 'order') {
986
+ setActiveDropTarget(null);
987
+ return;
988
+ }
989
+ if (dragData.dragType !== 'move' || !overData) {
820
990
  setActiveDropTarget(null);
821
991
  return;
822
992
  }
@@ -832,62 +1002,132 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
832
1002
  }, []);
833
1003
  const handleDragEnd = React.useCallback(async (event)=>{
834
1004
  const dragData = getPageTreeDragData(event.active.data.current);
835
- const rowID = dragData.rowID;
1005
+ const { dragType, rowID } = dragData;
836
1006
  const activeDoc = typeof rowID === 'string' ? paginatedDocsByID.get(rowID) ?? null : null;
837
1007
  const overData = event.over?.data.current;
1008
+ const overDragData = getPageTreeDragData(event.over?.data.current);
838
1009
  setActiveDragRowID(null);
1010
+ setActiveDragType(null);
839
1011
  setActiveDropTarget(null);
840
- 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
+ }
841
1066
  return;
842
1067
  }
843
- if (manualOrderIsActive && orderableFieldName) {
844
- await handleManualOrderMoveEnd({
845
- activeDoc,
846
- dropTarget: overData,
847
- rowID
848
- });
1068
+ if (dragType !== 'move' || typeof rowID !== 'string' || !activeDoc || !overData) {
849
1069
  return;
850
1070
  }
851
- const targetDoc = getDropTargetParentDoc({
1071
+ const dropValidation = getDropTargetValidation({
1072
+ activeDoc,
852
1073
  docsByID: allDocsByID,
853
1074
  dropTarget: overData
854
1075
  });
855
- const dropValidation = getDropValidation({
856
- activeDoc,
857
- targetDoc: targetDoc ?? undefined
858
- });
859
1076
  if (!dropValidation.isValid) {
860
1077
  if (!shouldSilenceMoveMessage(dropValidation.message)) {
861
1078
  toast.error(dropValidation.message ?? 'Could not move document.');
862
1079
  }
863
1080
  return;
864
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
+ }
865
1090
  setPendingMoveRowID(rowID);
866
1091
  try {
867
- await moveDocument({
868
- parentID: dropValidation.parentID,
869
- 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'
870
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
+ }
871
1111
  toast.success(`Moved "${getDocDisplayLabel(activeDoc)}".`);
872
1112
  React.startTransition(()=>{
873
1113
  router.refresh();
874
1114
  });
875
1115
  } catch (error) {
876
1116
  const message = error instanceof Error && error.message ? error.message : 'Could not move document.';
877
- if (shouldSilenceMoveMessage(message)) {
878
- return;
879
- }
880
1117
  toast.error(message);
881
1118
  } finally{
882
1119
  setPendingMoveRowID(null);
883
1120
  }
884
1121
  }, [
885
1122
  allDocsByID,
886
- handleManualOrderMoveEnd,
887
- manualOrderIsActive,
888
- moveDocument,
1123
+ config.routes.api,
1124
+ currentSort,
1125
+ displayedPaginatedDocs,
1126
+ i18n.language,
1127
+ locale?.code,
889
1128
  orderableFieldName,
890
1129
  paginatedDocsByID,
1130
+ props.collectionSlug,
891
1131
  router
892
1132
  ]);
893
1133
  const handleDragEndSync = React.useCallback((event)=>{
@@ -905,10 +1145,12 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
905
1145
  children: [
906
1146
  /*#__PURE__*/ _jsx(HierarchyTable, {
907
1147
  activeDragRowID: activeDragRowID,
1148
+ activeDragType: activeDragType,
1149
+ activeDropTarget: activeDropTarget,
908
1150
  allDocsByID: allDocsByID,
909
- allowSameParentDrops: manualOrderIsActive,
1151
+ canReorderRows: canReorderRows,
910
1152
  columns: tableColumns,
911
- data: paginatedDocs,
1153
+ data: displayedPaginatedDocs,
912
1154
  isMovePending: isMovePending,
913
1155
  titleCellAccessor: useAsTitle
914
1156
  }),
@@ -926,35 +1168,57 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
926
1168
  }), [
927
1169
  activeDragPreviewPath,
928
1170
  activeDragRowID,
1171
+ activeDragType,
1172
+ activeDropTarget,
1173
+ allDocsByID,
1174
+ canReorderRows,
1175
+ displayedPaginatedDocs,
929
1176
  handleDragCancel,
930
1177
  handleDragEndSync,
931
1178
  handleDragOver,
932
1179
  handleDragStart,
933
1180
  isMovePending,
934
- allDocsByID,
935
- manualOrderIsActive,
936
- paginatedDocs,
937
1181
  sensors,
938
1182
  tableColumns,
939
1183
  useAsTitle
940
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
+ ]);
941
1204
  return /*#__PURE__*/ _jsx("div", {
942
1205
  className: styles.root,
943
1206
  children: /*#__PURE__*/ _jsx(PageTreeProvider, {
944
1207
  value: hierarchyValue,
945
1208
  children: /*#__PURE__*/ _jsx(ListQueryProvider, {
946
1209
  collectionSlug: props.collectionSlug,
947
- data: paginatedData,
1210
+ data: displayedPaginatedData,
948
1211
  modifySearchParams: true,
949
1212
  orderableFieldName: orderableFieldName,
950
1213
  query: {
951
1214
  ...query,
952
1215
  limit: currentLimit,
953
- page: paginatedData.page,
1216
+ page: displayedPaginatedData.page,
954
1217
  sort: currentSort
955
1218
  },
956
1219
  children: /*#__PURE__*/ _jsx(DefaultListView, {
957
1220
  ...props,
1221
+ beforeActions: beforeActions,
958
1222
  columnState: paginatedColumnState,
959
1223
  Table: tableNode
960
1224
  })