Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 39 additions & 10 deletions apps/apollo-vertex/content/docs/patterns/shell.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,6 @@ import { PreviewFullScreen } from '../../../app/_components/preview-full-screen'
<ShellTemplate />
</PreviewFullScreen>

## Minimal Header Variant

Use the `variant="minimal"` prop to render a horizontal header layout instead of the default sidebar.

<PreviewFullScreen title="Shell minimal preview">
<ShellTemplate variant="minimal" />
</PreviewFullScreen>

## Features

- **Optional OAuth2 Authentication**: Plug-and-play authorization code flow with PKCE
Expand Down Expand Up @@ -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.

<PreviewFullScreen title="Nested navigation preview">
<ShellTemplate initialPath="/preview/shell/projects/active" />
</PreviewFullScreen>

```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.

<PreviewFullScreen title="Shell minimal preview">
<ShellTemplate variant="minimal" />
</PreviewFullScreen>

## 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';
Expand Down Expand Up @@ -166,7 +195,7 @@ Names that don't resolve are denied. Requires `@uipath/vs-core` 4.4.0 or later.
</GroupMembershipGuard>
```

## 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
Expand Down
11 changes: 10 additions & 1 deletion apps/apollo-vertex/templates/shell/ShellRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
12 changes: 12 additions & 0 deletions apps/apollo-vertex/templates/shell/ShellRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ export const shellProjectsRoute = createRoute({
component: () => <PlaceholderPage title="Projects" />,
});

export const shellProjectsActiveRoute = createRoute({
getParentRoute: () => shellRoute,
path: "/projects/active",
component: () => <PlaceholderPage title="Active" />,
});

export const shellProjectsArchiveRoute = createRoute({
getParentRoute: () => shellRoute,
path: "/projects/archive",
component: () => <PlaceholderPage title="Archive" />,
});

export const shellAnalyticsRoute = createRoute({
getParentRoute: () => shellRoute,
path: "/analytics",
Expand Down
24 changes: 19 additions & 5 deletions apps/apollo-vertex/templates/shell/ShellTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
shellDashboardRoute,
shellIndexRoute,
shellInvoiceDetailRoute,
shellProjectsActiveRoute,
shellProjectsArchiveRoute,
shellProjectsRoute,
shellRoute,
shellSettingsRoute,
Expand All @@ -31,6 +33,7 @@ import { rootRoute } from "./ShellRouteTree";

export interface ShellTemplateProps {
variant?: "minimal";
initialPath?: string;
}

const SHELL_PREVIEW_PATH_KEY = "shell-preview-path";
Expand All @@ -54,6 +57,8 @@ const routeTree = rootRoute.addChildren([
shellDashboardRoute,
shellInvoiceDetailRoute,
shellProjectsRoute,
shellProjectsActiveRoute,
shellProjectsArchiveRoute,
shellAnalyticsRoute,
shellTeamRoute,
shellSettingsRoute,
Expand All @@ -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";
Expand All @@ -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 (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
Expand Down
4 changes: 4 additions & 0 deletions packages/apollo-vertex/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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.",
Expand Down
8 changes: 8 additions & 0 deletions packages/apollo-vertex/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion packages/apollo-vertex/registry/shell/shell-company.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -187,6 +187,7 @@ export const Company = ({
<Button
variant="ghost"
size="icon-sm"
aria-label={t("close_sidebar")}
onClick={toggleSidebar}
className="ml-auto shrink-0 text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-foreground cursor-pointer"
>
Expand Down
18 changes: 11 additions & 7 deletions packages/apollo-vertex/registry/shell/shell-locale-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import {
useEffectEvent,
useState,
} from "react";
import { useVertexTranslation } from "@/lib/vertex-translation";
import { Spinner } from "@/components/ui/spinner";
import { configurei18n } from "@/lib/i18n";
import { useVertexTranslation } from "@/lib/vertex-translation";
import { LANGUAGE_CHANGED_EVENT } from "./shell-constants";

interface LocaleProviderProps {
loadingElement?: ReactNode;
configureI18n?: () => Promise<void>;
}

export const LocaleProviderComponent = ({ children }: PropsWithChildren) => {
Expand Down Expand Up @@ -50,16 +51,19 @@ export const LocaleProviderComponent = ({ children }: PropsWithChildren) => {
export const LocaleProvider = ({
children,
loadingElement,
configureI18n = configurei18n,
}: PropsWithChildren<LocaleProviderProps>) => {
const [isConfigured, setIsConfigured] = useState(false);
const configure = async () => {
await configurei18n();
setIsConfigured(true);
};

useEffect(() => {
void configure();
}, []);
let cancelled = false;
void configureI18n().then(() => {
if (!cancelled) setIsConfigured(true);
});
return () => {
cancelled = true;
};
}, [configureI18n]);

if (!isConfigured)
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
import { Link, useLocation } from "@tanstack/react-router";
import { Link } from "@tanstack/react-router";
import { cn } from "@/lib/utils";
import { Text } from "./shell-text";
import type { TranslationKey } from "./shell-translation-key";
import type { ShellNavLabel } from "./shell-translation-key";

interface MinimalNavItemProps {
to: string;
label: TranslationKey;
active: boolean;
label: ShellNavLabel;
}

export const MinimalNavItem = ({ to, label }: MinimalNavItemProps) => {
const { pathname } = useLocation();
const isActive = pathname === to;

export const MinimalNavItem = ({ to, active, label }: MinimalNavItemProps) => {
return (
<Link
to={to}
className={cn(
"px-4 py-1.5 rounded-full text-sm font-medium transition-colors duration-200 shrink-0 whitespace-nowrap",
isActive
active
? "bg-[oklch(0.92_0.035_218)] dark:bg-primary-900/30 text-primary-700 dark:text-primary-400 font-semibold"
: "text-muted-foreground hover:text-foreground hover:bg-black/[0.04] dark:hover:bg-white/[0.06]",
)}
Expand Down
Loading
Loading