feat: Add folder system for feature flags organization (#271)#360
feat: Add folder system for feature flags organization (#271)#360FraktalDeFiDAO wants to merge 1 commit intodatabuddy-analytics:mainfrom
Conversation
- 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>
|
@FraktalDeFiDAO is attempting to deploy a commit to the Databuddy OSS Team on Vercel. A member of the Team first needs to authorize it. |
|
|
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryThis 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
Confidence Score: 3/5
Important Files Changed
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]
Reviews (1): Last reviewed commit: "feat: add folder system for feature flag..." | Re-trigger Greptile |
| const folderTree = useState(() => { | ||
| const root: FolderNode = { | ||
| name: "Root", | ||
| path: "", | ||
| flags: [], | ||
| children: new Map(), | ||
| parent: null, | ||
| }; | ||
|
|
||
| // Group flags by folder | ||
| for (const flag of flags) { |
There was a problem hiding this comment.
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.
| 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, |
There was a problem hiding this comment.
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:
-
API level — The
updateFlagSchemadeclaresfolderasz.string().max(200).optional(), which only acceptsstring | undefined. There is nonulltype 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 ❌
-
Form level — In
flag-sheet.tsx, theonChangehandler sendsnullwhen the input is cleared (e.target.value || null), but the sharedflagFormSchemaschema 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,| <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> |
There was a problem hiding this comment.
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:
- All options are always rendered regardless of what the user types — the search input is purely decorative.
CommandEmptynever renders whenfolderOptions.length > 0, because cmdk only shows it when zeroCommandItems 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.
| "use client"; | ||
|
|
||
| import { Folder, FolderOpen } from "@phosphor-icons/react/dist/ssr"; | ||
| import { CheckIcon, PlusIcon } from "lucide-react"; | ||
| import { useMemo, useState } from "react"; |
There was a problem hiding this comment.
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.
📁 Feature Flag Folders Implementation
What was added:
Technical Details:
foldertext field to flags table with indexfolder-selector.tsx- Folder selection dropdown with create capabilityfolder-tree.tsx- Hierarchical folder tree viewflags-list.tsx- Grouped flags by folders with expand/collapseflag-sheet.tsx- Added folder field to formauth/login,checkout/payment)Files Modified:
packages/db/src/drizzle/schema.ts- Added folder field and indexpackages/shared/src/flags/index.ts- Added folder to schemapackages/rpc/src/routers/flags.ts- Updated API endpointsapps/dashboard/app/(main)/websites/[id]/flags/_components/*- UI componentsTesting:
Closes #271