From 0edf10c2759b5bfbe96854321f906825e0345bfc Mon Sep 17 00:00:00 2001 From: Lewis Carhart Date: Fri, 21 Mar 2025 14:47:39 +0000 Subject: [PATCH 1/4] Implement organization switching and create organization modal - Added `OrgMenu` component for organization switching in the sidebar. - Introduced `CreateOrgModal` for creating new organizations with form validation. - Updated `changeOrganizationAction` to revalidate paths after organization change. - Enhanced `OrganizationSwitcher` to handle organization changes with loading states and user feedback. - Improved header and sidebar components for better organization management experience. - Updated translations to include new status messages for organization switching. --- apps/app/src/actions/change-organization.ts | 121 ++-- apps/app/src/components/header.tsx | 91 ++- .../components/modals/create-org-modal.tsx | 281 +++++++++ apps/app/src/components/org-menu.tsx | 38 ++ .../src/components/organization-switcher.tsx | 250 ++++++-- apps/app/src/components/sidebar.tsx | 54 +- .../components/ui/data-table/DataTable.tsx | 569 +++++++++--------- apps/app/src/components/user-menu.tsx | 4 - apps/app/src/locales/en.ts | 1 + packages/ui/src/components/chart-tooltip.tsx | 4 +- packages/ui/src/components/chart.tsx | 42 +- 11 files changed, 949 insertions(+), 506 deletions(-) create mode 100644 apps/app/src/components/modals/create-org-modal.tsx create mode 100644 apps/app/src/components/org-menu.tsx diff --git a/apps/app/src/actions/change-organization.ts b/apps/app/src/actions/change-organization.ts index f2d860b380..4f9c67febb 100644 --- a/apps/app/src/actions/change-organization.ts +++ b/apps/app/src/actions/change-organization.ts @@ -3,71 +3,74 @@ import { authActionClient } from "./safe-action"; import { z } from "zod"; import { db } from "@bubba/db"; +import { revalidatePath } from "next/cache"; export const changeOrganizationAction = authActionClient - .schema( - z.object({ - organizationId: z.string(), - }) - ) - .metadata({ - name: "change-organization", - track: { - event: "create-employee", - channel: "server", - }, - }) - .action(async ({ parsedInput, ctx }) => { - const { organizationId } = parsedInput; - const { user } = ctx; + .schema( + z.object({ + organizationId: z.string(), + }), + ) + .metadata({ + name: "change-organization", + track: { + event: "create-employee", + channel: "server", + }, + }) + .action(async ({ parsedInput, ctx }) => { + const { organizationId } = parsedInput; + const { user } = ctx; - const organizationMember = await db.organizationMember.findFirst({ - where: { - userId: user.id, - organizationId, - }, - }); + const organizationMember = await db.organizationMember.findFirst({ + where: { + userId: user.id, + organizationId, + }, + }); - if (!organizationMember) { - return { - success: false, - error: "Unauthorized", - }; - } + if (!organizationMember) { + return { + success: false, + error: "Unauthorized", + }; + } - try { - const organization = await db.organization.findUnique({ - where: { - id: organizationId, - }, - }); + try { + const organization = await db.organization.findUnique({ + where: { + id: organizationId, + }, + }); - if (!organization) { - return { - success: false, - error: "Organization not found", - }; - } + if (!organization) { + return { + success: false, + error: "Organization not found", + }; + } - await db.user.update({ - where: { - id: user.id, - }, - data: { - organizationId: organization.id, - }, - }); + await db.user.update({ + where: { + id: user.id, + }, + data: { + organizationId: organization.id, + }, + }); - return { - success: true, - data: organization, - }; - } catch (error) { - console.error("Error changing organization:", error); + revalidatePath(`/${organization.id}`, "layout"); - return { - success: false, - error: "Failed to change organization", - }; - } - }); + return { + success: true, + data: organization, + }; + } catch (error) { + console.error("Error changing organization:", error); + + return { + success: false, + error: "Failed to change organization", + }; + } + }); diff --git a/apps/app/src/components/header.tsx b/apps/app/src/components/header.tsx index 207488d5a8..cbff22182b 100644 --- a/apps/app/src/components/header.tsx +++ b/apps/app/src/components/header.tsx @@ -14,53 +14,46 @@ import { auth } from "@/auth"; import { redirect } from "next/navigation"; export async function Header() { - const t = await getI18n(); - const { organizations } = await getOrganizations(); - - const session = await auth(); - const user = session?.user; - const currentOrganizationId = user?.organizationId; - - if (!currentOrganizationId) { - redirect("/"); - } - - return ( -
- - - }> - - - -
-
- - -
- - - - }> - - -
-
- ); + const t = await getI18n(); + const { organizations } = await getOrganizations(); + + const session = await auth(); + const user = session?.user; + const currentOrganizationId = user?.organizationId; + + if (!currentOrganizationId) { + redirect("/"); + } + + return ( +
+ + +
+
+ + +
+ + + + }> + + +
+
+ ); } diff --git a/apps/app/src/components/modals/create-org-modal.tsx b/apps/app/src/components/modals/create-org-modal.tsx new file mode 100644 index 0000000000..252377d504 --- /dev/null +++ b/apps/app/src/components/modals/create-org-modal.tsx @@ -0,0 +1,281 @@ +"use client"; + +import { createOrganizationAction } from "@/actions/organization/create-organization-action"; +import { organizationSchema } from "@/actions/schema"; +import { useI18n } from "@/locales/client"; +import type { Framework, Organization } from "@bubba/db/types"; +import { Checkbox } from "@bubba/ui/checkbox"; +import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@bubba/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@bubba/ui/form"; +import { Input } from "@bubba/ui/input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useSession } from "next-auth/react"; +import { useAction } from "next-safe-action/hooks"; +import { useState, useRef, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import type { z } from "zod"; +import { cn } from "@bubba/ui/cn"; +import { Button } from "@bubba/ui/button"; +import { Loader2 } from "lucide-react"; +import { useRealtimeRun } from "@trigger.dev/react-hooks"; +import { useRouter } from "next/navigation"; +import { LogoSpinner } from "../logo-spinner"; + +type Props = { + onOpenChange: (isOpen: boolean) => void; + frameworks: Framework[]; +}; + +export function CreateOrgModal({ onOpenChange, frameworks }: Props) { + const t = useI18n(); + const { data: session } = useSession(); + + const [runId, setRunId] = useState(null); + const [publicAccessToken, setPublicAccessToken] = useState( + null, + ); + const newOrganizationRef = useRef | null>( + null, + ); + + const createOrganization = useAction(createOrganizationAction, { + onSuccess: async (data) => { + setRunId(data.data?.runId ?? null); + setPublicAccessToken(data.data?.publicAccessToken ?? null); + newOrganizationRef.current = data.data?.newOrganization ?? null; + }, onError: () => { + toast.error(t("common.actions.error"), { duration: 5000 }); + }, + }); + + const form = useForm>({ + resolver: zodResolver(organizationSchema), + defaultValues: { + fullName: session?.user?.name ?? "", + name: "", + website: "", + frameworks: [], + }, + mode: "onChange", + }); + + const onSubmit = async (data: z.infer) => { + createOrganization.execute(data); + }; + + return ( + +
+ + {t("onboarding.title")} + + {t("onboarding.description")} + + + + {runId && publicAccessToken && ( +
+ +
+ )} + + {!runId && !publicAccessToken && ( +
+ + ( + + + {t("onboarding.fields.fullName.label")} + + + + + + + )} + /> + + ( + + + {t("onboarding.fields.name.label")} + + + + + + + )} + /> + + ( + + + {t("onboarding.fields.website.label")} + + + + + + + )} + /> + ( + + + {t("frameworks.overview.grid.title")} + + +
+ + {t("frameworks.overview.grid.title")} + + {frameworks.map((framework) => ( + + ))} +
+
+ +
+ )} + /> + +
+ +
+ + +
+
+
+ + + )} +
+
+ ); +} + +function RealtimeStatus({ + runId, + publicAccessToken, + newOrganization, +}: { + runId: string; + publicAccessToken: string; + newOrganization: Pick | null; +}) { + const t = useI18n(); + const router = useRouter(); + + const { run, error } = useRealtimeRun(runId, { + accessToken: publicAccessToken, + onComplete: (run, error) => { + if (error) { + toast.error(t("common.actions.error"), { duration: 5000 }); + return; + } + }, + }); + + useEffect(() => { + if (run?.status === "COMPLETED" && newOrganization) { + router.push(`/${newOrganization.id}`); + } + }, [run, router, newOrganization]); + + return ( +
+
+ +

+ {t("onboarding.trigger.title")} +

+

+ {t("onboarding.trigger.creating")} +

+
+
+ ); +} diff --git a/apps/app/src/components/org-menu.tsx b/apps/app/src/components/org-menu.tsx new file mode 100644 index 0000000000..591c7ff36b --- /dev/null +++ b/apps/app/src/components/org-menu.tsx @@ -0,0 +1,38 @@ +import { auth } from "@/auth"; +import { Skeleton } from "@bubba/ui/skeleton"; +import { cache, Suspense } from "react"; +import { OrganizationSwitcher } from "./organization-switcher"; +import { redirect } from "next/navigation"; +import { getOrganizations } from "@/data/getOrganizations"; +import { db } from "@bubba/db"; + +export async function OrgMenu() { + const session = await auth(); + const { organizations } = await getOrganizations(); + const frameworks = await getFrameworks(); + + const user = session?.user; + const currentOrganizationId = user?.organizationId; + + if (!currentOrganizationId) { + redirect("/"); + } + + return ( + }> + + + ); +} + +const getFrameworks = cache(async () => { + return await db.framework.findMany({ + orderBy: { + name: "asc", + }, + }); +}); diff --git a/apps/app/src/components/organization-switcher.tsx b/apps/app/src/components/organization-switcher.tsx index 5c53b5f53b..260c2709cb 100644 --- a/apps/app/src/components/organization-switcher.tsx +++ b/apps/app/src/components/organization-switcher.tsx @@ -1,79 +1,205 @@ "use client"; import { changeOrganizationAction } from "@/actions/change-organization"; -import type { Organization } from "@bubba/db/types"; +import type { Framework, Organization } from "@bubba/db/types"; import { Button } from "@bubba/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@bubba/ui/dropdown-menu"; -import { Building2, ChevronDown, Plus } from "lucide-react"; +import { Dialog } from "@bubba/ui/dialog"; +import { motion } from "framer-motion"; import { useAction } from "next-safe-action/hooks"; import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useClickAway } from "@uidotdev/usehooks"; +import { CreateOrgModal } from "./modals/create-org-modal"; +import { Avatar, AvatarImageNext, AvatarFallback } from "@bubba/ui/avatar"; +import { Icons } from "@bubba/ui/icons"; +import { cn } from "@bubba/ui/cn"; +import { LogoSpinner } from "./logo-spinner"; +import { useI18n } from "@/locales/client"; +import { createPortal } from "react-dom"; + +interface OrgItem { + id: string; + name?: string; + logo_url?: string; +} interface OrganizationSwitcherProps { - organizations: Organization[]; - organizationId: string | undefined; + organizations: Organization[]; + organizationId: string | undefined; + frameworks: Framework[]; } export function OrganizationSwitcher({ - organizations, - organizationId, + organizations, + organizationId, + frameworks, }: OrganizationSwitcherProps) { - const router = useRouter(); + const t = useI18n(); + const router = useRouter(); + const [isOpen, onOpenChange] = useState(false); + const [isActive, setActive] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const { execute } = useAction(changeOrganizationAction, { + onSuccess: (result) => { + if (result.data?.success) { + // Add a 2 second delay before navigation + setTimeout(() => { + const currentPath = window.location.pathname; + const currentPathParts = currentPath.split('/'); + // Replace the organization ID part with the new one + if (currentPathParts.length > 1) { + currentPathParts[1] = result.data?.data?.id || ''; + router.push(currentPathParts.join('/')); + } else { + router.push(`/${result.data?.data?.id}`); + } + router.refresh(); + }, 1500); + } + }, + onError: (error) => { + console.error(error); + setIsLoading(false); + }, + }); + + const ref = useClickAway(() => { + setActive(false); + }); + + + const handleOrganizationChange = async (organizationId: string) => { + setIsLoading(true); + execute({ organizationId }); + }; + + const currentOrganization = organizations.find( + (org) => org.id === organizationId, + ); + + const sortedOrganizations = [currentOrganization, ...organizations.filter(org => org.id !== currentOrganization?.id)]; - const { execute } = useAction(changeOrganizationAction, { - onSuccess: (result) => { - if (result.data?.success) { - router.push(`/${result.data.data?.id}`); - } - }, - onError: (error) => { - console.error(error); - }, - }); + return ( + <> + {isLoading && typeof window !== 'undefined' && createPortal( + <> + +
+
+ +
+ + + {t("onboarding.switch")} + +
+
+
+ , + document.body + )} - const currentOrganization = organizations.find( - (org) => org.id === organizationId, - ); + + } layout className="w-[32px] h-[32px] relative"> + {[ + ...sortedOrganizations.filter(Boolean).map(org => ({ + org: { + id: org?.id || "", + name: org?.name || "", + } as OrgItem + })), + { org: { id: "add" } as OrgItem } + ].map(({ org }, index) => ( + + {org.id === "add" ? ( + <> + - return ( - - - - - - {organizations.map((org) => ( - handleOrganizationChange(org.id)} - className="flex items-center gap-2 cursor-pointer" - > - {org.name} - - ))} - { - router.push("/setup"); - }} - > - - Create new org - - - - ); + + + ) : ( + { + if (isLoading) return; + if (index === 0) { + setActive(true); + } else { + handleOrganizationChange(org.id); + } + }} + > + + + + {org?.name?.charAt(0)?.toUpperCase()} + {org?.name?.charAt(1)?.toUpperCase()} + + + + )} + + ))} + + + + ); } diff --git a/apps/app/src/components/sidebar.tsx b/apps/app/src/components/sidebar.tsx index 2070eae585..e4d443208e 100644 --- a/apps/app/src/components/sidebar.tsx +++ b/apps/app/src/components/sidebar.tsx @@ -5,34 +5,40 @@ import Link from "next/link"; import { MainMenu } from "./main-menu"; import { auth } from "@/auth"; import { redirect } from "next/navigation"; +import { Suspense } from "react"; +import { OrgMenu } from "./org-menu"; export async function Sidebar() { - const session = await auth(); - const user = session?.user; - const organizationId = user?.organizationId; + const session = await auth(); + const user = session?.user; + const organizationId = user?.organizationId; - if (!organizationId) { - redirect("/"); - } + if (!organizationId) { + redirect("/"); + } - const cookieStore = await cookies(); + const cookieStore = await cookies(); - const initialItems = cookieStore.has(Cookies.MenuConfig) - ? JSON.parse(cookieStore.get(Cookies.MenuConfig)?.value ?? "") - : null; + const initialItems = cookieStore.has(Cookies.MenuConfig) + ? JSON.parse(cookieStore.get(Cookies.MenuConfig)?.value ?? "") + : null; - return ( - - ); + return ( + + ); } diff --git a/apps/app/src/components/ui/data-table/DataTable.tsx b/apps/app/src/components/ui/data-table/DataTable.tsx index f974afd335..9308bd8224 100644 --- a/apps/app/src/components/ui/data-table/DataTable.tsx +++ b/apps/app/src/components/ui/data-table/DataTable.tsx @@ -1,13 +1,13 @@ "use client"; import { - type Column, - flexRender, - getCoreRowModel, - useReactTable, - getSortedRowModel, - type SortingState, - type ColumnDef, + type Column, + flexRender, + getCoreRowModel, + useReactTable, + getSortedRowModel, + type SortingState, + type ColumnDef, } from "@tanstack/react-table"; import { Table, TableBody, TableCell, TableRow } from "@bubba/ui/table"; import { cn } from "@bubba/ui/cn"; @@ -17,12 +17,12 @@ import { DataTablePagination } from "./DataTablePagination"; import { Input } from "@bubba/ui/input"; import { Button } from "@bubba/ui/button"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuTrigger, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuCheckboxItem, + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuCheckboxItem, } from "@bubba/ui/dropdown-menu"; import { Filter, Search, X } from "lucide-react"; import { DataTableSkeleton } from "./DataTableSkeleton"; @@ -30,294 +30,293 @@ import { Badge } from "@bubba/ui/badge"; import { Avatar, AvatarFallback, AvatarImage } from "@bubba/ui/avatar"; interface FilterItem { - label: string; - value: string; - checked: boolean; - onChange: (checked: boolean) => void; - icon?: React.ReactNode; + label: string; + value: string; + checked: boolean; + onChange: (checked: boolean) => void; + icon?: React.ReactNode; } interface FilterCategory { - label: string; - items: FilterItem[]; - maxHeight?: string; + label: string; + items: FilterItem[]; + maxHeight?: string; } interface DataTableProps { - data: TData[]; - columns: ColumnDef[]; - onRowClick?: (row: TData) => void; - className?: string; - emptyMessage?: string; - isLoading?: boolean; - pagination?: { - page: number; - pageSize: number; - totalCount: number; - totalPages: number; - hasNextPage: boolean; - hasPreviousPage: boolean; - }; - onPageChange?: (page: number) => void; - onPageSizeChange?: (pageSize: number) => void; - search?: { - value: string; - onChange: (value: string) => void; - placeholder?: string; - }; - filters?: { - categories: FilterCategory[]; - hasActiveFilters: boolean; - onClearFilters: () => void; - activeFilterCount?: number; - }; - ctaButton?: { - label: string; - onClick: () => void; - icon?: React.ReactNode; - }; + data: TData[]; + columns: ColumnDef[]; + onRowClick?: (row: TData) => void; + className?: string; + emptyMessage?: string; + isLoading?: boolean; + pagination?: { + page: number; + pageSize: number; + totalCount: number; + totalPages: number; + hasNextPage: boolean; + hasPreviousPage: boolean; + }; + onPageChange?: (page: number) => void; + onPageSizeChange?: (pageSize: number) => void; + search?: { + value: string; + onChange: (value: string) => void; + placeholder?: string; + }; + filters?: { + categories: FilterCategory[]; + hasActiveFilters: boolean; + onClearFilters: () => void; + activeFilterCount?: number; + }; + ctaButton?: { + label: string; + onClick: () => void; + icon?: React.ReactNode; + }; } export function DataTable({ - data, - columns, - onRowClick, - className, - emptyMessage = "No data found.", - isLoading = false, - pagination, - onPageChange, - onPageSizeChange, - search, - filters, - ctaButton, + data, + columns, + onRowClick, + className, + emptyMessage = "No data found.", + isLoading = false, + pagination, + onPageChange, + onPageSizeChange, + search, + filters, + ctaButton, }: DataTableProps) { - const [sorting, setSorting] = useState([]); - const [searchValue, setSearchValue] = useState(search?.value || ""); + const [sorting, setSorting] = useState([]); + const [searchValue, setSearchValue] = useState(search?.value || ""); - // Internal debounced search - useEffect(() => { - const timer = setTimeout(() => { - search?.onChange(searchValue); - }, 300); + // Internal debounced search + useEffect(() => { + const timer = setTimeout(() => { + search?.onChange(searchValue); + }, 300); - return () => { - clearTimeout(timer); - }; - }, [searchValue, search]); + return () => { + clearTimeout(timer); + }; + }, [searchValue, search]); - const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - enableColumnResizing: true, - columnResizeMode: "onChange", - defaultColumn: { - minSize: 40, - size: 150, - }, - state: { - sorting, - }, - onSortingChange: setSorting, - }); + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + enableColumnResizing: true, + columnResizeMode: "onChange", + defaultColumn: { + minSize: 40, + size: 150, + }, + state: { + sorting, + }, + onSortingChange: setSorting, + }); - // Only show pagination when we have valid data and total count - const showPagination = - pagination && data.length > 0 && pagination.totalCount > 0; + // Only show pagination when we have valid data and total count + const showPagination = + pagination && data.length > 0 && pagination.totalCount > 0; - return ( -
- {(search || filters) && ( -
- {search && ( -
-
- - setSearchValue(e.target.value)} - className="pl-8 pr-8" - /> - {searchValue && ( - - )} -
-
- )} - {filters && ( - - - - - -
- {filters.categories.map((category, index) => ( -
- {category.label} -
- {category.items.map((item) => ( - - {item.icon ? ( -
- {item.icon} - {item.label} -
- ) : ( - item.label - )} -
- ))} -
-
- ))} -
+ return ( +
+ {(search || filters) && ( +
+ {search && ( +
+
+ + setSearchValue(e.target.value)} + className="pl-8 pr-8" + /> + {searchValue && ( + + )} +
+
+ )} + {filters && ( + + + + + +
+ {filters.categories.map((category, index) => ( +
+ {category.label} +
+ {category.items.map((item) => ( + + {item.icon ? ( +
+ {item.icon} + {item.label} +
+ ) : ( + item.label + )} +
+ ))} +
+
+ ))} +
- {filters.hasActiveFilters && ( -
- - -
- )} -
-
- )} - {ctaButton && ( - - )} -
- )} + {filters.hasActiveFilters && ( +
+ + +
+ )} + + + )} + {ctaButton && ( + + )} +
+ )} -
-
- - - - {isLoading ? ( - - - - - - ) : table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - onRowClick?.(row.original)} - > - {row.getVisibleCells().map((cell) => ( - -
- {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} -
-
{ - // Stop propagation to prevent row click when resizing - e.stopPropagation(); - }} - /> - - ))} - - )) - ) : ( - - - {emptyMessage} - - - )} - -
-
-
+
+
+ + + + {isLoading ? ( + + + + + + ) : table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + onRowClick?.(row.original)} + > + {row.getVisibleCells().map((cell) => ( + +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} +
+
{ + // Stop propagation to prevent row click when resizing + e.stopPropagation(); + }} + /> + + ))} + + )) + ) : ( + + + {emptyMessage} + + + )} + +
+
+
- {showPagination && onPageChange && onPageSizeChange && ( - - )} -
- ); + {showPagination && onPageChange && onPageSizeChange && ( + + )} +
+ ); } diff --git a/apps/app/src/components/user-menu.tsx b/apps/app/src/components/user-menu.tsx index bf2c744bbb..6b0a7732ab 100644 --- a/apps/app/src/components/user-menu.tsx +++ b/apps/app/src/components/user-menu.tsx @@ -4,14 +4,10 @@ import { Avatar, AvatarFallback, AvatarImageNext } from "@bubba/ui/avatar"; import { DropdownMenu, DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, - DropdownMenuShortcut, DropdownMenuTrigger, } from "@bubba/ui/dropdown-menu"; -import Link from "next/link"; import { LocaleSwitch } from "./locale-switch"; import { SignOut } from "./sign-out"; import { ThemeSwitch } from "./theme-switch"; diff --git a/apps/app/src/locales/en.ts b/apps/app/src/locales/en.ts index 177a152579..754c528833 100644 --- a/apps/app/src/locales/en.ts +++ b/apps/app/src/locales/en.ts @@ -301,6 +301,7 @@ export default { setup: "Welcome to Comp AI", description: "Tell us a bit about your organization and what framework(s) you want to get started with.", + switch: "Switching organization...", trigger: { title: "Hold tight, we're creating your organization", creating: "This may take a minute or two...", diff --git a/packages/ui/src/components/chart-tooltip.tsx b/packages/ui/src/components/chart-tooltip.tsx index cd4615b378..36f93b1fe2 100644 --- a/packages/ui/src/components/chart-tooltip.tsx +++ b/packages/ui/src/components/chart-tooltip.tsx @@ -153,7 +153,7 @@ const TooltipContent = React.forwardRef< return createPortal( isMobile ? (
{children} diff --git a/packages/ui/src/components/chart.tsx b/packages/ui/src/components/chart.tsx index a084d38280..eaba83a253 100644 --- a/packages/ui/src/components/chart.tsx +++ b/packages/ui/src/components/chart.tsx @@ -84,13 +84,13 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { ([theme, prefix]) => ` ${prefix} [data-chart=${id}] { ${colorConfig - .map(([key, itemConfig]) => { - const color = - itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || - itemConfig.color; - return color ? ` --color-${key}: ${color};` : null; - }) - .join("\n")} + .map(([key, itemConfig]) => { + const color = + itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || + itemConfig.color; + return color ? ` --color-${key}: ${color};` : null; + }) + .join("\n")} } `, ) @@ -105,13 +105,13 @@ const ChartTooltip = RechartsPrimitive.Tooltip; const ChartTooltipContent = React.forwardRef< HTMLDivElement, React.ComponentProps & - React.ComponentProps<"div"> & { - hideLabel?: boolean; - hideIndicator?: boolean; - indicator?: "line" | "dot" | "dashed"; - nameKey?: string; - labelKey?: string; - } + React.ComponentProps<"div"> & { + hideLabel?: boolean; + hideIndicator?: boolean; + indicator?: "line" | "dot" | "dashed"; + nameKey?: string; + labelKey?: string; + } >( ( { @@ -179,7 +179,7 @@ const ChartTooltipContent = React.forwardRef<
@@ -261,10 +261,10 @@ const ChartLegend = RechartsPrimitive.Legend; const ChartLegendContent = React.forwardRef< HTMLDivElement, React.ComponentProps<"div"> & - Pick & { - hideIcon?: boolean; - nameKey?: string; - } + Pick & { + hideIcon?: boolean; + nameKey?: string; + } >( ( { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, @@ -328,8 +328,8 @@ function getPayloadConfigFromPayload( const payloadPayload = "payload" in payload && - typeof payload.payload === "object" && - payload.payload !== null + typeof payload.payload === "object" && + payload.payload !== null ? payload.payload : undefined; From d178df9bc1395c1d42f6ddfbe868257928c8ca51 Mon Sep 17 00:00:00 2001 From: Languine Bot Date: Fri, 21 Mar 2025 14:48:24 +0000 Subject: [PATCH 2/4] chore: (i18n) update translations using Languine.ai --- apps/app/languine.lock | 1 + apps/app/src/locales/es.ts | 3 ++- apps/app/src/locales/fr.ts | 3 ++- apps/app/src/locales/no.ts | 3 ++- apps/app/src/locales/pt.ts | 3 ++- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/app/languine.lock b/apps/app/languine.lock index a2759dd588..071cf56b79 100644 --- a/apps/app/languine.lock +++ b/apps/app/languine.lock @@ -204,6 +204,7 @@ files: onboarding.submit: 85a18a0474d135c1f4c6da6c383a2d81 onboarding.setup: 54efd9605180a9a74e6d1a53529f858e onboarding.description: fcfd2a71d9095421e290f1b1229d1c6e + onboarding.switch: 7dd9e91261dd30c4cbbb77084b9af1bc onboarding.trigger.title: c0a683b95c32208c79a08309225647ee onboarding.trigger.creating: 5c1f94a8b72c6f33012732e6b8ba44e5 onboarding.trigger.completed: 435e0545f0bbfbbbe640e9e12e54c663 diff --git a/apps/app/src/locales/es.ts b/apps/app/src/locales/es.ts index fc4619a56c..b4bf7b614e 100644 --- a/apps/app/src/locales/es.ts +++ b/apps/app/src/locales/es.ts @@ -286,7 +286,8 @@ export default { "continue": "Continuar al panel de control", error: "Algo salió mal, por favor intenta de nuevo." }, - creating: "Creando tu organización..." + creating: "Creando tu organización...", + "switch": "Cambiando de organización..." }, overview: { title: "Resumen", diff --git a/apps/app/src/locales/fr.ts b/apps/app/src/locales/fr.ts index eb676a8399..056a41c713 100644 --- a/apps/app/src/locales/fr.ts +++ b/apps/app/src/locales/fr.ts @@ -286,7 +286,8 @@ export default { "continue": "Continuer vers le tableau de bord", error: "Une erreur s'est produite, veuillez réessayer." }, - creating: "Création de votre organisation..." + creating: "Création de votre organisation...", + "switch": "Changement d'organisation..." }, overview: { title: "Aperçu", diff --git a/apps/app/src/locales/no.ts b/apps/app/src/locales/no.ts index d1482ff900..eddd3ffc6e 100644 --- a/apps/app/src/locales/no.ts +++ b/apps/app/src/locales/no.ts @@ -286,7 +286,8 @@ export default { "continue": "Fortsett til dashbordet", error: "Noe gikk galt, vennligst prøv igjen." }, - creating: "Oppretter organisasjonen din..." + creating: "Oppretter organisasjonen din...", + "switch": "Bytter organisasjon..." }, overview: { title: "Oversikt", diff --git a/apps/app/src/locales/pt.ts b/apps/app/src/locales/pt.ts index a98fbe258e..6b90f1f2dc 100644 --- a/apps/app/src/locales/pt.ts +++ b/apps/app/src/locales/pt.ts @@ -286,7 +286,8 @@ export default { "continue": "Continuar para o painel", error: "Algo deu errado, por favor, tente novamente." }, - creating: "Criando sua organização..." + creating: "Criando sua organização...", + "switch": "Mudando de organização..." }, overview: { title: "Visão Geral", From c3fd4338b30c7e7bea23480a8ca5b81ec3da7f9f Mon Sep 17 00:00:00 2001 From: Lewis Carhart Date: Fri, 21 Mar 2025 15:00:46 +0000 Subject: [PATCH 3/4] Refactor organization switcher and create organization modal for improved UX - Simplified the `OrganizationSwitcher` component by removing unnecessary animations and styling. - Enhanced `CreateOrgModal` to prevent closing during execution and conditionally render titles and descriptions based on execution state. - Updated `DialogContent` to support new props for controlling close behavior and overlay interactions. --- .../components/modals/create-org-modal.tsx | 31 +++++++++++++++---- .../src/components/organization-switcher.tsx | 11 ++----- packages/ui/src/components/dialog.tsx | 4 ++- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/apps/app/src/components/modals/create-org-modal.tsx b/apps/app/src/components/modals/create-org-modal.tsx index 252377d504..bb8ac26470 100644 --- a/apps/app/src/components/modals/create-org-modal.tsx +++ b/apps/app/src/components/modals/create-org-modal.tsx @@ -71,14 +71,33 @@ export function CreateOrgModal({ onOpenChange, frameworks }: Props) { createOrganization.execute(data); }; + const isExecuting = createOrganization.status === "executing" || (runId !== null && publicAccessToken !== null); + + // Prevent dialog from closing when executing + const handleOpenChange = (open: boolean) => { + if (isExecuting && !open) return; + onOpenChange(open); + }; + return ( - +
- {t("onboarding.title")} - - {t("onboarding.description")} - + {!isExecuting && (runId || publicAccessToken) ? ( + <> + {t("onboarding.title")} + + {t("onboarding.description")} + + + ) : ( + <> + {t("onboarding.title")} + + {t("onboarding.description")} + + + )} {runId && publicAccessToken && ( @@ -217,7 +236,7 @@ export function CreateOrgModal({ onOpenChange, frameworks }: Props) {
-