Task #463: @dnd-kit notes drag + reorder

- Replace HTML5+touch drag with @dnd-kit (PointerSensor distance:8,
  TouchSensor delay:200/tol:8) matching home.tsx pattern.
- Add sort_order column to notes; ORDER BY asc(sortOrder), updatedAt desc.
- New PATCH /notes/reorder endpoint: strict isPinned boolean validation,
  bucket+permission scoped, all writes in db.transaction for atomicity.
- PATCH /notes/:id stamps sort_order = min-1 on folder/pin bucket change.
- Client useReorderNotes hook with optimistic cache update.
- handleDragEnd builds reorder payload from FULL bucket (owner notes or
  shared-folder bucket via ref), not the filtered/search subset, so
  hidden siblings retain stable order.
- SharedFolderView publishes its data.notes via bucketRef when viewer
  has edit permission, enabling correct reorder in shared folders.
- Layout fix at narrow viewport: rail stacks above notes
  (flex-col md:flex-row) so iPad portrait drag has proper bbox.
- Playwright tests: notes-folders.spec.mjs both desktop pointer drag
  and touch long-press drag pass (32s).
- OpenAPI codegen skipped: notes-api.ts is hand-written.
- Out of scope (pre-existing failures): executive-meetings reorder/font,
  notes-share PATCH 403/404, groups-crud rollback.
This commit is contained in:
riyadhafraa
2026-05-10 13:10:01 +00:00
parent 17c7b1bdaa
commit daa4f6c038
6 changed files with 562 additions and 338 deletions
+8
View File
@@ -28,6 +28,14 @@ export const notesTable = pgTable("notes", {
items: jsonb("items").$type<ChecklistItem[]>(),
isPinned: boolean("is_pinned").notNull().default(false),
isArchived: boolean("is_archived").notNull().default(false),
// Per-bucket manual sort order. A "bucket" is the unique combination of
// (userId, folderId, isPinned). Smaller numbers sort first; ties fall back
// to updatedAt DESC so legacy notes (all default 0) keep their existing
// recency-based order until the user manually reorders them. Updated by
// POST /notes/reorder (drag-to-reorder) and on folder-change via PATCH
// /notes/:id (the moved note is placed at the top of the destination
// bucket so it lands where the user can see it immediately).
sortOrder: integer("sort_order").notNull().default(0),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => new Date()),
});