fix(ui): restore prefix-aware company export/import links (#6648)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Each company workspace in the web UI is mounted under a URL prefix
(e.g. `/NEU/company/...`), and `Link` from `@/lib/router` applies that
prefix automatically via `applyCompanyPrefix`
> - Company Settings rendered its Org Chart, Export, Import, and Cloud
Upstream buttons as raw `<a href>` anchors, which drop the prefix, so
those pages 404 on prefixed instances (#2910);
`CompanyExport.filePathFromLocation` also failed to locate
`/company/export/files/` inside prefixed URLs, breaking export file
previews
> - #2951 fixed the Settings links with `<Link to>` plus tests, but the
sandbox-settings work in #4415 reverted the links back to `<a href>`,
silently reintroducing the bug (#6647)
> - This pull request restores the prefix-aware `<Link>` for all four
Settings links, normalizes the pathname with `toCompanyRelativePath()`
before matching the export-files marker, and adds regression tests
covering every route so the fix cannot be lost again
> - The benefit is that export/import/org-chart/cloud-upstream
navigation and export file previews work on every prefixed deployment

## Linked Issues or Issue Description

Fixes: #6647
Refs #2910 (original report: `/company/export` → prefix `COMPANY` → not
found)
Refs #2951 (original fix with `<Link to>` + tests — merged, then lost)
Refs #4415 (sandbox settings PR that reverted Settings back to `<a
href>`)

## What Changed

- **`ui/src/pages/CompanySettings.tsx`**: use `Link` from `@/lib/router`
for the Org Chart, Export, Import, and Cloud Upstream buttons (replacing
raw `<a href>`)
- **`ui/src/pages/CompanyExport.tsx`**: resolve file paths from prefixed
URLs by normalizing with `toCompanyRelativePath()` before matching
`/company/export/files/`
- **`ui/src/lib/company-routes.test.ts`**: regression tests for
export/import/cloud-upstream/org prefix rewriting, double-prefix
prevention, and export file URL normalization

## Verification

```bash
pnpm vitest run ui/src/lib/company-routes.test.ts
```

Manual:

1. Open `http://localhost:3100/NEU/company/settings` (or your company
prefix).
2. Click **Export** / **Import** — URL should stay under
`/:prefix/company/...`.
3. On export, select a file — URL should be
`/:prefix/company/export/files/...` and preview should load.

The change is navigation-target-only (no visual/layout changes), so
before/after is shown as the resolved URLs:

| Link | Before (prefix dropped → not found) | After |
|------|-------------------------------------|-------|
| Export | `/company/export` | `/NEU/company/export` |
| Import | `/company/import` | `/NEU/company/import` |
| Org Chart | `/org` | `/NEU/org` |
| Cloud Upstream | `/company/settings/cloud-upstream` |
`/NEU/company/settings/cloud-upstream` |

## Risks

Low — same approach as #2951; only navigation/parsing, no API changes.

## Model Used

- Original implementation: authored by @qbamca in Cursor (agentic
editor; the session's exact model ID was not recorded)
- Follow-up commit (merge-conflict resolution) and this description
update: Claude Fable 5 (Anthropic, `claude-fable-5`, extended thinking,
agentic tool use), operated by the Commit Capital triage team

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes (no
doc changes required — behavior matches documented routing)
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(pending re-review of the conflict-resolution commit)
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Jakub Mikiciuk <jmikiciuk@igus.net>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Andrew Aymeloglu <aaymeloglu@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakub Mikiciuk 2026-07-16 05:52:40 +02:00 committed by GitHub
parent df2404d443
commit f44a002b8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 31 additions and 9 deletions

View File

@ -36,6 +36,25 @@ describe("company routes", () => {
expect(toCompanyRelativePath("/PAP/search?q=foo")).toBe("/search?q=foo");
});
it("rewrites company package paths with the active prefix", () => {
expect(applyCompanyPrefix("/company/export", "NEU")).toBe("/NEU/company/export");
expect(applyCompanyPrefix("/company/import", "NEU")).toBe("/NEU/company/import");
expect(applyCompanyPrefix("/company/settings/cloud-upstream", "NEU")).toBe(
"/NEU/company/settings/cloud-upstream",
);
expect(applyCompanyPrefix("/org", "NEU")).toBe("/NEU/org");
});
it("does not double-apply the company prefix", () => {
expect(applyCompanyPrefix("/NEU/company/export", "NEU")).toBe("/NEU/company/export");
});
it("normalizes prefixed company export file URLs for parsing", () => {
expect(toCompanyRelativePath("/NEU/company/export/files/agents/ceo/AGENTS.md")).toBe(
"/company/export/files/agents/ceo/AGENTS.md",
);
});
// Regression for PAP-10257: Team Catalog navigation (auto-select + row/file
// clicks) produces company-relative `/teams-catalog/<key>` paths. Without
// `teams-catalog` in the board-route allowlist, `extractCompanyPrefixFromPath`

View File

@ -20,6 +20,7 @@ import { Button } from "@/components/ui/button";
import { EmptyState } from "../components/EmptyState";
import { PageSkeleton } from "../components/PageSkeleton";
import { MarkdownBody } from "../components/MarkdownBody";
import { toCompanyRelativePath } from "@/lib/company-routes";
import { cn } from "../lib/utils";
import { queryKeys } from "../lib/queryKeys";
import { createZipArchive } from "../lib/zip";
@ -559,9 +560,10 @@ function ExportPreviewPane({
/** Extract the file path from the current URL pathname (after /company/export/files/) */
function filePathFromLocation(pathname: string): string | null {
const marker = "/company/export/files/";
const idx = pathname.indexOf(marker);
const relativePathname = toCompanyRelativePath(pathname);
const idx = relativePathname.indexOf(marker);
if (idx === -1) return null;
const filePath = decodeURIComponent(pathname.slice(idx + marker.length));
const filePath = decodeURIComponent(relativePathname.slice(idx + marker.length));
return filePath || null;
}

View File

@ -10,6 +10,7 @@ import { companiesApi } from "../api/companies";
import { assetsApi } from "../api/assets";
import { instanceSettingsApi } from "../api/instanceSettings";
import { queryKeys } from "../lib/queryKeys";
import { Link } from "@/lib/router";
import { Button } from "@/components/ui/button";
import { Settings, CloudUpload, Download, Upload } from "lucide-react";
import { CompanyPatternIcon } from "../components/CompanyPatternIcon";
@ -374,28 +375,28 @@ export function CompanySettings() {
<div className="rounded-md border border-border px-4 py-4">
<p className="text-sm text-muted-foreground">
Import and export have moved to dedicated pages accessible from the{" "}
<a href="/org" className="underline hover:text-foreground">Org Chart</a> header.
<Link to="/org" className="underline hover:text-foreground">Org Chart</Link> header.
</p>
<div className="mt-3 flex flex-wrap items-center gap-2">
{cloudSyncEnabled ? (
<Button size="sm" asChild>
<a href="/company/settings/cloud-upstream">
<Link to="/company/settings/cloud-upstream">
<CloudUpload className="mr-1.5 h-3.5 w-3.5" />
Send to Paperclip Cloud
</a>
</Link>
</Button>
) : null}
<Button size="sm" variant="outline" asChild>
<a href="/company/export">
<Link to="/company/export">
<Download className="mr-1.5 h-3.5 w-3.5" />
Export
</a>
</Link>
</Button>
<Button size="sm" variant="outline" asChild>
<a href="/company/import">
<Link to="/company/import">
<Upload className="mr-1.5 h-3.5 w-3.5" />
Import
</a>
</Link>
</Button>
</div>
</div>