diff --git a/apps/apollo-vertex/content/docs/patterns/shell.mdx b/apps/apollo-vertex/content/docs/patterns/shell.mdx index 51af506f4..ba6e070f7 100644 --- a/apps/apollo-vertex/content/docs/patterns/shell.mdx +++ b/apps/apollo-vertex/content/docs/patterns/shell.mdx @@ -11,14 +11,6 @@ import { PreviewFullScreen } from '../../../app/_components/preview-full-screen' -## Minimal Header Variant - -Use the `variant="minimal"` prop to render a horizontal header layout instead of the default sidebar. - - - - - ## Features - **Optional OAuth2 Authentication**: Plug-and-play authorization code flow with PKCE @@ -52,12 +44,49 @@ function App() { } ``` +### Nested navigation + +A group sets `path` for the prefix and `to` for the link. This preview opens on Active. That child has the current background. Projects stays in the active color for `/preview/shell/projects` and the routes under it, and does not take that background. + + + + + +```tsx +import { FolderOpen } from 'lucide-react'; + +const projects = { + path: '/preview/shell/projects', + to: '/preview/shell/projects/active', + label: 'projects', + icon: FolderOpen, + subItems: [ + { path: '/preview/shell/projects/active', label: 'active' }, + { path: '/preview/shell/projects/archive', label: 'archive' }, + ], +}; +``` + +Labels are translation keys. A key in the app's default catalog is used first, including keys that are not in Apollo Vertex. Vertex keys such as `dashboard` still resolve when the app catalog does not define them. + +### App translations + +Pass `configureI18n` to `ApolloShell` or `LocaleProvider` when the app initializes i18next itself. The shell then runs that function instead of replacing the app catalog with the Vertex one. + +### Minimal header + +Use `variant="minimal"` for a horizontal header instead of the sidebar. The header highlights only the most specific matching path, so a landing page is not selected together with a section under it. A nested page still highlights that section. `to` is only the link target. + + + + + ## Adding Authentication To enable built-in OAuth2 authentication with PKCE, wrap the shell with `ShellAuthProvider`. Both `ShellAuthProvider` and `ApolloShell` must be inside a `QueryClientProvider` from `@tanstack/react-query`. -When a `ShellAuthProvider` is present, the shell automatically shows a login screen until the user is authenticated. +When a `ShellAuthProvider` is present, the shell automatically shows a login screen until the user is authenticated. While the token is still loading, it shows the loading skeleton instead of that login screen. ```tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; @@ -166,7 +195,7 @@ Names that don't resolve are denied. Requires `@uipath/vs-core` 4.4.0 or later. ``` -## Resolving group ids by role +### Resolving group ids by role `GroupMembershipGuard` and `useIsGroupMember` need group ids, but applications typically reason in terms of roles whose backing group names are known ahead of diff --git a/apps/apollo-vertex/templates/shell/ShellRoot.tsx b/apps/apollo-vertex/templates/shell/ShellRoot.tsx index 0eedb8f70..1c78561c7 100644 --- a/apps/apollo-vertex/templates/shell/ShellRoot.tsx +++ b/apps/apollo-vertex/templates/shell/ShellRoot.tsx @@ -6,7 +6,16 @@ import { ShellThemeProvider } from "./ShellThemeProvider"; const navItems: ShellNavItem[] = [ { path: "/preview/shell/dashboard", label: "dashboard", icon: Home }, - { path: "/preview/shell/projects", label: "projects", icon: FolderOpen }, + { + path: "/preview/shell/projects", + to: "/preview/shell/projects/active", + label: "projects", + icon: FolderOpen, + subItems: [ + { path: "/preview/shell/projects/active", label: "active" }, + { path: "/preview/shell/projects/archive", label: "archive" }, + ], + }, { path: "/preview/shell/analytics", label: "analytics", icon: BarChart3 }, { path: "/preview/shell/team", label: "team", icon: Users }, { path: "/preview/shell/settings", label: "settings", icon: Settings }, diff --git a/apps/apollo-vertex/templates/shell/ShellRoutes.tsx b/apps/apollo-vertex/templates/shell/ShellRoutes.tsx index 03fbbc8bb..601a36cfe 100644 --- a/apps/apollo-vertex/templates/shell/ShellRoutes.tsx +++ b/apps/apollo-vertex/templates/shell/ShellRoutes.tsx @@ -39,6 +39,18 @@ export const shellProjectsRoute = createRoute({ component: () => , }); +export const shellProjectsActiveRoute = createRoute({ + getParentRoute: () => shellRoute, + path: "/projects/active", + component: () => , +}); + +export const shellProjectsArchiveRoute = createRoute({ + getParentRoute: () => shellRoute, + path: "/projects/archive", + component: () => , +}); + export const shellAnalyticsRoute = createRoute({ getParentRoute: () => shellRoute, path: "/analytics", diff --git a/apps/apollo-vertex/templates/shell/ShellTemplate.tsx b/apps/apollo-vertex/templates/shell/ShellTemplate.tsx index 8dbc2022d..d3a841a67 100644 --- a/apps/apollo-vertex/templates/shell/ShellTemplate.tsx +++ b/apps/apollo-vertex/templates/shell/ShellTemplate.tsx @@ -22,6 +22,8 @@ import { shellDashboardRoute, shellIndexRoute, shellInvoiceDetailRoute, + shellProjectsActiveRoute, + shellProjectsArchiveRoute, shellProjectsRoute, shellRoute, shellSettingsRoute, @@ -31,6 +33,7 @@ import { rootRoute } from "./ShellRouteTree"; export interface ShellTemplateProps { variant?: "minimal"; + initialPath?: string; } const SHELL_PREVIEW_PATH_KEY = "shell-preview-path"; @@ -54,6 +57,8 @@ const routeTree = rootRoute.addChildren([ shellDashboardRoute, shellInvoiceDetailRoute, shellProjectsRoute, + shellProjectsActiveRoute, + shellProjectsArchiveRoute, shellAnalyticsRoute, shellTeamRoute, shellSettingsRoute, @@ -68,7 +73,12 @@ const routeTree = rootRoute.addChildren([ catchAllRoute, ]); -function getInitialEntry(storageKey: ShellPreviewPathKey, variant?: "minimal") { +function getInitialEntry( + storageKey: ShellPreviewPathKey, + variant?: "minimal", + initialPath?: string, +) { + if (initialPath) return initialPath; const stored = localStorage.getItem(storageKey); if (stored) return stored; return variant === "minimal" ? "/preview/shell-minimal" : "/preview/shell"; @@ -77,26 +87,30 @@ function getInitialEntry(storageKey: ShellPreviewPathKey, variant?: "minimal") { function createShellRouter( storageKey: ShellPreviewPathKey, variant?: "minimal", + initialPath?: string, ) { const history = createMemoryHistory({ - initialEntries: [getInitialEntry(storageKey, variant)], + initialEntries: [getInitialEntry(storageKey, variant, initialPath)], }); return createRouter({ routeTree, history }); } -export function ShellTemplate({ variant }: ShellTemplateProps) { +export function ShellTemplate({ variant, initialPath }: ShellTemplateProps) { const storageKey = variant === "minimal" ? SHELL_MINIMAL_PREVIEW_PATH_KEY : SHELL_PREVIEW_PATH_KEY; - const [router] = useState(() => createShellRouter(storageKey, variant)); + const [router] = useState(() => + createShellRouter(storageKey, variant, initialPath), + ); useEffect(() => { + if (initialPath) return; const unsubscribe = router.subscribe("onResolved", ({ toLocation }) => { localStorage.setItem(storageKey, toLocation.pathname); }); return unsubscribe; - }, [router, storageKey]); + }, [router, storageKey, initialPath]); return ( diff --git a/packages/apollo-vertex/locales/en.json b/packages/apollo-vertex/locales/en.json index 13e05ef3f..86086d6c6 100644 --- a/packages/apollo-vertex/locales/en.json +++ b/packages/apollo-vertex/locales/en.json @@ -3,6 +3,7 @@ "access_denied": "Access Denied", "access_denied_description": "You don't have access to this application. Ask your administrator to add you to the users group.", "actions": "Actions", + "active": "Active", "actual_input": "Actual input", "actual_output": "Actual output", "add_attachment": "Add attachment", @@ -24,6 +25,7 @@ "analytics": "Analytics", "apollo_vertex": "Apollo Vertex", "apollo_vertex_description": "- UiPath Apollo Vertex", + "archive": "Archive", "are_you_sure": "Are you sure?", "asc": "Asc", "ask_ai": "Ask AI", @@ -186,6 +188,8 @@ "more_pages": "More pages", "multi_line_too_few_metrics": "Multi-line needs two distinct metrics, but the request resolved to fewer than two.", "my_template": "My Template", + "nav_collapse": "Collapse {{label}}", + "nav_expand": "Expand {{label}}", "need_help": "Need help? Contact your administrator", "new_conversation": "New conversation", "new_conversation_confirm_description": "This will clear all messages and cannot be undone.", diff --git a/packages/apollo-vertex/registry.json b/packages/apollo-vertex/registry.json index 64fca8247..239949608 100644 --- a/packages/apollo-vertex/registry.json +++ b/packages/apollo-vertex/registry.json @@ -2406,6 +2406,14 @@ "path": "registry/shell/shell-sidebar.tsx", "type": "registry:ui" }, + { + "path": "registry/shell/shell-nav-active.ts", + "type": "registry:ui" + }, + { + "path": "registry/shell/shell-nav-label.ts", + "type": "registry:ui" + }, { "path": "registry/shell/shell-company.tsx", "type": "registry:ui" diff --git a/packages/apollo-vertex/registry/shell/shell-company.tsx b/packages/apollo-vertex/registry/shell/shell-company.tsx index 28eb02120..ea0cd790c 100644 --- a/packages/apollo-vertex/registry/shell/shell-company.tsx +++ b/packages/apollo-vertex/registry/shell/shell-company.tsx @@ -2,7 +2,6 @@ import { Link } from "@tanstack/react-router"; import { AnimatePresence, motion } from "framer-motion"; import { PanelLeft } from "lucide-react"; import { useState } from "react"; -import { useVertexTranslation } from "@/lib/vertex-translation"; import { Button } from "@/components/ui/button"; import { useSidebar } from "@/components/ui/sidebar"; import { @@ -12,6 +11,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; +import { useVertexTranslation } from "@/lib/vertex-translation"; import type { CompanyLogo } from "./shell"; import { fastFadeTransition, @@ -187,6 +187,7 @@ export const Company = ({ + + )} + {item.subItems.map((subItem) => { - const subActive = isActive(subItem.path); + const sub = navItemState(pathname, { + path: subItem.path, + to: subItem.to, + }); return ( - + @@ -301,11 +321,11 @@ function SidebarNav({ - + diff --git a/packages/apollo-vertex/registry/shell/shell-text.tsx b/packages/apollo-vertex/registry/shell/shell-text.tsx index eea2388ae..58c3d80c7 100644 --- a/packages/apollo-vertex/registry/shell/shell-text.tsx +++ b/packages/apollo-vertex/registry/shell/shell-text.tsx @@ -3,12 +3,13 @@ import { useVertexTranslation, VERTEX_NAMESPACE, } from "@/lib/vertex-translation"; -import type { TranslationKey } from "./shell-translation-key"; +import { navCatalogFromHook, readNavLabel } from "./shell-nav-label"; +import type { ShellNavLabel } from "./shell-translation-key"; -export const Text = ({ value }: { value: TranslationKey }) => { - const { t } = useVertexTranslation(); +export const Text = ({ value }: { value: ShellNavLabel }) => { + const { i18n } = useVertexTranslation(); if (typeof value === "string") { - return t(value); + return readNavLabel(navCatalogFromHook(i18n), value); } return ( ; } | ParseKeys; + +type TranslationObject = Extract; + +// Plain strings are app-catalog keys or vertex keys. Vertex keys are strings, +// so they still typecheck; the app catalog is what makes invoice labels resolve. +export type ShellNavLabel = string | TranslationObject; diff --git a/packages/apollo-vertex/registry/shell/shell.tsx b/packages/apollo-vertex/registry/shell/shell.tsx index 8425c7b4c..f7fef7543 100644 --- a/packages/apollo-vertex/registry/shell/shell.tsx +++ b/packages/apollo-vertex/registry/shell/shell.tsx @@ -8,9 +8,18 @@ import { AuthContext, useAuth } from "./shell-auth-provider"; import { ShellLayout } from "./shell-layout"; import { LocaleProvider } from "./shell-locale-provider"; import { ShellLogin } from "./shell-login"; -import type { TranslationKey } from "./shell-translation-key"; +import type { ShellNavLabel } from "./shell-translation-key"; import { ShellUserProvider } from "./shell-user-provider"; +function ShellLoadingSkeleton() { + return ( +
+ + +
+ ); +} + export interface CompanyLogo { url: string; darkUrl?: string; @@ -20,12 +29,14 @@ export interface CompanyLogo { export interface ShellSubNavItem { path: string; - label: TranslationKey; + to?: string; + label: ShellNavLabel; } export interface ShellNavItem { path: string; - label: TranslationKey; + to?: string; + label: ShellNavLabel; icon: LucideIcon; subItems?: ShellSubNavItem[]; } @@ -37,6 +48,7 @@ export interface ApolloShellProps extends PropsWithChildren { companyLogo?: CompanyLogo; navItems: ShellNavItem[]; loginDescription?: string; + configureI18n?: () => Promise; } const ApolloShellContent: FC = ({ @@ -49,7 +61,11 @@ const ApolloShellContent: FC = ({ loginDescription, }) => { const authContext = useContext(AuthContext); - const { accessToken } = useAuth(); + const { accessToken, isLoading } = useAuth(); + + if (authContext && isLoading) { + return ; + } if (authContext && !accessToken) { return ; @@ -78,15 +94,12 @@ export const ApolloShell: FC = ({ variant, navItems, loginDescription, + configureI18n, }) => { return ( - - - - } + configureI18n={configureI18n} + loadingElement={} >