diff --git a/apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx b/apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx index 26ec7baf3..e3bdb192a 100644 --- a/apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx +++ b/apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx @@ -50,6 +50,7 @@ import { orpc } from "@/lib/orpc"; import { cn } from "@/lib/utils"; import { GroupSelector } from "../groups/_components/group-selector"; import { DependencySelector } from "./dependency-selector"; +import { FolderSelector } from "./folder-selector"; import type { Flag, FlagSheetProps, TargetGroup } from "./types"; import { UserRulesBuilder } from "./user-rules-builder"; import { VariantEditor } from "./variant-editor"; @@ -248,6 +249,7 @@ export function FlagSheet({ dependencies: [], environment: undefined, targetGroupIds: [], + folder: undefined, }, schedule: undefined, }, @@ -290,6 +292,7 @@ export function FlagSheet({ dependencies: flag.dependencies ?? [], environment: flag.environment || undefined, targetGroupIds: extractTargetGroupIds(), + folder: flag.folder || undefined, }, schedule: undefined, }); @@ -311,6 +314,7 @@ export function FlagSheet({ variants: template.type === "multivariant" ? template.variants : [], dependencies: [], targetGroupIds: [], + folder: undefined, }, schedule: undefined, }); @@ -332,6 +336,7 @@ export function FlagSheet({ variants: [], dependencies: [], targetGroupIds: [], + folder: undefined, }, schedule: undefined, }); @@ -549,6 +554,39 @@ export function FlagSheet({ )} /> + + ( + + + Folder (optional) + + + + field.onChange(folder ?? null) + } + placeholder="Select or create a folder…" + availableFolders={ + flagsList + ?.map((f) => f.folder) + .filter((f): f is string => Boolean(f)) ?? [] + } + onCreateFolder={(newFolder) => { + field.onChange(newFolder); + }} + /> + + +

+ Organize flags into folders for better management +

+
+ )} + /> {/* Separator */} diff --git a/apps/dashboard/app/(main)/websites/[id]/flags/_components/flags-list.tsx b/apps/dashboard/app/(main)/websites/[id]/flags/_components/flags-list.tsx index 494263900..de0ea2aa3 100644 --- a/apps/dashboard/app/(main)/websites/[id]/flags/_components/flags-list.tsx +++ b/apps/dashboard/app/(main)/websites/[id]/flags/_components/flags-list.tsx @@ -2,9 +2,12 @@ import { ArchiveIcon, + CaretDownIcon, + CaretRightIcon, DotsThreeIcon, FlagIcon, FlaskIcon, + Folder, GaugeIcon, LinkIcon, PencilSimpleIcon, @@ -12,7 +15,7 @@ import { TrashIcon, } from "@phosphor-icons/react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { @@ -43,6 +46,13 @@ interface FlagsListProps { onDelete: (flagId: string) => void; } +interface FolderGroup { + name: string; + path: string; + flags: Flag[]; + children: Map; +} + const TYPE_CONFIG = { boolean: { icon: FlagIcon, label: "Boolean", color: "text-blue-500" }, rollout: { icon: GaugeIcon, label: "Rollout", color: "text-violet-500" }, @@ -359,7 +369,7 @@ function FlagRow({
{ruleCount > 0 && ( - {ruleCount} {ruleCount !== 1 ? "rules" : "rule"} + {ruleCount} {ruleCount === 1 ? "rule" : "rules"} )} {variantCount > 0 && ( @@ -427,15 +437,84 @@ export function FlagsList({ flags, groups, onEdit, onDelete }: FlagsListProps) { return map; }, [flags]); + // Group flags by folder + const folderGroups = useMemo(() => { + const root: FolderGroup = { + name: "Root", + path: "", + flags: [], + children: new Map(), + }; + + for (const flag of flags) { + if (flag.folder) { + const parts = flag.folder.split("/"); + let currentNode = root; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const currentPath = parts.slice(0, i + 1).join("/"); + + if (!currentNode.children.has(part)) { + currentNode.children.set(part, { + name: part, + path: currentPath, + flags: [], + children: new Map(), + }); + } + + const nextNode = currentNode.children.get(part); + if (nextNode) { + currentNode = nextNode; + } + } + + currentNode.flags.push(flag); + } else { + root.flags.push(flag); + } + } + + return root; + }, [flags]); + return (
- {flags.map((flag) => ( - 0 && ( +
+
+ + + Uncategorized ({folderGroups.flags.length}) + +
+ {folderGroups.flags.map((flag) => ( + + ))} +
+ )} + + {/* Folder groups */} + {Array.from(folderGroups.children.values()).map((folder) => ( + @@ -444,6 +523,154 @@ export function FlagsList({ flags, groups, onEdit, onDelete }: FlagsListProps) { ); } +function FolderSection({ + folder, + flagMap, + dependentsMap, + groups, + onEdit, + onDelete, +}: { + folder: FolderGroup; + flagMap: Map; + dependentsMap: Map; + groups: Map; + onEdit: (flag: Flag) => void; + onDelete: (flagId: string) => void; +}) { + const [isExpanded, setIsExpanded] = useState(true); + const totalFlags = countAllFlags(folder); + + return ( +
+ + + {isExpanded && ( +
+ {/* Direct flags in this folder */} + {folder.flags.map((flag) => ( + + ))} + + {/* Nested folders */} + {Array.from(folder.children.values()).map((childFolder) => ( + + ))} +
+ )} +
+ ); +} + +function NestedFolderSection({ + folder, + flagMap, + dependentsMap, + groups, + onEdit, + onDelete, +}: { + folder: FolderGroup; + flagMap: Map; + dependentsMap: Map; + groups: Map; + onEdit: (flag: Flag) => void; + onDelete: (flagId: string) => void; +}) { + const [isExpanded, setIsExpanded] = useState(true); + const totalFlags = countAllFlags(folder); + + return ( +
+ + + {isExpanded && ( +
+ {folder.flags.map((flag) => ( + + ))} + + {Array.from(folder.children.values()).map((childFolder) => ( + + ))} +
+ )} +
+ ); +} + +function countAllFlags(folder: FolderGroup): number { + let count = folder.flags.length; + for (const child of folder.children.values()) { + count += countAllFlags(child); + } + return count; +} + export function FlagsListSkeleton() { return (
diff --git a/apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-management-modal.tsx b/apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-management-modal.tsx new file mode 100644 index 000000000..7e7fa2ca4 --- /dev/null +++ b/apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-management-modal.tsx @@ -0,0 +1,374 @@ +"use client"; + +import { + CaretDownIcon, + CaretRightIcon, + DotsThreeIcon, + Folder, + FolderOpen, + PencilSimpleIcon, + PlusIcon, + TrashIcon, +} from "@phosphor-icons/react/dist/ssr"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { cn } from "@/lib/utils"; + +interface FolderNode { + name: string; + path: string; + flags: number; + children: Map; +} + +interface FolderManagementModalProps { + isOpen: boolean; + onClose: () => void; + folders: string[]; + flagCounts: Record; + onCreateFolder: (name: string) => void; + onRenameFolder: (oldPath: string, newPath: string) => void; + onDeleteFolder: (path: string) => void; +} + +export function FolderManagementModal({ + isOpen, + onClose, + folders, + flagCounts, + onCreateFolder, + onRenameFolder, + onDeleteFolder, +}: FolderManagementModalProps) { + const [newFolderName, setNewFolderName] = useState(""); + const [editingFolder, setEditingFolder] = useState(null); + const [editValue, setEditValue] = useState(""); + const [deleteConfirmPath, setDeleteConfirmPath] = useState(null); + + // Build folder tree + const folderTree = useState(() => { + const root: FolderNode = { + name: "Root", + path: "", + flags: 0, + children: new Map(), + }; + + for (const folder of folders) { + if (!folder) continue; + + const parts = folder.split("/"); + let currentNode = root; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const currentPath = parts.slice(0, i + 1).join("/"); + + if (!currentNode.children.has(part)) { + currentNode.children.set(part, { + name: part, + path: currentPath, + flags: 0, + children: new Map(), + }); + } + + const nextNode = currentNode.children.get(part); + if (nextNode) { + currentNode = nextNode; + } + } + + // Add flag count to the leaf folder + currentNode.flags = flagCounts[folder] ?? 0; + } + + return root; + })[0]; + + const handleCreateFolder = () => { + if (newFolderName.trim()) { + onCreateFolder(newFolderName.trim()); + setNewFolderName(""); + } + }; + + const handleStartEdit = (folder: FolderNode) => { + setEditingFolder(folder.path); + setEditValue(folder.name); + }; + + const handleSaveEdit = (folder: FolderNode) => { + if (editValue.trim() && editValue.trim() !== folder.name) { + const newPath = folder.path + .split("/") + .slice(0, -1) + .concat(editValue.trim()) + .join("/"); + onRenameFolder(folder.path, newPath); + } + setEditingFolder(null); + setEditValue(""); + }; + + const handleCancelEdit = () => { + setEditingFolder(null); + setEditValue(""); + }; + + const handleStartDelete = (path: string) => { + setDeleteConfirmPath(path); + }; + + const handleConfirmDelete = () => { + if (deleteConfirmPath) { + onDeleteFolder(deleteConfirmPath); + setDeleteConfirmPath(null); + } + }; + + const handleCancelDelete = () => { + setDeleteConfirmPath(null); + }; + + return ( + <> + + + + Manage Folders + + Organize your feature flags into folders. Create, rename, or + delete folders. + + + +
+ {/* Create New Folder */} +
+ setNewFolderName(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleCreateFolder(); + } + }} + /> + +
+ + {/* Folder Tree */} +
+ {folderTree.children.size === 0 ? ( +

+ No folders yet. Create one above! +

+ ) : ( +
+ {Array.from(folderTree.children.values()).map((node) => ( + + ))} +
+ )} +
+
+ + + + +
+
+ + {/* Delete Confirmation Dialog */} + + + + Delete Folder + + Are you sure you want to delete this folder? Flags in this folder + will be moved to "Uncategorized". + + + + + + + + + + ); +} + +function FolderTreeItem({ + node, + editingFolder, + editValue, + onStartEdit, + onSaveEdit, + onCancelEdit, + onEditValueChange, + onStartDelete, + level = 0, +}: { + node: FolderNode; + editingFolder: string | null; + editValue: string; + onStartEdit: (folder: FolderNode) => void; + onSaveEdit: (folder: FolderNode) => void; + onCancelEdit: () => void; + onEditValueChange: (value: string) => void; + onStartDelete: (path: string) => void; + level?: number; +}) { + const [isExpanded, setIsExpanded] = useState(true); + const hasChildren = node.children.size > 0; + const isEditing = editingFolder === node.path; + + return ( +
+
+ {hasChildren ? ( + + ) : ( + + )} + + + + {isEditing ? ( + onSaveEdit(node)} + onChange={(e) => onEditValueChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + onSaveEdit(node); + } else if (e.key === "Escape") { + onCancelEdit(); + } + }} + value={editValue} + /> + ) : ( + + {node.name} + + )} + + + {node.flags} {node.flags === 1 ? "flag" : "flags"} + + + + + + + + onStartEdit(node)} + > + + Rename + + onStartDelete(node.path)} + variant="destructive" + > + + Delete + + + +
+ + {isExpanded && node.children.size > 0 && ( +
+ {Array.from(node.children.values()).map((childNode) => ( + + ))} +
+ )} +
+ ); +} diff --git a/apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-selector.tsx b/apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-selector.tsx new file mode 100644 index 000000000..2e4e289a4 --- /dev/null +++ b/apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-selector.tsx @@ -0,0 +1,191 @@ +"use client"; + +import { Folder, FolderOpen } from "@phosphor-icons/react/dist/ssr"; +import { CheckIcon, PlusIcon } from "lucide-react"; +import { useMemo, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; + +interface FolderOption { + value: string; + label: string; + disabled?: boolean; +} + +interface FolderSelectorProps { + value?: string | null; + onChange: (folder: string | null) => void; + availableFolders?: string[]; + placeholder?: string; + className?: string; + disabled?: boolean; + onCreateFolder?: (folder: string) => void; +} + +export function FolderSelector({ + value, + onChange, + availableFolders = [], + placeholder = "Select folder...", + className, + disabled, + onCreateFolder, +}: FolderSelectorProps) { + const [open, setOpen] = useState(false); + const [searchValue, setSearchValue] = useState(""); + + // Extract unique folders and build hierarchy + const folderOptions = useMemo(() => { + const folders = new Set(); + + // Add existing folders + for (const folder of availableFolders) { + if (folder) { + folders.add(folder); + } + } + + // Add current value if not in list + if (value && !folders.has(value)) { + folders.add(value); + } + + // Convert to options with hierarchy display + const options: FolderOption[] = Array.from(folders) + .sort() + .map((folder) => ({ + value: folder, + label: folder, + })); + + return options; + }, [availableFolders, value]); + + // Get parent folders for display + const getFolderDisplay = (folder: string) => { + const parts = folder.split("/"); + if (parts.length === 1) { + return folder; + } + return parts.join(" / "); + }; + + const handleSelect = (folderValue: string) => { + onChange(folderValue === value ? null : folderValue); + setOpen(false); + }; + + const handleCreateFolder = () => { + if (searchValue.trim() && onCreateFolder) { + onCreateFolder(searchValue.trim()); + setSearchValue(""); + } + }; + + return ( + + + + + + + + + + {searchValue.trim() && onCreateFolder ? ( +
+ + No folder found. Create one? + + +
+ ) : ( + "No folders found." + )} +
+ + {/* No Folder Option */} + onChange(null)} + value="__no_folder__" + > + + No folder (root) + + + {/* Existing Folders */} + {folderOptions.map((option) => ( + handleSelect(option.value)} + value={option.value} + > + + + + {getFolderDisplay(option.value)} + + + ))} + +
+
+
+
+ ); +} diff --git a/apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-tree.tsx b/apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-tree.tsx new file mode 100644 index 000000000..7b100f35b --- /dev/null +++ b/apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-tree.tsx @@ -0,0 +1,234 @@ +"use client"; + +import { + CaretDownIcon, + CaretRightIcon, + Folder, + FolderOpen, +} from "@phosphor-icons/react/dist/ssr"; +import { useState } from "react"; +import { cn } from "@/lib/utils"; +import type { Flag } from "./types"; + +interface FolderTreeProps { + flags: Flag[]; + onFolderSelect?: (folder: string | null) => void; + selectedFolder?: string | null; +} + +interface FolderNode { + name: string; + path: string; + flags: Flag[]; + children: Map; + parent: string | null; +} + +export function FolderTree({ + flags, + onFolderSelect, + selectedFolder, +}: FolderTreeProps) { + // Build folder tree from flags + const folderTree = useState(() => { + const root: FolderNode = { + name: "Root", + path: "", + flags: [], + children: new Map(), + parent: null, + }; + + // Group flags by folder + for (const flag of flags) { + if (flag.folder) { + const parts = flag.folder.split("/"); + let currentNode = root; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const currentPath = parts.slice(0, i + 1).join("/"); + + if (!currentNode.children.has(part)) { + currentNode.children.set(part, { + name: part, + path: currentPath, + flags: [], + children: new Map(), + parent: i === 0 ? null : parts.slice(0, i).join("/"), + }); + } + + const nextNode = currentNode.children.get(part); + if (nextNode) { + currentNode = nextNode; + } + } + + currentNode.flags.push(flag); + } else { + root.flags.push(flag); + } + } + + return root; + })[0]; + + return ( +
+ {/* Root/Uncategorized flags */} + {folderTree.flags.length > 0 && ( +
+
+ + Uncategorized ({folderTree.flags.length}) + +
+ {folderTree.flags.map((flag) => ( + onFolderSelect?.(null)} + /> + ))} +
+ )} + + {/* Folder tree */} + {folderTree.children.size > 0 && ( +
+
+ + Folders + +
+ {Array.from(folderTree.children.values()).map((node) => ( + + ))} +
+ )} +
+ ); +} + +function FolderNode({ + node, + selectedFolder, + onFolderSelect, +}: { + node: FolderNode; + selectedFolder?: string | null; + onFolderSelect?: (folder: string | null) => void; +}) { + const [isExpanded, setIsExpanded] = useState(true); + const hasChildren = node.children.size > 0; + const isSelected = selectedFolder === node.path; + + const handleClick = () => { + if (hasChildren) { + setIsExpanded(!isExpanded); + } + onFolderSelect?.(node.path); + }; + + const totalFlags = countAllFlags(node); + + return ( +
+ + + {isExpanded && node.children.size > 0 && ( +
+ {Array.from(node.children.values()).map((childNode) => ( + + ))} +
+ )} + + {isExpanded && node.flags.length > 0 && ( +
+ {node.flags.map((flag) => ( + {}} + /> + ))} +
+ )} +
+ ); +} + +function FlagFolderItem({ + flag, + isSelected, + onSelect, +}: { + flag: Flag; + isSelected: boolean; + onSelect: () => void; +}) { + return ( + + ); +} + +function countAllFlags(node: FolderNode): number { + let count = node.flags.length; + for (const child of node.children.values()) { + count += countAllFlags(child); + } + return count; +} diff --git a/apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts b/apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts index 8410ed0b8..0fcbd27cd 100644 --- a/apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts +++ b/apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts @@ -27,6 +27,7 @@ export interface Flag { organizationId?: string | null; userId?: string | null; createdBy: string; + folder?: string | null; createdAt: Date; updatedAt: Date; deletedAt?: Date | null; diff --git a/apps/dashboard/app/(main)/websites/[id]/flags/page.tsx b/apps/dashboard/app/(main)/websites/[id]/flags/page.tsx index ede8eb81b..1770baccb 100644 --- a/apps/dashboard/app/(main)/websites/[id]/flags/page.tsx +++ b/apps/dashboard/app/(main)/websites/[id]/flags/page.tsx @@ -1,19 +1,22 @@ "use client"; import { GATED_FEATURES } from "@databuddy/shared/types/features"; -import { FlagIcon } from "@phosphor-icons/react/dist/ssr/Flag"; +import { FlagIcon, FolderIcon } from "@phosphor-icons/react/dist/ssr"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useAtom } from "jotai"; import { useParams } from "next/navigation"; import { Suspense, useMemo, useState } from "react"; +import { toast } from "sonner"; import { EmptyState } from "@/components/empty-state"; import { ErrorBoundary } from "@/components/error-boundary"; import { FeatureGate } from "@/components/feature-gate"; +import { Button } from "@/components/ui/button"; import { DeleteDialog } from "@/components/ui/delete-dialog"; import { orpc } from "@/lib/orpc"; import { isFlagSheetOpenAtom } from "@/stores/jotai/flagsAtoms"; import { FlagSheet } from "./_components/flag-sheet"; import { FlagsList, FlagsListSkeleton } from "./_components/flags-list"; +import { FolderManagementModal } from "./_components/folder-management-modal"; import type { Flag, TargetGroup } from "./_components/types"; export default function FlagsPage() { @@ -23,6 +26,7 @@ export default function FlagsPage() { const [isFlagSheetOpen, setIsFlagSheetOpen] = useAtom(isFlagSheetOpenAtom); const [editingFlag, setEditingFlag] = useState(null); const [flagToDelete, setFlagToDelete] = useState(null); + const [isFolderModalOpen, setIsFolderModalOpen] = useState(false); const { data: flags, isLoading: flagsLoading } = useQuery({ ...orpc.flags.list.queryOptions({ input: { websiteId } }), @@ -87,35 +91,138 @@ export default function FlagsPage() { setEditingFlag(null); }; + // Folder management + const updateFlagMutation = useMutation({ + ...orpc.flags.update.mutationOptions(), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: orpc.flags.list.key({ input: { websiteId } }), + }); + }, + }); + + // Extract unique folders and flag counts + const folderData = useMemo(() => { + const folders = new Set(); + const flagCounts: Record = {}; + + for (const flag of activeFlags) { + if (flag.folder) { + folders.add(flag.folder); + flagCounts[flag.folder] = (flagCounts[flag.folder] ?? 0) + 1; + } + } + + return { + folders: Array.from(folders).sort(), + flagCounts, + }; + }, [activeFlags]); + + const handleCreateFolder = (name: string) => { + // Creating a folder is just a UI concept - flags will be assigned to it + toast.success(`Folder "${name}" created`); + setIsFolderModalOpen(false); + }; + + const handleRenameFolder = async (oldPath: string, newPath: string) => { + try { + // Find all flags in the old folder and update them + const flagsToUpdate = activeFlags.filter( + (f) => f.folder === oldPath || f.folder?.startsWith(oldPath + "/") + ); + + for (const flag of flagsToUpdate) { + const newFolder = flag.folder === oldPath + ? newPath + : newPath + flag.folder.slice(oldPath.length); + + await updateFlagMutation.mutateAsync({ + id: flag.id, + folder: newFolder || null, + }); + } + + toast.success(`Folder renamed to "${newPath.split("/").pop()}"`); + } catch (error) { + console.error("Failed to rename folder:", error); + toast.error("Failed to rename folder"); + } + }; + + const handleDeleteFolder = async (path: string) => { + try { + // Find all flags in the folder (including nested) and move them to root + const flagsToUpdate = activeFlags.filter( + (f) => f.folder === path || f.folder?.startsWith(path + "/") + ); + + for (const flag of flagsToUpdate) { + await updateFlagMutation.mutateAsync({ + id: flag.id, + folder: null, + }); + } + + toast.success("Folder deleted, flags moved to Uncategorized"); + setIsFolderModalOpen(false); + } catch (error) { + console.error("Failed to delete folder:", error); + toast.error("Failed to delete folder"); + } + }; + return ( -
- }> - {flagsLoading ? ( - - ) : activeFlags.length === 0 ? ( -
- } - title="No feature flags yet" - variant="minimal" - /> +
+ {/* Folder Management Toolbar */} + {!flagsLoading && activeFlags.length > 0 && ( +
+
+ + + {folderData.folders.length} folder{folderData.folders.length !== 1 ? "s" : ""} +
- ) : ( - - )} - + +
+ )} + +
+ }> + {flagsLoading ? ( + + ) : activeFlags.length === 0 ? ( +
+ } + title="No feature flags yet" + variant="minimal" + /> +
+ ) : ( + + )} +
+
{isFlagSheetOpen && ( @@ -136,6 +243,16 @@ export default function FlagsPage() { onConfirm={handleConfirmDelete} title="Delete Feature Flag" /> + + setIsFolderModalOpen(false)} + onCreateFolder={handleCreateFolder} + onDeleteFolder={handleDeleteFolder} + onRenameFolder={handleRenameFolder} + />
diff --git a/biome.jsonc b/biome.jsonc index 95c67a0d8..69df07a54 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -58,7 +58,6 @@ }, "nursery": { "noShadow": "off", - "useMaxParams": "off", "noLeakedRender": "off" }, "a11y": { diff --git a/bun.lock b/bun.lock index 8616b471e..1d26c6bc8 100644 --- a/bun.lock +++ b/bun.lock @@ -114,7 +114,7 @@ "name": "@databuddy/dashboard", "version": "0.1.0", "dependencies": { - "@ai-sdk/react": "^3.0.0", + "@ai-sdk/react": "^3.0.118", "@cossistant/next": "^0.0.29", "@cossistant/react": "^0.0.29", "@databuddy/api-keys": "workspace:*", @@ -128,8 +128,8 @@ "@hello-pangea/dnd": "^18.0.1", "@hookform/resolvers": "^5.2.2", "@json-render/react": "^0.2.0", - "@orpc/client": "^1.13.0", - "@orpc/tanstack-query": "^1.13.0", + "@orpc/client": "^1.13.9", + "@orpc/tanstack-query": "^1.13.9", "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-collapsible": "^1.1.12", @@ -144,79 +144,79 @@ "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-use-controllable-state": "^1.2.2", - "@tanstack/react-pacer": "^0.19.2", - "@tanstack/react-query": "^5.90.12", + "@tanstack/react-pacer": "^0.19.4", + "@tanstack/react-query": "^5.91.2", "@tanstack/react-table": "^8.21.3", "@types/d3-scale": "^4.0.9", "@types/geojson": "^7946.0.16", "@types/leaflet": "^1.9.21", "@types/react-grid-layout": "^2.1.0", - "@xyflow/react": "^12.10.0", - "ai": "^6.0.0", - "autumn-js": "^0.1.63", + "@xyflow/react": "^12.10.1", + "ai": "^6.0.116", + "autumn-js": "^0.1.85", "babel-plugin-react-compiler": "^19.1.0-rc.1-rc-af1b7da-20250421", - "better-auth": "^1.4.9", + "better-auth": "^1.5.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "d3-scale": "^4.0.2", - "dayjs": "^1.11.19", + "dayjs": "^1.11.20", "embla-carousel-react": "^8.6.0", "flag-icons": "^7.5.0", - "framer-motion": "^12.23.26", + "framer-motion": "^12.38.0", "idb": "^8.0.3", "input-otp": "^1.4.2", - "jotai": "^2.16.0", + "jotai": "^2.18.1", "leaflet": "^1.9.4", "lucide-react": "^0.562.0", - "maplibre-gl": "^5.15.0", - "motion": "^12.23.26", - "nanoid": "^5.1.6", - "next": "^16.1.1", + "maplibre-gl": "^5.20.2", + "motion": "^12.38.0", + "nanoid": "^5.1.7", + "next": "^16.2.0", "next-themes": "^0.4.6", - "nuqs": "^2.8.6", + "nuqs": "^2.8.9", "ogl": "^1.0.11", - "pg": "^8.16.3", + "pg": "^8.20.0", "qrcode.react": "^4.2.0", "radix-ui": "latest", "react": "catalog:", - "react-day-picker": "^9.13.0", + "react-day-picker": "^9.14.0", "react-dom": "catalog:", "react-grid-layout": "^2.2.2", - "react-hook-form": "^7.69.0", - "react-hotkeys-hook": "^5.2.1", + "react-hook-form": "^7.71.2", + "react-hotkeys-hook": "^5.2.4", "react-image-crop": "^11.0.10", "react-leaflet": "^5.0.0", "react-qrcode-logo": "^4.0.0", "react-resizable-panels": "^3.0.6", "react-textarea-autosize": "^8.5.9", "recharts": "^2.15.4", - "shiki": "^3.20.0", - "simple-icons": "^16.2.0", + "shiki": "^3.23.0", + "simple-icons": "^16.12.0", "sonner": "^2.0.7", - "streamdown": "^2.1.0", - "tailwind-merge": "^3.4.0", + "streamdown": "^2.5.0", + "tailwind-merge": "^3.5.0", "tokenlens": "^1.3.1", "tw-animate-css": "^1.4.0", - "use-stick-to-bottom": "^1.1.1", + "use-stick-to-bottom": "^1.1.3", "vaul": "^1.1.2", "zod": "catalog:", }, "devDependencies": { "@biomejs/biome": "catalog:", - "@orpc/server": "^1.13.0", - "@tailwindcss/postcss": "^4.1.18", - "@tanstack/react-query-devtools": "^5.91.2", + "@orpc/server": "^1.13.9", + "@tailwindcss/postcss": "^4.2.2", + "@tanstack/react-query-devtools": "^5.91.3", "@types/d3-geo": "^3.1.0", - "@types/node": "^22.19.3", - "@types/pg": "^8.16.0", + "@types/node": "^22.19.15", + "@types/pg": "^8.18.0", "@types/react": "catalog:", "@types/react-dom": "catalog:", "@types/react-simple-maps": "^3.0.6", "@types/topojson-client": "^3.1.5", "husky": "^9.1.7", - "lint-staged": "^16.2.7", - "tailwindcss": "^4.1.18", + "lint-staged": "^16.4.0", + "tailwindcss": "^4.2.2", "typescript": "^5.9.3", "ultracite": "catalog:", }, @@ -767,7 +767,7 @@ "@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="], - "@better-auth/core": ["@better-auth/core@1.4.10", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.7", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-AThrfb6CpG80wqkanfrbN2/fGOYzhGladHFf3JhaWt/3/Vtf4h084T6PJLrDE7M/vCCGYvDI1DkvP3P1OB2HAg=="], + "@better-auth/core": ["@better-auth/core@1.5.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA=="], "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0", "drizzle-orm": ">=0.41.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-HAi9xAP40oDt48QZeYBFTcmg3vt1Jik90GwoRIfangd7VGbxesIIDBJSnvwMbZ52GBIc6+V4FRw9lasNiNrPfw=="], @@ -781,9 +781,9 @@ "@better-auth/sso": ["@better-auth/sso@1.4.10", "", { "dependencies": { "@better-fetch/fetch": "1.1.21", "fast-xml-parser": "^5.2.5", "jose": "^6.1.0", "samlify": "^2.10.1", "zod": "^4.1.12" }, "peerDependencies": { "better-auth": "1.4.10" } }, "sha512-td8Mg32JHpyFRIwJ6sfqZ0NDa9Easf+sXw5wnWLLgmnd7/osg4xTKTFsMLEvr5j4n/1mzSFVU/RBthOV2lCD+A=="], - "@better-auth/telemetry": ["@better-auth/telemetry@1.4.10", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.10" } }, "sha512-Dq4XJX6EKsUu0h3jpRagX739p/VMOTcnJYWRrLtDYkqtZFg+sFiFsSWVcfapZoWpRSUGYX9iKwl6nDHn6Ju2oQ=="], + "@better-auth/telemetry": ["@better-auth/telemetry@1.5.5", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.5.5" } }, "sha512-1+lklxArn4IMHuU503RcPdXrSG2tlXt4jnGG3omolmspQ7tktg/Y9XO/yAkYDurtvMn1xJ8X1Ov01Ji/r5s9BQ=="], - "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], + "@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], @@ -2027,7 +2027,7 @@ "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], - "better-auth": ["better-auth@1.4.10", "", { "dependencies": { "@better-auth/core": "1.4.10", "@better-auth/telemetry": "1.4.10", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.7", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.12" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-0kqwEBJLe8eyFzbUspRG/htOriCf9uMLlnpe34dlIJGdmDfPuQISd4shShvUrvIVhPxsY1dSTXdXPLpqISYOYg=="], + "better-auth": ["better-auth@1.5.5", "", { "dependencies": { "@better-auth/core": "1.5.5", "@better-auth/drizzle-adapter": "1.5.5", "@better-auth/kysely-adapter": "1.5.5", "@better-auth/memory-adapter": "1.5.5", "@better-auth/mongo-adapter": "1.5.5", "@better-auth/prisma-adapter": "1.5.5", "@better-auth/telemetry": "1.5.5", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.11", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-GpVPaV1eqr3mOovKfghJXXk6QvlcVeFbS3z+n+FPDid5rK/2PchnDtiaVCzWyXA9jH2KkirOfl+JhAUvnja0Eg=="], "better-auth-harmony": ["better-auth-harmony@1.2.5", "", { "dependencies": { "libphonenumber-js": "^1.12.8", "mailchecker": "^6.0.17", "validator": "^13.15.15" }, "peerDependencies": { "better-auth": "^1.0.3" } }, "sha512-4YaAK5vrLnB6heImYJB8Pf524BPFrOYmUy1IFTHk6btGDCbgh3xT/hBCM6Ougwv/drURxtfZlB/FPktIjKLMtg=="], @@ -3075,7 +3075,7 @@ "nanoid": ["nanoid@5.1.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ=="], - "nanostores": ["nanostores@1.1.0", "", {}, "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA=="], + "nanostores": ["nanostores@1.2.0", "", {}, "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg=="], "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], @@ -3999,17 +3999,11 @@ "@better-auth/core/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@better-auth/core/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], - - "@better-auth/drizzle-adapter/@better-auth/core": ["@better-auth/core@1.5.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA=="], - - "@better-auth/kysely-adapter/@better-auth/core": ["@better-auth/core@1.5.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA=="], + "@better-auth/core/better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], - "@better-auth/memory-adapter/@better-auth/core": ["@better-auth/core@1.5.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA=="], + "@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "@better-auth/mongo-adapter/@better-auth/core": ["@better-auth/core@1.5.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA=="], - - "@better-auth/prisma-adapter/@better-auth/core": ["@better-auth/core@1.5.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA=="], + "@better-auth/sso/better-auth": ["better-auth@1.4.10", "", { "dependencies": { "@better-auth/core": "1.4.10", "@better-auth/telemetry": "1.4.10", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.7", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.12" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-0kqwEBJLe8eyFzbUspRG/htOriCf9uMLlnpe34dlIJGdmDfPuQISd4shShvUrvIVhPxsY1dSTXdXPLpqISYOYg=="], "@better-auth/sso/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], @@ -4043,8 +4037,6 @@ "@databuddy/dashboard/autumn-js": ["autumn-js@0.1.85", "", { "dependencies": { "query-string": "^9.2.2", "rou3": "^0.6.1", "swr": "^2.3.3", "zod": "^4.0.0" }, "peerDependencies": { "better-auth": "^1.3.17", "better-call": "^1.0.12", "convex": "^1.25.4" }, "optionalPeers": ["better-auth", "better-call", "convex"] }, "sha512-PDud/t8z5bDJcD7ptyHzTaoJ0A8zkxvQ4TYcJ48RtgKDdOkVY36D1T6udVLwLDnWw4J5KXwJgEuGxHdd+cuABw=="], - "@databuddy/dashboard/better-auth": ["better-auth@1.5.5", "", { "dependencies": { "@better-auth/core": "1.5.5", "@better-auth/drizzle-adapter": "1.5.5", "@better-auth/kysely-adapter": "1.5.5", "@better-auth/memory-adapter": "1.5.5", "@better-auth/mongo-adapter": "1.5.5", "@better-auth/prisma-adapter": "1.5.5", "@better-auth/telemetry": "1.5.5", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.11", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-GpVPaV1eqr3mOovKfghJXXk6QvlcVeFbS3z+n+FPDid5rK/2PchnDtiaVCzWyXA9jH2KkirOfl+JhAUvnja0Eg=="], - "@databuddy/dashboard/dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="], "@databuddy/dashboard/tokenlens": ["tokenlens@1.3.1", "", { "dependencies": { "@tokenlens/core": "1.3.0", "@tokenlens/fetch": "1.3.0", "@tokenlens/helpers": "1.3.1", "@tokenlens/models": "1.3.0" } }, "sha512-7oxmsS5PNCX3z+b+z07hL5vCzlgHKkCGrEQjQmWl5l+v5cUrtL7S1cuST4XThaL1XyjbTX8J5hfP0cjDJRkaLA=="], @@ -4563,10 +4555,16 @@ "babel-plugin-react-compiler/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], - "better-auth/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], + "better-auth/better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], + + "better-auth/kysely": ["kysely@0.28.13", "", {}, "sha512-jCkYDvlfzOyHaVsrvR4vnNZxG30oNv2jbbFBjTQAUG8n0h07HW0sZJHk4KAQIRyu9ay+Rg+L8qGa3lwt8Gve9w=="], + + "better-auth/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "better-auth-harmony/libphonenumber-js": ["libphonenumber-js@1.12.35", "", {}, "sha512-T/Cz6iLcsZdb5jDncDcUNhSAJ0VlSC9TnsqtBNdpkaAmy24/R1RhErtNWVWBrcUZKs9hSgaVsBkc7HxYnazIfw=="], + "better-call/@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], + "better-call/rou3": ["rou3@0.7.11", "", {}, "sha512-ELguG3ENDw5NKNmWHO3OGEjcgdxkCNvnMR22gKHEgRXuwiriap5RIYdummOaOiqUNcC5yU5txGCHWNm7KlHuAA=="], "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], @@ -4843,45 +4841,15 @@ "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], - "@better-auth/drizzle-adapter/@better-auth/core/@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], - - "@better-auth/drizzle-adapter/@better-auth/core/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@better-auth/drizzle-adapter/@better-auth/core/better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], - - "@better-auth/drizzle-adapter/@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - - "@better-auth/kysely-adapter/@better-auth/core/@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], - - "@better-auth/kysely-adapter/@better-auth/core/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@better-auth/kysely-adapter/@better-auth/core/better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], + "@better-auth/core/better-call/set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], - "@better-auth/kysely-adapter/@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@better-auth/sso/better-auth/@better-auth/core": ["@better-auth/core@1.4.10", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.7", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-AThrfb6CpG80wqkanfrbN2/fGOYzhGladHFf3JhaWt/3/Vtf4h084T6PJLrDE7M/vCCGYvDI1DkvP3P1OB2HAg=="], - "@better-auth/memory-adapter/@better-auth/core/@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], + "@better-auth/sso/better-auth/@better-auth/telemetry": ["@better-auth/telemetry@1.4.10", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.10" } }, "sha512-Dq4XJX6EKsUu0h3jpRagX739p/VMOTcnJYWRrLtDYkqtZFg+sFiFsSWVcfapZoWpRSUGYX9iKwl6nDHn6Ju2oQ=="], - "@better-auth/memory-adapter/@better-auth/core/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@better-auth/sso/better-auth/@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], - "@better-auth/memory-adapter/@better-auth/core/better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], - - "@better-auth/memory-adapter/@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - - "@better-auth/mongo-adapter/@better-auth/core/@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], - - "@better-auth/mongo-adapter/@better-auth/core/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@better-auth/mongo-adapter/@better-auth/core/better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], - - "@better-auth/mongo-adapter/@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - - "@better-auth/prisma-adapter/@better-auth/core/@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], - - "@better-auth/prisma-adapter/@better-auth/core/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@better-auth/prisma-adapter/@better-auth/core/better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], - - "@better-auth/prisma-adapter/@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@better-auth/sso/better-auth/nanostores": ["nanostores@1.1.0", "", {}, "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA=="], "@databuddy/ai/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], @@ -5019,20 +4987,6 @@ "@databuddy/dashboard/autumn-js/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], - "@databuddy/dashboard/better-auth/@better-auth/core": ["@better-auth/core@1.5.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA=="], - - "@databuddy/dashboard/better-auth/@better-auth/telemetry": ["@better-auth/telemetry@1.5.5", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.5.5" } }, "sha512-1+lklxArn4IMHuU503RcPdXrSG2tlXt4jnGG3omolmspQ7tktg/Y9XO/yAkYDurtvMn1xJ8X1Ov01Ji/r5s9BQ=="], - - "@databuddy/dashboard/better-auth/@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], - - "@databuddy/dashboard/better-auth/better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], - - "@databuddy/dashboard/better-auth/kysely": ["kysely@0.28.13", "", {}, "sha512-jCkYDvlfzOyHaVsrvR4vnNZxG30oNv2jbbFBjTQAUG8n0h07HW0sZJHk4KAQIRyu9ay+Rg+L8qGa3lwt8Gve9w=="], - - "@databuddy/dashboard/better-auth/nanostores": ["nanostores@1.2.0", "", {}, "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg=="], - - "@databuddy/dashboard/better-auth/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "@databuddy/dashboard/tokenlens/@tokenlens/core": ["@tokenlens/core@1.3.0", "", {}, "sha512-d8YNHNC+q10bVpi95fELJwJyPVf1HfvBEI18eFQxRSZTdByXrP+f/ZtlhSzkx0Jl0aEmYVeBA5tPeeYRioLViQ=="], "@databuddy/dashboard/tokenlens/@tokenlens/fetch": ["@tokenlens/fetch@1.3.0", "", { "dependencies": { "@tokenlens/core": "1.3.0" } }, "sha512-RONDRmETYly9xO8XMKblmrZjKSwCva4s5ebJwQNfNlChZoA5kplPoCgnWceHnn1J1iRjLVlrCNB43ichfmGBKQ=="], @@ -5525,6 +5479,8 @@ "@xyflow/system/@types/d3-interpolate/@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + "better-auth/better-call/set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], + "bl/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -5823,15 +5779,7 @@ "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@better-auth/drizzle-adapter/@better-auth/core/better-call/set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], - - "@better-auth/kysely-adapter/@better-auth/core/better-call/set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], - - "@better-auth/memory-adapter/@better-auth/core/better-call/set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], - - "@better-auth/mongo-adapter/@better-auth/core/better-call/set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], - - "@better-auth/prisma-adapter/@better-auth/core/better-call/set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], + "@better-auth/sso/better-auth/@better-auth/core/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@databuddy/api/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.208.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fGvAg3zb8fC0oJAzfz7PQppADI2HYB7TSt/XoCaBJFi1mSquNUjtHXEoviMgObLAa1NRIgOC1lsV1OUKi+9+lQ=="], @@ -5855,10 +5803,6 @@ "@databuddy/dashboard/ai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@databuddy/dashboard/better-auth/@better-auth/core/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@databuddy/dashboard/better-auth/better-call/set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], - "@databuddy/dashboard/ultracite/trpc-cli/@trpc/server": ["@trpc/server@11.7.2", "", { "peerDependencies": { "typescript": ">=5.7.2" } }, "sha512-AgB26PXY69sckherIhCacKLY49rxE2XP5h38vr/KMZTbLCL1p8IuIoKPjALTcugC2kbyQ7Lbqo2JDVfRSmPmfQ=="], "@databuddy/dashboard/ultracite/trpc-cli/commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], diff --git a/packages/db/src/drizzle/schema.ts b/packages/db/src/drizzle/schema.ts index 214ed8384..7006cc349 100644 --- a/packages/db/src/drizzle/schema.ts +++ b/packages/db/src/drizzle/schema.ts @@ -669,6 +669,7 @@ export const flags = pgTable( dependencies: text("dependencies").array(), targetGroupIds: text("target_group_ids").array(), environment: text("environment"), + folder: text("folder"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), deletedAt: timestamp("deleted_at"), @@ -687,6 +688,10 @@ export const flags = pgTable( "btree", table.createdBy.asc().nullsLast().op("text_ops") ), + index("idx_flags_folder").using( + "btree", + table.folder.asc().nullsLast().op("text_ops") + ), foreignKey({ columns: [table.websiteId], foreignColumns: [websites.id], @@ -1037,10 +1042,7 @@ export const feedback = pgTable( "btree", table.organizationId.asc().nullsLast().op("text_ops") ), - index("feedback_status_idx").using( - "btree", - table.status.asc().nullsLast() - ), + index("feedback_status_idx").using("btree", table.status.asc().nullsLast()), foreignKey({ columns: [table.userId], foreignColumns: [user.id], diff --git a/packages/rpc/src/routers/flags.ts b/packages/rpc/src/routers/flags.ts index 83cdef619..41880c7d9 100644 --- a/packages/rpc/src/routers/flags.ts +++ b/packages/rpc/src/routers/flags.ts @@ -29,11 +29,11 @@ import { z } from "zod"; import { rpcError } from "../errors"; import type { Context } from "../orpc"; import { protectedProcedure, publicProcedure } from "../orpc"; +import { isFullyAuthorized, withWorkspace } from "../procedures/with-workspace"; import { - isFullyAuthorized, - withWorkspace, -} from "../procedures/with-workspace"; -import { requireFeatureWithLimit, requireUsageWithinLimit } from "../types/billing"; + requireFeatureWithLimit, + requireUsageWithinLimit, +} from "../types/billing"; import { getCacheAuthContext } from "../utils/cache-keys"; const flagsCache = createDrizzleCache({ redis, namespace: "flags" }); @@ -101,6 +101,7 @@ const createFlagSchema = z organizationId: z.string().optional(), payload: z.any().optional(), persistAcrossAuth: z.boolean().optional(), + folder: z.string().max(200, "Folder path too long").optional(), ...flagFormSchema.shape, }) .refine((data) => data.websiteId || data.organizationId, { @@ -125,6 +126,7 @@ const updateFlagSchema = z dependencies: z.array(z.string()).optional(), environment: z.string().optional(), targetGroupIds: z.array(z.string()).optional(), + folder: z.string().max(200, "Folder path too long").optional(), }) .superRefine((data, ctx) => { if (data.type === "multivariant" && data.variants) { @@ -225,7 +227,7 @@ function sanitizeFlagForDemo(flag: T): T { ...flag, rules: Array.isArray(flag.rules) && flag.rules.length > 0 ? [] : flag.rules, targetGroups: flag.targetGroups?.map( - (group: { rules?: unknown;[key: string]: unknown }) => ({ + (group: { rules?: unknown; [key: string]: unknown }) => ({ ...group, rules: Array.isArray(group.rules) && group.rules.length > 0 @@ -479,14 +481,14 @@ export const flagsRouter = { const workspace = wsId ? await withWorkspace(context, { - websiteId: wsId, - permissions: ["update"], - }) + websiteId: wsId, + permissions: ["update"], + }) : await withWorkspace(context, { - organizationId: orgId, - resource: "website", - permissions: ["create"], - }); + organizationId: orgId, + resource: "website", + permissions: ["create"], + }); const createdBy = await workspace.getCreatedBy(); @@ -572,6 +574,7 @@ export const flagsRouter = { variants: input.variants, dependencies: input.dependencies, environment: input.environment, + folder: input.folder ?? existingFlag[0].folder, deletedAt: null, updatedAt: new Date(), }) @@ -629,6 +632,7 @@ export const flagsRouter = { websiteId: input.websiteId || null, organizationId: input.organizationId || null, environment: input.environment || existingFlag?.[0]?.environment, + folder: input.folder || null, userId: null, createdBy, }) @@ -696,23 +700,22 @@ export const flagsRouter = { const flag = existingFlag[0]; - let workspace; - if (flag.websiteId) { - workspace = await withWorkspace(context, { - websiteId: flag.websiteId, - permissions: ["update"], - }); - } else if (flag.organizationId) { - workspace = await withWorkspace(context, { - organizationId: flag.organizationId, - resource: "website", - permissions: ["create"], - }); - } else { - throw rpcError.forbidden( - "Flags must be scoped to a website or organization" - ); - } + const workspace = flag.websiteId + ? await withWorkspace(context, { + websiteId: flag.websiteId, + permissions: ["update"], + }) + : flag.organizationId + ? await withWorkspace(context, { + organizationId: flag.organizationId, + resource: "website", + permissions: ["create"], + }) + : (() => { + throw rpcError.forbidden( + "Flags must be scoped to a website or organization" + ); + })(); const isUnarchiving = flag.status === "archived" && diff --git a/packages/shared/src/flags/index.ts b/packages/shared/src/flags/index.ts index 59183a816..e3b64c57e 100644 --- a/packages/shared/src/flags/index.ts +++ b/packages/shared/src/flags/index.ts @@ -62,6 +62,7 @@ export const flagFormSchema = z .optional(), environment: z.string().nullable().optional(), targetGroupIds: z.array(z.string()).optional(), + folder: z.string().max(200, "Folder path too long").optional(), }) .superRefine((data, ctx) => { if (data.type === "multivariant" && data.variants) { @@ -116,38 +117,7 @@ export const flagScheduleSchema = z if (!data.isEnabled) { return; } - if (data.type !== "update_rollout") { - if (data.rolloutSteps && data?.rolloutSteps?.length > 0) { - ctx.addIssue({ - code: "custom", - path: ["rolloutSteps"], - message: "Rollout steps allowed only for update_rollout type", - }); - } - if (!data.scheduledAt) { - ctx.addIssue({ - code: "custom", - path: ["scheduledAt"], - message: "Date time is required for enable/disable schedule types", - }); - } - const scheduledDate = new Date(data.scheduledAt ?? ""); - if (Number.isNaN(scheduledDate.getTime())) { - ctx.addIssue({ - code: "custom", - path: ["scheduledAt"], - message: "Invalid schedule date", - }); - } - - if (Date.now() > new Date(data.scheduledAt ?? "").getTime()) { - ctx.addIssue({ - code: "custom", - path: ["scheduledAt"], - message: "Scheduled time must be in the future", - }); - } - } else { + if (data.type === "update_rollout") { if (data.scheduledAt) { ctx.addIssue({ code: "custom", @@ -191,6 +161,37 @@ export const flagScheduleSchema = z }); } } + } else { + if (data.rolloutSteps && data?.rolloutSteps?.length > 0) { + ctx.addIssue({ + code: "custom", + path: ["rolloutSteps"], + message: "Rollout steps allowed only for update_rollout type", + }); + } + if (!data.scheduledAt) { + ctx.addIssue({ + code: "custom", + path: ["scheduledAt"], + message: "Date time is required for enable/disable schedule types", + }); + } + const scheduledDate = new Date(data.scheduledAt ?? ""); + if (Number.isNaN(scheduledDate.getTime())) { + ctx.addIssue({ + code: "custom", + path: ["scheduledAt"], + message: "Invalid schedule date", + }); + } + + if (Date.now() > new Date(data.scheduledAt ?? "").getTime()) { + ctx.addIssue({ + code: "custom", + path: ["scheduledAt"], + message: "Scheduled time must be in the future", + }); + } } }); export type FlagSchedule = z.infer;