Skip to content

Commit 9ae0ca6

Browse files
authored
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
1 parent da69fa0 commit 9ae0ca6

File tree

6 files changed

+51
-35
lines changed

6 files changed

+51
-35
lines changed

apps/webapp/app/components/navigation/SideMenu.tsx

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -357,23 +357,31 @@ function ProjectSelector({
357357
<Fragment key={organization.id}>
358358
<PopoverSectionHeader title={organization.title} />
359359
<div className="flex flex-col gap-1 p-1">
360-
{organization.projects.map((p) => {
361-
const isSelected = p.id === project.id;
362-
return (
363-
<PopoverMenuItem
364-
key={p.id}
365-
to={projectPath(organization, p)}
366-
title={
367-
<div className="flex w-full items-center justify-between text-bright">
368-
<span className="grow truncate text-left">{p.name}</span>
369-
<MenuCount count={p.jobCount} />
370-
</div>
371-
}
372-
isSelected={isSelected}
373-
icon="folder"
374-
/>
375-
);
376-
})}
360+
{organization.projects.length > 0 ? (
361+
organization.projects.map((p) => {
362+
const isSelected = p.id === project.id;
363+
return (
364+
<PopoverMenuItem
365+
key={p.id}
366+
to={projectPath(organization, p)}
367+
title={
368+
<div className="flex w-full items-center justify-between text-bright">
369+
<span className="grow truncate text-left">{p.name}</span>
370+
<MenuCount count={p.jobCount} />
371+
</div>
372+
}
373+
isSelected={isSelected}
374+
icon="folder"
375+
/>
376+
);
377+
})
378+
) : (
379+
<PopoverMenuItem
380+
to={newProjectPath(organization)}
381+
title="New project"
382+
icon="plus"
383+
/>
384+
)}
377385
</div>
378386
</Fragment>
379387
))}

apps/webapp/app/presenters/NewOrganizationPresenter.server.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@ export class NewOrganizationPresenter {
1010

1111
public async call({ userId }: { userId: User["id"] }) {
1212
const organizations = await this.#prismaClient.organization.findMany({
13+
select: {
14+
projects: {
15+
where: { deletedAt: null },
16+
},
17+
},
1318
where: { members: { some: { userId } } },
1419
});
1520

1621
return {
17-
hasOrganizations: organizations.length > 0,
22+
hasOrganizations: organizations.filter((o) => o.projects.length > 0).length > 0,
1823
};
1924
}
2025
}

apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export class SelectBestProjectPresenter {
1414
const projectId = await getCurrentProjectId(request);
1515
if (projectId) {
1616
const project = await this.#prismaClient.project.findUnique({
17-
where: { id: projectId, organization: { members: { some: { userId } } } },
17+
where: { id: projectId, deletedAt: null, organization: { members: { some: { userId } } } },
1818
include: { organization: true },
1919
});
2020
if (project) {
@@ -28,6 +28,7 @@ export class SelectBestProjectPresenter {
2828
organization: true,
2929
},
3030
where: {
31+
deletedAt: null,
3132
organization: {
3233
members: { some: { userId } },
3334
},

apps/webapp/app/routes/_app.orgs.new/route.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { parse } from "@conform-to/zod";
33
import { RadioGroup } from "@radix-ui/react-radio-group";
44
import type { ActionFunction, LoaderFunctionArgs } from "@remix-run/node";
55
import { json, redirect } from "@remix-run/node";
6-
import { Form, useActionData } from "@remix-run/react";
6+
import { Form, useActionData, useNavigation } from "@remix-run/react";
77
import { typedjson, useTypedLoaderData } from "remix-typedjson";
88
import { z } from "zod";
99
import { MainCenteredContainer } from "~/components/layout/AppLayout";
@@ -23,7 +23,7 @@ import { createOrganization } from "~/models/organization.server";
2323
import { NewOrganizationPresenter } from "~/presenters/NewOrganizationPresenter.server";
2424
import { commitCurrentProjectSession, setCurrentProjectId } from "~/services/currentProject.server";
2525
import { requireUserId } from "~/services/session.server";
26-
import { plansPath, projectPath, rootPath, selectPlanPath } from "~/utils/pathBuilder";
26+
import { projectPath, rootPath, selectPlanPath } from "~/utils/pathBuilder";
2727

2828
const schema = z.object({
2929
orgName: z.string().min(3).max(50),
@@ -86,6 +86,7 @@ export default function NewOrganizationPage() {
8686
const { hasOrganizations } = useTypedLoaderData<typeof loader>();
8787
const lastSubmission = useActionData();
8888
const { isManagedCloud } = useFeatures();
89+
const navigation = useNavigation();
8990

9091
const [form, { orgName, projectName }] = useForm({
9192
id: "create-organization",
@@ -95,8 +96,11 @@ export default function NewOrganizationPage() {
9596
return parse(formData, { schema });
9697
},
9798
shouldRevalidate: "onSubmit",
99+
shouldValidate: "onSubmit",
98100
});
99101

102+
const isLoading = navigation.state === "submitting" || navigation.state === "loading";
103+
100104
return (
101105
<MainCenteredContainer className="max-w-[22rem]">
102106
<FormTitle LeadingIcon="organization" title="Create an Organization" />
@@ -161,7 +165,12 @@ export default function NewOrganizationPage() {
161165

162166
<FormButtons
163167
confirmButton={
164-
<Button type="submit" variant={"primary/small"} TrailingIcon="arrow-right">
168+
<Button
169+
type="submit"
170+
variant={"primary/small"}
171+
TrailingIcon="arrow-right"
172+
disabled={isLoading}
173+
>
165174
Create
166175
</Button>
167176
}

apps/webapp/app/services/deleteOrganization.server.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
1+
import { DateFormatter } from "@internationalized/date";
12
import { PrismaClient } from "@trigger.dev/database";
23
import { prisma } from "~/db.server";
3-
import { DisableJobService } from "./jobs/disableJob.server";
4-
import { AuthenticatedEnvironment } from "./apiAuth.server";
5-
import { DeleteJobService } from "./jobs/deleteJob.server";
6-
import { DeleteEndpointService } from "./endpoints/deleteEndpointService";
7-
import { logger } from "./logger.server";
8-
import { DisableScheduleSourceService } from "./schedules/disableScheduleSource.server";
94
import { featuresForRequest } from "~/features.server";
10-
import { DeleteProjectService } from "./deleteProject.server";
115
import { BillingService } from "./billing.server";
12-
import { DateFormatter } from "@internationalized/date";
6+
import { DeleteProjectService } from "./deleteProject.server";
137

148
export class DeleteOrganizationService {
159
#prismaClient: PrismaClient;

apps/webapp/app/services/deleteProject.server.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { PrismaClient } from "@trigger.dev/database";
22
import { prisma } from "~/db.server";
3-
import { DisableJobService } from "./jobs/disableJob.server";
4-
import { AuthenticatedEnvironment } from "./apiAuth.server";
5-
import { DeleteJobService } from "./jobs/deleteJob.server";
63
import { DeleteEndpointService } from "./endpoints/deleteEndpointService";
74
import { logger } from "./logger.server";
85
import { DisableScheduleSourceService } from "./schedules/disableScheduleSource.server";
96

10-
type Options = { projectId: string; userId: string } | { projectSlug: string; userId: string };
7+
type Options = ({ projectId: string } | { projectSlug: string }) & {
8+
userId: string;
9+
};
1110

1211
export class DeleteProjectService {
1312
#prismaClient: PrismaClient;
@@ -52,7 +51,7 @@ export class DeleteProjectService {
5251
}
5352

5453
if (project.deletedAt) {
55-
throw new Error("Project already deleted");
54+
return;
5655
}
5756

5857
//disable and delete all jobs

0 commit comments

Comments
 (0)