## What does this PR do? - Redesigns the organization onboarding flow by merging the brand and details pages - Improves the organization details page with a scrollable interface and visual previews - Adds a new organization-specific browser preview component ## Visual Demo (For contributors especially) #### Image Demo: - The PR replaces the separate brand page with an integrated details page that includes logo and banner uploads - The new organization browser view shows a preview of the organization profile with the selected branding ## Mandatory Tasks (DO NOT REMOVE) - [ ] I have self-reviewed the code. - [ ] I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. If N/A, write N/A here and check the checkbox. - [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works. ## How should this be tested? - Go through the organization onboarding flow - Test uploading logos and banners - Verify that the organization browser preview updates in real-time with the form inputs - Confirm that the form validation works correctly for organization name and slug - Check that the scrollable interface works properly with fade effects at top and bottom ## Checklist - I have read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md) - My code follows the style guidelines of this project - I have commented my code, particularly in hard-to-understand areas - I have checked if my changes generate no new warnings <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Split organization brand from the details step and added live previews for organizations and teams. Revamped org/team invites with reusable components, a dedicated email-invite page, and a CSV upload modal. - **New Features** - Separate Brand step with logo, banner, and color; instant org preview via OnboardingOrganizationBrowserView. - Teams browser preview added; invites include email substep (/onboarding/organization/invite/email, /onboarding/teams/invite/email), CSV upload (template + parsing), and Google Workspace (behind flag). - Shared components (EmailInviteForm, InviteOptions, RoleSelector) used across org and team invites. - **Refactors** - Updated org flow: Details → Brand → Teams → Invites; OnboardingLayout now supports dynamic step counts (org=4, team=3, personal=2). - UI polish (OnboardingCard header padding) and org-specific previews now replace generic views across details/brand/invites/teams; ensured org welcome modal takes precedence over personal. <sup>Written for commit d9b55c0b5505aa0d4ca1c4298a513bcd90606915. Summary will update automatically on new commits.</sup> <!-- End of auto-generated description by cubic. -->
124 lines
3.7 KiB
TypeScript
124 lines
3.7 KiB
TypeScript
"use client";
|
|
|
|
import { useFormContext } from "react-hook-form";
|
|
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
import { Button } from "@calcom/ui/components/button";
|
|
import { Label, TextField, Select } from "@calcom/ui/components/form";
|
|
import { Icon } from "@calcom/ui/components/icon";
|
|
|
|
import type { InviteRole } from "../store/onboarding-store";
|
|
|
|
type BaseInviteFormData = {
|
|
email: string;
|
|
role: InviteRole;
|
|
};
|
|
|
|
type InviteFormDataWithOptionalTeam = BaseInviteFormData & {
|
|
team?: string;
|
|
};
|
|
|
|
type InviteFormDataWithRequiredTeam = BaseInviteFormData & {
|
|
team: string;
|
|
};
|
|
|
|
type InviteFormData = InviteFormDataWithOptionalTeam | InviteFormDataWithRequiredTeam;
|
|
|
|
type EmailInviteFormProps = {
|
|
fields: Array<{ id: string }>;
|
|
append: (value: InviteFormData) => void;
|
|
remove: (index: number) => void;
|
|
defaultRole: InviteRole;
|
|
showTeamSelect?: boolean;
|
|
teams?: Array<{ value: string; label: string }>;
|
|
emailPlaceholder?: string;
|
|
};
|
|
|
|
export function EmailInviteForm({
|
|
fields,
|
|
append,
|
|
remove,
|
|
defaultRole,
|
|
showTeamSelect = false,
|
|
teams = [],
|
|
emailPlaceholder,
|
|
}: EmailInviteFormProps) {
|
|
const { t } = useLocale();
|
|
const { register, watch, setValue } = useFormContext();
|
|
|
|
return (
|
|
<div className="flex w-full flex-col gap-4">
|
|
<div className="flex flex-col gap-2">
|
|
{showTeamSelect ? (
|
|
<div className="grid grid-cols-2">
|
|
<Label className="text-emphasis mb-0 text-sm font-medium" htmlFor="invites.0.email">
|
|
{t("email")}
|
|
</Label>
|
|
<Label className="text-emphasis mb-0 text-sm font-medium" htmlFor="invites.0.team">
|
|
{t("team")}
|
|
</Label>
|
|
</div>
|
|
) : (
|
|
<Label className="text-emphasis text-sm font-medium">{t("email")}</Label>
|
|
)}
|
|
|
|
<div
|
|
className={
|
|
showTeamSelect ? "flex flex-col gap-2" : "scroll-bar flex max-h-72 flex-col gap-1 overflow-y-auto"
|
|
}>
|
|
{fields.map((field, index) => (
|
|
<div key={field.id} className="flex items-start gap-0.5 p-0.5">
|
|
<div className={showTeamSelect ? "grid flex-1 items-start gap-2 md:grid-cols-2" : "flex-1"}>
|
|
<TextField
|
|
labelSrOnly
|
|
{...register(`invites.${index}.email`)}
|
|
placeholder={emailPlaceholder || `rick@cal.com`}
|
|
type="email"
|
|
size="sm"
|
|
/>
|
|
{showTeamSelect && (
|
|
<Select
|
|
size="sm"
|
|
options={teams}
|
|
value={teams.find((t) => t.value === watch(`invites.${index}.team`))}
|
|
onChange={(option) => {
|
|
if (option) {
|
|
setValue(`invites.${index}.team`, option.value);
|
|
}
|
|
}}
|
|
placeholder={t("select_team")}
|
|
/>
|
|
)}
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
color="minimal"
|
|
variant="icon"
|
|
size="sm"
|
|
className="h-7 w-7"
|
|
disabled={fields.length === 1}
|
|
onClick={() => remove(index)}>
|
|
<Icon name="x" className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<Button
|
|
type="button"
|
|
color="secondary"
|
|
size="sm"
|
|
StartIcon="plus"
|
|
className={showTeamSelect ? "mt-2 w-fit" : "w-fit"}
|
|
onClick={() =>
|
|
append(
|
|
showTeamSelect ? { email: "", team: "", role: defaultRole } : { email: "", role: defaultRole }
|
|
)
|
|
}>
|
|
{t("add")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|