From 9ae0ca64afb7fac9f9c21e5b49f3500a9954ce79 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 9 Feb 2024 11:05:28 +0000 Subject: [PATCH] Fix edge cases with deleting last org/project (#892) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * If you have an org with no projects, it displays in the project dropdown with a “New project” button * When creating a new org disable the button whilst it’s doing the request * If an org already had any deleted projects it couldn’t be deleted… * When selecting the best project, factor in deleted ones * Don’t show the cancel button when creating a new org if there are no non-deleted projects * If a project has already been deleted just return --- .../app/components/navigation/SideMenu.tsx | 42 +++++++++++-------- .../NewOrganizationPresenter.server.ts | 7 +++- .../SelectBestProjectPresenter.server.ts | 3 +- .../webapp/app/routes/_app.orgs.new/route.tsx | 15 +++++-- .../app/services/deleteOrganization.server.ts | 10 +---- .../app/services/deleteProject.server.ts | 9 ++-- 6 files changed, 51 insertions(+), 35 deletions(-) diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 1f660acdf..c32089a3a 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -357,23 +357,31 @@ function ProjectSelector({
- {organization.projects.map((p) => { - const isSelected = p.id === project.id; - return ( - - {p.name} - -
- } - isSelected={isSelected} - icon="folder" - /> - ); - })} + {organization.projects.length > 0 ? ( + organization.projects.map((p) => { + const isSelected = p.id === project.id; + return ( + + {p.name} + + + } + isSelected={isSelected} + icon="folder" + /> + ); + }) + ) : ( + + )}
))} diff --git a/apps/webapp/app/presenters/NewOrganizationPresenter.server.ts b/apps/webapp/app/presenters/NewOrganizationPresenter.server.ts index 4d3fe0aae..c81fda8a0 100644 --- a/apps/webapp/app/presenters/NewOrganizationPresenter.server.ts +++ b/apps/webapp/app/presenters/NewOrganizationPresenter.server.ts @@ -10,11 +10,16 @@ export class NewOrganizationPresenter { public async call({ userId }: { userId: User["id"] }) { const organizations = await this.#prismaClient.organization.findMany({ + select: { + projects: { + where: { deletedAt: null }, + }, + }, where: { members: { some: { userId } } }, }); return { - hasOrganizations: organizations.length > 0, + hasOrganizations: organizations.filter((o) => o.projects.length > 0).length > 0, }; } } diff --git a/apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts b/apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts index 55adbd6cc..e87239cc5 100644 --- a/apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts +++ b/apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts @@ -14,7 +14,7 @@ export class SelectBestProjectPresenter { const projectId = await getCurrentProjectId(request); if (projectId) { const project = await this.#prismaClient.project.findUnique({ - where: { id: projectId, organization: { members: { some: { userId } } } }, + where: { id: projectId, deletedAt: null, organization: { members: { some: { userId } } } }, include: { organization: true }, }); if (project) { @@ -28,6 +28,7 @@ export class SelectBestProjectPresenter { organization: true, }, where: { + deletedAt: null, organization: { members: { some: { userId } }, }, diff --git a/apps/webapp/app/routes/_app.orgs.new/route.tsx b/apps/webapp/app/routes/_app.orgs.new/route.tsx index 61b23c6da..c30e0bd4a 100644 --- a/apps/webapp/app/routes/_app.orgs.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.new/route.tsx @@ -3,7 +3,7 @@ import { parse } from "@conform-to/zod"; import { RadioGroup } from "@radix-ui/react-radio-group"; import type { ActionFunction, LoaderFunctionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; -import { Form, useActionData } from "@remix-run/react"; +import { Form, useActionData, useNavigation } from "@remix-run/react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { MainCenteredContainer } from "~/components/layout/AppLayout"; @@ -23,7 +23,7 @@ import { createOrganization } from "~/models/organization.server"; import { NewOrganizationPresenter } from "~/presenters/NewOrganizationPresenter.server"; import { commitCurrentProjectSession, setCurrentProjectId } from "~/services/currentProject.server"; import { requireUserId } from "~/services/session.server"; -import { plansPath, projectPath, rootPath, selectPlanPath } from "~/utils/pathBuilder"; +import { projectPath, rootPath, selectPlanPath } from "~/utils/pathBuilder"; const schema = z.object({ orgName: z.string().min(3).max(50), @@ -86,6 +86,7 @@ export default function NewOrganizationPage() { const { hasOrganizations } = useTypedLoaderData(); const lastSubmission = useActionData(); const { isManagedCloud } = useFeatures(); + const navigation = useNavigation(); const [form, { orgName, projectName }] = useForm({ id: "create-organization", @@ -95,8 +96,11 @@ export default function NewOrganizationPage() { return parse(formData, { schema }); }, shouldRevalidate: "onSubmit", + shouldValidate: "onSubmit", }); + const isLoading = navigation.state === "submitting" || navigation.state === "loading"; + return ( @@ -161,7 +165,12 @@ export default function NewOrganizationPage() { + } diff --git a/apps/webapp/app/services/deleteOrganization.server.ts b/apps/webapp/app/services/deleteOrganization.server.ts index 4887d5d18..c93ac8b0d 100644 --- a/apps/webapp/app/services/deleteOrganization.server.ts +++ b/apps/webapp/app/services/deleteOrganization.server.ts @@ -1,15 +1,9 @@ +import { DateFormatter } from "@internationalized/date"; import { PrismaClient } from "@trigger.dev/database"; import { prisma } from "~/db.server"; -import { DisableJobService } from "./jobs/disableJob.server"; -import { AuthenticatedEnvironment } from "./apiAuth.server"; -import { DeleteJobService } from "./jobs/deleteJob.server"; -import { DeleteEndpointService } from "./endpoints/deleteEndpointService"; -import { logger } from "./logger.server"; -import { DisableScheduleSourceService } from "./schedules/disableScheduleSource.server"; import { featuresForRequest } from "~/features.server"; -import { DeleteProjectService } from "./deleteProject.server"; import { BillingService } from "./billing.server"; -import { DateFormatter } from "@internationalized/date"; +import { DeleteProjectService } from "./deleteProject.server"; export class DeleteOrganizationService { #prismaClient: PrismaClient; diff --git a/apps/webapp/app/services/deleteProject.server.ts b/apps/webapp/app/services/deleteProject.server.ts index 8b030c3fa..fa9f1b792 100644 --- a/apps/webapp/app/services/deleteProject.server.ts +++ b/apps/webapp/app/services/deleteProject.server.ts @@ -1,13 +1,12 @@ import { PrismaClient } from "@trigger.dev/database"; import { prisma } from "~/db.server"; -import { DisableJobService } from "./jobs/disableJob.server"; -import { AuthenticatedEnvironment } from "./apiAuth.server"; -import { DeleteJobService } from "./jobs/deleteJob.server"; import { DeleteEndpointService } from "./endpoints/deleteEndpointService"; import { logger } from "./logger.server"; import { DisableScheduleSourceService } from "./schedules/disableScheduleSource.server"; -type Options = { projectId: string; userId: string } | { projectSlug: string; userId: string }; +type Options = ({ projectId: string } | { projectSlug: string }) & { + userId: string; +}; export class DeleteProjectService { #prismaClient: PrismaClient; @@ -52,7 +51,7 @@ export class DeleteProjectService { } if (project.deletedAt) { - throw new Error("Project already deleted"); + return; } //disable and delete all jobs