mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 08:22:20 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - A person can archive a company from that company's settings page. > - After the archive, the page stays on the archived company. The only visible change is the button text "Already archived". > - It is unclear that anything happened, and the user has no next step on screen. > - This pull request navigates away after a successful archive: to another active company, to the Cloud portfolio when no active company remains on a managed instance, or to the companies list when self-hosted. > - The benefit is a clear outcome: the user sees where they are now and a toast that names what happened. ## Linked Issues or Issue Description No public issue exists. Description follows the enhancement template: **What existing behavior does this improve?** The archive action on the company settings page. The mutation works, but the view stays on the archived company and gives no feedback beyond a disabled button. **Subsystem affected** UI: company settings page, company selection helpers, cloud links. **Current behavior** Archive succeeds. The button changes to "Already archived". The user stays on the archived company's settings. On a single-company instance nothing else changes. **Proposed behavior** After a successful archive, the app departs: another active company's dashboard with a toast; the Cloud portfolio's manage view when no active company remains on a managed instance; the companies list when self-hosted with nothing active, because that list shows the archived state and owns the unarchive action. **Reason and benefit** Staying on the archived company reads as "nothing happened". Leaving to a live surface makes the outcome clear and gives the user their next step. ## What Changed - `ui/src/lib/company-selection.ts`: new pure `resolveCompanyArchiveDeparture` helper. Another active company wins; the just-archived company is excluded by id, so a stale cached status cannot select it. Cloud portfolio is the fallback on managed instances; the companies list is the final fallback. - `ui/src/lib/cloudLinks.ts`: new `cloudPortfolioManageUrl` (`/orgs?manage=1`). The manage view matters: the plain launchpad auto-forwards a solo user back into their one openable stack — the page this navigation is escaping. - `ui/src/pages/CompanySettings.tsx`: the archive mutation now departs on success — toast + `navigate` for in-app destinations, a top-level navigation for the Cloud portfolio — instead of only setting the selected company id. - `ui/src/pages/CompanySettingsRenameHint.test.tsx`: render harness wraps the page in a `MemoryRouter` for the new router dependency. ## Verification - `pnpm exec vitest run src/lib/company-selection.test.ts src/lib/cloudLinks.test.ts src/pages/CompanySettingsRenameHint.test.tsx src/pages/CompanySettings.test.tsx` — 22 tests pass. - New unit tests cover: active sibling wins (also on cloud), the just-archived company is never the destination even with a stale cached status, cloud portfolio fallback, self-hosted companies-list fallback, and the portfolio URL helper (null base included). - `pnpm exec tsc --noEmit` in `ui/` is clean after the plugin SDK build. ## Risks - Low risk. The archive API call is unchanged; only post-success navigation is new. The toast uses the optional actions hook, so surfaces without a ToastProvider stay safe. - On Cloud, the portfolio navigation is a full top-level navigation, so the query-cache invalidation for the departed document is skipped intentionally. ## Model Used - Claude Fable 5 (`claude-fable-5`), via Claude Code CLI, extended thinking and tool use enabled. ## 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 - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge
89 lines
3.6 KiB
TypeScript
89 lines
3.6 KiB
TypeScript
export type CompanySelectionSource = "manual" | "route_sync" | "bootstrap";
|
|
|
|
interface BounceCandidateCompany {
|
|
id: string;
|
|
name: string;
|
|
issuePrefix: string;
|
|
status: string;
|
|
}
|
|
|
|
/**
|
|
* Decides whether a navigation that landed on an archived company's URL
|
|
* should bounce to an active company instead of dwelling in the archive.
|
|
*
|
|
* Stale state deposits users into archived companies long after archiving:
|
|
* remembered last-visited paths, browser history, bookmarks, and restored
|
|
* tabs all outlive the archive. Rendering those pages is safe, but it is
|
|
* never where the user wants to *be* — the sidebar does not even list the
|
|
* company. Cold arrivals therefore bounce to an active company.
|
|
*
|
|
* Deliberate visits still work: when the archived company is already the
|
|
* selection (the user chose it from the companies list), there is no
|
|
* bounce, so its settings and unarchive flows stay reachable. When no
|
|
* active company exists there is nowhere better to go, so the archive
|
|
* renders rather than bouncing into a dead end.
|
|
*/
|
|
export function resolveArchivedCompanyBounce(params: {
|
|
matchedCompany: BounceCandidateCompany | null;
|
|
selectedCompanyId: string | null;
|
|
companies: BounceCandidateCompany[];
|
|
}): BounceCandidateCompany | null {
|
|
const { matchedCompany, selectedCompanyId, companies } = params;
|
|
if (!matchedCompany || matchedCompany.status !== "archived") return null;
|
|
if (selectedCompanyId === matchedCompany.id) return null;
|
|
|
|
const selectedActive = companies.find(
|
|
(company) => company.id === selectedCompanyId && company.status !== "archived",
|
|
);
|
|
return selectedActive ?? companies.find((company) => company.status !== "archived") ?? null;
|
|
}
|
|
|
|
export type CompanyArchiveDeparture =
|
|
| { kind: "company"; company: BounceCandidateCompany }
|
|
| { kind: "cloud_portfolio"; url: string }
|
|
| { kind: "companies_list" };
|
|
|
|
/**
|
|
* Decides where the app should take the user after they archive a company
|
|
* from its own settings page. Staying put is never right: the page they are
|
|
* on belongs to the company they just archived, and the only visible change
|
|
* would be the archive button going inert.
|
|
*
|
|
* Another active company wins — the user still has somewhere to work, so
|
|
* switch there (mirroring the cold-arrival bounce above). With no active
|
|
* company left on a Cloud instance, the whole organization is on its way to
|
|
* being archived by the control plane, so leave for the Cloud portfolio's
|
|
* manage view. Self-hosted with nothing active falls back to the companies
|
|
* list, which shows the archived state and owns the unarchive affordance.
|
|
*/
|
|
export function resolveCompanyArchiveDeparture(params: {
|
|
archivedCompanyId: string;
|
|
companies: BounceCandidateCompany[];
|
|
cloudPortfolioUrl: string | null;
|
|
}): CompanyArchiveDeparture {
|
|
const { archivedCompanyId, companies, cloudPortfolioUrl } = params;
|
|
const nextCompany = companies.find(
|
|
(company) => company.id !== archivedCompanyId && company.status !== "archived",
|
|
);
|
|
if (nextCompany) return { kind: "company", company: nextCompany };
|
|
if (cloudPortfolioUrl) return { kind: "cloud_portfolio", url: cloudPortfolioUrl };
|
|
return { kind: "companies_list" };
|
|
}
|
|
|
|
export function shouldSyncCompanySelectionFromRoute(params: {
|
|
selectionSource: CompanySelectionSource;
|
|
selectedCompanyId: string | null;
|
|
routeCompanyId: string;
|
|
}): boolean {
|
|
const { selectionSource, selectedCompanyId, routeCompanyId } = params;
|
|
|
|
if (selectedCompanyId === routeCompanyId) return false;
|
|
|
|
// Let manual company switches finish their remembered-path navigation first.
|
|
if (selectionSource === "manual" && selectedCompanyId) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|