Skip to content

feat(flags): add folder organization for feature flags#385

Open
jaysu66 wants to merge 2 commits intodatabuddy-analytics:stagingfrom
jaysu66:feat/flag-folders
Open

feat(flags): add folder organization for feature flags#385
jaysu66 wants to merge 2 commits intodatabuddy-analytics:stagingfrom
jaysu66:feat/flag-folders

Conversation

@jaysu66
Copy link
Copy Markdown

@jaysu66 jaysu66 commented Apr 6, 2026

Summary

Closes #271

Adds a folder field to feature flags so teams can organize flags into logical groups (e.g. auth/login, payments, experiments).

Changes:

  • DB schema (packages/db): new folder text column on flags table + composite index on (websiteId, folder) for efficient folder queries
  • RPC router (packages/rpc): new listFolders endpoint returning distinct folder names per website; folder field propagated through create and update handlers
  • Shared schema (packages/shared): folder added to flagFormSchema with 100-char max
  • Dashboard – flag sheet: folder text input in the create/edit form under Description
  • Dashboard – flags page: FolderNav sidebar (All Flags / named folders / Uncategorized) with active folder filtering; sidebar only renders when folders exist
  • Dashboard – flags list: folder badge (folder icon + path) shown inline per row next to the flag key

Test plan

  • Create a flag with a folder value (e.g. payments) → folder badge appears in list, sidebar shows payments entry
  • Create a flag without a folder → appears under Uncategorized in sidebar
  • Filter by folder in sidebar → only matching flags shown
  • Edit an existing flag to assign/change/clear folder → list and sidebar update
  • listFolders returns sorted distinct folder names for the website
  • bun run db:push applies the new column and index without errors

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 6, 2026

@jaysu66 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
Copy Markdown
Contributor

coderabbitai bot commented Apr 6, 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: 56411ac7-42d7-443a-a615-2dd09e78a86d

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.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Apr 6, 2026

CLA assistant check
All committers have signed the CLA.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 6, 2026

Greptile Summary

This PR adds a folder field to feature flags, backed by a new additive DB column + composite index (websiteId, folder), a listFolders RPC endpoint returning sorted distinct folder names, a text input in the flag sheet, and a FolderNav sidebar with client-side filtering (All / named folder / Uncategorized). Cache invalidation for listFolders is correctly wired through the sheet close action. All remaining findings are P2.

Confidence Score: 5/5

Safe to merge — all findings are P2 style/cleanup suggestions with no correctness or data-integrity impact.

The feature is well-scoped and additive: the DB change is a nullable column with an appropriate index, the RPC endpoint is straightforward, cache invalidation for listFolders is correctly handled via the sheet close action, and UI integration is clean. The three P2 issues (dead folderCounts variable, raw-SQL orderBy, missing min(1)) are non-blocking.

packages/shared/src/flags/index.ts (missing min(1) on folder field) and apps/dashboard/app/(main)/websites/[id]/flags/page.tsx (unused folderCounts Map).

Important Files Changed

Filename Overview
packages/db/src/drizzle/schema.ts Adds nullable folder text column and composite index (websiteId, folder) to flags — clean additive schema change.
packages/rpc/src/routers/flags.ts Adds listFolders endpoint and propagates folder through create/update; minor non-idiomatic raw-SQL orderBy.
packages/shared/src/flags/index.ts Adds folder to flagFormSchema with max(100) but missing min(1), allowing blank strings through validation.
apps/dashboard/app/(main)/websites/[id]/flags/page.tsx Adds FolderNav sidebar and client-side folder filtering; contains an unused folderCounts Map (dead code).
apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx Adds folder text input; correctly trims and normalises empty strings to null/undefined before submission.
apps/dashboard/app/(main)/websites/[id]/flags/_components/flags-list.tsx Renders folder badge (icon + path) inline per flag row when flag.folder is set — minimal, clean change.
apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts Adds `folder?: string

Sequence Diagram

sequenceDiagram
    participant U as User
    participant UI as FlagsPage
    participant FN as FolderNav
    participant FS as FlagSheet
    participant RPC as flags RPC
    participant DB as PostgreSQL

    U->>UI: Open Flags Page
    UI->>RPC: flags.list({ websiteId })
    UI->>RPC: flags.listFolders({ websiteId })
    RPC->>DB: SELECT * FROM flags WHERE ...
    RPC->>DB: SELECT DISTINCT folder FROM flags WHERE folder IS NOT NULL ORDER BY folder ASC
    DB-->>RPC: flags[] + folder[]
    RPC-->>UI: flags[] + string[]
    UI->>FN: folders, activeFolder, totalCount, uncategorizedCount
    FN-->>U: Sidebar with All Flags / named folders / Uncategorized

    U->>FN: Click named folder
    FN->>UI: setActiveFolder(folder)
    UI->>UI: filteredFlags = activeFlags.filter(f => f.folder === folder)
    UI-->>U: Filtered flag list

    U->>FS: Create / Edit flag with folder value
    FS->>RPC: flags.create / flags.update { folder }
    RPC->>DB: INSERT / UPDATE flags SET folder = ?
    DB-->>RPC: saved flag
    RPC-->>FS: success
    FS->>UI: invalidate flags.list → onCloseAction()
    UI->>UI: handleFlagSheetClose → invalidate flags.listFolders
    UI->>RPC: flags.listFolders({ websiteId })
    RPC-->>UI: updated string[]
    UI->>FN: re-render sidebar
Loading

Reviews (1): Last reviewed commit: "feat(flags): add folder organization for..." | Re-trigger Greptile

}) {
if (folders.length === 0) return null;

const folderCounts = new Map<string, number>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Unused folderCounts variable

folderCounts is allocated but never written to or read — it is dead code. This also leaves a visual inconsistency: the "All Flags" and "Uncategorized" entries show count badges, but named folder buttons do not. Either remove the variable or populate it and render counts per folder.

Suggested change
const folderCounts = new Map<string, number>();

isNotNull(flags.folder)
)
)
.orderBy(sql`${flags.folder} asc`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Non-idiomatic raw-SQL orderBy

sql`${flags.folder} asc` bypasses Drizzle's type-safe ordering helpers. Prefer asc(flags.folder) (import asc from @databuddy/db).

Suggested change
.orderBy(sql`${flags.folder} asc`);
.orderBy(asc(flags.folder));

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

.optional(),
environment: z.string().nullable().optional(),
targetGroupIds: z.array(z.string()).optional(),
folder: z.string().max(100, "Folder path too long").nullable().optional(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Missing min(1) on folder string

z.string().max(100) accepts "". The client trims and normalises empty strings to null/undefined, but a direct API caller could persist a blank folder, causing an empty entry to appear in listFolders and FolderNav. Adding .min(1) closes this gap.

Suggested change
folder: z.string().max(100, "Folder path too long").nullable().optional(),
folder: z.string().min(1).max(100, "Folder path too long").nullable().optional(),

Adds a `folder` field to feature flags so teams can organize flags into
logical groups (e.g. "auth/login", "payments"). Includes:

- DB: `folder text` column + composite index on (websiteId, folder)
- RPC: `listFolders` endpoint returning distinct folder names; folder
  propagated through create and update handlers
- Shared schema: `folder` field added to `flagFormSchema`
- Dashboard: folder input in the create/edit sheet; FolderNav sidebar
  with per-folder filtering (All / named folder / Uncategorized);
  folder badge on each row in the flags list

Closes databuddy-analytics#271
@jaysu66 jaysu66 force-pushed the feat/flag-folders branch from 1ce4fea to 5ef44f6 Compare April 6, 2026 19:37
- Add min(1) to folder schema to prevent empty strings passing validation
- Replace raw sql`...asc` with drizzle asc() helper for type safety
- Clean up unused sql import
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.

2 participants