Skip to content

Commit e40e837

Browse files
committed
feat(webapp): stay on the same page when switching project or organization
The project and organization switchers carried you to the Tasks page of wherever you landed. They now carry the page you were on: the switcher link names it, and the project index loader appends it to the environment it already resolves, so the environment is still picked server-side. Pages named after a resource truncate to their list page, from one list shared with the environment switcher — which previously only truncated runs, deploys and schedules, and so carried ids from the other 16 into the new environment.
1 parent 429c004 commit e40e837

7 files changed

Lines changed: 494 additions & 73 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Switching project or organization in the sidebar now keeps you on the same page instead of sending you back to Tasks. Pages for a specific run, deploy or other single item open the matching list instead.

apps/webapp/app/components/navigation/SideMenu.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import { VercelLogo } from "~/components/integrations/VercelLogo";
7272
import { Avatar } from "~/components/primitives/Avatar";
7373
import { UserProfilePhoto } from "~/components/UserProfilePhoto";
7474
import { type MatchedEnvironment } from "~/hooks/useEnvironment";
75+
import { usePageSwitcher } from "~/hooks/useEnvironmentSwitcher";
7576
import { useFeatureFlags } from "~/hooks/useFeatureFlags";
7677
import { useFeatures } from "~/hooks/useFeatures";
7778
import { type MatchedOrganization } from "~/hooks/useOrganizations";
@@ -99,7 +100,6 @@ import {
99100
logoutPath,
100101
newOrganizationPath,
101102
newProjectPath,
102-
organizationPath,
103103
organizationRolesPath,
104104
organizationSettingsPath,
105105
organizationSlackIntegrationPath,
@@ -122,7 +122,6 @@ import {
122122
v3LogsPath,
123123
v3ModelsPath,
124124
v3ProjectAlertsPath,
125-
v3ProjectPath,
126125
v3ProjectSettingsGeneralPath,
127126
v3ProjectSettingsIntegrationsPath,
128127
v3PromptsPath,
@@ -2007,6 +2006,7 @@ function ProjectSelector({
20072006
}) {
20082007
const [isMenuOpen, setIsMenuOpen] = useState(false);
20092008
const navigation = useNavigation();
2009+
const { urlForProject } = usePageSwitcher();
20102010

20112011
useEffect(() => {
20122012
setIsMenuOpen(false);
@@ -2083,7 +2083,7 @@ function ProjectSelector({
20832083
return (
20842084
<PopoverMenuItem
20852085
key={p.id}
2086-
to={v3ProjectPath(organization, p)}
2086+
to={urlForProject(organization, p)}
20872087
title={
20882088
<div className="flex w-full items-center justify-between text-text-bright">
20892089
<SideMenuLabel className="min-w-0 grow text-left">{p.name}</SideMenuLabel>
@@ -2183,6 +2183,8 @@ function SwitchOrganizations({
21832183
organizations: MatchedOrganization[];
21842184
organization: MatchedOrganization;
21852185
}) {
2186+
const { urlForOrganization } = usePageSwitcher();
2187+
21862188
return (
21872189
<SideMenuPopoverSubMenu title="Switch organization" icon={ArrowLeftRightIcon}>
21882190
<div className="flex flex-col gap-1 p-1">
@@ -2198,7 +2200,7 @@ function SwitchOrganizations({
21982200
{organizations.map((org) => (
21992201
<PopoverMenuItem
22002202
key={org.id}
2201-
to={organizationPath(org)}
2203+
to={urlForOrganization(org)}
22022204
title={org.title}
22032205
icon={<Avatar size={1.25} avatar={org.avatar} orgName={org.title} />}
22042206
leadingIconClassName="text-text-dimmed"
Lines changed: 33 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,32 @@
1-
import { type Path, useMatches } from "@remix-run/react";
1+
import { useMatches } from "@remix-run/react";
22
import { type RuntimeEnvironment } from "@trigger.dev/database";
3+
import {
4+
ENVIRONMENT_MATCH_ID,
5+
pageBelowEnvironment,
6+
pathForEnvironmentSwitch,
7+
portablePage,
8+
portablePageSearch,
9+
} from "~/utils/pageSwitching";
10+
import {
11+
organizationPath,
12+
type OrgForPath,
13+
type ProjectForPath,
14+
v3ProjectPath,
15+
} from "~/utils/pathBuilder";
316
import { useOptimisticLocation } from "./useOptimisticLocation";
417

518
/**
619
* It gives the URLs for the current page for other environments
720
* @returns
821
*/
922
export function useEnvironmentSwitcher() {
10-
const matches = useMatches();
1123
const location = useOptimisticLocation();
24+
const environmentPathname = useEnvironmentPathname();
1225

1326
const urlForEnvironment = (newEnvironment: Pick<RuntimeEnvironment, "id" | "slug">) => {
14-
return routeForEnvironmentSwitch({
27+
return pathForEnvironmentSwitch({
1528
location,
16-
matchId: matches[matches.length - 1].id,
29+
environmentPathname,
1730
environmentSlug: newEnvironment.slug,
1831
});
1932
};
@@ -23,71 +36,24 @@ export function useEnvironmentSwitcher() {
2336
};
2437
}
2538

26-
/** Function that takes in a UIMatch id, the current URL, the new environment slug, and returns a new URL */
27-
export function routeForEnvironmentSwitch({
28-
location,
29-
matchId,
30-
environmentSlug,
31-
}: {
32-
location: Path;
33-
matchId: string;
34-
environmentSlug: string;
35-
}) {
36-
switch (matchId) {
37-
// Run page
38-
case "routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam": {
39-
const newLocation: Path = {
40-
pathname: replaceEnvInPath(location.pathname, environmentSlug).replace(
41-
/\/runs\/.*/,
42-
"/runs"
43-
),
44-
search: "",
45-
hash: "",
46-
};
47-
return fullPath(newLocation);
48-
}
49-
case "routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam": {
50-
const newLocation: Path = {
51-
pathname: replaceEnvInPath(location.pathname, environmentSlug).replace(
52-
/\/deployments\/.*/,
53-
"/deployments"
54-
),
55-
search: "",
56-
hash: "",
57-
};
58-
return fullPath(newLocation);
59-
}
60-
case "routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam":
61-
case "routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.edit.$scheduleParam": {
62-
const newLocation: Path = {
63-
pathname: replaceEnvInPath(location.pathname, environmentSlug).replace(
64-
/\/schedules\/.*/,
65-
"/schedules"
66-
),
67-
search: "",
68-
hash: "",
69-
};
70-
return fullPath(newLocation);
71-
}
72-
default: {
73-
const newLocation: Path = {
74-
pathname: replaceEnvInPath(location.pathname, environmentSlug),
75-
search: location.search,
76-
hash: location.hash,
77-
};
78-
return fullPath(newLocation);
79-
}
80-
}
81-
}
82-
8339
/**
84-
* Replace the /env/<slug>/ in the path so it's /env/<environmentSlug>
40+
* It gives the URLs for the current page in another project or organization. Which environment
41+
* that page opens in is left to the server, which picks the same one it would without a page.
8542
*/
86-
function replaceEnvInPath(path: string, environmentSlug: string) {
87-
//allow anything except /
88-
return path.replace(/env\/([^/]+)/, `env/${environmentSlug}`);
43+
export function usePageSwitcher() {
44+
const location = useOptimisticLocation();
45+
const environmentPathname = useEnvironmentPathname();
46+
const page = portablePage(pageBelowEnvironment(location.pathname, environmentPathname));
47+
const search = portablePageSearch(page);
48+
49+
return {
50+
urlForProject: (organization: OrgForPath, project: ProjectForPath) =>
51+
`${v3ProjectPath(organization, project)}${search}`,
52+
urlForOrganization: (organization: OrgForPath) => `${organizationPath(organization)}${search}`,
53+
};
8954
}
9055

91-
function fullPath(location: Path) {
92-
return `${location.pathname}${location.search}${location.hash}`;
56+
function useEnvironmentPathname() {
57+
const matches = useMatches();
58+
return matches.find((match) => match.id === ENVIRONMENT_MATCH_ID)?.pathname;
9359
}

apps/webapp/app/routes/_app.orgs.$organizationSlug._index/route.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { prisma } from "~/db.server";
33
import { SelectBestEnvironmentPresenter } from "~/presenters/SelectBestEnvironmentPresenter.server";
44
import { logger } from "~/services/logger.server";
55
import { requireUser } from "~/services/session.server";
6+
import { portablePageSearch, requestedPortablePage } from "~/utils/pageSwitching";
67
import {
78
newOrganizationPath,
89
newProjectPath,
@@ -49,5 +50,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
4950
throw redirect(newProjectPath({ slug: organizationSlug }));
5051
}
5152

52-
return redirect(v3ProjectPath({ slug: organizationSlug }, bestProject));
53+
const projectPath = v3ProjectPath({ slug: organizationSlug }, bestProject);
54+
55+
return redirect(`${projectPath}${portablePageSearch(requestedPortablePage(request))}`);
5356
};

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { redirect, type LoaderFunctionArgs } from "@remix-run/server-runtime";
22
import { prisma } from "~/db.server";
33
import { SelectBestEnvironmentPresenter } from "~/presenters/SelectBestEnvironmentPresenter.server";
44
import { requireUser } from "~/services/session.server";
5+
import { pagePath, requestedPortablePage } from "~/utils/pageSwitching";
56
import { ProjectParamSchema, v3EnvironmentPath } from "~/utils/pathBuilder";
67

78
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
@@ -40,5 +41,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
4041
const selector = new SelectBestEnvironmentPresenter();
4142
const environment = await selector.selectBestEnvironment(project.id, user, project.environments);
4243

43-
return redirect(v3EnvironmentPath({ slug: organizationSlug }, project, environment));
44+
const environmentPath = v3EnvironmentPath({ slug: organizationSlug }, project, environment);
45+
46+
return redirect(pagePath(environmentPath, requestedPortablePage(request)));
4447
};

0 commit comments

Comments
 (0)