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:
@@ -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 } },
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user