All checks were successful
CD / Build and push images (push) Successful in 3m1s
CI / Lint, typecheck, test (push) Successful in 2m13s
CI / Auth e2e pack (push) Successful in 2m36s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m13s
CD / Promote to Int (push) Successful in 11s
Enable the third sidebar sort mode — a freely defined order. - api: `PATCH /pages/:id/position` (before/after neighbour) recomputes only the moved page's fractional `sort_key`. Pure `sort-key.ts` helpers (`nextKeyOrRebalance`, `evenlySpacedKeys`) decide between the cheap single-key path and a full pond rebalance to evenly-spaced keys when a key would exceed MAX_SORT_KEY_LENGTH or the client's neighbours are stale; rebalance runs in one transaction. Order is server-authoritative. - web: enable 'manual' in the sort-mode switch; in manual mode the owner can reorder via native drag-and-drop (drop above/below by pointer half) or the keyboard (per-row up/down buttons), each announced through an aria-live region. Reordering is hidden while a label filter narrows the list. New pages already append at the end (create uses generateKeyBetween(last, null)). Pure `reorder.ts` neighbour helpers, unit-tested. - i18n: manual sort mode + reorder strings (de + en). - tests: sort-key property test (10.000 adversarial reorders never collide or overflow — rebalance verified); reposition db test (persist, server-order, sort-mode switch keeps manual order); reorder e2e pack (keyboard reorder persists across reload + identical on a fresh read; aria-live announced). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
38 lines
1.4 KiB
TypeScript
38 lines
1.4 KiB
TypeScript
/**
|
|
* Pure helpers for manual page reordering in the sidebar (issue #45). The
|
|
* server recomputes the moved page's `sort_key` between two neighbours, so the
|
|
* client only has to name them. Given the current order and where the page
|
|
* should land, these compute the `afterId`/`beforeId` the position endpoint
|
|
* expects — kept separate from the component so they are unit-testable.
|
|
*/
|
|
|
|
/** The moved page's new neighbours when it lands at `newIndex` in the list with
|
|
* itself removed. `afterId` is its predecessor (null at the top), `beforeId`
|
|
* its successor (null at the bottom). */
|
|
export function neighborsForMove(
|
|
orderedIds: string[],
|
|
movedId: string,
|
|
newIndex: number,
|
|
): { afterId: string | null; beforeId: string | null } {
|
|
const without = orderedIds.filter((id) => id !== movedId);
|
|
const clamped = Math.max(0, Math.min(newIndex, without.length));
|
|
return {
|
|
afterId: clamped > 0 ? without[clamped - 1]! : null,
|
|
beforeId: clamped < without.length ? without[clamped]! : null,
|
|
};
|
|
}
|
|
|
|
/** Target index for a drop onto `targetId`: before it, or after it when the
|
|
* pointer is over the item's lower half (so the very bottom is reachable). */
|
|
export function dropIndex(
|
|
orderedIds: string[],
|
|
movedId: string,
|
|
targetId: string,
|
|
after: boolean,
|
|
): number {
|
|
const without = orderedIds.filter((id) => id !== movedId);
|
|
const base = without.indexOf(targetId);
|
|
if (base === -1) return without.length;
|
|
return after ? base + 1 : base;
|
|
}
|