Skip to content

feat: Add folder system for feature flag organization#352

Open
promisingcoder wants to merge 2 commits intodatabuddy-analytics:mainfrom
promisingcoder:bounty/271-feature-flag-folders
Open

feat: Add folder system for feature flag organization#352
promisingcoder wants to merge 2 commits intodatabuddy-analytics:mainfrom
promisingcoder:bounty/271-feature-flag-folders

Conversation

@promisingcoder
Copy link

@promisingcoder promisingcoder commented Mar 20, 2026

Summary

Adds a folder system for organizing feature flags in the dashboard UI, as requested in #271.

Implementation: Option A (Simple String Folder Field)

Database Schema (packages/db/src/drizzle/schema.ts)

  • Added optional folder text field to flags table
  • Added idx_flags_folder index for query performance
  • Backward compatible — existing flags default to null

Shared Validation (packages/shared/src/flags/index.ts)

  • Added folder field to flagFormSchema: optional string, max 100 chars

API Router (packages/rpc/src/routers/flags.ts)

  • flags.list: Added folder filter parameter + updated cache key
  • flags.update: folder field included via schema spread
  • flags.create: Added folder to both new flag insert and soft-deleted flag restore

Dashboard UI

  • Folder Sidebar (folder-sidebar.tsx): Left sidebar showing "All Flags", "Uncategorized", and named folders with counts. Create/rename/delete folder modals.
  • Flags List (flags-list.tsx): groupByFolder prop with collapsible FolderSection components
  • Flag Sheet (flag-sheet.tsx): FolderCombobox component in create/edit form with search, suggestions, and "create new" option
  • Page (page.tsx): Sidebar layout with folder filtering state and grouped/filtered modes

Key Design Decisions

  • Purely UI organization — no business logic changes
  • Existing flags without folders appear under "Uncategorized"
  • Uses existing Databuddy UI components (Dialog, Command, Popover, etc.)
  • Mobile-responsive layout
  • Uses drizzle-kit push (no migration files) — apply with bun run db:push

Files Changed (8 files, +888/-47)

File Change
packages/db/src/drizzle/schema.ts Added folder field + index
packages/shared/src/flags/index.ts Added folder to validation schema
packages/rpc/src/routers/flags.ts Added folder filter/create/update support
apps/.../flags/_components/types.ts Added folder to Flag type
apps/.../flags/_components/folder-sidebar.tsx New — full sidebar with CRUD modals
apps/.../flags/_components/flags-list.tsx FolderSection grouped view
apps/.../flags/_components/flag-sheet.tsx FolderCombobox in create/edit form
apps/.../flags/page.tsx Sidebar layout + folder filtering

/claim #271

…ytics#271)

Adds a folder organization system to the feature flags dashboard using
Option A (simple string folder field on the flags table).

## Database
- Add optional `folder` text column to `flags` table
- Add `idx_flags_folder` btree index for query performance
- Fully backward compatible — existing flags without folders work unchanged

## API (packages/rpc, packages/shared)
- `flags.list`: add optional `folder` filter param + update cache key
- `flags.create`: persist `folder` field on new and restored flags
- `flags.update`: support `folder` updates (null clears the folder)
- `flagFormSchema`: add validated `folder` field (max 100 chars, safe chars only)

## Dashboard UI (apps/dashboard)
- **FolderSidebar** (new): collapsible left sidebar showing All Flags,
  Uncategorized, and named folders with flag counts; create/rename/delete
  folder actions with confirmation dialogs; batch-updates flags on rename/delete
- **FlagsList**: new `groupByFolder` prop renders collapsible folder sections
  when viewing all flags; uncategorized flags shown at bottom
- **FlagSheet**: Folder combobox field in create/edit form — shows existing
  folders as suggestions, supports typing a new folder name, clear button
- **FlagsPage**: flex-row layout with sidebar + content; client-side folder
  filtering; empty state when selected folder has no flags

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@vercel
Copy link

vercel bot commented Mar 20, 2026

@promisingcoder is attempting to deploy a commit to the Databuddy OSS Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 57a7013a-4c13-46a4-8de7-b3092c27558f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 20, 2026

Greptile Summary

This PR adds a folder/grouping system for feature flags, including a schema column, RPC changes, a new sidebar navigation component, and grouped list rendering. The overall design is solid — the change is purely additive (nullable column, no breaking API changes) and client-side filtering avoids extra round-trips.

Key issues found:

  • FolderCombobox popover can't be closed by re-clicking the trigger (flag-sheet.tsx ~line 97): A manual onClick={() => setOpen(true)} on the PopoverTrigger button conflicts with Radix's built-in toggle, preventing dismiss-on-click UX.
  • Folder rename/delete has no rollback on partial failure (folder-sidebar.tsx ~lines 651-684): Promise.all fans out N individual flag updates; a mid-sequence failure leaves flags split across old and new folder names with no way to recover from the UI.
  • Dead-code null branch in the list filter (flags.ts ~line 1149): listFlagsSchema.folder is typed z.string().optional() (never null), making the isNull(flags.folder) path unreachable.
  • Empty string bypasses folder validation server-side (flags.ts ~line 1132, shared/src/flags/index.ts): Neither updateFlagSchema nor flagFormSchema includes min(1), so folder: "" is accepted and stored.

Confidence Score: 3/5

  • Safe to merge after fixing the PopoverTrigger dismiss bug and the Promise.all partial-failure issue; the other issues are validation gaps and dead code that don't break existing behaviour.
  • Two P1 issues (UX-breaking popover close bug and no-rollback on folder rename/delete), two P2 issues (dead-code null branch and missing min(1) validation), but no data-loss or security risk. Flag evaluation and existing flags are completely untouched.
  • flag-sheet.tsx (PopoverTrigger onClick conflict) and folder-sidebar.tsx (Promise.all race on rename/delete) need the most attention before merging.

Important Files Changed

Filename Overview
apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx Adds FolderCombobox component and folder form field to the flag create/edit sheet. The combobox has a UX bug: a redundant onClick handler on the PopoverTrigger button prevents the user from closing the popover by clicking the trigger again.
apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx New component providing folder navigation, create/rename/delete dialogs. Folder rename and delete use Promise.all without a transaction or rollback, risking partial updates that leave flags split across old and new folder names.
apps/dashboard/app/(main)/websites/[id]/flags/_components/flags-list.tsx Adds FolderSection component for grouped-by-folder display and groupByFolder prop. Implementation is clean — collapsible sections, correct keying, and proper forwarding of dependentsMap and flagMap.
apps/dashboard/app/(main)/websites/[id]/flags/page.tsx Integrates FolderSidebar and folder-aware filtering/grouping into the page. Client-side filtering is clean; the shouldGroupByFolder logic correctly shows grouped view only when "All" is selected and there are folders.
packages/rpc/src/routers/flags.ts Adds folder to list/update schemas and the create/restore code paths. The list schema's folder filter has a dead-code null branch (schema uses z.string().optional() which never produces null), and the update schema allows empty-string folders without a min(1) guard.
packages/db/src/drizzle/schema.ts Adds nullable folder text column and a btree index on it. Additive, backward-compatible schema change. Index is appropriate for the expected query pattern.
packages/shared/src/flags/index.ts Adds folder field to flagFormSchema with character allowlist regex. The regex uses * (zero-or-more) so empty strings pass validation; a min(1) check would close this gap.
apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts Adds `folder?: string

Sequence Diagram

sequenceDiagram
    participant User
    participant FolderSidebar
    participant FlagsPage
    participant FlagsList
    participant FlagSheet
    participant FlagsRPC

    User->>FolderSidebar: Select folder / "All" / "Uncategorized"
    FolderSidebar->>FlagsPage: onSelectFolder(selection)
    FlagsPage->>FlagsPage: filteredFlags = useMemo(filter by selectedFolder)
    FlagsPage->>FlagsList: flags=filteredFlags, groupByFolder=shouldGroupByFolder
    FlagsList->>FlagsList: Render FolderSection(s) or flat list

    User->>FolderSidebar: Rename folder
    FolderSidebar->>FlagsRPC: Promise.all(update each flag with newFolder)
    Note over FolderSidebar,FlagsRPC: ⚠️ No transaction — partial failure possible
    FlagsRPC-->>FolderSidebar: results (may be partial)
    FolderSidebar->>FlagsPage: onRenamedAction(old, new)

    User->>FlagSheet: Create/Edit flag with folder
    FlagSheet->>FlagsRPC: create({ folder: data.folder?.trim() || null })
    FlagsRPC->>FlagsRPC: Insert or restore soft-deleted flag
    FlagsRPC-->>FlagSheet: created flag
Loading

Comments Outside Diff (4)

  1. apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx, line 97-98 (link)

    P1 Redundant onClick fights Popover toggle, breaks dismiss-on-click

    The PopoverTrigger asChild already merges a toggle handler (open → close, closed → open) into the button. Adding a separate onClick={() => setOpen(true)} races against it in React 18's automatic batching. When the popover is already open and the user clicks the trigger button, the Radix toggle schedules setOpen(false) while this handler schedules setOpen(true); the last update wins, so the popover stays open — there's no way to close it by clicking the trigger. Users are forced to click outside to dismiss the combobox.

    Remove the redundant onClick prop and let PopoverTrigger manage open/close:

  2. packages/rpc/src/routers/flags.ts, line 1149-1155 (link)

    P2 Unreachable null branch — dead code in the folder filter

    listFlagsSchema declares folder as z.string().optional(), which means the field is either a string or undefined; Zod will never produce null here. The input.folder === null branch in the condition below can therefore never be entered, and the isNull(flags.folder) query for "uncategorized" flags is never reachable through this schema.

    If you want callers to be able to query uncategorized flags (e.g., folder: null), the schema needs to allow it:

    Otherwise, if server-side filtering of uncategorized flags isn't needed, the === null branch and the empty-string case can be simplified to:

    if (input.folder !== undefined) {
        conditions.push(eq(flags.folder, input.folder));
    }
  3. packages/rpc/src/routers/flags.ts, line 1132 (link)

    P2 Empty string passes server-side validation for folder in update

    z.string().max(100).nullable().optional() accepts an empty string "", which would be written to the database as an empty-string folder name — a semantically invalid state. The shared flagFormSchema regex (^[a-zA-Z0-9_\-/ ]*$) also uses * (zero or more), so it allows empty strings too.

    The UI form guards against this with data.folder?.trim() || null, but a direct API call with folder: "" bypasses that protection.

    Add a min(1) constraint so the server rejects empty strings:

    The same fix should be applied to flagFormSchema in packages/shared/src/flags/index.ts.

  4. apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx, line 651-666 (link)

    P1 Promise.all rename/delete has no rollback on partial failure

    Both handleRename and handleDelete fan out individual updateMutation.mutateAsync calls via Promise.all. If any single call fails mid-way (network timeout, rate limit, etc.), the flags that already succeeded get the new name while the rest retain the old one — leaving the folder in an inconsistent split state. The catch block shows a generic error toast but makes no attempt to undo the already-applied changes.

    Consider either:

    1. Adding a server-side bulk-update RPC that runs a single WHERE folder = $old SET folder = $new UPDATE inside a transaction.
    2. If staying client-side, run updates sequentially so you can detect the first failure and surface a more actionable error indicating which flags were not updated.

    The same concern applies to handleDelete which follows the same Promise.all pattern.

Last reviewed commit: "feat: add folder sys..."

- Remove manual onClick on FolderCombobox PopoverTrigger that prevented
  closing on re-click (Radix handles toggle natively)
- Replace Promise.all with Promise.allSettled in folder rename/delete to
  handle partial failures gracefully with per-outcome toasts
- Remove dead isNull branch in listFlags folder filter (z.string().optional()
  never produces null)
- Add .min(1) validation to folder field in updateFlagSchema and flagFormSchema
  to reject empty string folder names

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants