Skip to content

Folders + sidebar workspace index (COPLAN items 6+7)#145

Merged
HamptonMakes merged 5 commits into
mainfrom
hampton/folders-workspace
Jul 16, 2026
Merged

Folders + sidebar workspace index (COPLAN items 6+7)#145
HamptonMakes merged 5 commits into
mainfrom
hampton/folders-workspace

Conversation

@HamptonMakes

Copy link
Copy Markdown
Collaborator

Why

Two flagship gaps in CoPlan's plan browsing (COPLAN items 6 and 7):

  1. No way to organize plans by location. Tags are cross-cutting labels, but there was no Dropbox-Paper-style "where does this plan live?" answer. Teams want a Team EBT/Q3 folder they (and their AI librarian agents) can file plans into.
  2. The /plans index didn't scale. One flat paginated list meant brainstorms drowned the author's index (COPLAN-32), filters were hidden, and there was no at-a-glance "what needs my attention".

What

Folders (backend + API)

  • coplan_folders table: UUID string PK, required name, self-referential parent_id (max depth 3, cycle-proof), created_by_user_id, unique [parent_id, name] (case-insensitive); nullable folder_id on coplan_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, transactional find_or_create_by_path!("Team EBT/Q3"), shared paths_by_id bulk path builder.
  • API: GET/POST /api/v1/folders, PATCH/DELETE /api/v1/folders/:id; PATCH /api/v1/plans/:id accepts folder_id or folder_path (find-or-create), blank clears. Moves are logged as moved_to_folder plan events (before/after paths) and shown in plan history.
  • Permissions: anyone signed in can create folders and move their own plans (PlanPolicy#update?); rename/delete is creator-or-admin (FolderPolicy).
  • Agent instructions page documents the folder endpoints and invites agents to offer to organize plans.

Brainstorm privacy (critical invariant)

Brainstorm plans are private drafts. Every folder count, folder listing, sidebar tag count, and API plans_count goes through Plan.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

  • Left sidebar: collapsible folder tree (visible-plan counts include subfolders, matching what clicking shows), "New folder" form, top tags by usage, My plans / All scope, plan types. Filters combine, render as clearable chips, and the sidebar stacks on narrow viewports.
  • Main pane: "Needs attention" strip (top 5 plans with unread comments + "N more" link), then collapsible status groups in order developing → considering → live → brainstorm → abandoned. Collapsed state persists per user in localStorage; brainstorm is collapsed by default, solving COPLAN-32 structurally. Each group has its own turbo-frame "load more" pagination, so collapsed groups never load hidden pages.
  • Rows: compact — title, status badge, folder breadcrumb, tags, unread badge, relative time, avatar, muted AI summary line.
  • Moving plans: drag a row onto a sidebar folder (HTML5 DnD → PATCH /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.
  • Onboarding banner, ?status= / ?plan_type= / ?tag= params, and the search modal are untouched.

Housekeeping

  • bin/rails co_plan:install:migrations also picked up a previously-uncopied engine migration (drop_coplan_automated_plan_reviewers) — included since the host app was missing it.
  • Third commit applies a full code-review pass: page-param clamping, unknown-parent/unknown-folder handling, cycle guards on tree walks, one-query folder tree per request, dead code + orphaned CSS removal.

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_id
  • spec/requests/api/v1/folders_spec.rb — CRUD, permissions, count visibility (brainstorm privacy)
  • spec/requests/api/v1/plans_spec.rbfolder_id / folder_path assignment, event logging
  • spec/requests/plans_spec.rb — folder filter incl. subfolders, filter combinations, sidebar count privacy regexes, needs-attention strip, move_to_folder endpoint (authorization, JSON + HTML), web folder creation, stale-folder redirect, page clamp
  • spec/system/folders_workspace_spec.rb — sidebar navigation, folder creation, group collapse persistence, real HTML5 drag_to move, row-menu fallback, no move controls on other users' rows

🤖 Generated with Claude Code

HamptonMakes and others added 3 commits July 16, 2026 10:50
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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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).

Comment on lines +298 to +301
direct_counts = Plan.visible_to(current_user)
.where.not(folder_id: nil)
.group(:folder_id)
.count

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

HamptonMakes and others added 2 commits July 16, 2026 12:59
- 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
@HamptonMakes
HamptonMakes merged commit 7450839 into main Jul 16, 2026
4 checks passed
HamptonMakes added a commit that referenced this pull request Jul 16, 2026
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant