Fix edge cases with deleting last org/project (#892)

* 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
This commit is contained in:
Matt Aitken
2024-02-09 11:05:28 +00:00
committed by GitHub
parent da69fa0613
commit 9ae0ca64af
6 changed files with 51 additions and 35 deletions
@@ -357,23 +357,31 @@ function ProjectSelector({
<Fragment key={organization.id}>
<PopoverSectionHeader title={organization.title} />
<div className="flex flex-col gap-1 p-1">
{organization.projects.map((p) => {
const isSelected = p.id === project.id;
return (
<PopoverMenuItem
key={p.id}
to={projectPath(organization, p)}
title={
<div className="flex w-full items-center justify-between text-bright">
<span className="grow truncate text-left">{p.name}</span>
<MenuCount count={p.jobCount} />
</div>
}
isSelected={isSelected}
icon="folder"
/>
);
})}
{organization.projects.length > 0 ? (
organization.projects.map((p) => {
const isSelected = p.id === project.id;
return (
<PopoverMenuItem
key={p.id}
to={projectPath(organization, p)}
title={
<div className="flex w-full items-center justify-between text-bright">
<span className="grow truncate text-left">{p.name}</span>
<MenuCount count={p.jobCount} />
</div>
}
isSelected={isSelected}
icon="folder"
/>
);
})
) : (
<PopoverMenuItem
to={newProjectPath(organization)}
title="New project"
icon="plus"
/>
)}
</div>
</Fragment>
))}
@@ -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,
};
}
}
@@ -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 } },
},
+12 -3
View File
@@ -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<typeof loader>();
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 (
<MainCenteredContainer className="max-w-[22rem]">
<FormTitle LeadingIcon="organization" title="Create an Organization" />
@@ -161,7 +165,12 @@ export default function NewOrganizationPage() {
<FormButtons
confirmButton={
<Button type="submit" variant={"primary/small"} TrailingIcon="arrow-right">
<Button
type="submit"
variant={"primary/small"}
TrailingIcon="arrow-right"
disabled={isLoading}
>
Create
</Button>
}
@@ -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;
@@ -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