Skip to content

feat: Add folder system for feature flags organization (#271)#360

Open
FraktalDeFiDAO wants to merge 1 commit intodatabuddy-analytics:mainfrom
FraktalDeFiDAO:main
Open

feat: Add folder system for feature flags organization (#271)#360
FraktalDeFiDAO wants to merge 1 commit intodatabuddy-analytics:mainfrom
FraktalDeFiDAO:main

Conversation

@FraktalDeFiDAO
Copy link

📁 Feature Flag Folders Implementation

What was added:

  • Folder organization for feature flags in dashboard UI
  • Expand/collapse folder groups with nested support
  • Folder field in flag create/edit sheet
  • Database schema with folder field and index
  • API endpoints updated to support folder operations
  • Purely UI organization (no backend business logic changes)

Technical Details:

  • Database: Added optional folder text field to flags table with index
  • API: Extended ORPC endpoints (create, update, list) to support folder field
  • UI Components:
    • folder-selector.tsx - Folder selection dropdown with create capability
    • folder-tree.tsx - Hierarchical folder tree view
    • Updated flags-list.tsx - Grouped flags by folders with expand/collapse
    • Updated flag-sheet.tsx - Added folder field to form
  • Nested folders: Supported via path separator (e.g., auth/login, checkout/payment)
  • TypeScript: Strict types throughout
  • Accessibility: Semantic button elements, proper ARIA attributes

Files Modified:

  • packages/db/src/drizzle/schema.ts - Added folder field and index
  • packages/shared/src/flags/index.ts - Added folder to schema
  • packages/rpc/src/routers/flags.ts - Updated API endpoints
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/* - UI components

Testing:

  • ✅ Lint passes (biome)
  • ✅ TypeScript types valid
  • ✅ Backward compatible (existing flags work without folders)

Closes #271

- Add folder field to flags table with database index
- Update ORPC API endpoints to support folder operations
- Add folder grouping to flags list with expand/collapse
- Add folder field to flag create/edit sheet
- Support nested folders via path separator (e.g., auth/login)
- Add folder selector component with create capability
- Purely UI organization (no backend business logic changes)
- TypeScript strict types throughout
- Accessibility improvements (semantic button elements)

Implements databuddy-analytics#271

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@vercel
Copy link

vercel bot commented Mar 23, 2026

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

A member of the Team first needs to authorize it.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 23, 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: c76eaa01-dbb3-4639-9bb0-0944a0f0af37

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 23, 2026

Greptile Summary

This PR adds a folder-based organization system for feature flags, touching the DB schema, shared Zod validation, the ORPC router, and several dashboard UI components. The core grouping logic in flags-list.tsx (the active UI path) is well-structured and backward compatible. However, there are three bugs that need attention before the feature is fully usable:

  • Cannot clear a folder once assigned — The flagFormSchema defines folder as z.string().optional() (no .nullable()), and the form's onChange passes null when the input is cleared. Zod rejects null, blocking form submission. Even if it reached the API, the update handler uses ?? (nullish coalescing) which also silently falls back to the existing folder value on null, so clearing is impossible at both layers.
  • FolderTree stale after data refreshfolder-tree.tsx computes the tree inside useState instead of useMemo([flags]), so the tree never updates when the flags prop changes.
  • FolderSelector search and create-folder flow is brokenshouldFilter={false} is set but folderOptions is never manually filtered against searchValue. As a result, the search input has no visible effect, and CommandEmpty (which contains the only "Create folder" button) is permanently hidden whenever any folder already exists.

FolderSelector and FolderTree are not yet imported or used anywhere, so the two latter bugs won't surface for end users today — but they should be fixed before the components are wired up.

Confidence Score: 3/5

  • The active folder grouping in the flags list works, but the inability to clear a folder assignment is a real user-facing bug that should be fixed before merge.
  • The DB schema and list-view grouping are solid. But there are two production bugs in active code (can't clear folder via form or API) plus two bugs in the two new components that are shipped but unused. Addressing the clear-folder issue is a focused, targeted fix but it spans the shared schema, the API router, and the form component simultaneously.
  • packages/shared/src/flags/index.ts (add .nullable() to folder), packages/rpc/src/routers/flags.ts (fix ?? to allow clearing), apps/dashboard/.../flag-sheet.tsx (onChange null vs schema), apps/dashboard/.../folder-selector.tsx (manual filtering + CommandEmpty), and apps/dashboard/.../folder-tree.tsx (useMemo instead of useState).

Important Files Changed

Filename Overview
apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-tree.tsx New component for sidebar folder tree navigation — unused by any page yet, and contains a critical bug: useState is used to derive the tree from the flags prop, so the tree goes stale on any data refresh or flag update.
apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-selector.tsx New dropdown component for choosing/creating folders — not yet wired into any page. The shouldFilter={false} + unfiltered options list means search is non-functional and CommandEmpty (which hosts the create-folder button) is unreachable when any folders already exist.
packages/rpc/src/routers/flags.ts Added folder field to create/update schemas and handlers. Update handler uses ?? which prevents folder clearing; the updateFlagSchema also lacks .nullable(), compounding the issue. Import formatting and workspace/billing refactors are purely cosmetic.
apps/dashboard/app/(main)/websites/[id]/flags/_components/flags-list.tsx Added in-memory folder tree building and FolderSection / NestedFolderSection render components. Logic is solid and uses useMemo correctly. Pluralisation bug (rules vs rule) also fixed. Minor: tree-building logic is duplicated with folder-tree.tsx.
packages/db/src/drizzle/schema.ts Added nullable folder text column and a btree index to the flags table. Change is backward compatible (column is optional). An associated Drizzle migration file should be generated and committed alongside this schema change.
packages/shared/src/flags/index.ts Added folder to flagFormSchema as z.string().optional() (missing .nullable()). Also refactored flagScheduleSchema if/else branches — functionally equivalent, just inverted the condition. The missing .nullable() on folder causes the clear-folder form bug.
apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx Added folder Input field to the flag create/edit form. The onChange sends null on clear, which conflicts with the Zod schema that doesn't accept null. All other form state changes look correct.
apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts Added `folder?: string

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User opens flag form] --> B{Creating or editing?}
    B -- Creating --> C[folder: undefined]
    B -- Editing --> D[folder: flag.folder OR undefined]

    C --> E[FlagSheet Input onChange]
    D --> E

    E -- value cleared --> F[field.onChange null]
    E -- value typed --> G[field.onChange string]

    F --> H{Zod validate flagFormSchema}
    G --> H

    H -- null ❌ schema rejects --> I[Validation error shown]
    H -- string ✅ --> J[Form submits]

    J --> K{Create or Update?}

    K -- Create --> L["folder: input.folder OR null"]
    K -- Update --> M["folder: input.folder ?? existingFlag.folder"]

    M -- input.folder = undefined --> N[Keeps existing folder]
    M -- input.folder = null --> N
    M -- input.folder = string --> O[Updates folder]

    L --> P[(DB flags table)]
    O --> P
    N --> P

    P --> Q[flags-list.tsx useMemo builds FolderGroup tree]
    Q --> R[FolderSection + NestedFolderSection rendered]
Loading

Reviews (1): Last reviewed commit: "feat: add folder system for feature flag..." | Re-trigger Greptile

Comment on lines +33 to +43
const folderTree = useState(() => {
const root: FolderNode = {
name: "Root",
path: "",
flags: [],
children: new Map(),
parent: null,
};

// Group flags by folder
for (const flag of flags) {
Copy link
Contributor

Choose a reason for hiding this comment

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

P1 useState won't track flags prop changes

The folder tree is built inside useState's initializer, which only runs once on mount. If the flags prop changes (e.g., a new flag is created, a flag is moved to a different folder, or the list is refetched), the rendered tree will be stale and won't reflect those updates.

useMemo with flags as a dependency is the correct tool here since this is derived state.

Suggested change
const folderTree = useState(() => {
const root: FolderNode = {
name: "Root",
path: "",
flags: [],
children: new Map(),
parent: null,
};
// Group flags by folder
for (const flag of flags) {
const folderTree = useMemo(() => {
const root: FolderNode = {
name: "Root",
path: "",
flags: [],
children: new Map(),
parent: null,
};
// Group flags by folder
for (const flag of flags) {

You'll also need to add useMemo to the imports at the top of the file.

variants: input.variants,
dependencies: input.dependencies,
environment: input.environment,
folder: input.folder ?? existingFlag[0].folder,
Copy link
Contributor

Choose a reason for hiding this comment

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

P1 Cannot clear a flag's folder assignment

There are two compounding issues that make it impossible to remove a flag from a folder once assigned:

  1. API level — The updateFlagSchema declares folder as z.string().max(200).optional(), which only accepts string | undefined. There is no null type accepted. Using nullish coalescing (??) here means:

    • undefined (field omitted) → fall back to the existing folder ✅
    • null → fall back to the existing folder ❌ (should clear it)
    • "" (empty string) → coerced to empty string, not cleared ❌
  2. Form level — In flag-sheet.tsx, the onChange handler sends null when the input is cleared (e.target.value || null), but the shared flagFormSchema schema doesn't have .nullable(), so Zod will reject this and the form will show a validation error, blocking submission.

To fix this, accept null in the schema and use explicit null to indicate "clear the folder":

// updateFlagSchema
folder: z.string().max(200, "Folder path too long").nullable().optional(),
// update handler
folder: input.folder !== undefined ? input.folder : existingFlag[0].folder,

Comment on lines +121 to +147
<Command shouldFilter={false}>
<CommandInput
onValueChange={setSearchValue}
placeholder="Search folders..."
value={searchValue}
/>
<CommandList>
<CommandEmpty>
{searchValue.trim() && onCreateFolder ? (
<div className="flex items-center justify-between py-2">
<span className="text-muted-foreground text-sm">
No folder found. Create one?
</span>
<Button
className="h-7 px-2"
onClick={handleCreateFolder}
size="sm"
variant="ghost"
>
<PlusIcon className="mr-1 size-3" />
Create
</Button>
</div>
) : (
"No folders found."
)}
</CommandEmpty>
Copy link
Contributor

Choose a reason for hiding this comment

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

P1 CommandEmpty is unreachable when folders exist; create-folder flow is broken

shouldFilter={false} disables cmdk's built-in filtering, but folderOptions is never manually filtered against searchValue. This means:

  1. All options are always rendered regardless of what the user types — the search input is purely decorative.
  2. CommandEmpty never renders when folderOptions.length > 0, because cmdk only shows it when zero CommandItems are visible. Since filtering is disabled, items are always visible.

This breaks the primary UX path for creating a new folder via search: the "No folder found. Create one?" button in CommandEmpty becomes completely unreachable once any folder exists. A user can only reach it on a fresh setup with zero folders.

Fix by filtering folderOptions based on searchValue before rendering:

const filteredOptions = folderOptions.filter((opt) =>
  opt.value.toLowerCase().includes(searchValue.toLowerCase())
);

Then render filteredOptions.map(...) instead of folderOptions.map(...). With this, CommandEmpty will correctly appear when the search has no matches, allowing folder creation.

Comment on lines +1 to +5
"use client";

import { Folder, FolderOpen } from "@phosphor-icons/react/dist/ssr";
import { CheckIcon, PlusIcon } from "lucide-react";
import { useMemo, useState } from "react";
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 FolderSelector and FolderTree are currently dead code

Neither folder-selector.tsx nor folder-tree.tsx is imported or used anywhere in the codebase. The folder UI in flag-sheet.tsx uses a plain <Input> instead. If these components are intended for a follow-up, consider either wiring them up in this PR or deferring them to a dedicated PR to avoid shipping untested, unreachable code.

The same applies to folder-tree.tsx — it is defined but never imported.

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.

🎯 Bounty: Feature Flag Folders for Organization

2 participants