feat: implement feature flag folders (schema, rpc, ui)#289
feat: implement feature flag folders (schema, rpc, ui)#289Sadik111 wants to merge 3 commits intodatabuddy-analytics:mainfrom
Conversation
|
@abhibhore 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 You can disable this status message by setting the Note Other AI code review bot(s) detectedCodeRabbit 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. WalkthroughThis PR introduces folder-based organization for feature flags across the entire stack. Changes include a new Changes
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 OverviewGreptile SummaryThis 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:
Critical Issue:
Architecture:
Confidence Score: 2/5
Important Files Changed
Sequence DiagramsequenceDiagram
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
|
| > | ||
| <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 |
There was a problem hiding this comment.
"mvoed" is misspelled
| 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.
apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx
Show resolved
Hide resolved
a47759e to
565f2ae
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
Additional Comments (1)
Prompt To Fix With AIThis 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>
There was a problem hiding this comment.
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.
listnow 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
folderwith 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
foldervalue (lines 418-443), but it's not included in eithercreateDataorupdateDatapayloads. 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
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (10)
apps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/types.tsapps/dashboard/app/(main)/websites/[id]/flags/page.tsxpackages/db/src/drizzle/schema.tspackages/rpc/src/routers/flags.tspackages/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.tspackages/db/src/drizzle/schema.tsapps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsxapps/dashboard/app/(main)/websites/[id]/flags/page.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsxpackages/rpc/src/routers/flags.tsapps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/types.tsapps/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 likenuqs
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 likevirtua
**/*.{ts,tsx,js,jsx}: Don't useaccessKeyattribute on any HTML element.
Don't setaria-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 thescopeprop 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 assigntabIndexto non-interactive HTML elements.
Don't use positive integers fortabIndexproperty.
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 atitleelement 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.
AssigntabIndexto non-interactive HTML elements witharia-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 atypeattribute for button elements.
Make elements with interactive roles and handlers focusable.
Give heading elements content that's accessible to screen readers (not hidden witharia-hidden).
Always include...
Files:
packages/shared/src/flags/index.tspackages/db/src/drizzle/schema.tsapps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsxapps/dashboard/app/(main)/websites/[id]/flags/page.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsxpackages/rpc/src/routers/flags.tsapps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/types.tsapps/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 setinerton dragged element and containers
Animations must be interruptible and input-driven; avoid autoplay
Files:
packages/shared/src/flags/index.tspackages/db/src/drizzle/schema.tsapps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsxapps/dashboard/app/(main)/websites/[id]/flags/page.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsxpackages/rpc/src/routers/flags.tsapps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/types.tsapps/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 bothchildrenanddangerouslySetInnerHTMLprops 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 usetarget="_blank"withoutrel="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.tspackages/db/src/drizzle/schema.tsapps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsxapps/dashboard/app/(main)/websites/[id]/flags/page.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsxpackages/rpc/src/routers/flags.tsapps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/types.tsapps/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.
Useas constinstead of literal types and type annotations.
Use eitherT[]orArray<T>consistently.
Initialize each enum member value explicitly.
Useexport typefor types.
Useimport typefor 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.tspackages/db/src/drizzle/schema.tsapps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsxapps/dashboard/app/(main)/websites/[id]/flags/page.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsxpackages/rpc/src/routers/flags.tsapps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/types.tsapps/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.tspackages/db/src/drizzle/schema.tsapps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsxapps/dashboard/app/(main)/websites/[id]/flags/page.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsxpackages/rpc/src/routers/flags.tsapps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/types.tsapps/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 ofwebpackoresbuildfor bundling
Files:
packages/shared/src/flags/index.tspackages/db/src/drizzle/schema.tsapps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsxapps/dashboard/app/(main)/websites/[id]/flags/page.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsxpackages/rpc/src/routers/flags.tsapps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/types.tsapps/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.tspackages/db/src/drizzle/schema.tsapps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsxapps/dashboard/app/(main)/websites/[id]/flags/page.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsxpackages/rpc/src/routers/flags.tsapps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/types.tsapps/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.tspackages/db/src/drizzle/schema.tspackages/rpc/src/routers/flags.tsapps/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.tspackages/db/src/drizzle/schema.tsapps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsxapps/dashboard/app/(main)/websites/[id]/flags/page.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsxpackages/rpc/src/routers/flags.tsapps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/types.tsapps/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}: Useautocompleteattribute with meaningfulname; use correcttypeandinputmode
Disable spellcheck for email/code/username inputs usingspellcheck="false"
Links are<a>or<Link>components for navigation; support Cmd/Ctrl/middle-click
Use politearia-livefor 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 viaaria-label; mark decorative elements witharia-hidden; verify in Accessibility Tree
Icon-only buttons must have descriptivearia-label
Prefer native semantics (button,a,label,table) before ARIA
Use non-breaking spaces to glue terms:10 MB,⌘ + K,Vercel SDK
Preload only above-the-fold images; lazy-load the rest
Files:
apps/dashboard/app/(main)/websites/[id]/flags/_components/rename-folder-dialog.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsxapps/dashboard/app/(main)/websites/[id]/flags/page.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsxapps/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
Usescroll-margin-topon 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.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsxapps/dashboard/app/(main)/websites/[id]/flags/page.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsxapps/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.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsxapps/dashboard/app/(main)/websites/[id]/flags/page.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsxapps/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.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsxapps/dashboard/app/(main)/websites/[id]/flags/page.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsxapps/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.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsxapps/dashboard/app/(main)/websites/[id]/flags/page.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsxapps/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.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-list-item.tsxapps/dashboard/app/(main)/websites/[id]/flags/page.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/folder-sidebar.tsxapps/dashboard/app/(main)/websites/[id]/flags/_components/delete-folder-dialog.tsxapps/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
controlandnameprops. 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
folderDatacomputation efficiently groups flags by folder in a single pass, calculating counts and separating root (uncategorized) flags. The memoization dependency onactiveFlagsis correct.
128-197: LGTM!The
renderContentfunction 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 usingFolderListItem.
202-210: LGTM!The layout correctly implements a sidebar + content pattern. The
FolderSidebaris conditionally rendered only when flags exist, preventing an empty sidebar from appearing. The responsive behavior (desktop sidebar vs mobile sheet) is handled withinFolderSidebar.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. Thetype="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.
| <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> |
There was a problem hiding this comment.
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).
| <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> |
There was a problem hiding this comment.
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.
| 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> |
There was a problem hiding this comment.
🧩 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 -5Repository: 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 -50Repository: 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 -60Repository: 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 5Repository: 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 -20Repository: 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 -40Repository: databuddy-analytics/Databuddy
Length of output: 3495
🏁 Script executed:
# Look for weight prop usage on phosphor icons
rg "weight=" | grep -i phosphor | head -10Repository: 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 -30Repository: 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 -5Repository: 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.
| <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> |
There was a problem hiding this comment.
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.
| <div | ||
| className={cn( | ||
| "border-t border-border transition-all duration-200", | ||
| isOpen ? "block" : "hidden" | ||
| )} |
There was a problem hiding this comment.
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.
| const onSubmit = (values: z.infer<typeof formSchema>) => { | ||
| if (values.name === currentName) { | ||
| onClose(); | ||
| return; | ||
| } | ||
| onConfirm(values.name); | ||
| }; |
There was a problem hiding this comment.
🧹 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.
| <FormControl> | ||
| <Input placeholder="Folder name" {...field} /> | ||
| </FormControl> |
There was a problem hiding this comment.
🧹 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.
| <div className="flex-1 overflow-y-auto"> | ||
| <Suspense fallback={<FlagsListSkeleton />}> | ||
| {renderContent()} | ||
| </Suspense> |
There was a problem hiding this comment.
🧹 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.
| <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.
| 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"], | ||
| }) | ||
| ) |
There was a problem hiding this comment.
🧩 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:
- 1: z.string() validates empty strings colinhacks/zod#2466
- 2: The new v4 string helpers seem incorrectly validate strings that should fail (or be corrected) colinhacks/zod#4754
- 3: https://zod.dev/api?utm_source=openai
- 4: Unable to Chain .min() and Other Validation Methods After .refine() on z.string() colinhacks/zod#3725
- 5: Zod Preprocess returning unknown type colinhacks/zod#3537
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.
| // 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 | ||
| ); | ||
| } |
There was a problem hiding this comment.
🧹 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.
|
provide screenshots / videos of the changes please |
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
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).
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
✏️ Tip: You can customize this high-level summary in your review settings.