improve onboarding flow
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { ButtonContainer } from '@/components/button-container';
|
||||
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||
import { Alert } from '@/components/ui/alert';
|
||||
import { LinkButton } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { KeyIcon, LockIcon } from 'lucide-react';
|
||||
|
||||
import type { IServiceProjectWithClients } from '@openpanel/db';
|
||||
|
||||
@@ -32,6 +37,18 @@ const Connect = ({ project }: Props) => {
|
||||
</OnboardingDescription>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-4 rounded-xl border bg-slate-100 p-4 md:p-6">
|
||||
<div className="flex items-center gap-2 text-2xl capitalize">
|
||||
<LockIcon />
|
||||
Credentials
|
||||
</div>
|
||||
<InputWithLabel label="Client ID" disabled value={client.id} />
|
||||
<InputWithLabel
|
||||
label="Client Secret"
|
||||
disabled
|
||||
value={client.secret ?? 'unknown'}
|
||||
/>
|
||||
</div>
|
||||
{project.types.map((type) => {
|
||||
const Component = {
|
||||
website: ConnectWeb,
|
||||
|
||||
@@ -6,21 +6,33 @@ import { ButtonContainer } from '@/components/button-container';
|
||||
import { CheckboxItem } from '@/components/forms/checkbox-item';
|
||||
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { api, handleError } from '@/trpc/client';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { MonitorIcon, ServerIcon, SmartphoneIcon } from 'lucide-react';
|
||||
import {
|
||||
Building,
|
||||
MonitorIcon,
|
||||
ServerIcon,
|
||||
SmartphoneIcon,
|
||||
} from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import type { IServiceOrganization } from '@openpanel/db';
|
||||
import { zOnboardingProject } from '@openpanel/validation';
|
||||
|
||||
import OnboardingLayout, { OnboardingDescription } from '../onboarding-layout';
|
||||
|
||||
type IForm = z.infer<typeof zOnboardingProject>;
|
||||
|
||||
const Tracking = () => {
|
||||
const Tracking = ({
|
||||
organizations,
|
||||
}: {
|
||||
organizations: IServiceOrganization[];
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const mutation = api.onboarding.project.useMutation({
|
||||
onError: handleError,
|
||||
@@ -81,13 +93,43 @@ const Tracking = () => {
|
||||
}
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<InputWithLabel
|
||||
label="Workspace name"
|
||||
info="This is the name of your workspace. It can be anything you like."
|
||||
placeholder="Eg. The Music Company"
|
||||
error={form.formState.errors.organization?.message}
|
||||
{...form.register('organization')}
|
||||
/>
|
||||
{organizations.length > 0 ? (
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="organizationSlug"
|
||||
render={({ field, formState }) => {
|
||||
return (
|
||||
<div>
|
||||
<Label>Workspace name</Label>
|
||||
<Combobox
|
||||
className="w-full"
|
||||
placeholder="Select workspace"
|
||||
icon={Building}
|
||||
error={formState.errors.organizationSlug?.message}
|
||||
value={field.value}
|
||||
items={
|
||||
organizations
|
||||
.filter((item) => item.slug)
|
||||
.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.slug,
|
||||
})) ?? []
|
||||
}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<InputWithLabel
|
||||
label="Workspace name"
|
||||
info="This is the name of your workspace. It can be anything you like."
|
||||
placeholder="Eg. The Music Company"
|
||||
error={form.formState.errors.organization?.message}
|
||||
{...form.register('organization')}
|
||||
/>
|
||||
)}
|
||||
<InputWithLabel
|
||||
label="Project name"
|
||||
placeholder="Eg. The Music App"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { getCurrentOrganizations } from '@openpanel/db';
|
||||
|
||||
import OnboardingTracking from './onboarding-tracking';
|
||||
|
||||
const Tracking = () => {
|
||||
return <OnboardingTracking />;
|
||||
const Tracking = async () => {
|
||||
return <OnboardingTracking organizations={await getCurrentOrganizations()} />;
|
||||
};
|
||||
|
||||
export default Tracking;
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface ComboboxProps<T> {
|
||||
label?: string;
|
||||
align?: 'start' | 'end' | 'center';
|
||||
portal?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export type ExtendedComboboxProps<T> = Omit<
|
||||
@@ -59,6 +60,7 @@ export function Combobox<T extends string>({
|
||||
size,
|
||||
align = 'start',
|
||||
portal,
|
||||
error,
|
||||
}: ComboboxProps<T>) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [search, setSearch] = React.useState('');
|
||||
@@ -77,7 +79,11 @@ export function Combobox<T extends string>({
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn('justify-between', className)}
|
||||
className={cn(
|
||||
'justify-between',
|
||||
!!error && 'border-destructive',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 items-center">
|
||||
{Icon ? <Icon size={16} className="mr-2 shrink-0" /> : null}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import { clerkClient } from '@clerk/fastify';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { hashPassword, slug, stripTrailingSlash } from '@openpanel/common';
|
||||
import { db, getId } from '@openpanel/db';
|
||||
@@ -8,6 +9,27 @@ import { zOnboardingProject } from '@openpanel/validation';
|
||||
|
||||
import { createTRPCRouter, protectedProcedure } from '../trpc';
|
||||
|
||||
async function createOrGetOrganization(
|
||||
input: z.infer<typeof zOnboardingProject>,
|
||||
userId: string
|
||||
) {
|
||||
if (input.organizationSlug) {
|
||||
return await clerkClient.organizations.getOrganization({
|
||||
slug: input.organizationSlug,
|
||||
});
|
||||
}
|
||||
|
||||
if (input.organization) {
|
||||
return await clerkClient.organizations.createOrganization({
|
||||
name: input.organization,
|
||||
slug: slug(input.organization),
|
||||
createdBy: userId,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export const onboardingRouter = createTRPCRouter({
|
||||
project: protectedProcedure
|
||||
.input(zOnboardingProject)
|
||||
@@ -17,13 +39,12 @@ export const onboardingRouter = createTRPCRouter({
|
||||
if (input.app) types.push('app');
|
||||
if (input.backend) types.push('backend');
|
||||
|
||||
const organization = await clerkClient.organizations.createOrganization({
|
||||
name: input.organization,
|
||||
slug: slug(input.organization),
|
||||
createdBy: ctx.session.userId,
|
||||
});
|
||||
const organization = await createOrGetOrganization(
|
||||
input,
|
||||
ctx.session.userId
|
||||
);
|
||||
|
||||
if (!organization.slug) {
|
||||
if (!organization?.slug) {
|
||||
throw new Error('Organization slug is missing');
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,8 @@ export const zCreateReference = z.object({
|
||||
|
||||
export const zOnboardingProject = z
|
||||
.object({
|
||||
organization: z.string().min(3),
|
||||
organization: z.string().optional(),
|
||||
organizationSlug: z.string().optional(),
|
||||
project: z.string().min(3),
|
||||
domain: z.string().url().or(z.literal('').or(z.null())),
|
||||
website: z.boolean(),
|
||||
@@ -108,6 +109,19 @@ export const zOnboardingProject = z
|
||||
backend: z.boolean(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (!data.organization && !data.organizationSlug) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'Organization is required',
|
||||
path: ['organization'],
|
||||
});
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'Organization is required',
|
||||
path: ['organizationSlug'],
|
||||
});
|
||||
}
|
||||
|
||||
if (data.website && !data.domain) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
|
||||
Reference in New Issue
Block a user