From 6c7b78ad4bbc48ac07ab7c7dd5295f4fc8ded917 Mon Sep 17 00:00:00 2001 From: Aneurin Barker Snook Date: Sat, 5 Sep 2026 01:04:50 +0100 Subject: [PATCH] Explore: cards can be dragged out to the inbox One-directional drag support, joining the shared "kanban" group: put: false and sort: false mean a card can leave Explore's list (to unfile it via the sidebar's inbox) but the list can't receive a drop itself (there's no status to assign an incoming card) or be reordered by dragging (it's sorted by name regardless). sortedCards moves from a computed to a ref rebuilt by a watch -- splices its bound list in place as the user drags, which a plain computed would just discard on its next recomputation. No local @change handler is needed: the splice already happens locally, and the inbox's own handler (AppSidebar) persists the move and reloads this project's cards regardless of which side of the drag it's reacting to. AppSidebar: widened the "is a project open" check for the post-drag cards refresh back to both project routes (Explore's list is now also a place a card can leave from), and reused the same route-name set already defined for the sidebar's project switcher. Co-Authored-By: Claude Sonnet 5 --- web/README.md | 30 +++++++++++++------- web/src/components/AppSidebar.vue | 7 ++--- web/src/style.css | 7 ++++- web/src/views/ProjectExploreView.vue | 41 ++++++++++++++++++++++------ 4 files changed, 62 insertions(+), 23 deletions(-) diff --git a/web/README.md b/web/README.md index f9cf215..49428cc 100644 --- a/web/README.md +++ b/web/README.md @@ -160,21 +160,31 @@ project) the way switching to a different project's id still does. ### Explore The flat card list, **sorted by name (case-insensitive)** via a `sortedCards` -computed — there is no manual order here. Each row links to the card's own -view (`/cards/:id` — see [Card detail](#card-detail)) and shows its status -chip (`card.status.name` or "No status") next to the text; a delete button -sits outside that link. +ref (rebuilt by a `watch` on the store's `cards.cards` -- a plain computed +can't be handed to ``, which splices its bound list in place as +the user drags). Each row links to the card's own view (`/cards/:id` — see +[Card detail](#card-detail)); there is no manual order here, and no delete +button either -- deleting lives on that view now. + +The list is a `` too, but one-directional: `group: { name: +'kanban', put: false }` and `sort: false` mean a card can be dragged *out* -- +to the sidebar's inbox, unfiling it from the project -- but Explore can't +receive a drop itself (there's no status to put an incoming card in), nor +reorder on its own drag (it's sorted by name regardless). No `@change` +handler is needed on this side: `` already splices the card out of +`sortedCards` locally, and the inbox's own handler (see above) persists the +move and reloads this project's `cards`, which rebuilds the list from the +authoritative result regardless of which side reacted to the drop. ### Kanban One column per project status, in `position` order -- the inbox is *not* a column here; it's in the sidebar (see above), though it's still a valid drag -source/target (the only view where that's true -- Explore has no draggable -list of its own, which the sidebar accounts for when deciding whether to -refresh a project's cards after an inbox drag). Unlike the cards, statuses -are this route's own fetch (`GET /api/projects/:id/statuses`) -- Explore has -no use for them. `board` is derived from `cards.cards` + those statuses and -rebuilt by a `watch` whenever either changes. +target as well as a source (unlike Explore, which can only send a card *to* +the inbox, not receive one). Unlike the cards, statuses are this route's own +fetch (`GET /api/projects/:id/statuses`) -- Explore has no use for them. +`board` is derived from `cards.cards` + those statuses and rebuilt by a +`watch` whenever either changes. Every drop — whether reordering within a column (`moved`) or dragging in from another column or the sidebar's inbox (`added`) — calls diff --git a/web/src/components/AppSidebar.vue b/web/src/components/AppSidebar.vue index 7b34ac8..ee3f5b3 100644 --- a/web/src/components/AppSidebar.vue +++ b/web/src/components/AppSidebar.vue @@ -89,10 +89,9 @@ async function onInboxChange(change: ColumnChange) { } catch (e) { inboxError.value = e instanceof ApiError ? e.message : 'Something went wrong.' } finally { - // The card may have come from (or gone to) the kanban board currently - // open -- the only place a project's cards are a drag target/source - // alongside the inbox (Explore has no draggable list of its own). - const openProjectId = route.name === 'project-kanban' ? Number(route.params.id) : null + // The card may have come from (or gone to) the project currently open -- + // Explore can drag a card out (but not receive one), Kanban can do both. + const openProjectId = ON_PROJECT_ROUTES.has(String(route.name)) ? Number(route.params.id) : null await Promise.all([ loadInbox(), openProjectId !== null ? cards.load(openProjectId) : Promise.resolve(), diff --git a/web/src/style.css b/web/src/style.css index c66c1ea..1635326 100644 --- a/web/src/style.css +++ b/web/src/style.css @@ -546,6 +546,7 @@ h1 { border-radius: var(--radius-md); padding: 0.4rem 0.55rem; background: var(--surface); + cursor: grab; } /* Same hover treatment as .kanban-card -- just the border, no background @@ -554,8 +555,12 @@ h1 { border-color: var(--accent); } +.card-row--ghost { + opacity: var(--opacity-ghost); +} + /* Wraps the text + status badge -- the whole row is a link to the card's own - view, except the delete button (a sibling, so it isn't nested inside it). */ + view. */ .card-row__link { flex: 1; min-width: 0; diff --git a/web/src/views/ProjectExploreView.vue b/web/src/views/ProjectExploreView.vue index 52cdbf7..0c445b6 100644 --- a/web/src/views/ProjectExploreView.vue +++ b/web/src/views/ProjectExploreView.vue @@ -1,8 +1,10 @@