Skip to content

feat: implement feature flag folders (schema, rpc, ui)#289

Open
Sadik111 wants to merge 3 commits intodatabuddy-analytics:mainfrom
Sadik111:feature/flag-folders
Open

feat: implement feature flag folders (schema, rpc, ui)#289
Sadik111 wants to merge 3 commits intodatabuddy-analytics:mainfrom
Sadik111:feature/flag-folders

Conversation

@Sadik111
Copy link

@Sadik111 Sadik111 commented Jan 25, 2026

This PR implements a full folder system for organizing feature flags in the dashboard, improving manageability for large projects. It includes Schema updates, RPC endpoints to handle folder operations, and a responsive UI for managing folders.

Key Changes

  1. Database & API (packages/db, packages/rpc)
    Using the existing folder column in the flags table scheme.
    Added renameFolder and deleteFolder endpoints to the flags router.
    Rename: Bulk updates the folder field for all matching flags.
    Delete: Safely moves all flags in the folder to "Uncategorized" (root) by setting folder to null (preventing data loss).

  2. Dashboard UI (apps/dashboard)
    Folder Sidebar: A clean sidebar listing all folders with their flag counts.
    Mobile Responsive: On mobile, the sidebar collapses into a drawer (Sheet) accessible via a "Folders" button.
    Actions: Added a dropdown menu to Rename or Delete folders directly from the sidebar.

Actions Dialogs:

RenameFolderDialog
: Validates folder names.
DeleteFolderDialog
: Confirms action and clarifies that flags will be moved, not deleted.

List Integration: Flags are grouped by folder in the main list view, matching the existing design system.
🎨 Design Choices
Phosphor Icons: Used
Folder
, FolderOpen, DotsThree etc. to match the strict design guidelines.
Optimistic UI: Integration with useMutation ensures the UI updates immediately after actions.
Mobile First: The sidebar uses a
Sheet

component on smaller screens to ensure the dashboard remains usable on mobile devices.

✅ Verification
Schema changes verified
API endpoints typesafe
UI matches Databuddy design system
Mobile responsive
/claim #271

Summary by CodeRabbit

  • New Features
    • Organize flags into custom folders for improved management and navigation.
    • Rename and delete folders via a new sidebar interface.
    • Navigate between folders and view flag counts per folder.
    • Flags moved from deleted folders automatically relocate to "Uncategorized."
    • Filter and display flags by folder or view all flags at once.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link

vercel bot commented Jan 25, 2026

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

CLAassistant commented Jan 25, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 25, 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.

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

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

This PR introduces folder-based organization for feature flags across the entire stack. Changes include a new folder column in the database schema, RPC endpoints for folder rename and delete operations, type definitions updated with the folder field, and new UI components for folder management (dialogs, sidebar, list items), plus a refactored main flags page with folder-aware rendering and navigation.

Changes

Cohort / File(s) Summary
Folder Management Dialogs
delete-folder-dialog.tsx, rename-folder-dialog.tsx
Two new dialog components: DeleteFolderDialog renders a confirmation dialog with messaging about moving flags to "Uncategorized", while RenameFolderDialog uses react-hook-form with zod validation for folder name input (non-empty, <50 chars, alphanumeric/hyphen/underscore).
Folder Navigation Components
folder-list-item.tsx, folder-sidebar.tsx
New components for folder UI: FolderListItem renders an accordion-style folder with count badge and collapsible children; FolderSidebar integrates folders navigation with rename/delete dropdown menus, wires mutations with cache invalidation, and supports desktop/mobile layouts.
Flag Form & Type Updates
flag-sheet.tsx, types.ts, packages/shared/src/flags/index.ts
Adds optional folder field to Flag type and FlagSheet form state with autocomplete suggestions from existing folders; schema and type definitions updated to include folder metadata. Field is not persisted to create/update mutations.
Main Flags Page
apps/dashboard/app/(main)/websites/[id]/flags/page.tsx
Refactored layout with folder-aware state management: introduces activeFolder state, folderData aggregation, two-pane layout with FolderSidebar for navigation and content area that renders flags grouped by folders via FolderListItem or single-folder view.
Database Schema
packages/db/src/drizzle/schema.ts
Adds nullable folder text column to flags table.
RPC API
packages/rpc/src/routers/flags.ts
Adds optional folder field to listFlagsSchema, createFlagSchema, and updateFlagSchema; implements renameFolder and deleteFolder protected procedures that update flag folder values and invalidate caches; list operation now filters by folder when provided.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as Rename Dialog
    participant RPC as RPC Router
    participant DB as Database
    participant Cache as Query Cache

    User->>UI: Submits new folder name
    UI->>RPC: renameFolder(oldName, newName)
    RPC->>DB: UPDATE flags SET folder=newName WHERE folder=oldName
    DB-->>RPC: Updated flag count
    RPC->>Cache: Invalidate flags for each updated flag
    Cache-->>RPC: Cache cleared
    RPC-->>UI: Success response
    UI->>User: Show success toast, close dialog
Loading
sequenceDiagram
    participant User
    participant UI as Delete Dialog
    participant RPC as RPC Router
    participant DB as Database
    participant Cache as Query Cache

    User->>UI: Confirms folder deletion
    UI->>RPC: deleteFolder(folder)
    RPC->>DB: UPDATE flags SET folder=null WHERE folder=folderName
    DB-->>RPC: Updated flag count
    RPC->>Cache: Invalidate flags for each updated flag
    Cache-->>RPC: Cache cleared
    RPC-->>UI: Success response
    UI->>User: Show success toast, close dialog
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

  • Databuddy #271 — Directly addresses feature-flag folder support implementation with schema, RPC endpoints, and dashboard UI components for folder management operations.

Possibly related PRs

  • Databuddy #236 — Modifies the flags router schema and adds multivariant/schedules support; overlaps in DB schema and RPC flags router layer.
  • Databuddy #265 — Updates flags UI surface including page layout refactoring and FlagsList component integration patterns; both PRs restructure how flags are rendered and organized.
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing a feature flag folders system across schema, RPC, and UI layers.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@dosubot
Copy link

dosubot bot commented Jan 25, 2026

Related Documentation

Checked 1 published document(s) in 1 knowledge base(s). No updates required.

How did I do? Any feedback?  Join Discord

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 25, 2026

Greptile Overview

Greptile Summary

This PR implements folder organization for feature flags with schema changes, RPC endpoints, and a responsive UI. The implementation is mostly solid but has a critical bug that prevents the feature from working.

Key Changes:

  • Added folder text column to flags table schema
  • Created renameFolder and deleteFolder RPC endpoints with proper authorization and cache invalidation
  • Built folder sidebar with mobile-responsive Sheet component using Phosphor icons
  • Integrated folder filtering and grouped display in the main flags page

Critical Issue:

  • The folder field is collected in the form but missing from the mutation payloads in flag-sheet.tsx:303-336, causing folder assignments to not persist to the database

Architecture:

  • Delete operation safely moves flags to "Uncategorized" (sets folder to null) preventing data loss
  • Rename operation bulk updates all flags in a folder with cache invalidation
  • Mobile-first design using Sheet component for smaller screens

Confidence Score: 2/5

  • This PR has a critical bug that breaks the core feature - folders cannot be saved
  • The folder field is missing from create/update mutations, meaning the entire folder feature won't work as intended. Users can select folders in the UI but those selections are never persisted. This is a blocking issue that must be fixed before merge.
  • Pay close attention to apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx - the mutation payload must include the folder field

Important Files Changed

Filename Overview
apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx Added folder field to form but missing from mutation payload - folder values won't save
packages/rpc/src/routers/flags.ts Added renameFolder and deleteFolder endpoints with proper authorization and cache invalidation
packages/db/src/drizzle/schema.ts Added folder text column to flags table, removed unused settings field from websites
apps/dashboard/app/(main)/websites/[id]/flags/page.tsx Integrated folder sidebar and grouped flag display with folder filtering logic

Sequence Diagram

sequenceDiagram
    participant User
    participant FlagSheet
    participant FolderSidebar
    participant RPC
    participant DB

    Note over User,DB: Creating/Editing Flag with Folder
    User->>FlagSheet: Fill form with folder selection
    FlagSheet->>FlagSheet: Form validation (folder in schema)
    User->>FlagSheet: Submit form
    FlagSheet->>RPC: create/update mutation (missing folder!)
    RPC->>DB: INSERT/UPDATE flags table
    DB-->>RPC: Success
    RPC-->>FlagSheet: Success response
    FlagSheet->>FlagSheet: Invalidate cache

    Note over User,DB: Renaming Folder
    User->>FolderSidebar: Click rename on folder
    FolderSidebar->>User: Show RenameFolderDialog
    User->>FolderSidebar: Enter new name & confirm
    FolderSidebar->>RPC: renameFolder mutation
    RPC->>RPC: Authorize scope
    RPC->>DB: UPDATE flags SET folder=newName WHERE folder=oldName
    DB-->>RPC: Return updated flags
    RPC->>RPC: Invalidate cache for each flag
    RPC-->>FolderSidebar: Success with count
    FolderSidebar->>FolderSidebar: Invalidate queries
    FolderSidebar-->>User: Show success toast

    Note over User,DB: Deleting Folder
    User->>FolderSidebar: Click delete on folder
    FolderSidebar->>User: Show DeleteFolderDialog
    User->>FolderSidebar: Confirm deletion
    FolderSidebar->>RPC: deleteFolder mutation
    RPC->>RPC: Authorize scope
    RPC->>DB: UPDATE flags SET folder=NULL WHERE folder=name
    DB-->>RPC: Return updated flags
    RPC->>RPC: Invalidate cache for each flag
    RPC-->>FolderSidebar: Success with count
    FolderSidebar->>FolderSidebar: Invalidate queries
    FolderSidebar-->>User: Show success toast
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

>
<div className="rounded-md bg-muted p-3 text-sm text-muted-foreground">
<p>
Flags in this folder will <strong>not</strong> be deleted. They will be mvoed to
Copy link
Contributor

Choose a reason for hiding this comment

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

"mvoed" is misspelled

Suggested change
Flags in this folder will <strong>not</strong> be deleted. They will be mvoed to
Flags in this folder will <strong>not</strong> be deleted. They will be moved to
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx
Line: 33:33

Comment:
"mvoed" is misspelled

```suggestion
                    Flags in this folder will <strong>not</strong> be deleted. They will be moved to
```

How can I resolve this? If you propose a fix, please make it concise.

@Sadik111 Sadik111 force-pushed the feature/flag-folders branch from a47759e to 565f2ae Compare January 25, 2026 07:41
@Sadik111
Copy link
Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 25, 2026

Additional Comments (1)

apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx
folder field missing from both updateData (line 303-317) and createData (line 320-335) - folder selections won't be saved. Add folder: data.folder || undefined to both objects

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx
Line: 303:336

Comment:
`folder` field missing from both `updateData` (line 303-317) and `createData` (line 320-335) - folder selections won't be saved. Add `folder: data.folder || undefined` to both objects

How can I resolve this? If you propose a fix, please make it concise.

…-sheet.tsx

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
packages/rpc/src/routers/flags.ts (2)

245-273: Cache key must include folder filter.

list now filters by folder but cache key omits it, which will serve wrong cached results across folders.

🐛 Fix cache key
-		const cacheKey = `list:${scope}:${input.status || "all"}`;
+		const folderKey = input.folder ?? "all";
+		const cacheKey = `list:${scope}:${input.status || "all"}:${folderKey}`;

522-595: Restore path ignores folder updates.

When recreating a soft-deleted flag, the restore update doesn’t apply the new folder, so the flag keeps its old folder. This makes the create flow inconsistent with the new folder feature.

✅ Apply folder during restore
 				.set({
 					name: input.name,
 					description: input.description,
 					type: input.type,
 					status: finalStatus,
 					defaultValue: input.defaultValue,
 					rules: input.rules,
 					persistAcrossAuth:
 						input.persistAcrossAuth ??
 						existingFlag[0].persistAcrossAuth ??
 						false,
 					rolloutPercentage: input.rolloutPercentage,
 					rolloutBy: input.rolloutBy,
+					folder: input.folder ?? existingFlag[0].folder ?? null,
 					variants: input.variants,
 					dependencies: input.dependencies,
 					environment: input.environment,
 					deletedAt: null,
 					updatedAt: new Date(),
 				})
packages/db/src/drizzle/schema.ts (1)

646-688: Add indexes to support folder-filtered queries by scope.

Folder-based filtering combines folder with scope columns (websiteId/organizationId/userId) in list, rename, and move operations. Without composite indexes, these queries will degrade to full table scans as the flags table grows. Add three scope-specific indexes to maintain performance.

💡 Recommended index additions
 	(table) => [
+		index("idx_flags_website_folder").using(
+			"btree",
+			table.websiteId.asc().nullsLast().op("text_ops"),
+			table.folder.asc().nullsLast().op("text_ops")
+		),
+		index("idx_flags_org_folder").using(
+			"btree",
+			table.organizationId.asc().nullsLast().op("text_ops"),
+			table.folder.asc().nullsLast().op("text_ops")
+		),
+		index("idx_flags_user_folder").using(
+			"btree",
+			table.userId.asc().nullsLast().op("text_ops"),
+			table.folder.asc().nullsLast().op("text_ops")
+		),
 		uniqueIndex("flags_key_website_unique")
packages/shared/src/flags/index.ts (1)

37-66: Trim folder names to prevent whitespace-only duplicates.

The schema accepts " " which creates distinct folders that appear identical. Add .trim().min(1) to enforce non-empty values after trimming, aligning with the form submission guideline to trim values and handle text expansion.

Suggested fix
-		folder: z.string().optional(),
+		folder: z.string().trim().min(1, "Folder name is required").optional(),
apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx (1)

303-335: Folder field is not persisted in create/update mutations.

The form collects the folder value (lines 418-443), but it's not included in either createData or updateData payloads. Users will set a folder but it won't be saved to the database.

🐛 Proposed fix to include folder in mutation payloads
 if (isEditing && flag) {
   const updateData = {
     id: flag.id,
     name: data.name,
     description: data.description,
     type: data.type,
     status: data.status,
     rules: data.rules || [],
     variants: data.variants || [],
     dependencies: data.dependencies || [],
     environment: data.environment?.trim() || undefined,
     defaultValue: data.defaultValue,
     rolloutPercentage: data.rolloutPercentage ?? 0,
     rolloutBy: data.rolloutBy || undefined,
     targetGroupIds: data.targetGroupIds || [],
+    folder: data.folder?.trim() || null,
   };
   await updateMutation.mutateAsync(updateData);
 } else {
   const createData = {
     websiteId,
     key: data.key,
     name: data.name,
     description: data.description,
     type: data.type,
     status: data.status,
     rules: data.rules || [],
     variants: data.variants || [],
     dependencies: data.dependencies || [],
     environment: data.environment?.trim() || undefined,
     defaultValue: data.defaultValue,
     rolloutPercentage: data.rolloutPercentage ?? 0,
     rolloutBy: data.rolloutBy || undefined,
     targetGroupIds: data.targetGroupIds || [],
+    folder: data.folder?.trim() || null,
   };
   await createMutation.mutateAsync(createData);
 }
🤖 Fix all issues with AI agents
In
`@apps/dashboard/app/`(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx:
- Around line 21-37: Replace the Dialog-based DeleteDialog usage with the
AlertDialog primitive: remove DeleteDialog and wrap the same content in
AlertDialog -> AlertDialogTrigger (if needed) -> AlertDialogContent, using
AlertDialogHeader/Title and AlertDialogDescription for the title/description;
move the confirm action into AlertDialogAction and the cancel into
AlertDialogCancel, passing the existing onConfirm handler to AlertDialogAction
and using onOpenChange/onClose as appropriate for AlertDialog state. Preserve
the inner warning box markup and reuse props (folderName, isDeleting,
confirmLabel) but map them to the AlertDialog equivalents (title ->
AlertDialogTitle, description -> AlertDialogDescription, confirmLabel ->
AlertDialogAction label, isDeleting -> disable or loading state on
AlertDialogAction). Ensure imports and component names (AlertDialog,
AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogTitle,
AlertDialogDescription) replace DeleteDialog and its props (isOpen, onClose,
onConfirm, title, description, confirmLabel, isDeleting, itemName).
- Around line 31-35: Update the styling in the delete-folder dialog: replace the
container class "rounded-md" with "rounded" on the div that currently has
className="rounded-md bg-muted p-3 text-sm text-muted-foreground", and add
"text-pretty" to the paragraph element (<p>) inside that div so it becomes
consistent with UI conventions; ensure you only change those class names in the
component DeleteFolderDialog (the JSX block containing the flagged message) and
keep the other classes (bg-muted, p-3, text-sm, text-muted-foreground)
unchanged.

In
`@apps/dashboard/app/`(main)/websites/[id]/flags/_components/folder-list-item.tsx:
- Around line 45-49: Remove the undesired animation by deleting "transition-all"
from the className string in the div that renders the folder list toggle (the
cn(...) call that currently uses "border-t border-border transition-all
duration-200" with isOpen ? "block" : "hidden"); keep the border and conditional
visibility classes intact (so leave "border-t border-border" and the isOpen
conditional), and remove any associated duration class if it's no longer needed.
- Around line 25-41: The container and count badge use non-compliant rounding
and missing tabular numerals: replace the outer container's "rounded-lg" with
"rounded" and update the count badge (the span rendering {count}) to use
"rounded" instead of "rounded-full" and add the "tabular-nums" class; locate
these in the FolderListItem component where the JSX renders the outer <div
className="border border-border rounded-lg..."> and the count <span
className="px-2 py-0.5 rounded-full ..."> and apply the class substitutions.
- Around line 3-38: Replace the current SSR imports and usages of CaretDown,
CaretRight, and Folder in FolderListItem: import them as named icons with an
Icon suffix from "@phosphor-icons/react" (e.g., import { CaretDown as
CaretDownIcon, CaretRight as CaretRightIcon, Folder as FolderIcon } from
"@phosphor-icons/react") and update the JSX to use CaretDownIcon,
CaretRightIcon, and FolderIcon while passing weight="duotone" (and preserving
size/className props) so the components follow the codebase naming and styling
conventions.

In
`@apps/dashboard/app/`(main)/websites/[id]/flags/_components/folder-sidebar.tsx:
- Around line 75-90: The deleteMutation currently reads state (deletingFolder)
inside onSuccess/onError which can be stale; change the callbacks to use the
mutation callback parameters (e.g. onSuccess: (data, variables) => { ... } and
onError: (error, variables) => { ... }) and reference the folder identifier and
websiteId from variables when invalidating queries, clearing deletingFolder, and
checking whether to call onSelectFolder(null) (instead of comparing activeFolder
=== deletingFolder use activeFolder === variables.<folderIdKey>), while keeping
the rest of the logic (queryClient.invalidateQueries(...), toast messages,
setDeletingFolder(null)) the same; adjust the variable key name to match the
deleteFolder input shape used by orpc.flags.deleteFolder.mutationOptions().
- Around line 97-172: FolderList is recreated on every render causing needless
re-renders; extract it into a standalone memoized component (e.g., export const
FolderList = React.memo(function FolderList({folders, activeFolder, counts,
onSelect, onRename, onDelete}) { ... })) that accepts props for folders,
activeFolder, counts, onSelect (use instead of handleSelect), onRename (call
setRenamingFolder), and onDelete (call setDeletingFolder), then replace the
inline FolderList with this component in both desktop and mobile usages so the
list only re-renders when those props change.
- Around line 56-73: The onSuccess closure for renameMutation captures
renamingFolder and can become stale; update the onSuccess handler to use the
mutation's variables (second parameter) or returned data to determine the
renamed folder instead of the outer renamingFolder variable: inside
renameMutation's onSuccess receive (_data, variables) (or use the mutation
response) to get the new and/or old folder name, use that value to decide if
activeFolder matches the renamed folder and call onSelectFolder(newName|null)
accordingly, then call queryClient.invalidateQueries(...) and
setRenamingFolder(null) and toast.success; also update onError to accept the
error param to surface error details if desired.

In
`@apps/dashboard/app/`(main)/websites/[id]/flags/_components/rename-folder-dialog.tsx:
- Around line 26-35: The formSchema's name validator currently allows
leading/trailing whitespace because the regex includes \s; update the zod schema
for name (the formSchema z.object entry) to call .trim() before the other
validators so the input is trimmed prior to .min, .max, and .regex checks,
ensuring values like " test " are rejected/normalized consistently.
- Around line 65-71: The onSubmit handler uses the raw values.name for
comparison and submission, so leading/trailing whitespace can make `" MyFolder"`
differ from `"MyFolder"`; update onSubmit to trim the input (use
values.name.trim()) before comparing to currentName and before calling
onConfirm, and if trimming yields an empty string handle it appropriately (e.g.,
treat as no-op or surface validation) so the logic in onSubmit, currentName
check, and onConfirm receive the normalized name.
- Around line 91-93: Add the autoFocus prop to the primary input in the rename
dialog so the user can start typing immediately; update the Input component
inside the RenameFolderDialog (the Input within FormControl that spreads
{...field}) to include autoFocus (e.g., <Input autoFocus ... />) ensuring the
single primary field receives focus on open.

In `@apps/dashboard/app/`(main)/websites/[id]/flags/page.tsx:
- Around line 211-214: The Suspense wrapper around the synchronous
renderContent() call is ineffective and redundant; remove the <Suspense
fallback={<FlagsListSkeleton />}> ... </Suspense> and render renderContent()
directly inside the div (keep the surrounding <div className="flex-1
overflow-y-auto">). Ensure renderContent() continues to return
<FlagsListSkeleton /> when flagsLoading is true; if you later convert any child
to a lazy component, reintroduce Suspense around that lazy component instead.

In `@packages/rpc/src/routers/flags.ts`:
- Around line 882-945: The deleteFolder handler currently invalidates cache
sequentially by awaiting invalidateFlagCache for each flag in updatedFlags,
which is slow for many flags; change this to run invalidations in parallel using
Promise.allSettled over updatedFlags.map(...) so all invalidateFlagCache calls
for the flags (using flag.id, input.websiteId, input.organizationId, flag.key)
are started concurrently and awaited as a group; ensure you keep await
Promise.allSettled(...) so failures don't short-circuit and consider logging or
handling rejected results if needed.
- Around line 839-907: The renameFolder and deleteFolder procedure inputs
currently accept empty or whitespace-only strings for folder names; update the
Zod schemas for the fields oldName and newName in renameFolder and folder in
deleteFolder to enforce non-empty trimmed values by replacing z.string() with
z.string().trim().min(1) (keeping the existing .optional() on
websiteId/organizationId and the refine scope check intact) so whitespace-only
names are rejected before handler logic and cache invalidation.
📜 Review details

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 585e3fe and f912a2b.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/page.tsx
  • packages/db/src/drizzle/schema.ts
  • packages/rpc/src/routers/flags.ts
  • packages/shared/src/flags/index.ts
🧰 Additional context used
📓 Path-based instructions (15)
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (.cursor/rules/guidelines.mdc)

**/*.{ts,tsx,js,jsx,vue}: Never block paste in <input> or <textarea> elements
Enter submits focused text input; in <textarea>, ⌘/Ctrl+Enter submits; Enter adds newline
Compatible with password managers and 2FA; allow pasting one-time codes

Files:

  • packages/shared/src/flags/index.ts
  • packages/db/src/drizzle/schema.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx
  • packages/rpc/src/routers/flags.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/guidelines.mdc)

**/*.{ts,tsx,js,jsx}: Trim values to handle text expansion and trailing spaces in form submissions
URL reflects state (deep-link filters/tabs/pagination/expanded panels); prefer libraries like nuqs
Back/Forward buttons restore scroll position
Delay first tooltip in a group; subsequent peers have no delay
Use locale-aware formatting for dates, times, numbers, and currency
Batch layout reads/writes; avoid unnecessary reflows/repaints
Virtualize large lists using libraries like virtua

**/*.{ts,tsx,js,jsx}: Don't use accessKey attribute on any HTML element.
Don't set aria-hidden="true" on focusable elements.
Don't add ARIA roles, states, and properties to elements that don't support them.
Don't use distracting elements like <marquee> or <blink>.
Only use the scope prop on <th> elements.
Don't assign non-interactive ARIA roles to interactive HTML elements.
Make sure label elements have text content and are associated with an input.
Don't assign interactive ARIA roles to non-interactive HTML elements.
Don't assign tabIndex to non-interactive HTML elements.
Don't use positive integers for tabIndex property.
Don't include "image", "picture", or "photo" in img alt prop.
Don't use explicit role property that's the same as the implicit/default role.
Make static elements with click handlers use a valid role attribute.
Always include a title element for SVG elements.
Give all elements requiring alt text meaningful information for screen readers.
Make sure anchors have content that's accessible to screen readers.
Assign tabIndex to non-interactive HTML elements with aria-activedescendant.
Include all required ARIA attributes for elements with ARIA roles.
Make sure ARIA properties are valid for the element's supported roles.
Always include a type attribute for button elements.
Make elements with interactive roles and handlers focusable.
Give heading elements content that's accessible to screen readers (not hidden with aria-hidden).
Always include...

Files:

  • packages/shared/src/flags/index.ts
  • packages/db/src/drizzle/schema.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx
  • packages/rpc/src/routers/flags.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx
**/*.{ts,tsx,js,jsx,css,scss,sass,less}

📄 CodeRabbit inference engine (.cursor/rules/guidelines.mdc)

**/*.{ts,tsx,js,jsx,css,scss,sass,less}: During drag operations, disable text selection and set inert on dragged element and containers
Animations must be interruptible and input-driven; avoid autoplay

Files:

  • packages/shared/src/flags/index.ts
  • packages/db/src/drizzle/schema.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx
  • packages/rpc/src/routers/flags.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx
**/*.{ts,tsx,jsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{ts,tsx,jsx}: Use semantic elements instead of role attributes in JSX.
Don't use unnecessary fragments.
Don't pass children as props.
Don't use the return value of React.render.
Make sure all dependencies are correctly specified in React hooks.
Make sure all React hooks are called from the top level of component functions.
Don't forget key props in iterators and collection literals.
Don't destructure props inside JSX components in Solid projects.
Don't define React components inside other components.
Don't use event handlers on non-interactive elements.
Don't assign to React component props.
Don't use both children and dangerouslySetInnerHTML props on the same element.
Don't use dangerous JSX props.
Don't use Array index in keys.
Don't insert comments as text nodes.
Don't assign JSX properties multiple times.
Don't add extra closing tags for components without children.
Use <>...</> instead of <Fragment>...</Fragment>.
Watch out for possible "wrong" semicolons inside JSX elements.
Make sure void (self-closing) elements don't have children.
Don't use target="_blank" without rel="noopener".
Don't use <img> elements in Next.js projects.
Don't use <head> elements in Next.js projects.

Files:

  • packages/shared/src/flags/index.ts
  • packages/db/src/drizzle/schema.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx
  • packages/rpc/src/routers/flags.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{ts,tsx}: Don't use primitive type aliases or misleading types.
Don't use empty type parameters in type aliases and interfaces.
Don't use any or unknown as type constraints.
Don't return a value from a function with the return type 'void'.
Don't use the TypeScript directive @ts-ignore.
Don't use TypeScript enums.
Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
Don't use TypeScript namespaces.
Don't use non-null assertions with the ! postfix operator.
Don't use parameter properties in class constructors.
Don't use user-defined types.
Use as const instead of literal types and type annotations.
Use either T[] or Array<T> consistently.
Initialize each enum member value explicitly.
Use export type for types.
Use import type for types.
Make sure all enum members are literal values.
Don't use TypeScript const enum.
Don't declare empty interfaces.
Don't let variables evolve into any type through reassignments.
Don't use the any type.
Don't misuse the non-null assertion operator (!) in TypeScript files.
Don't use implicit any type on variable declarations.
Don't merge interfaces and classes unsafely.
Don't use overload signatures that aren't next to each other.
Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
Use consistent accessibility modifiers on class properties and methods.
Use function types instead of object types with call signatures.
Don't use void type outside of generic or return types.

**/*.{ts,tsx}: Do NOT use types 'any', 'unknown' or 'never'. Use proper explicit types
Suffix functions with 'Action' in types, like 'type Test = { testAction }'

Files:

  • packages/shared/src/flags/index.ts
  • packages/db/src/drizzle/schema.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx
  • packages/rpc/src/routers/flags.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx
!(**/pages/_document.{ts,tsx,jsx})**/*.{ts,tsx,jsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

Don't import next/document outside of pages/_document.jsx in Next.js projects.

Files:

  • packages/shared/src/flags/index.ts
  • packages/db/src/drizzle/schema.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx
  • packages/rpc/src/routers/flags.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx
**/*.{ts,tsx,html,css}

📄 CodeRabbit inference engine (.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc)

Use bun build <file.html|file.ts|file.css> instead of webpack or esbuild for bundling

Files:

  • packages/shared/src/flags/index.ts
  • packages/db/src/drizzle/schema.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx
  • packages/rpc/src/routers/flags.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/01-MUST-DO.mdc)

**/*.{js,jsx,ts,tsx}: Split off components, utils, and reusable code to ensure better loading speed and less complexity
Use lower-case-like-this naming convention for variables, functions, and identifiers
NEVER add placeholders, mock data, or anything similar to production code
Use Dayjs for date handling, NEVER use date-fns. Use Tanstack query for hooks, NEVER use SWR
Use json.stringify() when adding debugging code
Never use barrel exports or create index files

**/*.{js,jsx,ts,tsx}: MUST use text-balance for headings and text-pretty for body/paragraphs
MUST use tabular-nums for data tables and numeric displays
SHOULD use truncate or line-clamp for dense UI
SHOULD use size-* for square elements instead of w-* + h-*

Files:

  • packages/shared/src/flags/index.ts
  • packages/db/src/drizzle/schema.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx
  • packages/rpc/src/routers/flags.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx
**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/01-MUST-DO.mdc)

Handle complex data transformations independently of React. Keep modules decoupled from React for improved modularity and testability

Files:

  • packages/shared/src/flags/index.ts
  • packages/db/src/drizzle/schema.ts
  • packages/rpc/src/routers/flags.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts
**/*.{js,jsx,ts,tsx,css}

📄 CodeRabbit inference engine (.cursor/rules/ui-guidelines.mdc)

**/*.{js,jsx,ts,tsx,css}: MUST use Tailwind CSS defaults unless custom values already exist or are explicitly requested
SHOULD use tw-animate-css for entrance and micro-animations in Tailwind CSS
NEVER use h-screen, use h-dvh
MUST respect safe-area-inset for fixed elements
NEVER add animation unless it is explicitly requested
MUST animate only compositor props (transform, opacity)
NEVER animate layout properties (width, height, top, left, margin, padding)
SHOULD avoid animating paint properties (background, color) except for small, local UI (text, icons)
SHOULD use ease-out on entrance animations
NEVER exceed 200ms for interaction feedback animations
SHOULD respect prefers-reduced-motion in animations
NEVER introduce custom easing curves unless explicitly requested
SHOULD avoid animating large images or full-screen surfaces
NEVER modify letter-spacing (tracking-) unless explicitly requested
MUST use a fixed z-index scale (no arbitrary z-
)
NEVER animate large blur() or backdrop-filter surfaces for performance
NEVER apply will-change outside an active animation
NEVER use gradients unless explicitly requested
NEVER use purple or multicolor gradients
SHOULD use Tailwind CSS default shadow scale unless explicitly requested
SHOULD use existing theme or Tailwind CSS color tokens before introducing new ones

Files:

  • packages/shared/src/flags/index.ts
  • packages/db/src/drizzle/schema.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx
  • packages/rpc/src/routers/flags.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx
**/*.{html,htm,tsx,jsx,vue}

📄 CodeRabbit inference engine (.cursor/rules/guidelines.mdc)

**/*.{html,htm,tsx,jsx,vue}: Use autocomplete attribute with meaningful name; use correct type and inputmode
Disable spellcheck for email/code/username inputs using spellcheck="false"
Links are <a> or <Link> components for navigation; support Cmd/Ctrl/middle-click
Use polite aria-live for toasts and inline validation feedback
Autofocus on desktop when there's a single primary input; rarely on mobile to avoid layout shift
<title> tag must match current context
Use redundant status cues (not color-only); icon-only elements have text labels
Use accurate accessible names via aria-label; mark decorative elements with aria-hidden; verify in Accessibility Tree
Icon-only buttons must have descriptive aria-label
Prefer native semantics (button, a, label, table) before ARIA
Use non-breaking spaces to glue terms: 10&nbsp;MB, ⌘&nbsp;+&nbsp;K, Vercel&nbsp;SDK
Preload only above-the-fold images; lazy-load the rest

Files:

  • apps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx
**/*.{css,scss,sass,less,html,htm,tsx,jsx,vue}

📄 CodeRabbit inference engine (.cursor/rules/guidelines.mdc)

**/*.{css,scss,sass,less,html,htm,tsx,jsx,vue}: No dead zones on checkboxes/radios; label and control share one generous hit target
Use scroll-margin-top on headings for anchored links; include 'Skip to content' link; use hierarchical <h1>–<h6>
Prevent Cumulative Layout Shift (CLS) from images by using explicit dimensions or reserved space

Files:

  • apps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx
**/*.{tsx,jsx,vue}

📄 CodeRabbit inference engine (.cursor/rules/guidelines.mdc)

Don't ship the schema—visuals may omit labels but accessible names must still exist

Files:

  • apps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (.cursor/rules/guidelines.mdc)

Prefer uncontrolled inputs; make controlled loops cheap (optimize keystroke cost)

Files:

  • apps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx
**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/01-MUST-DO.mdc)

**/*.{jsx,tsx}: When using 'text-right' utility class, always add 'text-balance' to prevent poor text layout
Think about mobile responsiveness when doing any UI tasks or changes
Always use 'rounded' Tailwind class, never 'rounded-xl' or 'rounded-md'
Don't use lucide for icons, ONLY use phosphor icons. Use width='duotone' for most icons, use fill for arrows, and don't add width attribute for plus icons
Decouple state management, data transformations, and API interactions from the React lifecycle in View Components
Simplify data flow to eliminate prop drilling and callback hell in components
Prioritize modularity and testability in all components
ALWAYS use error boundaries properly in React applications
Use 'Icon' suffix at the end of phosphor react icon imports, like CaretIcon not Caret. This is the default import, NOT as a named import
Almost NEVER use useEffect unless it's critical

**/*.{jsx,tsx}: MUST use motion/react (formerly framer-motion) when JavaScript animation is required
MUST use cn utility (clsx + tailwind-merge) for class logic
MUST use accessible component primitives for anything with keyboard or focus behavior (Base UI, React Aria, Radix)
SHOULD prefer Base UI for new primitives if compatible with the stack
MUST add an aria-label to icon-only buttons
NEVER rebuild keyboard or focus behavior by hand unless explicitly requested
MUST use an AlertDialog for destructive or irreversible actions
SHOULD use structural skeletons for loading states
MUST show errors next to where the action happens
NEVER block paste in input or textarea elements
MUST pause looping animations when off-screen
NEVER use useEffect for anything that can be expressed as render logic
MUST give empty states one clear next action

Files:

  • apps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx
🧠 Learnings (3)
📚 Learning: 2026-01-13T16:17:21.615Z
Learnt from: akash3444
Repo: databuddy-analytics/Databuddy PR: 266
File: apps/dashboard/app/(main)/websites/[id]/map/page.tsx:177-177
Timestamp: 2026-01-13T16:17:21.615Z
Learning: In dashboard TSX files under apps/dashboard (e.g., pages using CardHeader from `@/components/ui/card`), when you need to override padding and normal utility classes like pb-2 aren’t applying due to CSS specificity conflicts with internal component styles, you may need to use a highly specific selector such as [.border-b]:pb-2 to ensure the override takes effect. This should be used judiciously and only where necessary to preserve readability and maintainability. Consider documenting the specific override per component and, where possible, prefer adjusting the component's API or using more explicit class compositions to minimize reliance on highly specific selectors.

Applied to files:

  • apps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx
  • apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx
📚 Learning: 2025-12-25T14:32:46.935Z
Learnt from: CR
Repo: databuddy-analytics/Databuddy PR: 0
File: .cursor/rules/01-MUST-DO.mdc:0-0
Timestamp: 2025-12-25T14:32:46.935Z
Learning: Applies to **/*.{jsx,tsx} : Use 'Icon' suffix at the end of phosphor react icon imports, like CaretIcon not Caret. This is the default import, NOT as a named import

Applied to files:

  • apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsx
📚 Learning: 2026-01-14T15:54:48.013Z
Learnt from: CR
Repo: databuddy-analytics/Databuddy PR: 0
File: .cursor/rules/ui-guidelines.mdc:0-0
Timestamp: 2026-01-14T15:54:48.013Z
Learning: Applies to **/*.{jsx,tsx} : MUST use an AlertDialog for destructive or irreversible actions

Applied to files:

  • apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx
🧬 Code graph analysis (6)
apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsx (1)
apps/dashboard/lib/utils.ts (1)
  • cn (5-7)
apps/dashboard/app/(main)/websites/[id]/flags/page.tsx (2)
apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsx (1)
  • FolderListItem (16-57)
apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx (1)
  • FolderSidebar (42-235)
apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx (4)
apps/dashboard/lib/orpc.ts (1)
  • orpc (38-38)
apps/dashboard/lib/utils.ts (1)
  • cn (5-7)
apps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsx (1)
  • RenameFolderDialog (45-117)
apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx (1)
  • DeleteFolderDialog (13-39)
packages/rpc/src/routers/flags.ts (2)
packages/db/src/drizzle/schema.ts (1)
  • flags (646-717)
packages/shared/src/flags/utils.ts (2)
  • getScopeCondition (58-73)
  • invalidateFlagCache (24-56)
apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx (1)
apps/dashboard/components/ui/delete-dialog.tsx (1)
  • DeleteDialog (29-113)
apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx (1)
apps/dashboard/components/ui/autocomplete-input.tsx (1)
  • AutocompleteInput (15-116)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🔇 Additional comments (7)
apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts (1)

8-33: LGTM — folder field and operator union updates align with new folder feature.

Also applies to: 39-46

apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx (1)

418-443: LGTM!

The folder FormField is properly wired with control and name props. The AutocompleteInput correctly derives unique folder suggestions from existing flags, handles empty values, and is properly connected to the form state.

apps/dashboard/app/(main)/websites/[id]/flags/page.tsx (3)

95-118: LGTM!

The folderData computation efficiently groups flags by folder in a single pass, calculating counts and separating root (uncategorized) flags. The memoization dependency on activeFlags is correct.


128-197: LGTM!

The renderContent function cleanly handles the different view states (loading, empty, single folder, all folders with grouping). The logic correctly renders uncategorized flags separately and groups the rest by folder using FolderListItem.


202-210: LGTM!

The layout correctly implements a sidebar + content pattern. The FolderSidebar is conditionally rendered only when flags exist, preventing an empty sidebar from appearing. The responsive behavior (desktop sidebar vs mobile sheet) is handled within FolderSidebar.

apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsx (2)

144-153: LGTM!

The dropdown trigger button correctly includes a screen-reader-only label (<span className="sr-only">Folder options</span>), making it accessible. The type="button" attribute is properly set.


174-200: LGTM!

The component correctly implements responsive design with a desktop sidebar (hidden md:block) and a mobile sheet drawer (md:hidden). The mobile trigger button displays the current selection for context.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +21 to +37
<DeleteDialog
isOpen={isOpen}
onClose={onClose}
onConfirm={onConfirm}
title="Delete Folder"
description={`Are you sure you want to delete the folder "${folderName}"?`}
confirmLabel="Delete Folder"
isDeleting={isDeleting}
itemName={folderName}
>
<div className="rounded-md bg-muted p-3 text-sm text-muted-foreground">
<p>
Flags in this folder will <strong>not</strong> be deleted. They will be moved to
"Uncategorized".
</p>
</div>
</DeleteDialog>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use AlertDialog for destructive actions.

Guidelines require an AlertDialog for destructive/irreversible actions. DeleteDialog wraps Dialog, so this should be switched to an AlertDialog-based primitive.

🤖 Prompt for AI Agents
In
`@apps/dashboard/app/`(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx
around lines 21 - 37, Replace the Dialog-based DeleteDialog usage with the
AlertDialog primitive: remove DeleteDialog and wrap the same content in
AlertDialog -> AlertDialogTrigger (if needed) -> AlertDialogContent, using
AlertDialogHeader/Title and AlertDialogDescription for the title/description;
move the confirm action into AlertDialogAction and the cancel into
AlertDialogCancel, passing the existing onConfirm handler to AlertDialogAction
and using onOpenChange/onClose as appropriate for AlertDialog state. Preserve
the inner warning box markup and reuse props (folderName, isDeleting,
confirmLabel) but map them to the AlertDialog equivalents (title ->
AlertDialogTitle, description -> AlertDialogDescription, confirmLabel ->
AlertDialogAction label, isDeleting -> disable or loading state on
AlertDialogAction). Ensure imports and component names (AlertDialog,
AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogTitle,
AlertDialogDescription) replace DeleteDialog and its props (isOpen, onClose,
onConfirm, title, description, confirmLabel, isDeleting, itemName).

Comment on lines +31 to +35
<div className="rounded-md bg-muted p-3 text-sm text-muted-foreground">
<p>
Flags in this folder will <strong>not</strong> be deleted. They will be moved to
"Uncategorized".
</p>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Align styling with UI conventions.

Use rounded (not rounded-md) and add text-pretty for paragraph text.

🎨 Style tweak
-			<div className="rounded-md bg-muted p-3 text-sm text-muted-foreground">
+			<div className="rounded bg-muted p-3 text-sm text-pretty text-muted-foreground">
🤖 Prompt for AI Agents
In
`@apps/dashboard/app/`(main)/websites/[id]/flags/_components/delete-folder-dialog.tsx
around lines 31 - 35, Update the styling in the delete-folder dialog: replace
the container class "rounded-md" with "rounded" on the div that currently has
className="rounded-md bg-muted p-3 text-sm text-muted-foreground", and add
"text-pretty" to the paragraph element (<p>) inside that div so it becomes
consistent with UI conventions; ensure you only change those class names in the
component DeleteFolderDialog (the JSX block containing the flagged message) and
keep the other classes (bg-muted, p-3, text-sm, text-muted-foreground)
unchanged.

Comment on lines +3 to +38
import { CaretDown } from "@phosphor-icons/react/dist/ssr/CaretDown";
import { CaretRight } from "@phosphor-icons/react/dist/ssr/CaretRight";
import { Folder } from "@phosphor-icons/react/dist/ssr/Folder";
import { useState } from "react";
import { cn } from "@/lib/utils";

interface FolderListItemProps {
name: string;
count: number;
children: React.ReactNode;
defaultOpen?: boolean;
}

export function FolderListItem({
name,
count,
children,
defaultOpen = true,
}: FolderListItemProps) {
const [isOpen, setIsOpen] = useState(defaultOpen);

return (
<div className="border border-border rounded-lg overflow-hidden mb-4">
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="w-full flex items-center justify-between px-4 py-3 bg-card hover:bg-muted/50 transition-colors"
>
<div className="flex items-center gap-2">
{isOpen ? (
<CaretDown size={14} className="text-muted-foreground" />
) : (
<CaretRight size={14} className="text-muted-foreground" />
)}
<Folder size={18} className="text-muted-foreground" />
<span className="font-medium text-sm">{name}</span>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check the phosphor icons package to understand SSR import patterns
fd -e "json" -e "ts" -e "tsx" | head -20 | xargs grep -l "phosphor-icons" | head -5

Repository: databuddy-analytics/Databuddy

Length of output: 85


🏁 Script executed:

# Search for phosphor icon usage patterns in the codebase
rg "@phosphor-icons/react" -A 2 -B 1 | head -50

Repository: databuddy-analytics/Databuddy

Length of output: 2719


🏁 Script executed:

# Look at the full file to see the complete context
cat -n apps/dashboard/app/\(main\)/websites/\[id\]/flags/_components/folder-list-item.tsx 2>/dev/null || find . -name "folder-list-item.tsx" -type f -exec cat -n {} \;

Repository: databuddy-analytics/Databuddy

Length of output: 2393


🏁 Script executed:

# Search for phosphor icon imports to see the actual patterns used
rg "import.*Icon.*from.*@phosphor-icons" -A 1 | head -60

Repository: databuddy-analytics/Databuddy

Length of output: 3961


🏁 Script executed:

# Check the phosphor icons package structure in node_modules or lock file
rg "@phosphor-icons/react" package.json -A 5

Repository: databuddy-analytics/Databuddy

Length of output: 326


🏁 Script executed:

# Look for SSR-specific phosphor imports in the codebase
rg "from.*@phosphor-icons/react/dist/ssr" | head -20

Repository: databuddy-analytics/Databuddy

Length of output: 2735


🏁 Script executed:

# Check how SSR icons are being used elsewhere in the codebase
rg "from.*@phosphor-icons/react/dist/ssr" -A 3 | head -40

Repository: databuddy-analytics/Databuddy

Length of output: 3495


🏁 Script executed:

# Look for weight prop usage on phosphor icons
rg "weight=" | grep -i phosphor | head -10

Repository: databuddy-analytics/Databuddy

Length of output: 55


🏁 Script executed:

# Verify the actual import pattern in the codebase more thoroughly
rg "import.*Icon.*from.*@phosphor-icons/react" | head -30

Repository: databuddy-analytics/Databuddy

Length of output: 3314


🏁 Script executed:

# Check if weight prop is even supported for SSR imports
rg "weight=|weight:" | grep -i "caret\|folder" | head -5

Repository: databuddy-analytics/Databuddy

Length of output: 620


Use named Phosphor imports with Icon suffix and add weight="duotone" to icons.

The file should import icons as named imports with the Icon suffix for consistency with the codebase pattern, and add the duotone weight for styling.

✅ Suggested import/usage updates
-import { CaretDown } from "@phosphor-icons/react/dist/ssr/CaretDown";
-import { CaretRight } from "@phosphor-icons/react/dist/ssr/CaretRight";
-import { Folder } from "@phosphor-icons/react/dist/ssr/Folder";
+import { CaretDownIcon } from "@phosphor-icons/react/dist/ssr/CaretDown";
+import { CaretRightIcon } from "@phosphor-icons/react/dist/ssr/CaretRight";
+import { FolderIcon } from "@phosphor-icons/react/dist/ssr/Folder";
-						<CaretDown size={14} className="text-muted-foreground" />
+						<CaretDownIcon size={14} weight="duotone" className="text-muted-foreground" />
...
-						<CaretRight size={14} className="text-muted-foreground" />
+						<CaretRightIcon size={14} weight="duotone" className="text-muted-foreground" />
...
-					<Folder size={18} className="text-muted-foreground" />
+					<FolderIcon size={18} weight="duotone" className="text-muted-foreground" />
🤖 Prompt for AI Agents
In
`@apps/dashboard/app/`(main)/websites/[id]/flags/_components/folder-list-item.tsx
around lines 3 - 38, Replace the current SSR imports and usages of CaretDown,
CaretRight, and Folder in FolderListItem: import them as named icons with an
Icon suffix from "@phosphor-icons/react" (e.g., import { CaretDown as
CaretDownIcon, CaretRight as CaretRightIcon, Folder as FolderIcon } from
"@phosphor-icons/react") and update the JSX to use CaretDownIcon,
CaretRightIcon, and FolderIcon while passing weight="duotone" (and preserving
size/className props) so the components follow the codebase naming and styling
conventions.

Comment on lines +25 to +41
<div className="border border-border rounded-lg overflow-hidden mb-4">
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="w-full flex items-center justify-between px-4 py-3 bg-card hover:bg-muted/50 transition-colors"
>
<div className="flex items-center gap-2">
{isOpen ? (
<CaretDown size={14} className="text-muted-foreground" />
) : (
<CaretRight size={14} className="text-muted-foreground" />
)}
<Folder size={18} className="text-muted-foreground" />
<span className="font-medium text-sm">{name}</span>
<span className="px-2 py-0.5 rounded-full bg-muted text-muted-foreground text-xs">
{count}
</span>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use rounded and add tabular-nums for counts.

Guidelines require rounded (not rounded-lg/rounded-full), and numeric badges should use tabular-nums.

🎨 Suggested class updates
-		<div className="border border-border rounded-lg overflow-hidden mb-4">
+		<div className="border border-border rounded overflow-hidden mb-4">
...
-					<span className="px-2 py-0.5 rounded-full bg-muted text-muted-foreground text-xs">
+					<span className="px-2 py-0.5 rounded bg-muted text-muted-foreground text-xs tabular-nums">
🤖 Prompt for AI Agents
In
`@apps/dashboard/app/`(main)/websites/[id]/flags/_components/folder-list-item.tsx
around lines 25 - 41, The container and count badge use non-compliant rounding
and missing tabular numerals: replace the outer container's "rounded-lg" with
"rounded" and update the count badge (the span rendering {count}) to use
"rounded" instead of "rounded-full" and add the "tabular-nums" class; locate
these in the FolderListItem component where the JSX renders the outer <div
className="border border-border rounded-lg..."> and the count <span
className="px-2 py-0.5 rounded-full ..."> and apply the class substitutions.

Comment on lines +45 to +49
<div
className={cn(
"border-t border-border transition-all duration-200",
isOpen ? "block" : "hidden"
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove transition-all (animation not requested).

Guidelines forbid adding animations unless explicitly requested and disallow transition-all (can animate layout properties).

🚫 Remove transition-all
-				className={cn(
-					"border-t border-border transition-all duration-200",
-					isOpen ? "block" : "hidden"
-				)}
+				className={cn("border-t border-border", isOpen ? "block" : "hidden")}
🤖 Prompt for AI Agents
In
`@apps/dashboard/app/`(main)/websites/[id]/flags/_components/folder-list-item.tsx
around lines 45 - 49, Remove the undesired animation by deleting
"transition-all" from the className string in the div that renders the folder
list toggle (the cn(...) call that currently uses "border-t border-border
transition-all duration-200" with isOpen ? "block" : "hidden"); keep the border
and conditional visibility classes intact (so leave "border-t border-border" and
the isOpen conditional), and remove any associated duration class if it's no
longer needed.

Comment on lines +65 to +71
const onSubmit = (values: z.infer<typeof formSchema>) => {
if (values.name === currentName) {
onClose();
return;
}
onConfirm(values.name);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider trimming the folder name before comparison and submission.

The current implementation compares and submits the raw input value. Leading/trailing whitespace could cause unexpected behavior (e.g., " MyFolder" vs "MyFolder" being treated as different names).

♻️ Proposed fix
 const onSubmit = (values: z.infer<typeof formSchema>) => {
+    const trimmedName = values.name.trim();
-    if (values.name === currentName) {
+    if (trimmedName === currentName) {
         onClose();
         return;
     }
-    onConfirm(values.name);
+    onConfirm(trimmedName);
 };
🤖 Prompt for AI Agents
In
`@apps/dashboard/app/`(main)/websites/[id]/flags/_components/rename-folder-dialog.tsx
around lines 65 - 71, The onSubmit handler uses the raw values.name for
comparison and submission, so leading/trailing whitespace can make `" MyFolder"`
differ from `"MyFolder"`; update onSubmit to trim the input (use
values.name.trim()) before comparing to currentName and before calling
onConfirm, and if trimming yields an empty string handle it appropriately (e.g.,
treat as no-op or surface validation) so the logic in onSubmit, currentName
check, and onConfirm receive the normalized name.

Comment on lines +91 to +93
<FormControl>
<Input placeholder="Folder name" {...field} />
</FormControl>
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding autoFocus to the input.

Per coding guidelines, autofocus should be used when there's a single primary input. This improves UX by allowing users to immediately start typing.

♻️ Proposed enhancement
 <FormControl>
-    <Input placeholder="Folder name" {...field} />
+    <Input placeholder="Folder name" autoFocus {...field} />
 </FormControl>
🤖 Prompt for AI Agents
In
`@apps/dashboard/app/`(main)/websites/[id]/flags/_components/rename-folder-dialog.tsx
around lines 91 - 93, Add the autoFocus prop to the primary input in the rename
dialog so the user can start typing immediately; update the Input component
inside the RenameFolderDialog (the Input within FormControl that spreads
{...field}) to include autoFocus (e.g., <Input autoFocus ... />) ensuring the
single primary field receives focus on open.

Comment on lines +211 to +214
<div className="flex-1 overflow-y-auto">
<Suspense fallback={<FlagsListSkeleton />}>
{renderContent()}
</Suspense>
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Suspense boundary around synchronous render function is ineffective.

renderContent() is a synchronous function call, not a lazy component. The Suspense fallback will never be shown since the function executes immediately. The loading state is already handled by renderContent() returning <FlagsListSkeleton /> when flagsLoading is true.

♻️ Simplify by removing the unnecessary Suspense
 <div className="flex-1 overflow-y-auto">
-    <Suspense fallback={<FlagsListSkeleton />}>
-        {renderContent()}
-    </Suspense>
+    {renderContent()}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="flex-1 overflow-y-auto">
<Suspense fallback={<FlagsListSkeleton />}>
{renderContent()}
</Suspense>
<div className="flex-1 overflow-y-auto">
{renderContent()}
</div>
🤖 Prompt for AI Agents
In `@apps/dashboard/app/`(main)/websites/[id]/flags/page.tsx around lines 211 -
214, The Suspense wrapper around the synchronous renderContent() call is
ineffective and redundant; remove the <Suspense fallback={<FlagsListSkeleton
/>}> ... </Suspense> and render renderContent() directly inside the div (keep
the surrounding <div className="flex-1 overflow-y-auto">). Ensure
renderContent() continues to return <FlagsListSkeleton /> when flagsLoading is
true; if you later convert any child to a lazy component, reintroduce Suspense
around that lazy component instead.

Comment on lines +839 to +907
renameFolder: protectedProcedure
.input(
z
.object({
websiteId: z.string().optional(),
organizationId: z.string().optional(),
oldName: z.string(),
newName: z.string(),
})
.refine((data) => data.websiteId || data.organizationId, {
message: "Either websiteId or organizationId must be provided",
path: ["websiteId"],
})
)
.handler(async ({ context, input }) => {
await authorizeScope(
context,
input.websiteId,
input.organizationId,
"update"
);

const { oldName, newName } = input;

const updatedFlags = await context.db
.update(flags)
.set({
folder: newName,
updatedAt: new Date(),
})
.where(
and(
getScopeCondition(
input.websiteId,
input.organizationId,
context.user.id
),
eq(flags.folder, oldName),
isNull(flags.deletedAt)
)
)
.returning({ id: flags.id, key: flags.key });

// Invalidate cache for all affected flags
for (const flag of updatedFlags) {
await invalidateFlagCache(
flag.id,
input.websiteId,
input.organizationId,
flag.key
);
}

return { success: true, count: updatedFlags.length };
}),

deleteFolder: protectedProcedure
.input(
z
.object({
websiteId: z.string().optional(),
organizationId: z.string().optional(),
folder: z.string(),
})
.refine((data) => data.websiteId || data.organizationId, {
message: "Either websiteId or organizationId must be provided",
path: ["websiteId"],
})
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's check the actual file content at the specified lines
cat -n packages/rpc/src/routers/flags.ts | sed -n '839,907p'

Repository: databuddy-analytics/Databuddy

Length of output: 2120


🏁 Script executed:

# Check if Zod is available and what version is being used
fd package.json | head -5 | xargs grep -l "zod" | head -1 | xargs cat | grep -A 2 '"zod"'

Repository: databuddy-analytics/Databuddy

Length of output: 100


🌐 Web query:

Zod v4 string trim min validation best practices

💡 Result:

Best practices (Zod v4) for string + trim + min

1) Treat “required” as “non-empty after trimming”

z.string() accepts "", so for required text inputs you typically want:

import * as z from "zod/v4";

const requiredText = z.string().trim().min(1, { error: "Required" });

This pattern is commonly recommended specifically to avoid "" passing as a “required” string. (github.com)

2) Normalize first, then validate formats (email/url/etc.)

Zod’s string helpers (e.g. z.url()) won’t trim for you, so trim before format validation:

const website = z.string().trim().url({ error: "Invalid URL" });
const email = z.string().trim().email({ error: "Invalid email" });

Rationale: leading/trailing spaces can otherwise slip through unexpectedly. (github.com)

3) If you need effects/transforms + more validation, use .pipe()

Pipes are the intended way to validate after a transform step. (zod.dev)

const trimmedNonEmpty = z.string().trim().pipe(z.string().min(1, { error: "Required" }));

4) Prefer built-in checks (.min, .regex, .email, …) before custom .refine()

Once you move into effects/refinements, you can lose access to string-specific chain methods like .min() (common gotcha). Put .refine() last, or split with .pipe(). (github.com)

5) Optional fields: decide how to handle ""

If your UI sends "" for “unset”, decide whether it should:

  • fail: z.string().trim().min(1).optional() (empty string still fails)
  • be treated as missing (undefined): preprocess "" -> undefined, then validate

(Preprocess is useful but can affect inferred input types, so keep it deliberate/centralized.) (github.com)

Citations:


Add trim and min-length validation to folder name inputs.

oldName, newName, and folder accept empty strings and whitespace-only values, which breaks folder grouping and data organization. Add .trim().min(1) validation to all three fields.

Suggested validation
 				.object({
 					websiteId: z.string().optional(),
 					organizationId: z.string().optional(),
-					oldName: z.string(),
-					newName: z.string(),
+					oldName: z.string().trim().min(1, "Folder name is required"),
+					newName: z.string().trim().min(1, "Folder name is required"),
 				})
 				.object({
 					websiteId: z.string().optional(),
 					organizationId: z.string().optional(),
-					folder: z.string(),
+					folder: z.string().trim().min(1, "Folder name is required"),
 				})
🤖 Prompt for AI Agents
In `@packages/rpc/src/routers/flags.ts` around lines 839 - 907, The renameFolder
and deleteFolder procedure inputs currently accept empty or whitespace-only
strings for folder names; update the Zod schemas for the fields oldName and
newName in renameFolder and folder in deleteFolder to enforce non-empty trimmed
values by replacing z.string() with z.string().trim().min(1) (keeping the
existing .optional() on websiteId/organizationId and the refine scope check
intact) so whitespace-only names are rejected before handler logic and cache
invalidation.

Comment on lines +882 to +945
// Invalidate cache for all affected flags
for (const flag of updatedFlags) {
await invalidateFlagCache(
flag.id,
input.websiteId,
input.organizationId,
flag.key
);
}

return { success: true, count: updatedFlags.length };
}),

deleteFolder: protectedProcedure
.input(
z
.object({
websiteId: z.string().optional(),
organizationId: z.string().optional(),
folder: z.string(),
})
.refine((data) => data.websiteId || data.organizationId, {
message: "Either websiteId or organizationId must be provided",
path: ["websiteId"],
})
)
.handler(async ({ context, input }) => {
await authorizeScope(
context,
input.websiteId,
input.organizationId,
"update"
);

const { folder } = input;

const updatedFlags = await context.db
.update(flags)
.set({
folder: null,
updatedAt: new Date(),
})
.where(
and(
getScopeCondition(
input.websiteId,
input.organizationId,
context.user.id
),
eq(flags.folder, folder),
isNull(flags.deletedAt)
)
)
.returning({ id: flags.id, key: flags.key });

// Invalidate cache for all affected flags
for (const flag of updatedFlags) {
await invalidateFlagCache(
flag.id,
input.websiteId,
input.organizationId,
flag.key
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Batch cache invalidation for updated flags.

Sequential invalidations add avoidable latency for large folders. Use Promise.allSettled to parallelize.

⚡️ Parallel invalidations
-			for (const flag of updatedFlags) {
-				await invalidateFlagCache(
-					flag.id,
-					input.websiteId,
-					input.organizationId,
-					flag.key
-				);
-			}
+			await Promise.allSettled(
+				updatedFlags.map((flag) =>
+					invalidateFlagCache(
+						flag.id,
+						input.websiteId,
+						input.organizationId,
+						flag.key
+					)
+				)
+			);
🤖 Prompt for AI Agents
In `@packages/rpc/src/routers/flags.ts` around lines 882 - 945, The deleteFolder
handler currently invalidates cache sequentially by awaiting invalidateFlagCache
for each flag in updatedFlags, which is slow for many flags; change this to run
invalidations in parallel using Promise.allSettled over updatedFlags.map(...) so
all invalidateFlagCache calls for the flags (using flag.id, input.websiteId,
input.organizationId, flag.key) are started concurrently and awaited as a group;
ensure you keep await Promise.allSettled(...) so failures don't short-circuit
and consider logging or handling rejected results if needed.

@izadoesdev
Copy link
Member

provide screenshots / videos of the changes please

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.

3 participants