mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-06 00:15:32 +00:00
fix: run the tour through Pro's tabs instead of bouncing between routes
This commit is contained in:
@@ -38,7 +38,8 @@ vi.mock('@/stores/settings-store', () => ({
|
||||
}));
|
||||
|
||||
const { TourProvider, useTour } = await import('../tour-provider');
|
||||
const { getTourSteps } = await import('../tour-steps');
|
||||
const { getTourSteps, isTourStepOnPage, proTabOfTourStep } = await import('../tour-steps');
|
||||
const { useProTabStore } = await import('@/stores/pro-tab-store');
|
||||
|
||||
let api: ReturnType<typeof useTour>;
|
||||
function Probe() {
|
||||
@@ -140,3 +141,37 @@ describe('tour run', () => {
|
||||
expect(env.command).toHaveBeenCalledWith('tour-end');
|
||||
});
|
||||
});
|
||||
|
||||
describe('tour under the Pro shell', () => {
|
||||
const stepTo = (id: string) => {
|
||||
for (let i = 0; i < 30 && api.steps[api.currentStep]?.id !== id; i++) act(() => api.nextStep());
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
env.phone = false;
|
||||
env.pathname = '/pro';
|
||||
useProTabStore.setState({ tabs: [{ id: 'home-mail', kind: 'mail', labelKey: 'mail', closeable: false, paneId: 'main' }], activeTabId: 'home-mail', activeSplitTabId: null, focusedPaneId: 'main', loadedTabIds: ['home-mail'] });
|
||||
});
|
||||
|
||||
it('opens the tab of each step instead of leaving the Pro route', () => {
|
||||
start();
|
||||
stepTo('calendar-view');
|
||||
const focused = () => {
|
||||
const s = useProTabStore.getState();
|
||||
return s.tabs.find((t) => t.id === s.activeTabId)?.kind;
|
||||
};
|
||||
expect(focused()).toBe('calendar');
|
||||
expect(env.push).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('counts a step as on its page there', () => {
|
||||
expect(isTourStepOnPage({ page: '/calendar' }, '/pro')).toBe(true);
|
||||
expect(isTourStepOnPage({ page: '/calendar' }, '/settings')).toBe(false);
|
||||
});
|
||||
|
||||
it('leaves the composer step in the compose tab it just opened', () => {
|
||||
expect(proTabOfTourStep({ target: '[data-tour="composer"]' })).toBeNull();
|
||||
expect(proTabOfTourStep({ target: '[data-tour="sidebar"]' })).toBe('mail');
|
||||
expect(proTabOfTourStep({ target: 'x', page: '/settings' })).toBe('settings');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,8 @@ import { useCalendarStore } from "@/stores/calendar-store";
|
||||
import { useWebDAVStore } from "@/stores/webdav-store";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
import { usePolicyStore } from "@/stores/policy-store";
|
||||
import { getTourSteps, isTourStepOnPage, type TourStep } from "./tour-steps";
|
||||
import { getTourSteps, isTourStepOnPage, PRO_ROUTE, proTabOfTourStep, type TourStep } from "./tour-steps";
|
||||
import { useProTabStore } from "@/stores/pro-tab-store";
|
||||
import { useIsMobile, useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { requestAppCommand } from "@/lib/app-commands";
|
||||
import { TourOverlay } from "./tour-overlay";
|
||||
@@ -87,6 +88,13 @@ export function TourProvider({ children }: { children: ReactNode }) {
|
||||
// closes it first.
|
||||
useEffect(() => {
|
||||
if (!step) return;
|
||||
// Under Pro every app is a tab of the one route. Asking for another route
|
||||
// only comes back here, which went round for as long as the tour ran.
|
||||
if (pathname === PRO_ROUTE) {
|
||||
const tab = proTabOfTourStep(step);
|
||||
if (tab) useProTabStore.getState().openTab(tab);
|
||||
return;
|
||||
}
|
||||
if (!isTourStepOnPage(step, pathname)) {
|
||||
router.push(step.page ?? "/");
|
||||
} else if (phone && !step.page && step.view !== "reader") {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import type { ProAppTabKind } from "@/stores/pro-tab-store";
|
||||
|
||||
/** The one route of the Pro shell. */
|
||||
export const PRO_ROUTE = "/pro";
|
||||
|
||||
export interface TourStep {
|
||||
id: string;
|
||||
target: string;
|
||||
@@ -50,8 +55,17 @@ function isCovered(el: Element, r: DOMRect): boolean {
|
||||
return !!top && top !== el && !el.contains(top) && !top.contains(el);
|
||||
}
|
||||
|
||||
/** Whether the user is in the app the step is about. */
|
||||
/** The Pro tab that holds the app the step is about. */
|
||||
export function proTabOfTourStep(step: Pick<TourStep, "page" | "target">): ProAppTabKind | null {
|
||||
// The composer has a tab of its own, which the step just opened.
|
||||
if (step.target === COMPOSER_TARGET) return null;
|
||||
const page = step.page?.replace(/^\//, "");
|
||||
return page === "calendar" || page === "contacts" || page === "files" || page === "settings" ? page : "mail";
|
||||
}
|
||||
|
||||
/** Whether the user is in the app the step is about. Under Pro the provider opens its tab. */
|
||||
export function isTourStepOnPage(step: Pick<TourStep, "page">, pathname: string): boolean {
|
||||
if (pathname === PRO_ROUTE) return true;
|
||||
const page = step.page && step.page !== "/" ? step.page : null;
|
||||
if (!page) return pathname === "/" || pathname === "" || pathname === "/mail" || pathname.startsWith("/mail/");
|
||||
return pathname === page || pathname.startsWith(page + "/");
|
||||
@@ -62,6 +76,8 @@ function clickTourTarget(selector: string): void {
|
||||
(findTourTarget(selector) as HTMLElement | null)?.click();
|
||||
}
|
||||
|
||||
const COMPOSER_TARGET = '[data-tour="composer"]';
|
||||
|
||||
export const BASE_TOUR_STEPS: TourStep[] = [
|
||||
{
|
||||
id: "sidebar",
|
||||
@@ -162,7 +178,7 @@ export const BASE_TOUR_STEPS: TourStep[] = [
|
||||
export const DEMO_TOUR_STEPS: TourStep[] = [
|
||||
{
|
||||
id: "compose-open",
|
||||
target: '[data-tour="composer"]',
|
||||
target: COMPOSER_TARGET,
|
||||
titleKey: "tour.compose_open_title",
|
||||
descriptionKey: "tour.compose_open_desc",
|
||||
placement: "left",
|
||||
|
||||
Reference in New Issue
Block a user