diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/onboarding-connect.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/onboarding-connect.tsx index 85e1c657..90b54e50 100644 --- a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/onboarding-connect.tsx +++ b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/onboarding-connect.tsx @@ -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) => { } > +
+
+ + Credentials +
+ + +
{project.types.map((type) => { const Component = { website: ConnectWeb, diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/onboarding-tracking.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/onboarding-tracking.tsx index 17cfe446..7c5d210c 100644 --- a/apps/dashboard/src/app/(onboarding)/onboarding/onboarding-tracking.tsx +++ b/apps/dashboard/src/app/(onboarding)/onboarding/onboarding-tracking.tsx @@ -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; -const Tracking = () => { +const Tracking = ({ + organizations, +}: { + organizations: IServiceOrganization[]; +}) => { const router = useRouter(); const mutation = api.onboarding.project.useMutation({ onError: handleError, @@ -81,13 +93,43 @@ const Tracking = () => { } >
- + {organizations.length > 0 ? ( + { + return ( +
+ + item.slug) + .map((item) => ({ + label: item.name, + value: item.slug, + })) ?? [] + } + onChange={field.onChange} + /> +
+ ); + }} + /> + ) : ( + + )} { - return ; +const Tracking = async () => { + return ; }; export default Tracking; diff --git a/apps/dashboard/src/components/ui/combobox.tsx b/apps/dashboard/src/components/ui/combobox.tsx index e259c8b0..0cb72ae6 100644 --- a/apps/dashboard/src/components/ui/combobox.tsx +++ b/apps/dashboard/src/components/ui/combobox.tsx @@ -37,6 +37,7 @@ export interface ComboboxProps { label?: string; align?: 'start' | 'end' | 'center'; portal?: boolean; + error?: string; } export type ExtendedComboboxProps = Omit< @@ -59,6 +60,7 @@ export function Combobox({ size, align = 'start', portal, + error, }: ComboboxProps) { const [open, setOpen] = React.useState(false); const [search, setSearch] = React.useState(''); @@ -77,7 +79,11 @@ export function Combobox({ variant="outline" role="combobox" aria-expanded={open} - className={cn('justify-between', className)} + className={cn( + 'justify-between', + !!error && 'border-destructive', + className + )} >
{Icon ? : null} diff --git a/packages/trpc/src/routers/onboarding.ts b/packages/trpc/src/routers/onboarding.ts index 042f91a0..5a8c9135 100644 --- a/packages/trpc/src/routers/onboarding.ts +++ b/packages/trpc/src/routers/onboarding.ts @@ -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, + 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'); } diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 0f49491a..029023c4 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -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',