Fix mobile company settings navigation

This commit is contained in:
Dotta committed 2026-05-21 03:31:29 +00:00
1 parent 9835cbe1f5
commit 81d43e04fe
7 files changed
+90 -14

No files matched your search

+51 -3
View File
@@ -1,7 +1,7 @@
// @vitest-environment jsdom
import { act } from "react";
import { createRoot } from "react-dom/client";
import { flushSync } from "react-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { Layout } from "./Layout";
@@ -27,6 +27,10 @@ const mockPluginSlots = vi.hoisted(() => ({
}));
const mockUsePluginSlots = vi.hoisted(() => vi.fn());
const mockPluginSlotContexts = vi.hoisted(() => [] as Array<Record<string, unknown>>);
const mockSidebarState = vi.hoisted(() => ({
sidebarOpen: true,
isMobile: false,
}));
let currentPathname = "/PAP/dashboard";
vi.mock("@/lib/router", () => ({
@@ -161,10 +165,10 @@ vi.mock("../context/CompanyContext", () => ({
vi.mock("../context/SidebarContext", () => ({
useSidebar: () => ({
sidebarOpen: true,
sidebarOpen: mockSidebarState.sidebarOpen,
setSidebarOpen: mockSetSidebarOpen,
toggleSidebar: vi.fn(),
isMobile: false,
isMobile: mockSidebarState.isMobile,
}),
}));
@@ -201,6 +205,14 @@ vi.mock("../lib/main-content-focus", () => ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
async function act(callback: () => void | Promise<void>) {
let result: void | Promise<void> = undefined;
flushSync(() => {
result = callback();
});
await result;
}
async function flushReact() {
await act(async () => {
await Promise.resolve();
@@ -229,6 +241,8 @@ describe("Layout", () => {
});
mockPluginSlots.slots = [];
mockPluginSlotContexts.length = 0;
mockSidebarState.sidebarOpen = true;
mockSidebarState.isMobile = false;
});
afterEach(() => {
@@ -319,6 +333,40 @@ describe("Layout", () => {
});
});
it("renders a mobile company settings selector on company settings routes", async () => {
currentPathname = "/PAP/company/settings/secrets";
mockSidebarState.isMobile = true;
mockSidebarState.sidebarOpen = false;
const root = createRoot(container);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Layout />
</QueryClientProvider>,
);
});
await flushReact();
await flushReact();
const selector = container.querySelector("select");
expect(selector).not.toBeNull();
expect(selector?.value).toBe("secrets");
expect(selector?.textContent).toContain("General");
expect(selector?.textContent).toContain("Environments");
expect(selector?.textContent).toContain("Cloud upstream");
expect(selector?.textContent).toContain("Access");
expect(selector?.textContent).toContain("Invites");
expect(selector?.textContent).toContain("Secrets");
await act(async () => {
root.unmount();
});
});
it("renders the instance settings sidebar on instance settings routes", async () => {
currentPathname = "/instance/settings/general";
const root = createRoot(container);
+6
View File
@@ -4,6 +4,7 @@ import { Outlet, useLocation, useNavigate, useNavigationType, useParams } from "
import { Sidebar } from "./Sidebar";
import { InstanceSidebar } from "./InstanceSidebar";
import { CompanySettingsSidebar } from "./CompanySettingsSidebar";
import { CompanySettingsNav } from "./access/CompanySettingsNav";
import { BreadcrumbBar } from "./BreadcrumbBar";
import { PropertiesPanel } from "./PropertiesPanel";
import { CommandPalette } from "./CommandPalette";
@@ -421,6 +422,11 @@ export function Layout() {
)}
>
<BreadcrumbBar />
{isMobile && isCompanySettingsRoute ? (
<div className="border-b border-border px-4 pb-3">
<CompanySettingsNav />
</div>
) : null}
</div>
<div className={cn(isMobile ? "block" : "flex flex-1 min-h-0")}>
<main
@@ -1,7 +1,7 @@
// @vitest-environment jsdom
import { act } from "react";
import { createRoot } from "react-dom/client";
import { flushSync } from "react-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { CompanySettingsNav, getCompanySettingsTab } from "./CompanySettingsNav";
@@ -40,6 +40,14 @@ vi.mock("@/components/PageTabBar", () => ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
async function act(callback: () => void | Promise<void>) {
let result: void | Promise<void> = undefined;
flushSync(() => {
result = callback();
});
await result;
}
describe("CompanySettingsNav", () => {
let container: HTMLDivElement;
@@ -60,9 +68,11 @@ describe("CompanySettingsNav", () => {
expect(getCompanySettingsTab("/PAP/company/settings")).toBe("general");
expect(getCompanySettingsTab("/company/settings/environments")).toBe("environments");
expect(getCompanySettingsTab("/PAP/company/settings/environments")).toBe("environments");
expect(getCompanySettingsTab("/company/settings/cloud-upstream")).toBe("cloud-upstream");
expect(getCompanySettingsTab("/company/settings/access")).toBe("access");
expect(getCompanySettingsTab("/PAP/company/settings/access")).toBe("access");
expect(getCompanySettingsTab("/company/settings/invites")).toBe("invites");
expect(getCompanySettingsTab("/PAP/company/settings/secrets")).toBe("secrets");
});
it("renders the active tab and navigates when a different tab is selected", async () => {
@@ -80,8 +90,10 @@ describe("CompanySettingsNav", () => {
items: [
{ value: "general", label: "General" },
{ value: "environments", label: "Environments" },
{ value: "cloud-upstream", label: "Cloud upstream" },
{ value: "access", label: "Access" },
{ value: "invites", label: "Invites" },
{ value: "secrets", label: "Secrets" },
],
}),
);
@@ -5,8 +5,10 @@ import { useLocation, useNavigate } from "@/lib/router";
const items = [
{ value: "general", label: "General", href: "/company/settings" },
{ value: "environments", label: "Environments", href: "/company/settings/environments" },
{ value: "cloud-upstream", label: "Cloud upstream", href: "/company/settings/cloud-upstream" },
{ value: "access", label: "Access", href: "/company/settings/access" },
{ value: "invites", label: "Invites", href: "/company/settings/invites" },
{ value: "secrets", label: "Secrets", href: "/company/settings/secrets" },
] as const;
type CompanySettingsTab = (typeof items)[number]["value"];
@@ -16,6 +18,10 @@ export function getCompanySettingsTab(pathname: string): CompanySettingsTab {
return "environments";
}
if (pathname.includes("/company/settings/cloud-upstream")) {
return "cloud-upstream";
}
if (pathname.includes("/company/settings/access")) {
return "access";
}
@@ -24,6 +30,10 @@ export function getCompanySettingsTab(pathname: string): CompanySettingsTab {
return "invites";
}
if (pathname.includes("/company/settings/secrets")) {
return "secrets";
}
return "general";
}
+4 -4
View File
@@ -933,7 +933,7 @@ export function CompanyExport() {
{/* Sticky top action bar */}
<div className="sticky top-0 z-10 border-b border-border bg-background px-5 py-3">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-4 text-sm">
<div className="flex flex-wrap items-center gap-4 text-sm">
<span className="font-medium">
{selectedCompany?.name ?? "Company"} export
</span>
@@ -969,8 +969,8 @@ export function CompanyExport() {
)}
{/* Two-column layout */}
<div className="grid h-[calc(100vh-12rem)] gap-0 xl:grid-cols-[19rem_minmax(0,1fr)]">
<aside className="flex flex-col border-r border-border overflow-hidden">
<div className="grid gap-4 xl:h-[calc(100vh-12rem)] xl:grid-cols-[19rem_minmax(0,1fr)] xl:gap-0">
<aside className="flex max-h-[24rem] flex-col overflow-hidden border-b border-border xl:max-h-none xl:border-b-0 xl:border-r">
<div className="border-b border-border px-4 py-3 shrink-0">
<h2 className="text-base font-semibold">Package files</h2>
</div>
@@ -1011,7 +1011,7 @@ export function CompanyExport() {
)}
</div>
</aside>
<div className="min-w-0 overflow-y-auto pl-6">
<div className="min-w-0 overflow-y-auto xl:pl-6">
<ExportPreviewPane selectedFile={selectedFile} content={previewContent} allFiles={effectiveFiles} onSkillClick={handleSkillClick} />
</div>
</div>
+5 -5
View File
@@ -1227,7 +1227,7 @@ export function CompanyImport() {
</select>
</Field>
<div className="flex items-center gap-2">
<div className="flex flex-wrap items-center gap-2">
<Button
size="sm"
variant="outline"
@@ -1287,7 +1287,7 @@ export function CompanyImport() {
/>
{/* Import button — below renames */}
<div className="mx-5 mt-3 flex justify-end">
<div className="mx-5 mt-3 flex flex-wrap justify-end gap-2">
<Button
size="sm"
onClick={() => importMutation.mutate()}
@@ -1319,8 +1319,8 @@ export function CompanyImport() {
)}
{/* Two-column layout */}
<div className="grid h-[calc(100vh-16rem)] gap-0 xl:grid-cols-[19rem_minmax(0,1fr)]">
<aside className="flex flex-col border-r border-border overflow-hidden">
<div className="grid gap-4 xl:h-[calc(100vh-16rem)] xl:grid-cols-[19rem_minmax(0,1fr)] xl:gap-0">
<aside className="flex max-h-[24rem] flex-col overflow-hidden border-b border-border xl:max-h-none xl:border-b-0 xl:border-r">
<div className="border-b border-border px-4 py-3 shrink-0">
<h2 className="text-base font-semibold">Package files</h2>
</div>
@@ -1339,7 +1339,7 @@ export function CompanyImport() {
/>
</div>
</aside>
<div className="min-w-0 overflow-y-auto pl-6">
<div className="min-w-0 overflow-y-auto xl:pl-6">
<ImportPreviewPane
selectedFile={selectedFile}
content={previewContent}
+1 -1
View File
@@ -526,7 +526,7 @@ export function CompanySettings() {
Import and export have moved to dedicated pages accessible from the{" "}
<a href="/org" className="underline hover:text-foreground">Org Chart</a> header.
</p>
<div className="mt-3 flex items-center gap-2">
<div className="mt-3 flex flex-wrap items-center gap-2">
{cloudSyncEnabled ? (
<Button size="sm" asChild>
<a href="/company/settings/cloud-upstream">