feat(projects): drag the sidebar into the order you want

Projects have carried sort_order since the first migration and nothing
ever set it, so the sidebar was ordered by creation date for good.

POST /projects/reorder takes the whole list rather than one move. A
person has a handful of projects, so sending the order you want is
simpler to reason about than midpoints between neighbours — and it cannot
run out of precision, which is the failure the task ordering has to
respace a section to recover from. It also cannot land half-applied the
way a sequence of individual moves can.

Projects you do not own are skipped rather than refused. sort_order is a
column on the project, not a preference per viewer, so a shared project
sits where its owner put it and a member rearranging their own sidebar
does not rearrange somebody else's. The sidebar only makes your own
projects draggable, so that rule never shows up as a row that springs
back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kristian Wind
2026-08-17 15:59:31 +02:00
committed by Kristian Wind
co-authored by Claude Opus 5
parent 3f4af6c0ab
commit c4e9e1519b
8 changed files with 280 additions and 1 deletions
+23
View File
@@ -246,6 +246,29 @@ class AppState {
this.projects = projects;
}
/**
* Puts the projects in the given order.
*
* The whole list rather than one moved item: it is a handful of rows, and
* sending the order you want cannot land in the half-applied state a sequence
* of individual moves can. `sort_order` is set here as well as on the server,
* so the sidebar settles immediately rather than after a round trip.
*/
async reorderProjects(ids) {
const previous = this.projects;
const rank = new Map(ids.map((id, i) => [id, i]));
this.projects = this.projects.map((p) =>
rank.has(p.id) ? { ...p, sort_order: rank.get(p.id) } : p
);
try {
await api.reorderProjects(ids);
} catch (e) {
this.projects = previous;
this.toast(humanMessage(e));
}
}
// --- toasts -------------------------------------------------------------------
toast(message) {