Skip to content

Commit 116766f

Browse files
authored
Request v3 access (from the app) and disable v2 projects by default (#1123)
* Added v2Enabled and hasRequestedV3 columns to Organization * Don’t create a project when you create an org * Form for requesting v3 access * Reworked the new project form with the different version states. Refined copy on early access * If the project isn’t in the org then redirect to the new project page * Better message for existing users * Tidy imports * If it’s not the managed cloud then allow them to create v2 projects
1 parent c815f28 commit 116766f

File tree

9 files changed

+267
-89
lines changed

9 files changed

+267
-89
lines changed

apps/webapp/app/assets/icons/v3.svg

Lines changed: 4 additions & 0 deletions
Loading

apps/webapp/app/models/organization.server.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,12 @@ export async function createOrganization(
2121
{
2222
title,
2323
userId,
24-
projectName,
2524
companySize,
26-
projectVersion,
2725
}: Pick<Organization, "title" | "companySize"> & {
2826
userId: User["id"];
29-
projectName: string;
30-
projectVersion: "v2" | "v3";
3127
},
3228
attemptCount = 0
33-
): Promise<Organization & { projects: Project[] }> {
29+
): Promise<Organization> {
3430
if (typeof process.env.BLOCKED_USERS === "string" && process.env.BLOCKED_USERS.includes(userId)) {
3531
throw new Error("Organization could not be created.");
3632
}
@@ -50,9 +46,7 @@ export async function createOrganization(
5046
{
5147
title,
5248
userId,
53-
projectName,
5449
companySize,
55-
projectVersion,
5650
},
5751
attemptCount + 1
5852
);
@@ -76,14 +70,7 @@ export async function createOrganization(
7670
},
7771
});
7872

79-
const project = await createProject({
80-
organizationSlug: organization.slug,
81-
name: projectName,
82-
userId,
83-
version: projectVersion,
84-
});
85-
86-
return { ...organization, projects: [project] };
73+
return { ...organization };
8774
}
8875

8976
export async function createEnvironment(

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ export class OrganizationsPresenter {
6464
);
6565
}
6666

67+
if (project.organizationId !== organization.id) {
68+
throw redirect(newProjectPath({ slug: organizationSlug }), request);
69+
}
70+
6771
return { organizations, organization, project };
6872
}
6973

apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@ import { FormTitle } from "~/components/primitives/FormTitle";
1616
import { Input } from "~/components/primitives/Input";
1717
import { InputGroup } from "~/components/primitives/InputGroup";
1818
import { Label } from "~/components/primitives/Label";
19+
import { Paragraph } from "~/components/primitives/Paragraph";
1920
import { Select, SelectItem } from "~/components/primitives/Select";
21+
import { TextLink } from "~/components/primitives/TextLink";
2022
import { prisma } from "~/db.server";
2123
import { useFeatures } from "~/hooks/useFeatures";
24+
import { useUser } from "~/hooks/useUser";
2225
import { redirectWithSuccessMessage } from "~/models/message.server";
2326
import { createProject } from "~/models/project.server";
2427
import { requireUserId } from "~/services/session.server";
2528
import { OrganizationParamsSchema, organizationPath, projectPath } from "~/utils/pathBuilder";
29+
import { RequestV3Access } from "../resources.orgs.$organizationSlug.v3-access";
2630

2731
export async function loader({ params, request }: LoaderFunctionArgs) {
2832
const userId = await requireUserId(request);
@@ -34,10 +38,14 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
3438
id: true,
3539
title: true,
3640
v3Enabled: true,
41+
v2Enabled: true,
42+
hasRequestedV3: true,
3743
_count: {
3844
select: {
3945
projects: {
40-
where: { deletedAt: null },
46+
where: {
47+
deletedAt: null,
48+
},
4149
},
4250
},
4351
},
@@ -57,6 +65,8 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
5765
slug: organizationSlug,
5866
projectsCount: organization._count.projects,
5967
v3Enabled: organization.v3Enabled,
68+
v2Enabled: organization.v2Enabled,
69+
hasRequestedV3: organization.hasRequestedV3,
6070
},
6171
defaultVersion: url.searchParams.get("version") ?? "v2",
6272
});
@@ -98,11 +108,23 @@ export const action: ActionFunction = async ({ request, params }) => {
98108
};
99109

100110
export default function NewOrganizationPage() {
101-
const { organization, defaultVersion } = useTypedLoaderData<typeof loader>();
111+
const { organization } = useTypedLoaderData<typeof loader>();
102112
const lastSubmission = useActionData();
103-
const { v3Enabled } = useFeatures();
113+
const { v3Enabled, isManagedCloud } = useFeatures();
104114

105115
const canCreateV3Projects = organization.v3Enabled && v3Enabled;
116+
const canCreateV2Projects = organization.v2Enabled || !isManagedCloud;
117+
const canCreateProjects = canCreateV2Projects || canCreateV3Projects;
118+
119+
if (!canCreateProjects) {
120+
return (
121+
<RequestV3Access
122+
hasRequestedV3={organization.hasRequestedV3}
123+
organizationSlug={organization.slug}
124+
projectsCount={organization.projectsCount}
125+
/>
126+
);
127+
}
106128

107129
const [form, { projectName, projectVersion }] = useForm({
108130
id: "create-project",
@@ -119,7 +141,7 @@ export default function NewOrganizationPage() {
119141
<FormTitle
120142
LeadingIcon="folder"
121143
title="Create a new project"
122-
description={`This will create a new project in your "${organization.title}" organization. `}
144+
description={`This will create a new project in your "${organization.title}" organization.`}
123145
/>
124146
<Form method="post" {...form.props}>
125147
{organization.projectsCount === 0 && (
@@ -138,7 +160,7 @@ export default function NewOrganizationPage() {
138160
/>
139161
<FormError id={projectName.errorId}>{projectName.error}</FormError>
140162
</InputGroup>
141-
{canCreateV3Projects ? (
163+
{canCreateV2Projects && canCreateV3Projects ? (
142164
<InputGroup>
143165
<Label htmlFor={projectVersion.id}>Project version</Label>
144166
<Select
@@ -161,8 +183,16 @@ export default function NewOrganizationPage() {
161183
</Select>
162184
<FormError id={projectVersion.errorId}>{projectVersion.error}</FormError>
163185
</InputGroup>
186+
) : canCreateV3Projects ? (
187+
<>
188+
<Callout variant="info">This will be a v3 project</Callout>
189+
<input {...conform.input(projectVersion, { type: "hidden" })} value={"v3"} />
190+
</>
164191
) : (
165-
<input {...conform.input(projectVersion, { type: "hidden" })} value="v2" />
192+
<>
193+
<Callout variant="info">This will be a v2 project</Callout>
194+
<input {...conform.input(projectVersion, { type: "hidden" })} value={"v2"} />
195+
</>
166196
)}
167197
<FormButtons
168198
confirmButton={

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

Lines changed: 4 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,14 @@ import { Input } from "~/components/primitives/Input";
1717
import { InputGroup } from "~/components/primitives/InputGroup";
1818
import { Label } from "~/components/primitives/Label";
1919
import { RadioGroupItem } from "~/components/primitives/RadioButton";
20-
import { Select, SelectItem } from "~/components/primitives/Select";
21-
import { featuresForRequest } from "~/features.server";
2220
import { useFeatures } from "~/hooks/useFeatures";
2321
import { createOrganization } from "~/models/organization.server";
2422
import { NewOrganizationPresenter } from "~/presenters/NewOrganizationPresenter.server";
25-
import { commitCurrentProjectSession, setCurrentProjectId } from "~/services/currentProject.server";
2623
import { requireUserId } from "~/services/session.server";
27-
import { projectPath, rootPath, selectPlanPath } from "~/utils/pathBuilder";
24+
import { organizationPath, rootPath } from "~/utils/pathBuilder";
2825

2926
const schema = z.object({
3027
orgName: z.string().min(3).max(50),
31-
projectName: z.string().min(3).max(50),
32-
projectVersion: z.enum(["v2", "v3"]),
3328
companySize: z.string().optional(),
3429
});
3530

@@ -57,29 +52,10 @@ export const action: ActionFunction = async ({ request }) => {
5752
const organization = await createOrganization({
5853
title: submission.value.orgName,
5954
userId,
60-
projectName: submission.value.projectName,
6155
companySize: submission.value.companySize ?? null,
62-
projectVersion: submission.value.projectVersion,
6356
});
6457

65-
const project = organization.projects[0];
66-
const session = await setCurrentProjectId(project.id, request);
67-
68-
const { isManagedCloud } = featuresForRequest(request);
69-
70-
const headers = {
71-
"Set-Cookie": await commitCurrentProjectSession(session),
72-
};
73-
74-
if (isManagedCloud && submission.value.projectVersion === "v2") {
75-
return redirect(selectPlanPath(organization), {
76-
headers,
77-
});
78-
}
79-
80-
return redirect(projectPath(organization, project), {
81-
headers,
82-
});
58+
return redirect(organizationPath(organization));
8359
} catch (error: any) {
8460
return json({ errors: { body: error.message } }, { status: 400 });
8561
}
@@ -91,10 +67,7 @@ export default function NewOrganizationPage() {
9167
const { isManagedCloud } = useFeatures();
9268
const navigation = useNavigation();
9369

94-
//this is temporary whilst v3 is invite-only. Switch to the useFeatures value when v3 is generally available.
95-
const v3Enabled = false;
96-
97-
const [form, { orgName, projectName, projectVersion }] = useForm({
70+
const [form, { orgName }] = useForm({
9871
id: "create-organization",
9972
// TODO: type this
10073
lastSubmission: lastSubmission as any,
@@ -123,45 +96,9 @@ export default function NewOrganizationPage() {
12396
<Hint>E.g. your company name or your workspace name.</Hint>
12497
<FormError id={orgName.errorId}>{orgName.error}</FormError>
12598
</InputGroup>
126-
<InputGroup>
127-
<Label htmlFor={projectName.id}>Project name</Label>
128-
<Input
129-
{...conform.input(projectName, { type: "text" })}
130-
placeholder="Your Project name"
131-
icon="folder"
132-
/>
133-
<Hint>Your Jobs will live inside this Project.</Hint>
134-
<FormError id={projectName.errorId}>{projectName.error}</FormError>
135-
</InputGroup>
136-
{v3Enabled ? (
137-
<InputGroup>
138-
<Label htmlFor={projectVersion.id}>Project version</Label>
139-
<Select
140-
{...conform.select(projectVersion)}
141-
defaultValue={undefined}
142-
variant="tertiary/medium"
143-
placeholder="Select version"
144-
dropdownIcon
145-
text={(value) => {
146-
switch (value) {
147-
case "v2":
148-
return "Version 2";
149-
case "v3":
150-
return "Version 3";
151-
}
152-
}}
153-
>
154-
<SelectItem value="v2">Version 2</SelectItem>
155-
<SelectItem value="v3">Version 3 (Developer Preview)</SelectItem>
156-
</Select>
157-
<FormError id={projectVersion.errorId}>{projectVersion.error}</FormError>
158-
</InputGroup>
159-
) : (
160-
<input {...conform.input(projectVersion, { type: "hidden" })} value="v2" />
161-
)}
16299
{isManagedCloud && (
163100
<InputGroup>
164-
<Label htmlFor={projectName.id}>Number of employees</Label>
101+
<Label htmlFor={"companySize"}>Number of employees</Label>
165102
<RadioGroup name="companySize" className="flex items-center justify-between gap-2">
166103
<RadioGroupItem
167104
id="employees-1-5"

0 commit comments

Comments
 (0)