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
53 lines
2.2 KiB
TypeScript
53 lines
2.2 KiB
TypeScript
import { generateKeyBetween, generateNKeysBetween } from 'fractional-indexing';
|
|
|
|
/**
|
|
* Fractional-index keys for manual page ordering (issue #45, data-model.md
|
|
* `sort_key`). A move recomputes only the moved page's key as a value strictly
|
|
* between its two new neighbours, so no sibling is rewritten. Repeatedly
|
|
* inserting between two very close keys grows the key length; once a fresh key
|
|
* would exceed {@link MAX_SORT_KEY_LENGTH} the caller rebalances the whole pond
|
|
* to evenly-spaced keys instead. These helpers are pure so the rebalance
|
|
* behaviour can be property-tested without a database.
|
|
*/
|
|
|
|
/**
|
|
* Length at which a newly generated key triggers a rebalance. Fractional keys
|
|
* only grow under adversarial "always insert between the same tight pair"
|
|
* sequences; a healthy tree stays far below this. 40 leaves generous headroom
|
|
* over normal use while capping unbounded growth.
|
|
*/
|
|
export const MAX_SORT_KEY_LENGTH = 40;
|
|
|
|
/** A key strictly between `after` and `before` (either `null` for an open end). */
|
|
export function keyBetween(after: string | null, before: string | null): string {
|
|
return generateKeyBetween(after, before);
|
|
}
|
|
|
|
/** `n` evenly-spaced keys spanning the whole range — used to rebalance a pond. */
|
|
export function evenlySpacedKeys(n: number): string[] {
|
|
if (n <= 0) return [];
|
|
return generateNKeysBetween(null, null, n);
|
|
}
|
|
|
|
/**
|
|
* The key for a page moved between `afterKey` and `beforeKey`, or `null` when
|
|
* the result would be too long (or the neighbours are out of order, e.g. from a
|
|
* stale client) and the caller must rebalance instead. Never throws.
|
|
*/
|
|
export function nextKeyOrRebalance(
|
|
afterKey: string | null,
|
|
beforeKey: string | null,
|
|
): string | null {
|
|
// Guard reversed/equal neighbours ourselves: generateKeyBetween throws only
|
|
// on equal keys and silently returns a wrong key when after > before, so a
|
|
// stale client could otherwise corrupt the order — force a rebalance instead.
|
|
if (afterKey !== null && beforeKey !== null && afterKey >= beforeKey) return null;
|
|
let key: string;
|
|
try {
|
|
key = keyBetween(afterKey, beforeKey);
|
|
} catch {
|
|
return null;
|
|
}
|
|
return key.length > MAX_SORT_KEY_LENGTH ? null : key;
|
|
}
|