mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 09:07:46 +02:00
Fix mobile company settings navigation
This commit is contained in:
1 parent
9835cbe1f5
commit
81d43e04fe
7 files changed
+90
-14
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user