Admin Wizard Choose License (#6574)

* Implementation

* i18n

* More i18n

* extracted i18n, needs api to get most recent price, added hint: update later

* Fixing i18n var

* Fix booking filters not working for admin (#6576)

* fix: react-select overflow issue in some modals. (#6587)

* feat: add a disable overflow prop

* feat: use the disable overflow prop

* Tailwind Merge (#6596)

* Tailwind Merge

* Fix merge classNames

* [CAL-808] /availability/single - UI issue on buttons beside time inputs (#6561)

* [CAL-808] /availability/single - UI issue on buttons beside time inputs

* Update apps/web/public/static/locales/en/common.json

* Update packages/features/schedules/components/Schedule.tsx

* create new translation for tooltip

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>

* Bye bye submodules (#6585)

* WIP

* Uses ssh instead

* Update .gitignore

* Update .gitignore

* Update Makefile

* Update git-setup.sh

* Update git-setup.sh

* Replaced Makefile with bash script

* Update package.json

* fix: show button on empty string (#6601)

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: add delete in dropdown (#6599)

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* Update README.md

* Update README.md

* Changed a neutral- classes to gray (#6603)

* Changed a neutral- classes to gray

* Changed all border-1 to border

* Update package.json

* Test fixes

* Yarn lock fixes

* Fix string equality check in git-setup.sh

* [CAL-811] Avatar icon not redirecting user back to the main page (#6586)

* Remove cursor-pointer, remove old Avatar* files

* Fixed styling for checkedSelect + some cleanup

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Harsh/add member invite (#6598)

Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: root <harsh.singh@gocomet.com>

* Regenerated lockfile without upgrade (#6610)

* fix: remove annoying outline when <Button /> clicked (#6537)

* fix: remove annoying outline when <Button /> clicked

* Delete yarn.lock

* remove 1 on 1 icon (#6609)

* removed 1-on-1 badge

* changed user to users for group events

* fix: case-sensitivity in apps path (#6552)

* fix: lowercase slug

* fix: make fallback blocking

* Fix FAB (#6611)

* feat: add LocationSelect component (#6571)

* feat: add LocationSelect component

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: type error

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* chore: type error

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* Update booking filters design  (#6543)

* Update booking filters

* Add filter on YOUR bookings

* Fix pending members showing up in list

* Reduce the avatar size to 'sm' for now

* Bugfix/dropdown menu trigger as child remove class names (#6614)

* Fix UsernameTextfield to take right height

* Remove className side-effect

* Incorrect resolution version fixed

* Converted mobile DropdownMenuTrigger styles into Button

* v2.5.3

* fix: use items-center (#6618)

* fix tooltip and modal stacking issues (#6491)

* fix tooltip and modal stacking issues

* use z-index in larger screens and less

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Temporary fix (#6626)

* Fix Ga4 tracking (#6630)

* generic <UpgradeScreen> component (#6594)

* first attempt of <UpgradeScreen>

* changes to icons

* reverted changes back to initial state, needs fix: teams not showing

* WIP

* Fix weird reactnode error

* Fix loading text

* added upgradeTip to routing forms

* icon colors

* create and use hook to check if user has team plan

* use useTeamPlan for upgradeTeamsBadge

* replace huge svg with compressed jpeg

* responsive fixes

* Update packages/ui/components/badge/UpgradeTeamsBadge.tsx

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>

* Give team plan features to E2E tests

* Allow option to make a user part of team int ests

* Remove flash of paywall for team user

* Add team user for typeform tests as well

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>

* Removing env var to rely on db

* Restoring i18n keys, set loading moved

* Fixing tailwind-preset glob

* Wizard width fix for md+ screens

* Converting licenses options to radix radio

* Applying feedback + other tweaks

* Reverting this, not this PR related

* Unneeded code removal

* Reverting unneeded style change

* Applying feedback

* Removing licenseType

* Upgrades typescript

* Update yarn lock

* Typings

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712)

* Hotfix: ping,riverside,whereby and around not showing up in list (#6712) (#6713)

* Adds deployment settings to DB (#6706)

* WIP

* Adds DeploymentTheme

* Add missing migrations

* Adds client extensions for deployment

* Cleanup

* Delete migration.sql

* Relying on both, env var and new model

* Restoring env example doc for backward compat

* Maximum call stack size exceeded fix?

* Revert upgrade

* Update index.ts

* Delete index.ts

* Not exposing license key, fixed radio behavior

* Covering undefined env var

* Self contained checkLicense

* Feedback

* Moar feedback

* Feedback

* Feedback

* Feedback

* Cleanup

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Nafees Nazik <84864519+G3root@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Harsh Singh <51085015+harshsinghatz@users.noreply.github.com>
Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: root <harsh.singh@gocomet.com>
Co-authored-by: Luis Cadillo <luiscaf3r@gmail.com>
Co-authored-by: Mohammed Cherfaoui <hi@cherfaoui.dev>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
This commit is contained in:
Leo Giovanetti 2023-02-07 21:23:42 -03:00 committed by GitHub
parent 8c150264f4
commit a9af2fb255
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 584 additions and 136 deletions

View File

@ -1,20 +1,21 @@
# ********** INDEX **********
#
# - LICENSE
# - LICENSE (DEPRECATED)
# - DATABASE
# - SHARED
# - NEXTAUTH
# - E-MAIL SETTINGS
# - LICENSE *************************************************************************************************
# https://github.com/calendso/calendso/blob/main/LICENSE
# - LICENSE (DEPRECATED) ************************************************************************************
# https://github.com/calcom/cal.com/blob/main/LICENSE
#
# Summary of terms:
# - The codebase has to stay open source, whether it was modified or not
# - You can not repackage or sell the codebase
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
#
# To enable enterprise-only features, fill your license key in here.
# To enable enterprise-only features, as an admin, go to /auth/setup to select your license and follow
# instructions. This environment variable is deprecated although still supported for backward compatibility.
# @see https://console.cal.com
CALCOM_LICENSE_KEY=
# ***********************************************************************************************************

View File

@ -1,17 +1,32 @@
import { zodResolver } from "@hookform/resolvers/zod";
import classNames from "classnames";
import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import React from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import * as z from "zod";
import { isPasswordValid } from "@calcom/lib/auth";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmailField, Label, PasswordField, TextField } from "@calcom/ui";
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
import { FiUserCheck } from "@calcom/ui/components/icon";
const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
const router = useRouter();
export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser> & { userCount: number }) => {
const { t } = useLocale();
if (props.userCount > 0)
return (
<form id="wizard-step-1" name="wizard-step-1" className="space-y-4" onSubmit={props.onSuccess}>
<EmptyScreen
Icon={FiUserCheck}
headline={t("admin_user_created")}
description={t("admin_user_created_description")}
/>
</form>
);
return <AdminUser {...props} />;
};
export const AdminUser = (props: { onSubmit: () => void; onError: () => void; onSuccess: () => void }) => {
const { t } = useLocale();
const formSchema = z.object({
@ -45,11 +60,11 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
});
const onError = () => {
props.setIsLoading(false);
props.onError();
};
const onSubmit = formMethods.handleSubmit(async (data: z.infer<typeof formSchema>) => {
props.setIsLoading(true);
props.onSubmit();
const response = await fetch("/api/auth/setup", {
method: "POST",
body: JSON.stringify({
@ -69,9 +84,9 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
email: data.email_address.toLowerCase(),
password: data.password,
});
router.replace(`/auth/setup?step=2&category=calendar`);
props.onSuccess();
} else {
router.replace("/auth/setup");
props.onError();
}
}, onError);
@ -186,5 +201,3 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
</FormProvider>
);
};
export default SetupFormStep1;

View File

@ -0,0 +1,76 @@
import * as RadioGroup from "@radix-ui/react-radio-group";
import classNames from "classnames";
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
const ENTERPRISE_BOOKING_FEE = "$99"; // TODO: get this from a new API endpoint
const ChooseLicense = (
props: {
value: string;
onChange: (value: string) => void;
onSubmit: (value: string) => void;
} & Omit<JSX.IntrinsicElements["form"], "onSubmit" | "onChange">
) => {
const { value: initialValue = "FREE", onChange, onSubmit, ...rest } = props;
const [value, setValue] = useState(initialValue);
const { t } = useLocale();
return (
<form
{...rest}
className="space-y-4"
onSubmit={(e) => {
e.preventDefault();
onSubmit(value);
}}>
<RadioGroup.Root
defaultValue={initialValue}
value={value}
aria-label={t("choose_a_license")}
className="grid grid-rows-2 gap-4 md:grid-cols-2 md:grid-rows-1"
onValueChange={(value) => {
onChange(value);
setValue(value);
}}>
<RadioGroup.Item value="FREE">
<div
className={classNames(
"cursor-pointer space-y-2 rounded-md border bg-white p-4 hover:border-black",
value === "FREE" && "ring-2 ring-black"
)}>
<h2 className="font-cal text-xl text-black">{t("agplv3_license")}</h2>
<p className="font-medium text-green-800">{t("free_license_fee")}</p>
<p className="text-gray-500">{t("forever_open_and_free")}</p>
<ul className="ml-4 list-disc text-left text-xs text-gray-500">
<li>{t("required_to_keep_your_code_open_source")}</li>
<li>{t("cannot_repackage_and_resell")}</li>
<li>{t("no_enterprise_features")}</li>
</ul>
</div>
</RadioGroup.Item>
<RadioGroup.Item value="EE">
<div
className={classNames(
"cursor-pointer space-y-2 rounded-md border bg-white p-4 hover:border-black",
value === "EE" && "ring-2 ring-black"
)}>
<h2 className="font-cal text-xl text-black">{t("ee_enterprise_license")}</h2>
<p className="font-medium text-green-800">
{t("enterprise_booking_fee", { enterprise_booking_fee: ENTERPRISE_BOOKING_FEE })}
</p>
<p className="text-gray-500">{t("enterprise_license_includes")}</p>
<ul className="ml-4 list-disc text-left text-xs text-gray-500">
<li>{t("no_need_to_keep_your_code_open_source")}</li>
<li>{t("repackage_rebrand_resell")}</li>
<li>{t("a_vast_suite_of_enterprise_features")}</li>
</ul>
</div>
</RadioGroup.Item>
</RadioGroup.Root>
</form>
);
};
export default ChooseLicense;

View File

@ -0,0 +1,144 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { noop } from "lodash";
import { useCallback, useState } from "react";
import { Controller, FormProvider, useForm, useFormState } from "react-hook-form";
import * as z from "zod";
import { classNames } from "@calcom/lib";
import { CONSOLE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterInputs, RouterOutputs, trpc } from "@calcom/trpc/react";
import { Button, TextField } from "@calcom/ui";
import { FiCheck, FiExternalLink, FiLoader } from "@calcom/ui/components/icon";
type EnterpriseLicenseFormValues = {
licenseKey: string;
};
const makeSchemaLicenseKey = (args: { callback: (valid: boolean) => void; onSuccessValidate: () => void }) =>
z.object({
licenseKey: z
.string()
.uuid({
message: "License key must follow UUID format: 8-4-4-4-12",
})
.superRefine(async (data, ctx) => {
const parse = z.string().uuid().safeParse(data);
if (parse.success) {
args.callback(true);
const response = await fetch(`${CONSOLE_URL}/api/license?key=${data}`);
args.callback(false);
const json = await response.json();
if (!json.valid) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `License key ${json.message.toLowerCase()}`,
});
} else {
args.onSuccessValidate();
}
}
}),
});
const EnterpriseLicense = (
props: {
licenseKey?: string;
initialValue?: Partial<EnterpriseLicenseFormValues>;
onSuccessValidate: () => void;
onSubmit: (value: EnterpriseLicenseFormValues) => void;
onSuccess?: (
data: RouterOutputs["viewer"]["deploymentSetup"]["update"],
variables: RouterInputs["viewer"]["deploymentSetup"]["update"]
) => void;
} & Omit<JSX.IntrinsicElements["form"], "onSubmit">
) => {
const { onSubmit, onSuccess = noop, onSuccessValidate = noop, ...rest } = props;
const { t } = useLocale();
const [checkLicenseLoading, setCheckLicenseLoading] = useState(false);
const mutation = trpc.viewer.deploymentSetup.update.useMutation({
onSuccess,
});
const schemaLicenseKey = useCallback(
() =>
makeSchemaLicenseKey({
callback: setCheckLicenseLoading,
onSuccessValidate,
}),
[setCheckLicenseLoading, onSuccessValidate]
);
const formMethods = useForm<EnterpriseLicenseFormValues>({
defaultValues: {
licenseKey: props.licenseKey || "",
},
resolver: zodResolver(schemaLicenseKey()),
});
const handleSubmit = formMethods.handleSubmit((values) => {
onSubmit(values);
setCheckLicenseLoading(false);
mutation.mutate(values);
});
const { isDirty, errors } = useFormState(formMethods);
return (
<FormProvider {...formMethods}>
<form {...rest} className="space-y-4 rounded-md bg-white px-8 py-10" onSubmit={handleSubmit}>
<div>
<Button
className="w-full justify-center text-lg"
EndIcon={FiExternalLink}
href="https://console.cal.com"
target="_blank">
{t("purchase_license")}
</Button>
<div className="relative flex justify-center">
<hr className="my-8 w-full border-[1.5px] border-gray-200" />
<span className="absolute mt-[22px] bg-white px-3.5 text-sm">OR</span>
</div>
{t("already_have_key")}
<Controller
name="licenseKey"
control={formMethods.control}
render={({ field: { onBlur, onChange, value } }) => (
<TextField
{...formMethods.register("licenseKey")}
className={classNames(
"mb-0 group-hover:border-gray-400",
(checkLicenseLoading || (errors.licenseKey === undefined && isDirty)) && "border-r-0"
)}
placeholder="xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx"
labelSrOnly={true}
value={value}
addOnFilled={false}
addOnClassname={classNames(
"hover:border-gray-300",
errors.licenseKey === undefined && isDirty && "group-hover:border-gray-400"
)}
addOnSuffix={
checkLicenseLoading ? (
<FiLoader className="h-5 w-5 animate-spin" />
) : errors.licenseKey === undefined && isDirty ? (
<FiCheck className="h-5 w-5 text-green-700" />
) : undefined
}
color={errors.licenseKey ? "warn" : ""}
onBlur={onBlur}
onChange={async (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
formMethods.setValue("licenseKey", e.target.value);
await formMethods.trigger("licenseKey");
}}
/>
)}
/>
</div>
</form>
</FormProvider>
);
};
export default EnterpriseLicense;

View File

@ -1,20 +1,26 @@
import { useRouter } from "next/router";
import { Dispatch, SetStateAction } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { FiCheck } from "@calcom/ui/components/icon";
const StepDone = () => {
const StepDone = (props: {
currentStep: number;
nextStepPath: string;
setIsLoading: Dispatch<SetStateAction<boolean>>;
}) => {
const router = useRouter();
const { t } = useLocale();
return (
<form
id="wizard-step-1"
name="wizard-step-1"
className="space-y-4"
id={`wizard-step-${props.currentStep}`}
name={`wizard-step-${props.currentStep}`}
className="flex justify-center space-y-4"
onSubmit={(e) => {
props.setIsLoading(true);
e.preventDefault();
router.replace(`/auth/setup?step=2&category=calendar`);
router.replace(props.nextStepPath);
}}>
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">

View File

@ -360,7 +360,7 @@ export default NextAuth({
return token;
},
async session({ session, token }) {
const hasValidLicense = await checkLicense(process.env.CALCOM_LICENSE_KEY || "");
const hasValidLicense = await checkLicense(prisma);
const calendsoSession: Session = {
...session,
hasValidLicense,

View File

@ -1,40 +1,125 @@
import { UserPermissionRole } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import { useState } from "react";
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
import { getDeploymentKey } from "@calcom/features/ee/deployment/lib/getDeploymentKey";
import { getSession } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { WizardForm } from "@calcom/ui";
import { Meta, WizardForm } from "@calcom/ui";
import SetupFormStep1 from "./steps/SetupFormStep1";
import StepDone from "./steps/StepDone";
import { AdminUserContainer as AdminUser } from "@components/setup/AdminUser";
import ChooseLicense from "@components/setup/ChooseLicense";
import EnterpriseLicense from "@components/setup/EnterpriseLicense";
import { ssrInit } from "@server/lib/ssr";
export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
const [isLoadingStep1, setIsLoadingStep1] = useState(false);
const shouldDisable = props.userCount !== 0;
const router = useRouter();
const [value, setValue] = useState(props.isFreeLicense ? "FREE" : "EE");
const isFreeLicense = value === "FREE";
const [isEnabledEE, setIsEnabledEE] = useState(!props.isFreeLicense);
const setStep = (newStep: number) => {
router.replace(`/auth/setup?step=${newStep || 1}`, undefined, { shallow: true });
};
const steps = [
const steps: React.ComponentProps<typeof WizardForm>["steps"] = [
{
title: t("administrator_user"),
description: t("lets_create_first_administrator_user"),
content: shouldDisable ? <StepDone /> : <SetupFormStep1 setIsLoading={setIsLoadingStep1} />,
isLoading: isLoadingStep1,
content: (setIsLoading) => (
<AdminUser
onSubmit={() => {
setIsLoading(true);
}}
onSuccess={() => {
setStep(2);
}}
onError={() => {
setIsLoading(false);
}}
userCount={props.userCount}
/>
),
},
{
title: t("enable_apps"),
description: t("enable_apps_description"),
content: <AdminAppsList baseURL="/auth/setup?step=2" useQueryParam={true} />,
isLoading: false,
title: t("choose_a_license"),
description: t("choose_license_description"),
content: (setIsLoading) => {
return (
<ChooseLicense
id="wizard-step-2"
name="wizard-step-2"
value={value}
onChange={setValue}
onSubmit={() => {
setIsLoading(true);
setStep(3);
}}
/>
);
},
},
];
if (!isFreeLicense) {
steps.push({
title: t("step_enterprise_license"),
description: t("step_enterprise_license_description"),
content: (setIsLoading) => {
const currentStep = 3;
return (
<EnterpriseLicense
id={`wizard-step-${currentStep}`}
name={`wizard-step-${currentStep}`}
onSubmit={() => {
setIsLoading(true);
}}
onSuccess={() => {
setStep(currentStep + 1);
}}
onSuccessValidate={() => {
setIsEnabledEE(true);
}}
/>
);
},
isEnabled: isEnabledEE,
});
}
steps.push({
title: t("enable_apps"),
description: t("enable_apps_description"),
contentClassname: "!pb-0 mb-[-1px]",
content: (setIsLoading) => {
const currentStep = isFreeLicense ? 3 : 4;
return (
<AdminAppsList
classNames={{
form: "mb-4 rounded-md bg-white px-0 pt-0 md:max-w-full",
appCategoryNavigationContainer: " max-h-[400px] overflow-y-auto md:p-4",
verticalTabsItem: "!w-48 md:p-4",
}}
baseURL={`/auth/setup?step=${currentStep}`}
useQueryParam={true}
onSubmit={() => {
setIsLoading(true);
router.replace("/");
}}
/>
);
},
});
return (
<>
<main className="flex items-center bg-gray-100 print:h-full">
<Meta title={t("setup")} description={t("setup_description")} />
<main className="flex items-center bg-gray-100 print:h-full md:h-screen">
<WizardForm
href="/auth/setup"
steps={steps}
@ -42,6 +127,7 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
finishLabel={t("finish")}
prevLabel={t("prev_step")}
stepLabel={(currentStep, maxSteps) => t("current_step_of_total", { currentStep, maxSteps })}
containerClassname="md:w-[800px]"
/>
</main>
</>
@ -49,6 +135,7 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
}
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
const userCount = await prisma.user.count();
const { req } = context;
const session = await getSession({ req });
@ -62,8 +149,30 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
};
}
let deploymentKey = await getDeploymentKey(prisma);
// Check existant CALCOM_LICENSE_KEY env var and acccount for it
if (!!process.env.CALCOM_LICENSE_KEY && !deploymentKey) {
await prisma.deployment.upsert({
where: { id: 1 },
update: {
licenseKey: process.env.CALCOM_LICENSE_KEY,
agreedLicenseAt: new Date(),
},
create: {
licenseKey: process.env.CALCOM_LICENSE_KEY,
agreedLicenseAt: new Date(),
},
});
deploymentKey = await getDeploymentKey(prisma);
}
const isFreeLicense = deploymentKey === "";
return {
props: {
trpcState: ssr.dehydrate(),
isFreeLicense,
userCount,
},
};

View File

@ -105,7 +105,7 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
</p>
))}
</header>
<Steps maxSteps={steps.length} currentStep={currentStepIndex} navigateToStep={goToIndex} />
<Steps maxSteps={steps.length} currentStep={currentStepIndex + 1} navigateToStep={goToIndex} />
</div>
<StepCard>
{currentStep === "user-settings" && <UserSettings user={user} nextStep={() => goToIndex(1)} />}

View File

@ -515,6 +515,8 @@
"admin": "Admin",
"administrator_user": "Administrator user",
"lets_create_first_administrator_user": "Let's create the first administrator user.",
"admin_user_created": "Administrator user setup",
"admin_user_created_description": "You have already created an administrator user. You can now log in to your account.",
"new_member": "New Member",
"invite": "Invite",
"add_team_members": "Add team members",
@ -796,7 +798,7 @@
"number_apps_one": "{{count}} App",
"number_apps_other": "{{count}} Apps",
"trending_apps": "Trending Apps",
"most_popular":"Most Popular",
"most_popular": "Most Popular",
"explore_apps": "{{category}} apps",
"installed_apps": "Installed Apps",
"free_to_use_apps": "Free",
@ -841,7 +843,7 @@
"connect_metamask": "Connect Metamask",
"create_events_on": "Create events on",
"enterprise_license": "This is an enterprise feature",
"enterprise_license_description": "To enable this feature, get a deployment key at {{consoleUrl}} console and add it to your .env as CALCOM_LICENSE_KEY. If your team already has a license, please contact {{supportMail}} for help.",
"enterprise_license_description": "To enable this feature, have an administrator go to {{setupUrl}} to enter a license key. If a license key is already in place, please contact {{supportMail}} for help.",
"missing_license": "Missing License",
"signup_requires": "Commercial license required",
"signup_requires_description": "{{companyName}} currently does not offer a free open source version of the sign up page. To receive full access to the signup components you need to acquire a commercial license. For personal use we recommend the Prisma Data Platform or any other Postgres interface to create accounts.",
@ -1114,8 +1116,8 @@
"close": "Close",
"upgrade": "Upgrade",
"upgrade_to_access_recordings_title": "Upgrade to access recordings",
"upgrade_to_access_recordings_description":"Recordings are only available as part of our teams plan. Upgrade to start recording your calls",
"recordings_are_part_of_the_teams_plan":"Recordings are part of the teams plan",
"upgrade_to_access_recordings_description": "Recordings are only available as part of our teams plan. Upgrade to start recording your calls",
"recordings_are_part_of_the_teams_plan": "Recordings are part of the teams plan",
"team_feature_teams": "This is a Team feature. Upgrade to Team to see your team's availability.",
"team_feature_workflows": "This is a Team feature. Upgrade to Team to automate your event notifications and reminders with Workflows.",
"show_eventtype_on_profile": "Show on Profile",
@ -1414,7 +1416,7 @@
"team_url_required": "Must enter a team URL",
"team_url_taken": "This URL is already taken",
"team_publish": "Publish team",
"number_sms_notifications": "Phone number (SMS\u00a0notifications)",
"number_sms_notifications": "Phone number (SMS notifications)",
"attendee_email_variable": "Attendee email",
"attendee_email_info": "The person booking's email",
"kbar_search_placeholder": "Type a command or search...",
@ -1451,6 +1453,9 @@
"disabled_calendar": "If you have another calendar installed new bookings will be added to it. If not then connect a new calendar so you do not miss any new bookings.",
"enable_apps": "Enable Apps",
"enable_apps_description": "Enable apps that users can integrate with Cal.com",
"purchase_license": "Purchase a License",
"already_have_key": "I already have a key:",
"already_have_key_suggestion": "Please copy your existing CALCOM_LICENSE_KEY environment variable here.",
"app_is_enabled": "{{appName}} is enabled",
"app_is_disabled": "{{appName}} is disabled",
"keys_have_been_saved": "Keys have been saved",
@ -1496,11 +1501,11 @@
"not_verified": "Not yet verified",
"no_availability_in_month": "No availability in {{month}}",
"view_next_month": "View next month",
"send_code" : "Send code",
"send_code": "Send code",
"number_verified": "Number Verified",
"create_your_first_team_webhook_description": "Create your first webhook for this team event type",
"create_webhook_team_event_type": "Create a webhook for this team event type",
"disable_success_page":"Disable Success Page (only works if you have a redirect URL)",
"disable_success_page": "Disable Success Page (only works if you have a redirect URL)",
"invalid_admin_password": "You are set as an admin but you do not have a password length of at least 15 characters",
"change_password_admin": "Change Password to gain admin access",
"username_already_taken": "Username is already taken",
@ -1526,6 +1531,25 @@
"reporting_feature": "See all incoming from data and download it as a CSV",
"teams_plan_required": "Teams plan required",
"routing_forms_are_a_great_way": "Routing forms are a great way to route your incoming leads to the right person. Upgrade to a Teams plan to access this feature.",
"choose_a_license": "Choose a license",
"choose_license_description": "Cal.com comes with an accessible and free AGPLv3 license which some limitations which can be upgraded to an Enterprise license at any time. You can upgrade at anytime later.",
"license": "License",
"agplv3_license": "AGPLv3 License",
"ee_enterprise_license": "“/ee” Enterprise License",
"enterprise_booking_fee": "Starting at {{enterprise_booking_fee}}/month",
"enterprise_license_includes": "Everything for a commercial use case",
"no_need_to_keep_your_code_open_source": "No need to keep your code open-source",
"repackage_rebrand_resell": "Repackage, rebrand and resell easily",
"a_vast_suite_of_enterprise_features": "A vast suite of enterprise features",
"free_license_fee": "$0.00/month",
"forever_open_and_free": "Forever Open & Free",
"required_to_keep_your_code_open_source": "Required to keep your code open source",
"cannot_repackage_and_resell": "Cannot repackage, rebrand and resell easily",
"no_enterprise_features": "No enterprise features",
"step_enterprise_license": "Enterprise License",
"step_enterprise_license_description": "Everything for a commercial use case with private hosting, repackaging, rebranding and reselling and access exclusive enterprise components.",
"setup": "Setup",
"setup_description": "Setup Cal.com instance",
"configure": "Configure",
"sso_configuration": "Single Sign-On",
"sso_configuration_description": "Configure SAML/OIDC SSO and allow team members to login using an Identity Provider",

View File

@ -1,7 +1,7 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useMemo } from "react";
import { classNames } from "@calcom/lib";
import { classNames as cs } from "@calcom/lib";
import { HorizontalTabs, VerticalTabs } from "@calcom/ui";
import getAppCategories from "../_utils/getAppCategories";
@ -11,33 +11,39 @@ const AppCategoryNavigation = ({
children,
containerClassname,
className,
fromAdmin,
classNames,
useQueryParam = false,
}: {
baseURL: string;
children: React.ReactNode;
containerClassname: string;
/** @deprecated use classNames instead */
containerClassname?: string;
/** @deprecated use classNames instead */
className?: string;
fromAdmin?: boolean;
classNames?: {
root?: string;
container?: string;
verticalTabsItem?: string;
};
useQueryParam?: boolean;
}) => {
const [animationRef] = useAutoAnimate<HTMLDivElement>();
const appCategories = useMemo(() => getAppCategories(baseURL, useQueryParam), [baseURL, useQueryParam]);
return (
<div className={classNames("flex flex-col gap-x-6 p-2 md:p-0 xl:flex-row", className)}>
<div className={cs("flex flex-col gap-x-6 p-2 md:p-0 xl:flex-row", classNames?.root ?? className)}>
<div className="hidden xl:block">
<VerticalTabs
tabs={appCategories}
sticky
linkProps={{ shallow: true }}
itemClassname={classNames(fromAdmin && "w-60")}
itemClassname={classNames?.verticalTabsItem}
/>
</div>
<div className="mb-4 block overflow-x-scroll xl:hidden">
<HorizontalTabs tabs={appCategories} linkProps={{ shallow: true }} />
</div>
<main className={containerClassname} ref={animationRef}>
<main className={classNames?.container ?? containerClassname} ref={animationRef}>
{children}
</main>
</div>

View File

@ -6,7 +6,7 @@ module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
"../../packages/app-store/**/{components,pages}/**/*.{js,ts,jsx,tsx}",
"../../packages/app-store/**/*{components,pages}/**/*.{js,ts,jsx,tsx}",
"../../packages/features/**/*.{js,ts,jsx,tsx}",
"../../packages/ui/**/*.{js,ts,jsx,tsx}",
],

View File

@ -1,12 +1,14 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { AppCategories } from "@prisma/client";
import { noop } from "lodash";
import { useRouter } from "next/router";
import { useState, useReducer, FC } from "react";
import { FC, useReducer, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
import { appKeysSchemas } from "@calcom/app-store/apps.keys-schemas.generated";
import { classNames as cs } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import {
@ -32,9 +34,9 @@ import {
} from "@calcom/ui";
import {
FiAlertCircle,
FiCheckCircle,
FiEdit,
FiMoreHorizontal,
FiCheckCircle,
FiXCircle,
} from "@calcom/ui/components/icon";
@ -148,26 +150,39 @@ const AdminAppsList = ({
baseURL,
className,
useQueryParam = false,
classNames,
onSubmit = noop,
...rest
}: {
baseURL: string;
classNames?: {
form?: string;
appCategoryNavigationRoot?: string;
appCategoryNavigationContainer?: string;
verticalTabsItem?: string;
};
className?: string;
useQueryParam?: boolean;
}) => {
const router = useRouter();
onSubmit?: () => void;
} & Omit<JSX.IntrinsicElements["form"], "onSubmit">) => {
return (
<form
id="wizard-step-2"
name="wizard-step-2"
{...rest}
className={
classNames?.form ?? "max-w-80 mb-4 rounded-md bg-white px-0 pt-0 md:max-w-full md:px-8 md:pt-10"
}
onSubmit={(e) => {
e.preventDefault();
router.replace("/");
onSubmit();
}}>
<AppCategoryNavigation
baseURL={baseURL}
fromAdmin
useQueryParam={useQueryParam}
containerClassname="min-w-0 w-full"
className={className}>
classNames={{
root: className,
verticalTabsItem: classNames?.verticalTabsItem,
container: cs("min-w-0 w-full", classNames?.appCategoryNavigationContainer ?? "max-w-[500px]"),
}}>
<AdminAppsListContainer />
</AppCategoryNavigation>
</form>

View File

@ -43,16 +43,8 @@ const ApiKeyListItem = ({
<div>
<p className="font-medium"> {apiKey?.note ? apiKey.note : t("api_key_no_note")}</p>
<div className="flex items-center space-x-3.5">
{!neverExpires && isExpired && (
<Badge className="-p-2" variant="red">
{t("expired")}
</Badge>
)}
{!isExpired && (
<Badge className="-p-2" variant="green">
{t("active")}
</Badge>
)}
{!neverExpires && isExpired && <Badge variant="red">{t("expired")}</Badge>}
{!isExpired && <Badge variant="green">{t("active")}</Badge>}
<p className="text-xs text-gray-600">
{" "}
{neverExpires ? (

View File

@ -7,7 +7,7 @@ import DOMPurify from "dompurify";
import { useSession } from "next-auth/react";
import React, { AriaRole, ComponentType, Fragment } from "react";
import { APP_NAME } from "@calcom/lib/constants";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmptyScreen } from "@calcom/ui";
import { FiAlertTriangle } from "@calcom/ui/components/icon";
@ -46,6 +46,7 @@ const LicenseRequired = ({ children, as = "", ...rest }: LicenseRequiredProps) =
consoleUrl: `<a href="https://go.cal.com/console" target="_blank" class="underline">
${APP_NAME}
</a>`,
setupUrl: `<a href="${WEBAPP_URL}/auth/setup">/auth/setup</a>`,
supportMail: `<a href="mailto:sales@cal.com" class="underline">
sales@cal.com
</a>`,

View File

@ -2,7 +2,7 @@ import DOMPurify from "dompurify";
import { useSession } from "next-auth/react";
import React, { AriaRole, ComponentType, Fragment } from "react";
import { APP_NAME, CONSOLE_URL, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
import { APP_NAME, CONSOLE_URL, SUPPORT_MAIL_ADDRESS, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmptyScreen } from "@calcom/ui";
import { FiAlertTriangle } from "@calcom/ui/components/icon";
@ -36,9 +36,9 @@ const LicenseRequired = ({ children, as = "", ...rest }: LicenseRequiredProps) =
consoleUrl: `<a href="${CONSOLE_URL}" target="_blank" rel="noopener noreferrer" class="underline">
${APP_NAME}
</a>`,
setupUrl: `<a href="${WEBAPP_URL}/auth/setup" class="underline">/auth/setup</a>`,
supportMail: `<a href="mailto:${SUPPORT_MAIL_ADDRESS}" class="underline">
${SUPPORT_MAIL_ADDRESS}
</a>`,
${SUPPORT_MAIL_ADDRESS}</a>`,
})
),
}}

View File

@ -1,3 +1,4 @@
import type { PrismaClient } from "@prisma/client";
import cache from "memory-cache";
import { z } from "zod";
@ -18,10 +19,21 @@ const schemaLicenseKey = z
: v;
});
async function checkLicense(license: string): Promise<boolean> {
async function checkLicense(
/** The prisma client to use (necessary for public API to handle custom prisma instances) */
prisma: PrismaClient
): Promise<boolean> {
/** We skip for E2E testing */
if (!!process.env.NEXT_PUBLIC_IS_E2E) return true;
if (!license) return false;
const url = `${CONSOLE_URL}/api/license?key=${schemaLicenseKey.parse(license)}`;
/** We check first on env */
let licenseKey = process.env.CALCOM_LICENSE_KEY;
if (!licenseKey) {
/** We try to check on DB only if env is undefined */
const deployment = await prisma.deployment.findFirst({ where: { id: 1 } });
licenseKey = deployment?.licenseKey ?? undefined;
}
if (!licenseKey) return false;
const url = `${CONSOLE_URL}/api/license?key=${schemaLicenseKey.parse(licenseKey)}`;
const cachedResponse = cache.get(url);
if (cachedResponse) {
return cachedResponse;

View File

@ -0,0 +1,9 @@
import type { PrismaClient } from "@prisma/client";
export async function getDeploymentKey(prisma: PrismaClient) {
const deployment = await prisma.deployment.findUnique({
where: { id: 1 },
select: { licenseKey: true },
});
return deployment?.licenseKey || process.env.CALCOM_LICENSE_KEY || "";
}

View File

@ -209,6 +209,14 @@ export const KBarRoot = ({ children }: { children: React.ReactNode }) => {
keywords: "user impersonation",
perform: () => router.push("/settings/security/impersonation"),
},
{
id: "license",
name: "choose_a_license",
section: "admin",
shortcut: ["u", "l"],
keywords: "license",
perform: () => router.push("/auth/setup?step=1"),
},
{
id: "webhooks",
name: "Webhooks",

View File

@ -32,6 +32,7 @@ import {
FiChevronRight,
FiPlus,
FiMenu,
FiExternalLink,
} from "@calcom/ui/components/icon";
const tabs: VerticalTabItemProps[] = [
@ -89,6 +90,7 @@ const tabs: VerticalTabItemProps[] = [
icon: FiLock,
children: [
//
{ name: "license", href: "/auth/setup?step=1" },
{ name: "impersonation", href: "/settings/admin/impersonation" },
{ name: "apps", href: "/settings/admin/apps/calendar" },
{ name: "users", href: "https://console.cal.com" },

View File

@ -41,6 +41,7 @@ import { appsRouter } from "./viewer/apps";
import { authRouter } from "./viewer/auth";
import { availabilityRouter } from "./viewer/availability";
import { bookingsRouter } from "./viewer/bookings";
import { deploymentSetupRouter } from "./viewer/deploymentSetup";
import { eventTypesRouter } from "./viewer/eventTypes";
import { slotsRouter } from "./viewer/slots";
import { ssoRouter } from "./viewer/sso";
@ -1150,6 +1151,7 @@ export const viewerRouter = mergeRouters(
loggedInViewerRouter,
public: publicViewerRouter,
auth: authRouter,
deploymentSetup: deploymentSetupRouter,
bookings: bookingsRouter,
eventTypes: eventTypesRouter,
availability: availabilityRouter,

View File

@ -0,0 +1,24 @@
import { z } from "zod";
import prisma from "@calcom/prisma";
import { router, authedAdminProcedure } from "../../trpc";
export const deploymentSetupRouter = router({
update: authedAdminProcedure
.input(
z.object({
licenseKey: z.string().optional(),
})
)
.mutation(async ({ input }) => {
const data = {
agreedLicenseAt: new Date(),
licenseKey: input.licenseKey,
};
await prisma.deployment.upsert({ where: { id: 1 }, create: data, update: data });
return;
}),
});

View File

@ -1,7 +1,5 @@
declare namespace NodeJS {
interface ProcessEnv {
/** Needed to enable enterprise-only features */
readonly CALCOM_LICENSE_KEY: string | undefined;
readonly CALCOM_TELEMETRY_DISABLED: string | undefined;
readonly CALENDSO_ENCRYPTION_KEY: string | undefined;
readonly DATABASE_URL: string | undefined;

View File

@ -211,7 +211,7 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonPr
<StartIcon
className={classNames(
variant === "icon" && "h-4 w-4",
variant === "button" && "h-4 w-4 stroke-[1.5px] ltr:mr-2 rtl:ml-2"
variant === "button" && "h-4 w-4 stroke-[1.5px] ltr:ml-2 rtl:mr-2"
)}
/>
)}

View File

@ -117,7 +117,7 @@ export const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function
</Skeleton>
)}
{addOnLeading || addOnSuffix ? (
<div className="relative mb-1 flex items-center rounded-md focus-within:outline-none focus-within:ring-2 focus-within:ring-neutral-800 focus-within:ring-offset-1">
<div className="group relative mb-1 flex items-center rounded-md focus-within:outline-none focus-within:ring-2 focus-within:ring-neutral-800 focus-within:ring-offset-1">
{addOnLeading && (
<Addon
isFilled={addOnFilled}

View File

@ -1,3 +1,4 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import Link from "next/link";
type DefaultStep = {
@ -16,12 +17,13 @@ function Stepper<T extends DefaultStep>(props: {
steps,
stepLabel = (currentStep, totalSteps) => `Step ${currentStep} of ${totalSteps}`,
} = props;
const [stepperRef] = useAutoAnimate<HTMLOListElement>();
return (
<>
{steps.length > 1 && (
<nav className="flex items-center justify-center" aria-label="Progress">
<p className="text-sm font-medium">{stepLabel(props.step, steps.length)}</p>
<ol role="list" className="ml-8 flex items-center space-x-5">
<ol role="list" className="ml-8 flex items-center space-x-5" ref={stepperRef}>
{steps.map((mapStep, index) => (
<li key={mapStep.title}>
<Link

View File

@ -1,29 +1,31 @@
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
interface ISteps {
maxSteps: number;
currentStep: number;
navigateToStep: (step: number) => void;
stepLabel?: (currentStep: number, maxSteps: number) => string;
}
const Steps = (props: ISteps) => {
const { maxSteps, currentStep, navigateToStep } = props;
const { t } = useLocale();
const {
maxSteps,
currentStep,
navigateToStep,
stepLabel = (currentStep, totalSteps) => `Step ${currentStep} of ${totalSteps}`,
} = props;
return (
<div className="mt-6 space-y-2">
<p className="text-xs font-medium text-gray-500 dark:text-white">
{t("current_step_of_total", { currentStep: currentStep + 1, maxSteps })}
</p>
<p className="text-xs font-medium text-gray-500 dark:text-white">{stepLabel(currentStep, maxSteps)}</p>
<div className="flex w-full space-x-2 rtl:space-x-reverse">
{new Array(maxSteps).fill(0).map((_s, index) => {
return index <= currentStep ? (
return index <= currentStep - 1 ? (
<div
key={`step-${index}`}
onClick={() => navigateToStep(index)}
className={classNames(
"h-1 w-full rounded-[1px] bg-black dark:bg-white",
index < currentStep ? "cursor-pointer" : ""
index < currentStep - 1 ? "cursor-pointer" : ""
)}
/>
) : (

View File

@ -1,16 +1,19 @@
import { noop } from "lodash";
import { useRouter } from "next/router";
import { ComponentProps } from "react";
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import classNames from "@calcom/lib/classNames";
import { Button, Stepper } from "../../..";
import { Button, Steps } from "../../..";
type DefaultStep = {
title: string;
containerClassname?: string;
contentClassname?: string;
description: string;
content: JSX.Element;
enabled?: boolean;
isLoading: boolean;
content?: ((setIsLoading: Dispatch<SetStateAction<boolean>>) => JSX.Element) | JSX.Element;
isEnabled?: boolean;
isLoading?: boolean;
};
function WizardForm<T extends DefaultStep>(props: {
@ -21,7 +24,7 @@ function WizardForm<T extends DefaultStep>(props: {
prevLabel?: string;
nextLabel?: string;
finishLabel?: string;
stepLabel?: ComponentProps<typeof Stepper>["stepLabel"];
stepLabel?: React.ComponentProps<typeof Steps>["stepLabel"];
}) {
const { href, steps, nextLabel = "Next", finishLabel = "Finish", prevLabel = "Back", stepLabel } = props;
const router = useRouter();
@ -30,55 +33,54 @@ function WizardForm<T extends DefaultStep>(props: {
const setStep = (newStep: number) => {
router.replace(`${href}?step=${newStep || 1}`, undefined, { shallow: true });
};
const [currentStepIsLoading, setCurrentStepIsLoading] = useState(false);
useEffect(() => {
setCurrentStepIsLoading(false);
}, [currentStep]);
return (
<div className="mx-auto mt-4 print:w-full">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img className="mx-auto mb-8 h-8" src="https://cal.com/logo.svg" alt="Cal.com Logo" />
<div
className={classNames(
"mb-8 divide-y divide-gray-200 overflow-hidden rounded-lg bg-white shadow print:divide-transparent print:shadow-transparent",
props.containerClassname
)}>
<div className="px-4 py-5 sm:px-6">
<div className={classNames("overflow-hidden md:mb-2 md:w-[700px]", props.containerClassname)}>
<div className="px-6 py-5 sm:px-14">
<h1 className="font-cal text-2xl text-gray-900">{currentStep.title}</h1>
<p className="text-sm text-gray-500">{currentStep.description}</p>
{!props.disableNavigation && (
<Steps maxSteps={steps.length} currentStep={step} navigateToStep={noop} stepLabel={stepLabel} />
)}
</div>
</div>
<div className={classNames("mb-8 overflow-hidden md:w-[700px]", props.containerClassname)}>
<div className={classNames("print:p-none max-w-3xl px-8 py-5 sm:p-6", currentStep.contentClassname)}>
{typeof currentStep.content === "function"
? currentStep.content(setCurrentStepIsLoading)
: currentStep.content}
</div>
<div className="print:p-none max-w-3xl px-4 py-5 sm:p-6">{currentStep.content}</div>
{!props.disableNavigation && (
<>
{currentStep.enabled !== false && (
<div className="flex justify-end px-4 py-4 print:hidden sm:px-6">
{step > 1 && (
<Button
color="secondary"
onClick={() => {
setStep(step - 1);
}}>
{prevLabel}
</Button>
)}
<Button
tabIndex={0}
loading={currentStep.isLoading}
type="submit"
color="primary"
form={`wizard-step-${step}`}
className="relative ml-2">
{step < steps.length ? nextLabel : finishLabel}
</Button>
</div>
<div className="flex justify-end px-4 py-4 print:hidden sm:px-6">
{step > 1 && (
<Button
color="secondary"
onClick={() => {
setStep(step - 1);
}}>
{prevLabel}
</Button>
)}
</>
<Button
tabIndex={0}
loading={currentStepIsLoading}
type="submit"
color="primary"
form={`wizard-step-${step}`}
disabled={currentStep.isEnabled === false}
className="relative ml-2">
{step < steps.length ? nextLabel : finishLabel}
</Button>
</div>
)}
</div>
{!props.disableNavigation && (
<div className="print:hidden">
<Stepper href={href} step={step} steps={steps} disableSteps stepLabel={stepLabel} />
</div>
)}
</div>
);
}

View File

@ -13,7 +13,7 @@ export interface NavTabProps {
itemClassname?: string;
}
const NavTabs = function ({ tabs, className = "", sticky, linkProps, ...props }: NavTabProps) {
const NavTabs = function ({ tabs, className = "", sticky, linkProps, itemClassname, ...props }: NavTabProps) {
return (
<nav
className={classNames(
@ -26,7 +26,7 @@ const NavTabs = function ({ tabs, className = "", sticky, linkProps, ...props }:
{sticky && <div className="pt-6" />}
{props.children}
{tabs.map((tab, idx) => (
<VerticalTabItem {...tab} key={idx} linkProps={linkProps} className={props.itemClassname} />
<VerticalTabItem {...tab} key={idx} linkProps={linkProps} className={itemClassname} />
))}
</nav>
);