Folders + sidebar workspace index (COPLAN items 6+7)#145
Conversation
Folders are org-wide locations (max depth 3, cycle-safe): each plan
lives in at most one folder, while tags remain cross-cutting labels.
- coplan_folders table (UUID PK, self-referential parent, unique
[parent_id, name]) + nullable folder_id on coplan_plans
- CoPlan::Folder with ancestors/descendants/path, depth + cycle
validations, and delete protection for non-empty folders
- FolderPolicy: anyone can create; rename/delete is creator-or-admin
- API: GET/POST /api/v1/folders, PATCH/DELETE /api/v1/folders/:id;
PATCH /api/v1/plans/:id accepts folder_id and find-or-create
folder_path ("Team EBT/Q3"); moved_to_folder plan events
- Folder counts only include plans visible to the caller so private
brainstorm plans never leak through counts
- Agent instructions Folders section, ActiveAdmin registration, seeds
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Left sidebar: collapsible folder tree with visibility-safe plan counts (parent counts include subfolders), top tags, My plans/All scope filters, plan types, and an inline New folder form - Main pane: "Needs attention" strip (plans with unread comments), then collapsible status groups (developing, considering, live, brainstorm, abandoned) with per-group lazy "load more" turbo-frame pagination; collapsed state persists per user in localStorage and brainstorms start collapsed - Compact plan rows: title, status badge, folder breadcrumb, tags, unread badge, relative time, author avatar, muted AI-summary line - Drag a row onto a sidebar folder to move a plan (author only), with a toast on success; no-JS fallback is a "Move to folder" row menu posting to the same authorized web endpoint - Filter chips show active folder/tag/type/status filters and clear individually or all at once; ?status=, ?tag=, ?plan_type= keep working; sidebar stacks on narrow viewports - System specs cover sidebar navigation, group collapsing, HTML5 drag-and-drop, and the row-menu fallback Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Correctness: - Clamp non-positive ?page= params instead of erroring on negative OFFSET - Web folder create now rejects an unknown parent instead of silently creating a root folder (matches the API's 422 behavior) - Unknown ?folder= filter redirects with an alert (empty page for stale turbo-frame fetches) instead of silently showing everything - Cycle guards in Folder#descendants and the subtree-height depth check so bad parent data can never recurse forever - Folder.find_or_create_by_path! is transactional — no partial hierarchies left behind when a deep path fails validation - Row tag/folder breadcrumb links preserve the active ?status= filter - Unique DOM ids for each row's "Move to folder" select, label association - Fix agent-instructions curl example so $PARENT_FOLDER_ID actually expands Efficiency / reuse: - Load the folder tree once per index request; derive subtree filter ids, expanded sidebar nodes, and aggregate counts from the in-memory map (no per-node descendants queries) - New Folder.paths_by_id shared by the folders API and view helpers; helper reuses the controller-loaded tree - Bound the "Needs attention" strip to the top 5 plans (with a "+N more" link) instead of loading every plan with unread notifications - Consolidate brainstorm privacy predicates onto Plan.visible_to (search scope, tags API, references API) - Drop dead Plan.prioritized_by_status scope and orphaned index CSS - Folder DnD passes the move URL through the drag payload; drop unused target and controller state Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4f8bfb31eb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| render json: { error: "Unknown folder_id" }, status: :unprocessable_content unless folder | ||
| folder | ||
| elsif params[:folder_path].present? | ||
| Folder.find_or_create_by_path!(params[:folder_path], created_by_user: current_user) |
There was a problem hiding this comment.
Wrap folder_path creation in the plan update transaction
When a PATCH combines a new folder_path with another invalid attribute, such as status: "bogus" or a blank title, this call creates the folder hierarchy before @plan.update! later raises and returns 422. Because the update action is not wrapped in a transaction, those shared folders remain even though the plan was not moved; please create/resolve the path inside the same transaction as the plan update or validate the rest of the update first.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
🤖 Reply from Hampton's AI agent (Claude Fable).
Confirmed — a PATCH combining folder_path with an invalid attribute created the folder hierarchy and then 422'd, stranding shared folders for a move that never happened. Fixed in b16764d: folder resolution (find_or_create_by_path!), the plan update, and tag assignment now run in one transaction, so a RecordInvalid from any part rolls everything back and still renders the existing 422. New request spec: folder_path: "New Team/Sub" + status: "bogus" → 422, plan unmoved, Folder.count == 0. Full suite green (1027 examples).
| direct_counts = Plan.visible_to(current_user) | ||
| .where.not(folder_id: nil) | ||
| .group(:folder_id) | ||
| .count |
There was a problem hiding this comment.
Scope folder counts to the selected workspace
In the default mine workspace, the main query is narrowed to created_by_user: current_user before applying the folder filter, but these sidebar counts are computed across all plans visible to the user. A folder containing only another user's published plan therefore shows a nonzero count in My plans and then opens to an empty list because the folder links preserve/default to the same scope; please derive direct_counts from the active scope or make the folder link switch to scope=all.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
🤖 Reply from Hampton's AI agent (Claude Fable).
Good catch — and tags had the same mismatch, so both are fixed in b16764d. Extracted a scoped_plans_base (mine → created_by_user: current_user, all → Plan.visible_to(current_user)) that the main-pane list, sidebar folder counts, and sidebar top-tags query now all share, with brainstorm-privacy filtering preserved underneath. A folder holding only another user's published plan now shows count 0 in My Plans (folders always render, matching the existing empty-folder display rule) and the real count in scope=all; other users' tags no longer appear in the mine-scope sidebar. New specs cover both scopes, and two existing cross-user sidebar privacy specs were moved to scope=all to preserve their original intent.
- API PATCH /plans/:id now resolves folder_path and applies the plan update in one transaction, so a request combining folder_path with an invalid attribute (e.g. bad status) rolls back the created folder hierarchy instead of leaving orphaned shared folders behind. - Sidebar folder and tag counts now use the same base relation as the active workspace scope (My plans vs All), on top of the existing visible_to brainstorm privacy filter — a folder holding only other users' plans no longer shows a nonzero count in My plans that opens to an empty list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pace * origin/main: Human editing UI: markdown editor, status dropdown, tag editing (items 1+2) (#141) Perf: instant comment feedback, cached markdown rendering, quieter presence (#139) Markdown extensions: footnotes, hover definitions, collapsible sections (#142) Fix checkbox toggle targeting via sourcepos-derived line verification (#138) Skip comment template digest on writes (#137) # Conflicts: # engine/app/assets/stylesheets/coplan/application.css # engine/app/controllers/coplan/plans_controller.rb
* origin/main: Folders + sidebar workspace index (COPLAN items 6+7) (#145) Human editing UI: markdown editor, status dropdown, tag editing (items 1+2) (#141) Perf: instant comment feedback, cached markdown rendering, quieter presence (#139) Markdown extensions: footnotes, hover definitions, collapsible sections (#142) Fix checkbox toggle targeting via sourcepos-derived line verification (#138) Skip comment template digest on writes (#137) # Conflicts: # db/schema.rb # engine/app/controllers/coplan/application_controller.rb # engine/app/helpers/coplan/plan_events_helper.rb # engine/app/models/coplan/plan.rb # engine/app/views/coplan/plans/show.html.erb
Why
Two flagship gaps in CoPlan's plan browsing (COPLAN items 6 and 7):
Team EBT/Q3folder they (and their AI librarian agents) can file plans into.What
Folders (backend + API)
coplan_folderstable: UUID string PK, required name, self-referentialparent_id(max depth 3, cycle-proof),created_by_user_id, unique[parent_id, name](case-insensitive); nullablefolder_idoncoplan_plans. A plan lives in at most one folder; folders are shared org-wide.CoPlan::Folder: ancestors/descendants/path/depth, depth + cycle validations, destroy blocked while it contains plans or subfolders, transactionalfind_or_create_by_path!("Team EBT/Q3"), sharedpaths_by_idbulk path builder.GET/POST /api/v1/folders,PATCH/DELETE /api/v1/folders/:id;PATCH /api/v1/plans/:idacceptsfolder_idorfolder_path(find-or-create), blank clears. Moves are logged asmoved_to_folderplan events (before/after paths) and shown in plan history.PlanPolicy#update?); rename/delete is creator-or-admin (FolderPolicy).Brainstorm privacy (critical invariant)
Brainstorm plans are private drafts. Every folder count, folder listing, sidebar tag count, and API
plans_countgoes throughPlan.visible_to(user)— a folder containing only someone else's brainstorm renders with a count of 0 and an empty listing, so private plan existence never leaks through counts. This PR also consolidates the previously duplicated inline predicates (search scope, tags API, references API) onto that one scope./plans workspace redesign
/plans/:id/move_to_folder→ toast + refresh), with a no-JS "Move to folder" row-menu fallback. Move controls are server-rendered only for the plan's author.?status=/?plan_type=/?tag=params, and the search modal are untouched.Housekeeping
bin/rails co_plan:install:migrationsalso picked up a previously-uncopied engine migration (drop_coplan_automated_plan_reviewers) — included since the host app was missing it.Screenshots
Verified visually in the browser (dark + light themes): sidebar tree with active folder state and counts, filter chips, collapsed brainstorm group persisting across reloads, drag-and-drop move with toast, row-menu fallback, narrow-viewport stacking. (Screenshots not attached — happy to add on request.)
Testing
Full suite green on an isolated MySQL database: 1024 examples, 0 failures (953 unit/request + 71 system).
New coverage includes:
spec/models/folder_spec.rb— depth limit, cycle rejection, deletion rules,find_or_create_by_path!(incl. transactional rollback),paths_by_idspec/requests/api/v1/folders_spec.rb— CRUD, permissions, count visibility (brainstorm privacy)spec/requests/api/v1/plans_spec.rb—folder_id/folder_pathassignment, event loggingspec/requests/plans_spec.rb— folder filter incl. subfolders, filter combinations, sidebar count privacy regexes, needs-attention strip,move_to_folderendpoint (authorization, JSON + HTML), web folder creation, stale-folder redirect, page clampspec/system/folders_workspace_spec.rb— sidebar navigation, folder creation, group collapse persistence, real HTML5drag_tomove, row-menu fallback, no move controls on other users' rows🤖 Generated with Claude Code