Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 22 additions & 23 deletions apps/webapp/app/components/navigation/SideMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
ArrowRightOnRectangleIcon,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes in this file are improvements to the sidebar, mostly unrelated to deleting/renaming projects.

  • The dropdown title is now the Project name (instead of the org name)
  • The section titles are now "Project" and "Organization" because it was confusing before.

ChartBarIcon,
CursorArrowRaysIcon,
EllipsisHorizontalIcon,
ShieldCheckIcon,
} from "@heroicons/react/20/solid";
import { UserGroupIcon, UserPlusIcon } from "@heroicons/react/24/solid";
Expand All @@ -15,6 +14,7 @@ import { useFeatures } from "~/hooks/useFeatures";
import { MatchedOrganization } from "~/hooks/useOrganizations";
import { MatchedProject } from "~/hooks/useProject";
import { User } from "~/models/user.server";
import { useV3Enabled } from "~/root";
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
import { cn } from "~/utils/cn";
import {
Expand All @@ -33,6 +33,7 @@ import {
projectHttpEndpointsPath,
projectPath,
projectRunsPath,
projectSettingsPath,
projectSetupPath,
projectTriggersPath,
} from "~/utils/pathBuilder";
Expand All @@ -56,9 +57,8 @@ import {
PopoverSectionHeader,
} from "../primitives/Popover";
import { StepNumber } from "../primitives/StepNumber";
import { MenuCount, SideMenuItem } from "./SideMenuItem";
import { SideMenuHeader } from "./SideMenuHeader";
import { useV3Enabled } from "~/root";
import { MenuCount, SideMenuItem } from "./SideMenuItem";

type SideMenuUser = Pick<User, "email" | "admin"> & { isImpersonating: boolean };
type SideMenuProject = Pick<
Expand Down Expand Up @@ -106,19 +106,15 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
showHeaderDivider ? " border-border" : "border-transparent"
)}
>
<ProjectSelector
organization={organization}
organizations={organizations}
project={project}
/>
<ProjectSelector organizations={organizations} project={project} />
<UserMenu user={user} />
</div>
<div
className="h-full overflow-hidden overflow-y-auto pt-2 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
ref={borderRef}
>
<div className="mb-6 flex flex-col gap-1 px-1">
<SideMenuHeader title={project.name || "No project found"}>
<SideMenuHeader title={"Project"}>
<PopoverMenuItem
to={projectSetupPath(organization, project)}
title="Framework setup"
Expand Down Expand Up @@ -168,9 +164,16 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
to={projectEnvironmentsPath(organization, project)}
data-action="environments & api keys"
/>
<SideMenuItem
name="Project settings"
icon="settings"
iconColor="text-teal-500"
to={projectSettingsPath(organization, project)}
data-action="project-settings"
/>
</div>
<div className="mb-1 flex flex-col gap-1 px-1">
<SideMenuHeader title={organization.title}>
<SideMenuHeader title={"Organization"}>
<PopoverMenuItem to={newProjectPath(organization)} title="New Project" icon="plus" />
<PopoverMenuItem
to={inviteTeamMemberPath(organization)}
Expand Down Expand Up @@ -209,7 +212,7 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
</div>
</div>
<div className="flex flex-col gap-1 border-t border-border p-1">
{currentPlan?.subscription?.isPaying === true ? (
{currentPlan?.subscription?.isPaying === true && (
<Dialog>
<DialogTrigger asChild>
<Button
Expand Down Expand Up @@ -265,16 +268,14 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
</div>
</DialogContent>
</Dialog>
) : (
<SideMenuItem
name="Join our Discord"
icon={DiscordIcon}
to="https://trigger.dev/discord"
data-action="join our discord"
target="_blank"
/>
)}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Paying users get the option of both Discord and Slack. Before it was only Slack if they're paying, which sucked because some people were happily using Discord.

<SideMenuItem
name="Join our Discord"
icon={DiscordIcon}
to="https://trigger.dev/discord"
data-action="join our discord"
target="_blank"
/>
<SideMenuItem
name="Documentation"
icon="docs"
Expand Down Expand Up @@ -317,11 +318,9 @@ export function SideMenu({ user, project, organization, organizations }: SideMen

function ProjectSelector({
project,
organization,
organizations,
}: {
project: SideMenuProject;
organization: MatchedOrganization;
organizations: MatchedOrganization[];
}) {
const [isOrgMenuOpen, setOrgMenuOpen] = useState(false);
Expand All @@ -339,7 +338,7 @@ function ProjectSelector({
className="h-7 w-full justify-between overflow-hidden py-1 pl-2"
>
<LogoIcon className="relative -top-px mr-2 h-4 w-4 min-w-[1rem]" />
<span className="truncate">{organization.title ?? "Select an organization"}</span>
<span className="truncate">{project.name ?? "Select a project"}</span>
</PopoverArrowTrigger>
<PopoverContent
className="min-w-[16rem] overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
Expand Down
25 changes: 8 additions & 17 deletions apps/webapp/app/models/runtimeEnvironment.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export async function findEnvironmentByApiKey(apiKey: string) {
},
});

//don't return deleted projects
if (environment?.project.deletedAt !== null) {
return null;
}

return environment;
}

Expand All @@ -28,24 +33,10 @@ export async function findEnvironmentByPublicApiKey(apiKey: string) {
},
});

return environment;
}

export async function getEnvironmentForOrganization(organizationSlug: string, slug: string) {
const organization = await prisma.organization.findUnique({
where: {
slug: organizationSlug,
},
include: {
environments: true,
},
});

if (!organization) {
return;
//don't return deleted projects
if (environment?.project.deletedAt !== null) {
return null;
}

const environment = organization.environments.find((environment) => environment.slug === slug);

return environment;
}
56 changes: 45 additions & 11 deletions apps/webapp/app/presenters/OrganizationsPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import { PrismaClient } from "@trigger.dev/database";
import { redirect } from "remix-typedjson";
import { prisma } from "~/db.server";
import {
clearCurrentProjectId,
commitCurrentProjectSession,
getCurrentProjectId,
setCurrentProjectId,
} from "~/services/currentProject.server";
import { logger } from "~/services/logger.server";
import { newProjectPath } from "~/utils/pathBuilder";
import { ProjectPresenter } from "./ProjectPresenter.server";
import { redirectWithErrorMessage } from "~/models/message.server";
import { match } from "assert";

export class OrganizationsPresenter {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file deals with selecting a project, as well as organizations. This is because we always have a selected project due to the nature of our sidebar/design.

Where possible we now redirect users to the correct page rather than just showing them an error.

#prismaClient: PrismaClient;
Expand Down Expand Up @@ -54,7 +57,11 @@ export class OrganizationsPresenter {
});

if (!project) {
throw new Response("Project not found", { status: 404 });
throw redirectWithErrorMessage(
newProjectPath({ slug: organizationSlug }),
request,
"No projects found in organization"
);
}

return { organizations, organization, project };
Expand All @@ -75,16 +82,21 @@ export class OrganizationsPresenter {

//no project in session, let's set one
if (!sessionProjectId) {
//no session id and no project slug so we need to select the best project
if (!projectSlug) {
const bestProject = await this.#selectBestProjectForOrganization(organizationSlug, userId);
const bestProject = await this.#selectBestProjectForOrganization(
organizationSlug,
userId,
request
);
const session = await setCurrentProjectId(bestProject.id, request);
throw redirect(request.url, {
headers: { "Set-Cookie": await commitCurrentProjectSession(session) },
});
}

//use the project param to find the project
const project = await prisma.project.findFirst({
//get all the projects
const projects = await prisma.project.findMany({

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows us to check if there are any projects, as well as find the exact project.

select: {
id: true,
slug: true,
Expand All @@ -93,21 +105,37 @@ export class OrganizationsPresenter {
organization: {
slug: organizationSlug,
},
deletedAt: null,
slug: projectSlug,
},
orderBy: {
updatedAt: "desc",
},
});

if (!project) {
throw redirect(newProjectPath({ slug: organizationSlug }));
if (projects.length === 0) {
throw redirectWithErrorMessage(
newProjectPath({ slug: organizationSlug }),
request,
"No projects in this organization"
);
}

const session = await setCurrentProjectId(project.id, request);
//try get the project which matches the URL
let matchingProject = projects.find((p) => p.slug === projectSlug);

//if there's no matching project, just use the most recently updated one
if (!matchingProject) {
matchingProject = projects[0];
}

//set the session
const session = await setCurrentProjectId(matchingProject.id, request);
throw redirect(request.url, {
headers: { "Set-Cookie": await commitCurrentProjectSession(session) },
});
}

//no project slug, so just return the session id
if (!projectSlug) {
return sessionProjectId;
}
Expand All @@ -123,6 +151,7 @@ export class OrganizationsPresenter {
organization: {
slug: organizationSlug,
},
deletedAt: null,
},
});

Expand Down Expand Up @@ -150,6 +179,7 @@ export class OrganizationsPresenter {
title: true,
runsEnabled: true,
projects: {
where: { deletedAt: null },
select: {
id: true,
slug: true,
Expand Down Expand Up @@ -202,13 +232,18 @@ export class OrganizationsPresenter {
});
}

async #selectBestProjectForOrganization(organizationSlug: string, userId: string) {
async #selectBestProjectForOrganization(
organizationSlug: string,
userId: string,
request: Request
) {
const projects = await this.#prismaClient.project.findMany({
select: {
id: true,
slug: true,
},
where: {
deletedAt: null,
organization: {
slug: organizationSlug,
members: { some: { userId } },
Expand All @@ -223,8 +258,7 @@ export class OrganizationsPresenter {
});

if (projects.length === 0) {
logger.info("Didn't find a project in this org", { organizationSlug, projects });
throw new Response("Not Found", { status: 404 });
throw redirect(newProjectPath({ slug: organizationSlug }), request);
}

return projects[0];
Expand Down
4 changes: 3 additions & 1 deletion apps/webapp/app/presenters/ProjectPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export class ProjectPresenter {
organizationId: true,
createdAt: true,
updatedAt: true,
deletedAt: true,
_count: {
select: {
sources: {
Expand Down Expand Up @@ -53,7 +54,7 @@ export class ProjectPresenter {
},
},
},
where: { id, organization: { members: { some: { userId } } } },
where: { id, deletedAt: null, organization: { members: { some: { userId } } } },
});

if (!project) {
Expand All @@ -67,6 +68,7 @@ export class ProjectPresenter {
organizationId: project.organizationId,
createdAt: project.createdAt,
updatedAt: project.updatedAt,
deletedAt: project.deletedAt,
hasInactiveExternalTriggers: project._count.sources > 0,
jobCount: project._count.jobs,
httpEndpointCount: project._count.httpEndpoints,
Expand Down
Loading