-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Rename and delete projects #880
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0f02b0f
aa6be07
627ddd8
ce9506f
79e69bd
0697054
01506d0
322d8ce
75197ff
ac6675d
e179304
12c5229
d76877b
319b115
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,6 @@ import { | |
| ArrowRightOnRectangleIcon, | ||
| ChartBarIcon, | ||
| CursorArrowRaysIcon, | ||
| EllipsisHorizontalIcon, | ||
| ShieldCheckIcon, | ||
| } from "@heroicons/react/20/solid"; | ||
| import { UserGroupIcon, UserPlusIcon } from "@heroicons/react/24/solid"; | ||
|
|
@@ -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 { | ||
|
|
@@ -33,6 +33,7 @@ import { | |
| projectHttpEndpointsPath, | ||
| projectPath, | ||
| projectRunsPath, | ||
| projectSettingsPath, | ||
| projectSetupPath, | ||
| projectTriggersPath, | ||
| } from "~/utils/pathBuilder"; | ||
|
|
@@ -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< | ||
|
|
@@ -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" | ||
|
|
@@ -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)} | ||
|
|
@@ -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 | ||
|
|
@@ -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" | ||
| /> | ||
| )} | ||
|
|
||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
|
|
@@ -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); | ||
|
|
@@ -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" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
|
@@ -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 }; | ||
|
|
@@ -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({ | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
|
@@ -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; | ||
| } | ||
|
|
@@ -123,6 +151,7 @@ export class OrganizationsPresenter { | |
| organization: { | ||
| slug: organizationSlug, | ||
| }, | ||
| deletedAt: null, | ||
| }, | ||
| }); | ||
|
|
||
|
|
@@ -150,6 +179,7 @@ export class OrganizationsPresenter { | |
| title: true, | ||
| runsEnabled: true, | ||
| projects: { | ||
| where: { deletedAt: null }, | ||
| select: { | ||
| id: true, | ||
| slug: true, | ||
|
|
@@ -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 } }, | ||
|
|
@@ -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]; | ||
|
|
||
There was a problem hiding this comment.
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.