mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Gate the quick-nav Processor entry on the Processor flag
This commit is contained in:
@@ -16,6 +16,7 @@ import type { ToolId } from "@app/types/toolId";
|
||||
|
||||
export interface QuickNavHostBridgeProps {
|
||||
portalAccess?: boolean;
|
||||
processorEnabled?: boolean;
|
||||
readerMode?: boolean;
|
||||
onSetReaderMode?: (on: boolean) => void;
|
||||
onOpenSettings: () => void;
|
||||
@@ -29,6 +30,7 @@ export interface QuickNavHostBridgeProps {
|
||||
/** Registers with the rail what only the app can see, and owns the notifications panel. */
|
||||
export function QuickNavHostBridge({
|
||||
portalAccess = false,
|
||||
processorEnabled = true,
|
||||
readerMode = false,
|
||||
onSetReaderMode,
|
||||
onOpenSettings,
|
||||
@@ -58,6 +60,7 @@ export function QuickNavHostBridge({
|
||||
identity: { displayName, profilePictureUrl },
|
||||
signingBadge,
|
||||
portalAccess,
|
||||
processorEnabled,
|
||||
readerMode,
|
||||
notificationsOpen,
|
||||
toolReasons: mergedToolReasons,
|
||||
|
||||
@@ -159,7 +159,13 @@ export function QuickNavRailHost() {
|
||||
|
||||
return (
|
||||
<QuickNavRailContainer
|
||||
groups={HAS_PORTAL ? [apps, within] : [within]}
|
||||
// Bundling the portal says nothing about the server: an editor-only one has
|
||||
// no Processor to offer, so drop the group rather than show it disabled.
|
||||
groups={
|
||||
HAS_PORTAL && (inPortal || host?.processorEnabled !== false)
|
||||
? [apps, within]
|
||||
: [within]
|
||||
}
|
||||
onReturnHome={returnHome}
|
||||
identity={host?.identity ?? null}
|
||||
onOpenSettings={host?.hasSettings ? openSettings : undefined}
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface QuickNavHostData {
|
||||
identity: QuickNavIdentity | null;
|
||||
signingBadge: number;
|
||||
portalAccess: boolean;
|
||||
/** False only on an editor-only server, where there is no Processor to offer. */
|
||||
processorEnabled: boolean;
|
||||
readerMode: boolean;
|
||||
/** The app owns the panel; the rail's bell only reports its state. */
|
||||
notificationsOpen: boolean;
|
||||
@@ -60,6 +62,7 @@ const EMPTY_DATA: QuickNavHostData = {
|
||||
identity: null,
|
||||
signingBadge: 0,
|
||||
portalAccess: false,
|
||||
processorEnabled: true,
|
||||
readerMode: false,
|
||||
notificationsOpen: false,
|
||||
hasSettings: false,
|
||||
@@ -89,6 +92,7 @@ export function QuickNavHostProvider({ children }: { children: ReactNode }) {
|
||||
merged.appMounted === prev.appMounted &&
|
||||
merged.signingBadge === prev.signingBadge &&
|
||||
merged.portalAccess === prev.portalAccess &&
|
||||
merged.processorEnabled === prev.processorEnabled &&
|
||||
merged.readerMode === prev.readerMode &&
|
||||
merged.notificationsOpen === prev.notificationsOpen &&
|
||||
merged.hasSettings === prev.hasSettings &&
|
||||
@@ -142,6 +146,7 @@ export function useRegisterQuickNavHost(
|
||||
identity,
|
||||
signingBadge,
|
||||
portalAccess,
|
||||
processorEnabled,
|
||||
readerMode,
|
||||
notificationsOpen,
|
||||
toolReasons,
|
||||
@@ -154,6 +159,7 @@ export function useRegisterQuickNavHost(
|
||||
identity: identity ?? null,
|
||||
signingBadge: signingBadge ?? 0,
|
||||
portalAccess: portalAccess ?? false,
|
||||
processorEnabled: processorEnabled ?? true,
|
||||
readerMode: readerMode ?? false,
|
||||
notificationsOpen: notificationsOpen ?? false,
|
||||
// Omitted when unknown, so the last answer survives a re-fetch.
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
getDisabledLabel,
|
||||
} from "@app/components/tools/fullscreen/shared";
|
||||
import { useOtherAppSwitch } from "@app/hooks/useOtherAppSwitch";
|
||||
import { useProcessorEnabled } from "@app/hooks/useProcessorEnabled";
|
||||
import { consumeReaderModeRequest } from "@app/utils/pendingReaderMode";
|
||||
import {
|
||||
FilesPageProvider,
|
||||
@@ -126,6 +127,7 @@ export default function HomePage() {
|
||||
const isProgrammaticScroll = useRef(false);
|
||||
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||
const otherApp = useOtherAppSwitch();
|
||||
const processorEnabled = useProcessorEnabled();
|
||||
const location = useLocation();
|
||||
// Persisted user preference for the FileSidebar collapsed state. Auto-
|
||||
// collapse on /files is layered on top in the transition effect below and
|
||||
@@ -519,6 +521,7 @@ export default function HomePage() {
|
||||
<HomePageExtensions />
|
||||
<QuickNavHostBridge
|
||||
portalAccess={Boolean(otherApp)}
|
||||
processorEnabled={processorEnabled}
|
||||
onOpenSettings={() => setConfigModalOpen(true)}
|
||||
requestNavigation={requestNavigation}
|
||||
readerMode={readerMode}
|
||||
|
||||
@@ -30,7 +30,10 @@ const ADMIN_WITH_PORTAL = {
|
||||
portalAccess: true,
|
||||
};
|
||||
|
||||
const SWITCH_APP = "Switch app";
|
||||
// The quick-nav rail's Processor entry, and the sidebar footer row. Both are
|
||||
// named: an assertion on a control that no longer exists passes vacuously,
|
||||
// which is how #7695 silently defanged this file.
|
||||
const RAIL_PROCESSOR = "Processor";
|
||||
const OPEN_PROCESSOR = "Open PDF Processor";
|
||||
|
||||
// Copy that only a Processor server can honour: a link tool pointing at the
|
||||
@@ -114,8 +117,10 @@ test.describe("editor-only server (processor.enabled=false)", () => {
|
||||
await page.goto("/editor", { waitUntil: "domcontentloaded" });
|
||||
await expect(page.locator(INPUT)).toBeVisible();
|
||||
|
||||
// The logo stays a plain logo: no editor -> processor switcher...
|
||||
await expect(page.getByLabel(SWITCH_APP)).toHaveCount(0);
|
||||
// The quick-nav rail offers no Processor entry...
|
||||
await expect(
|
||||
page.getByRole("button", { name: RAIL_PROCESSOR, exact: true }),
|
||||
).toHaveCount(0);
|
||||
// ...and the sidebar footer offers no "Open PDF Processor" row.
|
||||
await expect(page.getByLabel(OPEN_PROCESSOR)).toHaveCount(0);
|
||||
|
||||
@@ -168,15 +173,10 @@ test.describe("editor-only server (processor.enabled=false)", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("bounces a hand-typed /processor URL back to the editor", async ({
|
||||
page,
|
||||
}) => {
|
||||
// The last hole: hiding the entry points doesn't stop someone typing the URL.
|
||||
// The editor shell rendering here is the assertion - the portal has its own
|
||||
// shell and no #super-search-input, as the control below shows.
|
||||
await page.goto("/processor", { waitUntil: "domcontentloaded" });
|
||||
await expect(page.locator(INPUT)).toBeVisible();
|
||||
});
|
||||
// Not asserted here: whether /processor is routable depends on BUILD_PORTAL,
|
||||
// which the stubbed dev suite (portal always bundled) cannot vary. The
|
||||
// editor-only image is built with BUILD_PORTAL=false and its dist carries no
|
||||
// portal chunk - that is where the absence is verified.
|
||||
});
|
||||
|
||||
test.describe("same user on a server with the Processor on", () => {
|
||||
@@ -192,7 +192,9 @@ test.describe("same user on a server with the Processor on", () => {
|
||||
|
||||
test("is offered the switch to the Processor", async ({ page }) => {
|
||||
await expect(page.locator(INPUT)).toBeVisible();
|
||||
await expect(page.getByLabel(SWITCH_APP)).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: RAIL_PROCESSOR, exact: true }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("is offered Processor lanes in super search", async ({ page }) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ const mocks = vi.hoisted(() => ({
|
||||
navigate: vi.fn(),
|
||||
requestNavigation: vi.fn(),
|
||||
portalAccess: true,
|
||||
processorEnabled: true,
|
||||
}));
|
||||
|
||||
vi.mock("react-router-dom", () => ({
|
||||
@@ -14,6 +15,11 @@ vi.mock("react-router-dom", () => ({
|
||||
vi.mock("@app/auth/context", () => ({
|
||||
useAuth: () => ({ portalAccess: mocks.portalAccess }),
|
||||
}));
|
||||
// Stubbed rather than provider-wrapped: the real hook reads app-config, which
|
||||
// this suite never fetches.
|
||||
vi.mock("@app/hooks/useProcessorEnabled", () => ({
|
||||
useProcessorEnabled: () => mocks.processorEnabled,
|
||||
}));
|
||||
vi.mock("@app/contexts/NavigationContext", () => ({
|
||||
useNavigationActions: () => ({
|
||||
actions: { requestNavigation: mocks.requestNavigation },
|
||||
@@ -27,6 +33,7 @@ beforeEach(() => {
|
||||
sessionStorage.clear();
|
||||
vi.clearAllMocks();
|
||||
mocks.portalAccess = true;
|
||||
mocks.processorEnabled = true;
|
||||
});
|
||||
|
||||
describe("useOtherAppSwitch", () => {
|
||||
@@ -36,6 +43,12 @@ describe("useOtherAppSwitch", () => {
|
||||
expect(result.current).toBeNull();
|
||||
});
|
||||
|
||||
it("offers no switch on an editor-only server", () => {
|
||||
mocks.processorEnabled = false;
|
||||
const { result } = renderHook(() => useOtherAppSwitch());
|
||||
expect(result.current).toBeNull();
|
||||
});
|
||||
|
||||
it("routes the switch through the unsaved-changes guard", () => {
|
||||
const { result } = renderHook(() => useOtherAppSwitch());
|
||||
result.current?.onOpen();
|
||||
|
||||
Reference in New Issue
Block a user