feat(onboarding): add management tour with multi-page navigation
Guide admins through boards, apps, integrations, and users with per-page steps, TourTarget highlights, optimistic completion, and reset in settings.
This commit is contained in:
9 files changed
+253
-167
No files matched your search
@@ -11,6 +11,7 @@ import type { inferSearchParamsFromSchema } from "@homarr/common/types";
|
||||
import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
||||
import { Link, SearchInput, TablePagination } from "@homarr/ui";
|
||||
|
||||
import { TourTarget } from "~/components/layout/header/tour-target";
|
||||
import { ManageContainer } from "~/components/manage/manage-container";
|
||||
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
@@ -47,19 +48,21 @@ export default async function AppsPage(props: AppsPageProps) {
|
||||
<Group justify="space-between" align="center">
|
||||
<SearchInput placeholder={`${t("search")}...`} defaultValue={searchParams.search} flexExpand />
|
||||
{session.user.permissions.includes("app-create") && (
|
||||
<MobileAffixButton component={Link} href="/manage/apps/new">
|
||||
{t("page.create.title")}
|
||||
</MobileAffixButton>
|
||||
<TourTarget id="manage-apps-create">
|
||||
<MobileAffixButton component={Link} href="/manage/apps/new">
|
||||
{t("page.create.title")}
|
||||
</MobileAffixButton>
|
||||
</TourTarget>
|
||||
)}
|
||||
</Group>
|
||||
{apps.length === 0 && <AppNoResults />}
|
||||
{apps.length > 0 && (
|
||||
<TourTarget id="manage-apps-list">
|
||||
<Stack gap="sm">
|
||||
{apps.length === 0 && <AppNoResults />}
|
||||
{apps.map((app) => (
|
||||
<AppCard key={app.id} app={app} />
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</TourTarget>
|
||||
|
||||
{/* Added margin to not hide pagination behind affix-button */}
|
||||
<Group justify="end" mb={48}>
|
||||
|
||||
@@ -23,6 +23,7 @@ import { getScopedI18n } from "@homarr/translation/server";
|
||||
import { Link, UserAvatar } from "@homarr/ui";
|
||||
|
||||
import { getBoardPermissionsAsync } from "~/components/board/permissions/server";
|
||||
import { TourTarget } from "~/components/layout/header/tour-target";
|
||||
import { ManageContainer } from "~/components/manage/manage-container";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
import { BoardCardMenuDropdown } from "./_components/board-card-menu-dropdown";
|
||||
@@ -40,16 +41,22 @@ export default async function ManageBoardsPage() {
|
||||
<Stack>
|
||||
<Group justify="space-between">
|
||||
<Title mb="md">{t("title")}</Title>
|
||||
{canCreateBoards && <CreateBoardButton />}
|
||||
{canCreateBoards && (
|
||||
<TourTarget id="manage-boards-create">
|
||||
<CreateBoardButton />
|
||||
</TourTarget>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Grid mb={{ base: "xl", md: 0 }}>
|
||||
{boards.map((board) => (
|
||||
<GridCol span={{ base: 12, md: 6 }} key={board.id}>
|
||||
<BoardCard board={board} />
|
||||
</GridCol>
|
||||
))}
|
||||
</Grid>
|
||||
<TourTarget id="manage-boards-list">
|
||||
<Grid mb={{ base: "xl", md: 0 }}>
|
||||
{boards.map((board) => (
|
||||
<GridCol span={{ base: 12, md: 6 }} key={board.id}>
|
||||
<BoardCard board={board} />
|
||||
</GridCol>
|
||||
))}
|
||||
</Grid>
|
||||
</TourTarget>
|
||||
</Stack>
|
||||
</ManageContainer>
|
||||
);
|
||||
|
||||
@@ -37,6 +37,7 @@ import { getIntegrationName } from "@homarr/definitions";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
import { CountBadge, IntegrationAvatar, Link } from "@homarr/ui";
|
||||
|
||||
import { TourTarget } from "~/components/layout/header/tour-target";
|
||||
import { ManageContainer } from "~/components/manage/manage-container";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
import { NoResults } from "~/components/no-results";
|
||||
@@ -84,18 +85,22 @@ export default async function IntegrationsPage(props: IntegrationsPageProps) {
|
||||
</IntegrationSelectMenu>
|
||||
</Box>
|
||||
|
||||
<Box visibleFrom="md">
|
||||
<IntegrationSelectMenu>
|
||||
<MenuTarget>
|
||||
<Button rightSection={<IconChevronDown size={16} stroke={1.5} />}>{t("action.create")}</Button>
|
||||
</MenuTarget>
|
||||
</IntegrationSelectMenu>
|
||||
</Box>
|
||||
<TourTarget id="manage-integrations-create">
|
||||
<Box visibleFrom="md">
|
||||
<IntegrationSelectMenu>
|
||||
<MenuTarget>
|
||||
<Button rightSection={<IconChevronDown size={16} stroke={1.5} />}>{t("action.create")}</Button>
|
||||
</MenuTarget>
|
||||
</IntegrationSelectMenu>
|
||||
</Box>
|
||||
</TourTarget>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<IntegrationList integrations={integrations} activeTab={searchParams.tab} />
|
||||
<TourTarget id="manage-integrations-list">
|
||||
<IntegrationList integrations={integrations} activeTab={searchParams.tab} />
|
||||
</TourTarget>
|
||||
</Stack>
|
||||
</ManageContainer>
|
||||
);
|
||||
|
||||
+7
-11
@@ -7,14 +7,10 @@ import { clientApi } from "@homarr/api/client";
|
||||
import { showSuccessNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
interface ResetToursProps {
|
||||
completedManageTour: boolean;
|
||||
completedBoardTour: boolean;
|
||||
}
|
||||
|
||||
export const ResetTours = ({ completedManageTour, completedBoardTour }: ResetToursProps) => {
|
||||
export const ResetTours = () => {
|
||||
const t = useScopedI18n("management.page.user.setting.general.item.onboardingTours");
|
||||
const utils = clientApi.useUtils();
|
||||
const { data: tourStatus } = clientApi.user.getTourStatus.useQuery();
|
||||
const { mutate: resetTours, isPending } = clientApi.user.resetTours.useMutation({
|
||||
onSuccess() {
|
||||
showSuccessNotification({
|
||||
@@ -25,15 +21,15 @@ export const ResetTours = ({ completedManageTour, completedBoardTour }: ResetTou
|
||||
},
|
||||
});
|
||||
|
||||
const tourStatusMap: Record<string, { key: "management" | "dashboard"; completed: boolean }> = {
|
||||
management: { key: "management", completed: completedManageTour },
|
||||
dashboard: { key: "dashboard", completed: completedBoardTour },
|
||||
};
|
||||
const tours = [
|
||||
{ key: "management" as const, completed: tourStatus?.completedManageTour ?? false },
|
||||
{ key: "dashboard" as const, completed: tourStatus?.completedBoardTour ?? false },
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
<Group gap="xs">
|
||||
{Object.values(tourStatusMap).map(({ key, completed }) => (
|
||||
{tours.map(({ key, completed }) => (
|
||||
<Badge key={key} color={completed ? "green" : "gray"} variant="light">
|
||||
{t(`name.${key}`)}: {completed ? t("status.completed") : t("status.notStarted")}
|
||||
</Badge>
|
||||
|
||||
@@ -122,10 +122,7 @@ export default async function EditUserPage(props: Props) {
|
||||
{session?.user.id === user.id && (
|
||||
<Stack mb="lg">
|
||||
<Title order={2}>{tGeneral("item.onboardingTours.title")}</Title>
|
||||
<ResetTours
|
||||
completedManageTour={user.completedManageTour}
|
||||
completedBoardTour={user.completedBoardTour}
|
||||
/>
|
||||
<ResetTours />
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { Link, UserAvatar } from "@homarr/ui";
|
||||
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
|
||||
|
||||
import { TourTarget } from "~/components/layout/header/tour-target";
|
||||
|
||||
interface UserListComponentProps {
|
||||
initialUserList: RouterOutputs["user"]["getAll"];
|
||||
credentialsProviderEnabled: boolean;
|
||||
@@ -69,12 +71,6 @@ export const UserListComponent = ({ initialUserList, credentialsProviderEnabled
|
||||
enableFullScreenToggle: false,
|
||||
layoutMode: "grid-no-grow",
|
||||
getRowId: (row) => row.id,
|
||||
renderTopToolbarCustomActions: () =>
|
||||
credentialsProviderEnabled ? (
|
||||
<Button component={Link} href="/manage/users/create">
|
||||
{t("management.page.user.create.title")}
|
||||
</Button>
|
||||
) : null,
|
||||
state: {
|
||||
isLoading,
|
||||
},
|
||||
@@ -82,7 +78,18 @@ export const UserListComponent = ({ initialUserList, credentialsProviderEnabled
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title mb="md">{tUserList("title")}</Title>
|
||||
<Group justify="space-between" align="center" mb="md">
|
||||
<TourTarget id="manage-users-list">
|
||||
<Title>{tUserList("title")}</Title>
|
||||
</TourTarget>
|
||||
{credentialsProviderEnabled && (
|
||||
<TourTarget id="manage-users-create">
|
||||
<Button component={Link} href="/manage/users/create">
|
||||
{t("management.page.user.create.title")}
|
||||
</Button>
|
||||
</TourTarget>
|
||||
)}
|
||||
</Group>
|
||||
<MantineReactTable table={table} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { AppShellHeader, Box, Group, UnstyledButton } from "@mantine/core";
|
||||
import { AppShellHeader, Group, UnstyledButton } from "@mantine/core";
|
||||
|
||||
import { Spotlight } from "@homarr/spotlight";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
import { ClientBurger } from "./header/burger";
|
||||
import { DesktopSearchInput, MobileSearchButton } from "./header/search";
|
||||
import { TourTarget } from "./header/tour-target";
|
||||
import { UserButton } from "./header/user";
|
||||
import { HomarrLogoWithTitle } from "./logo/homarr-logo";
|
||||
|
||||
@@ -25,15 +26,15 @@ export const MainHeader = ({ logo, actions, hasNavigation = true }: Props) => {
|
||||
{logo ?? <HomarrLogoWithTitle size="md" />}
|
||||
</UnstyledButton>
|
||||
</Group>
|
||||
<Box data-onboarding-tour-id="board-search">
|
||||
<TourTarget id="board-search">
|
||||
<DesktopSearchInput />
|
||||
</Box>
|
||||
</TourTarget>
|
||||
<Group h="100%" align="center" justify="end" style={{ flex: 1 }} wrap="nowrap">
|
||||
{actions}
|
||||
<MobileSearchButton />
|
||||
<Box data-onboarding-tour-id="board-user-menu">
|
||||
<TourTarget id="board-user-menu">
|
||||
<UserButton />
|
||||
</Box>
|
||||
</TourTarget>
|
||||
</Group>
|
||||
</Group>
|
||||
<Spotlight />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { OnboardingTour } from "@gfazioli/mantine-onboarding-tour";
|
||||
import { NavLink } from "@mantine/core";
|
||||
|
||||
import { Link } from "@homarr/ui";
|
||||
@@ -9,20 +10,22 @@ import { Link } from "@homarr/ui";
|
||||
export const CommonNavLink = (props: ClientNavigationLink) =>
|
||||
"href" in props ? <NavLinkHref {...props} /> : <NavLinkWithItems {...props} />;
|
||||
|
||||
const TourTarget = ({ id, children }: { id?: string; children: ReactNode }) => {
|
||||
if (!id) return <>{children}</>;
|
||||
return <OnboardingTour.Target id={id}>{children}</OnboardingTour.Target>;
|
||||
};
|
||||
|
||||
const NavLinkHref = (props: NavigationLinkHref) => {
|
||||
const pathname = usePathname();
|
||||
const tourId = props["data-onboarding-tour-id"];
|
||||
return props.external ? (
|
||||
const link = props.external ? (
|
||||
<NavLink
|
||||
component="a"
|
||||
label={props.label}
|
||||
leftSection={props.icon}
|
||||
href={props.href}
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
}}
|
||||
style={{ borderRadius: 5 }}
|
||||
target="_blank"
|
||||
data-onboarding-tour-id={tourId}
|
||||
/>
|
||||
) : (
|
||||
<NavLink
|
||||
@@ -31,32 +34,23 @@ const NavLinkHref = (props: NavigationLinkHref) => {
|
||||
leftSection={props.icon}
|
||||
href={props.href}
|
||||
active={pathname === props.href}
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
}}
|
||||
data-onboarding-tour-id={tourId}
|
||||
style={{ borderRadius: 5 }}
|
||||
/>
|
||||
);
|
||||
return <TourTarget id={tourId}>{link}</TourTarget>;
|
||||
};
|
||||
|
||||
const NavLinkWithItems = (props: NavigationLinkWithItems) => {
|
||||
const pathname = usePathname();
|
||||
const isActive = props.items.some((item) => item.href === pathname);
|
||||
return (
|
||||
<NavLink
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
}}
|
||||
label={props.label}
|
||||
leftSection={props.icon}
|
||||
defaultOpened={isActive}
|
||||
data-onboarding-tour-id={props["data-onboarding-tour-id"]}
|
||||
>
|
||||
const nav = (
|
||||
<NavLink style={{ borderRadius: 5 }} label={props.label} leftSection={props.icon} defaultOpened={isActive}>
|
||||
{props.items.map((item) => (
|
||||
<NavLinkHref key={item.label} {...item} />
|
||||
))}
|
||||
</NavLink>
|
||||
);
|
||||
return <TourTarget id={props["data-onboarding-tour-id"]}>{nav}</TourTarget>;
|
||||
};
|
||||
|
||||
interface CommonNavigationLinkProps {
|
||||
|
||||
@@ -1,34 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import type { OnboardingTourStep } from "@gfazioli/mantine-onboarding-tour";
|
||||
import { OnboardingTour } from "@gfazioli/mantine-onboarding-tour";
|
||||
import { Box } from "@mantine/core";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
import {
|
||||
IconAffiliateFilled,
|
||||
IconAppsFilled,
|
||||
IconLayoutDashboardFilled,
|
||||
IconPlus,
|
||||
IconUserFilled,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { createDocumentationLink } from "@homarr/definitions";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { SkipHoldButton } from "./skip-hold-button";
|
||||
import { TourForkStep } from "./tour-fork-step";
|
||||
import { TourShell } from "./tour-shell";
|
||||
import { TourStepContent } from "./tour-step-content";
|
||||
|
||||
interface ManageTourProviderProps extends PropsWithChildren {
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
const stepRoutes: Record<string, string> = {
|
||||
"manage-welcome": "/manage",
|
||||
"manage-boards-list": "/manage/boards",
|
||||
"manage-boards-create": "/manage/boards",
|
||||
"manage-apps-list": "/manage/apps",
|
||||
"manage-apps-create": "/manage/apps",
|
||||
"manage-integrations-list": "/manage/integrations",
|
||||
"manage-integrations-create": "/manage/integrations",
|
||||
"manage-users-list": "/manage/users",
|
||||
"manage-users-create": "/manage/users",
|
||||
};
|
||||
|
||||
const usersStepFocusRevealProps = {
|
||||
withReveal: false,
|
||||
popoverProps: {
|
||||
position: "bottom",
|
||||
middlewares: { shift: { padding: 16 }, flip: true },
|
||||
},
|
||||
};
|
||||
|
||||
export const ManageTourProvider = ({ children, isAdmin }: ManageTourProviderProps) => {
|
||||
const router = useRouter();
|
||||
const t = useScopedI18n("onboardingTour.manage");
|
||||
const utils = clientApi.useUtils();
|
||||
const { data: tourStatus } = clientApi.user.getTourStatus.useQuery();
|
||||
const { mutate: completeTour } = clientApi.user.completeTour.useMutation();
|
||||
const { mutate: completeTour } = clientApi.user.completeTour.useMutation({
|
||||
onSuccess() {
|
||||
void utils.user.getTourStatus.invalidate();
|
||||
},
|
||||
});
|
||||
const isMobile = useMediaQuery("(max-width: 48em)");
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
const started = tourStatus !== undefined && !tourStatus.completedManageTour;
|
||||
const isManageHome = /^(\/[^/]+)?\/manage\/?$/.test(pathname);
|
||||
const isManageSection = /^(\/[^/]+)?\/manage(\/.*)?$/.test(pathname);
|
||||
|
||||
const handleEnd = () => {
|
||||
const [tourActive, setTourActive] = useState(false);
|
||||
const [tourDismissed, setTourDismissed] = useState(false);
|
||||
const wasManageTourCompletedRef = useRef(tourStatus?.completedManageTour);
|
||||
|
||||
useEffect(() => {
|
||||
if (tourDismissed) return;
|
||||
if (isManageHome && tourStatus !== undefined && !tourStatus.completedManageTour && !isMobile) {
|
||||
setTourActive(true);
|
||||
}
|
||||
}, [isManageHome, tourStatus, isMobile, tourDismissed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isManageSection) {
|
||||
setTourActive(false);
|
||||
}
|
||||
}, [isManageSection]);
|
||||
|
||||
useEffect(() => {
|
||||
const wasCompleted = wasManageTourCompletedRef.current;
|
||||
const isCompleted = tourStatus?.completedManageTour;
|
||||
if (wasCompleted === true && isCompleted === false) {
|
||||
setTourDismissed(false);
|
||||
}
|
||||
wasManageTourCompletedRef.current = isCompleted;
|
||||
}, [tourStatus?.completedManageTour]);
|
||||
|
||||
const handleEnd = useCallback(() => {
|
||||
setTourDismissed(true);
|
||||
setTourActive(false);
|
||||
utils.user.getTourStatus.setData(undefined, {
|
||||
completedManageTour: true,
|
||||
completedBoardTour: tourStatus?.completedBoardTour ?? false,
|
||||
});
|
||||
completeTour({ tour: "manage" });
|
||||
};
|
||||
router.push("/manage");
|
||||
}, [completeTour, router, tourStatus?.completedBoardTour, utils.user.getTourStatus]);
|
||||
|
||||
const steps = useMemo(() => {
|
||||
const allSteps: (OnboardingTourStep & { adminOnly?: boolean })[] = [
|
||||
@@ -36,109 +103,118 @@ export const ManageTourProvider = ({ children, isAdmin }: ManageTourProviderProp
|
||||
id: "manage-welcome",
|
||||
title: t("welcome.title"),
|
||||
content: (
|
||||
<TourStepContent description={t("welcome.description")} docPath="/docs/category/management" />
|
||||
),
|
||||
footer: (controller: { skipTour: () => void }) => (
|
||||
<SkipHoldButton onSkip={() => controller.skipTour()} />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "manage-boards",
|
||||
title: t("boards.title"),
|
||||
content: (
|
||||
<TourStepContent description={t("boards.description")} docPath="/docs/management/boards" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "manage-apps",
|
||||
title: t("apps.title"),
|
||||
content: (
|
||||
<TourStepContent description={t("apps.description")} docPath="/docs/management/apps" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "manage-integrations",
|
||||
title: t("integrations.title"),
|
||||
content: (controller: { endTour: () => void; nextStep: () => void }) => (
|
||||
<TourForkStep
|
||||
question={t("integrationsFork.question")}
|
||||
description={t("integrations.description")}
|
||||
docPath="/docs/management/integrations"
|
||||
onYes={() => {
|
||||
controller.endTour();
|
||||
router.push("/manage/integrations/new");
|
||||
}}
|
||||
onNo={() => controller.nextStep()}
|
||||
<TourStepContent
|
||||
description={t("welcome.description")}
|
||||
documentationHref={createDocumentationLink("/docs/category/management")}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "manage-search-engines",
|
||||
title: t("searchEngines.title"),
|
||||
content: (
|
||||
<TourStepContent description={t("searchEngines.description")} docPath="/docs/management/search-engines" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "manage-medias",
|
||||
title: t("medias.title"),
|
||||
content: (
|
||||
<TourStepContent description={t("medias.description")} docPath="/docs/management/media" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "manage-users",
|
||||
title: t("users.title"),
|
||||
adminOnly: true,
|
||||
content: (controller: { endTour: () => void; nextStep: () => void }) => (
|
||||
<TourForkStep
|
||||
question={t("usersFork.question")}
|
||||
description={t("users.description")}
|
||||
docPath="/docs/management/users"
|
||||
onYes={() => {
|
||||
controller.endTour();
|
||||
router.push("/manage/users/create");
|
||||
}}
|
||||
onNo={() => controller.nextStep()}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "manage-settings",
|
||||
title: t("settings.title"),
|
||||
adminOnly: true,
|
||||
content: (
|
||||
<TourStepContent description={t("settings.description")} docPath="/docs/management/settings" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "manage-complete",
|
||||
title: t("complete.title"),
|
||||
id: "manage-boards-list",
|
||||
title: t("boardsList.title"),
|
||||
content: (
|
||||
<TourStepContent
|
||||
description={t("complete.description")}
|
||||
docPath="/docs/getting-started/after-the-installation"
|
||||
description={t("boardsList.description")}
|
||||
documentationHref={createDocumentationLink("/docs/management/boards")}
|
||||
icon={<IconLayoutDashboardFilled size={18} />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "manage-boards-create",
|
||||
title: t("boardsCreate.title"),
|
||||
content: (
|
||||
<TourStepContent
|
||||
description={t("boardsCreate.description")}
|
||||
documentationHref={createDocumentationLink("/docs/management/boards")}
|
||||
icon={<IconPlus size={18} />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "manage-apps-list",
|
||||
title: t("appsList.title"),
|
||||
content: (
|
||||
<TourStepContent
|
||||
description={t("appsList.description")}
|
||||
documentationHref={createDocumentationLink("/docs/management/apps")}
|
||||
icon={<IconAppsFilled size={18} />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "manage-apps-create",
|
||||
title: t("appsCreate.title"),
|
||||
content: (
|
||||
<TourStepContent
|
||||
description={t("appsCreate.description")}
|
||||
documentationHref={createDocumentationLink("/docs/management/apps")}
|
||||
icon={<IconPlus size={18} />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "manage-integrations-list",
|
||||
title: t("integrationsList.title"),
|
||||
content: (
|
||||
<TourStepContent
|
||||
description={t("integrationsList.description")}
|
||||
documentationHref={createDocumentationLink("/docs/management/integrations")}
|
||||
icon={<IconAffiliateFilled size={18} />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "manage-integrations-create",
|
||||
title: t("integrationsCreate.title"),
|
||||
content: (
|
||||
<TourStepContent
|
||||
description={t("integrationsCreate.description")}
|
||||
documentationHref={createDocumentationLink("/docs/management/integrations")}
|
||||
icon={<IconPlus size={18} />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "manage-users-list",
|
||||
title: t("usersList.title"),
|
||||
adminOnly: true,
|
||||
focusRevealProps: usersStepFocusRevealProps,
|
||||
content: (
|
||||
<TourStepContent
|
||||
description={t("usersList.description")}
|
||||
documentationHref={createDocumentationLink("/docs/management/users")}
|
||||
icon={<IconUserFilled size={18} />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "manage-users-create",
|
||||
title: t("usersCreate.title"),
|
||||
adminOnly: true,
|
||||
focusRevealProps: usersStepFocusRevealProps,
|
||||
content: (
|
||||
<TourStepContent
|
||||
description={t("usersCreate.description")}
|
||||
documentationHref={createDocumentationLink("/docs/management/users")}
|
||||
icon={<IconPlus size={18} />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return allSteps.filter((step) => !step.adminOnly || isAdmin) as OnboardingTourStep[];
|
||||
}, [isAdmin, router, t]);
|
||||
}, [isAdmin, t]);
|
||||
|
||||
return (
|
||||
<OnboardingTour
|
||||
tour={steps}
|
||||
started={started}
|
||||
onOnboardingTourComplete={handleEnd}
|
||||
onOnboardingTourSkip={handleEnd}
|
||||
maw={420}
|
||||
<TourShell
|
||||
steps={steps}
|
||||
started={tourActive}
|
||||
onEnd={handleEnd}
|
||||
stepRoutes={stepRoutes}
|
||||
position={{ base: "bottom", sm: "right" }}
|
||||
>
|
||||
<Box
|
||||
data-onboarding-tour-id="manage-complete"
|
||||
style={{ position: "fixed", opacity: 0, pointerEvents: "none" }}
|
||||
/>
|
||||
{children}
|
||||
</OnboardingTour>
|
||||
</TourShell>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user