Skip to content
6 changes: 6 additions & 0 deletions .server-changes/keep-page-when-switching-project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: improvement
---

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.
10 changes: 6 additions & 4 deletions apps/webapp/app/components/navigation/SideMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import { VercelLogo } from "~/components/integrations/VercelLogo";
import { Avatar } from "~/components/primitives/Avatar";
import { UserProfilePhoto } from "~/components/UserProfilePhoto";
import { type MatchedEnvironment } from "~/hooks/useEnvironment";
import { usePageSwitcher } from "~/hooks/useEnvironmentSwitcher";
import { useFeatureFlags } from "~/hooks/useFeatureFlags";
import { useFeatures } from "~/hooks/useFeatures";
import { type MatchedOrganization } from "~/hooks/useOrganizations";
Expand Down Expand Up @@ -99,7 +100,6 @@ import {
logoutPath,
newOrganizationPath,
newProjectPath,
organizationPath,
organizationRolesPath,
organizationSettingsPath,
organizationSlackIntegrationPath,
Expand All @@ -122,7 +122,6 @@ import {
v3LogsPath,
v3ModelsPath,
v3ProjectAlertsPath,
v3ProjectPath,
v3ProjectSettingsGeneralPath,
v3ProjectSettingsIntegrationsPath,
v3PromptsPath,
Expand Down Expand Up @@ -2007,6 +2006,7 @@ function ProjectSelector({
}) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const navigation = useNavigation();
const { urlForProject } = usePageSwitcher();

useEffect(() => {
setIsMenuOpen(false);
Expand Down Expand Up @@ -2083,7 +2083,7 @@ function ProjectSelector({
return (
<PopoverMenuItem
key={p.id}
to={v3ProjectPath(organization, p)}
to={urlForProject(organization, p)}
title={
<div className="flex w-full items-center justify-between text-text-bright">
<SideMenuLabel className="min-w-0 grow text-left">{p.name}</SideMenuLabel>
Expand Down Expand Up @@ -2183,6 +2183,8 @@ function SwitchOrganizations({
organizations: MatchedOrganization[];
organization: MatchedOrganization;
}) {
const { urlForOrganization } = usePageSwitcher();

return (
<SideMenuPopoverSubMenu title="Switch organization" icon={ArrowLeftRightIcon}>
<div className="flex flex-col gap-1 p-1">
Expand All @@ -2198,7 +2200,7 @@ function SwitchOrganizations({
{organizations.map((org) => (
<PopoverMenuItem
key={org.id}
to={organizationPath(org)}
to={urlForOrganization(org)}
title={org.title}
icon={<Avatar size={1.25} avatar={org.avatar} orgName={org.title} />}
leadingIconClassName="text-text-dimmed"
Expand Down
103 changes: 36 additions & 67 deletions apps/webapp/app/hooks/useEnvironmentSwitcher.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
import { type Path, useMatches } from "@remix-run/react";
import { useMatches } from "@remix-run/react";
import { type RuntimeEnvironment } from "@trigger.dev/database";
import {
ENVIRONMENT_MATCH_ID,
organizationPortablePage,
pageBelowEnvironment,
pathForEnvironmentSwitch,
portablePageSearch,
projectPortablePage,
} from "~/utils/pageSwitching";
import {
organizationPath,
type OrgForPath,
type ProjectForPath,
v3ProjectPath,
} from "~/utils/pathBuilder";
import { useOptimisticLocation } from "./useOptimisticLocation";

/**
* It gives the URLs for the current page for other environments
* @returns
*/
export function useEnvironmentSwitcher() {
const matches = useMatches();
const location = useOptimisticLocation();
const environmentPathname = useEnvironmentPathname();

const urlForEnvironment = (newEnvironment: Pick<RuntimeEnvironment, "id" | "slug">) => {
return routeForEnvironmentSwitch({
return pathForEnvironmentSwitch({
location,
matchId: matches[matches.length - 1].id,
environmentPathname,
environmentSlug: newEnvironment.slug,
});
};
Expand All @@ -23,71 +37,26 @@ export function useEnvironmentSwitcher() {
};
}

/** Function that takes in a UIMatch id, the current URL, the new environment slug, and returns a new URL */
export function routeForEnvironmentSwitch({
location,
matchId,
environmentSlug,
}: {
location: Path;
matchId: string;
environmentSlug: string;
}) {
switch (matchId) {
// Run page
case "routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam": {
const newLocation: Path = {
pathname: replaceEnvInPath(location.pathname, environmentSlug).replace(
/\/runs\/.*/,
"/runs"
),
search: "",
hash: "",
};
return fullPath(newLocation);
}
case "routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam": {
const newLocation: Path = {
pathname: replaceEnvInPath(location.pathname, environmentSlug).replace(
/\/deployments\/.*/,
"/deployments"
),
search: "",
hash: "",
};
return fullPath(newLocation);
}
case "routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam":
case "routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.edit.$scheduleParam": {
const newLocation: Path = {
pathname: replaceEnvInPath(location.pathname, environmentSlug).replace(
/\/schedules\/.*/,
"/schedules"
),
search: "",
hash: "",
};
return fullPath(newLocation);
}
default: {
const newLocation: Path = {
pathname: replaceEnvInPath(location.pathname, environmentSlug),
search: location.search,
hash: location.hash,
};
return fullPath(newLocation);
}
}
}

/**
* Replace the /env/<slug>/ in the path so it's /env/<environmentSlug>
* It gives the URLs for the current page in another project or organization. Which environment
* that page opens in is left to the server, which picks the same one it would without a page.
*/
function replaceEnvInPath(path: string, environmentSlug: string) {
//allow anything except /
return path.replace(/env\/([^/]+)/, `env/${environmentSlug}`);
export function usePageSwitcher() {
const location = useOptimisticLocation();
const environmentPathname = useEnvironmentPathname();
const page = pageBelowEnvironment(location.pathname, environmentPathname);
const projectSearch = portablePageSearch(projectPortablePage(page));
const organizationSearch = portablePageSearch(organizationPortablePage(page));

return {
urlForProject: (organization: OrgForPath, project: ProjectForPath) =>
`${v3ProjectPath(organization, project)}${projectSearch}`,
urlForOrganization: (organization: OrgForPath) =>
`${organizationPath(organization)}${organizationSearch}`,
};
}

function fullPath(location: Path) {
return `${location.pathname}${location.search}${location.hash}`;
function useEnvironmentPathname() {
const matches = useMatches();
return matches.find((match) => match.id === ENVIRONMENT_MATCH_ID)?.pathname;
Comment thread
ericallam marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { prisma } from "~/db.server";
import { SelectBestEnvironmentPresenter } from "~/presenters/SelectBestEnvironmentPresenter.server";
import { logger } from "~/services/logger.server";
import { requireUser } from "~/services/session.server";
import { portablePageSearch, requestedOrganizationPortablePage } from "~/utils/pageSwitching";
import {
newOrganizationPath,
newProjectPath,
Expand Down Expand Up @@ -49,5 +50,9 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
throw redirect(newProjectPath({ slug: organizationSlug }));
}

return redirect(v3ProjectPath({ slug: organizationSlug }, bestProject));
const projectPath = v3ProjectPath({ slug: organizationSlug }, bestProject);

return redirect(
`${projectPath}${portablePageSearch(requestedOrganizationPortablePage(request))}`
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { redirect, type LoaderFunctionArgs } from "@remix-run/server-runtime";
import { prisma } from "~/db.server";
import { SelectBestEnvironmentPresenter } from "~/presenters/SelectBestEnvironmentPresenter.server";
import { requireUser } from "~/services/session.server";
import { pagePath, requestedProjectPortablePage } from "~/utils/pageSwitching";
import { ProjectParamSchema, v3EnvironmentPath } from "~/utils/pathBuilder";

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

return redirect(v3EnvironmentPath({ slug: organizationSlug }, project, environment));
const environmentPath = v3EnvironmentPath({ slug: organizationSlug }, project, environment);

return redirect(pagePath(environmentPath, requestedProjectPortablePage(request)));
};
Loading
Loading