Merge branch 'main' into testE2E-timezone

This commit is contained in:
GitStart-Cal.com 2023-10-09 17:16:00 +00:00 committed by GitHub
commit 9a711cb7e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 461 additions and 77 deletions

View File

@ -253,6 +253,8 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
#### Setting up your first user
##### Approach 1
1. Open [Prisma Studio](https://prisma.io/studio) to look at or modify the database content:
```sh
@ -264,6 +266,17 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
> New users are set on a `TRIAL` plan by default. You might want to adjust this behavior to your needs in the `packages/prisma/schema.prisma` file.
1. Open a browser to [http://localhost:3000](http://localhost:3000) and login with your just created, first user.
##### Approach 2
Seed the local db by running
```sh
cd packages/prisma
yarn db-seed
```
The above command will populate the local db with dummy users.
### E2E-Testing
Be sure to set the environment variable `NEXTAUTH_URL` to the correct value. If you are running locally, as the documentation within `.env.example` mentions, the value should be `http://localhost:3000`.

View File

@ -3,7 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { createContext } from "@calcom/trpc/server/createContext";
import { viewerRouter } from "@calcom/trpc/server/routers/viewer/_router";
import { slotsRouter } from "@calcom/trpc/server/routers/viewer/slots/_router";
import { TRPCError } from "@trpc/server";
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
@ -11,10 +11,10 @@ import { getHTTPStatusCodeFromError } from "@trpc/server/http";
async function handler(req: NextApiRequest, res: NextApiResponse) {
/** @see https://trpc.io/docs/server-side-calls */
const ctx = await createContext({ req, res });
const caller = viewerRouter.createCaller(ctx);
const caller = slotsRouter.createCaller(ctx);
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return await caller.slots.getSchedule(req.query as any /* Let tRPC handle this */);
return await caller.getSchedule(req.query as any /* Let tRPC handle this */);
} catch (cause) {
if (cause instanceof TRPCError) {
const statusCode = getHTTPStatusCodeFromError(cause);

View File

@ -15,7 +15,7 @@ interface Props {
export default function AuthContainer(props: React.PropsWithChildren<Props>) {
return (
<div className="flex min-h-screen flex-col justify-center bg-[#f3f4f6] py-12 sm:px-6 lg:px-8">
<div className="bg-subtle dark:bg-darkgray-50 flex min-h-screen flex-col justify-center py-12 sm:px-6 lg:px-8">
<HeadSeo title={props.title} description={props.description} />
{props.showLogo && <Logo small inline={false} className="mx-auto mb-auto" />}
@ -28,7 +28,7 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
</div>
)}
<div className="mb-auto mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-default border-subtle mx-2 rounded-md border px-4 py-10 sm:px-10">
<div className="bg-default dark:bg-muted border-subtle mx-2 rounded-md border px-4 py-10 sm:px-10">
{props.children}
</div>
<div className="text-default mt-8 text-center text-sm">{props.footerText}</div>

View File

@ -7,7 +7,7 @@ import prisma from "@calcom/prisma";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { TRPCError } from "@calcom/trpc/server";
import { createContext } from "@calcom/trpc/server/createContext";
import { viewerRouter } from "@calcom/trpc/server/routers/viewer/_router";
import { bookingsRouter } from "@calcom/trpc/server/routers/viewer/bookings/_router";
enum DirectAction {
ACCEPT = "accept",
@ -55,13 +55,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
try {
/** @see https://trpc.io/docs/server-side-calls */
const ctx = await createContext({ req, res }, sessionGetter);
const caller = viewerRouter.createCaller({
const caller = bookingsRouter.createCaller({
...ctx,
req,
res,
user: { ...user, locale: user?.locale ?? "en" },
});
await caller.bookings.confirm({
await caller.confirm({
bookingId: booking.id,
recurringEventId: booking.recurringEventId || undefined,
confirmed: action === DirectAction.ACCEPT,

View File

@ -141,7 +141,6 @@ export default function Page({ requestId, isRequestExpired, csrfToken }: Props)
);
}
Page.isThemeSupported = false;
Page.PageWrapper = PageWrapper;
export async function getServerSideProps(context: GetServerSidePropsContext) {
const id = context.params?.id as string;

View File

@ -126,8 +126,9 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
/>
<div className="space-y-2">
<Button
className="w-full justify-center"
className="w-full justify-center dark:bg-white dark:text-black"
type="submit"
color="primary"
disabled={loading}
aria-label={t("request_password_reset")}
loading={loading}>
@ -141,7 +142,6 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
);
}
ForgotPassword.isThemeSupported = false;
ForgotPassword.PageWrapper = PageWrapper;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {

View File

@ -15,11 +15,12 @@ import { SAMLLogin } from "@calcom/features/auth/SAMLLogin";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
import { WEBAPP_URL, WEBSITE_URL, HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import { trpc } from "@calcom/trpc/react";
import { Alert, Button, EmailField, PasswordField } from "@calcom/ui";
import { ArrowLeft, Lock } from "@calcom/ui/components/icon";
@ -160,6 +161,16 @@ export default function Login({
else setErrorMessage(errorMessages[res.error] || t("something_went_wrong"));
};
const { data, isLoading } = trpc.viewer.public.ssoConnections.useQuery(undefined, {
onError: (err) => {
setErrorMessage(err.message);
},
});
const displaySSOLogin = HOSTED_CAL_FEATURES
? true
: isSAMLLoginEnabled && !isLoading && data?.connectionExists;
return (
<div
style={
@ -225,14 +236,14 @@ export default function Login({
type="submit"
color="primary"
disabled={formState.isSubmitting}
className="w-full justify-center">
className="w-full justify-center dark:bg-white dark:text-black">
{twoFactorRequired ? t("submit") : t("sign_in")}
</Button>
</div>
</form>
{!twoFactorRequired && (
<>
{(isGoogleLoginEnabled || isSAMLLoginEnabled) && <hr className="border-subtle my-8" />}
{(isGoogleLoginEnabled || displaySSOLogin) && <hr className="border-subtle my-8" />}
<div className="space-y-3">
{isGoogleLoginEnabled && (
<Button
@ -247,7 +258,7 @@ export default function Login({
{t("signin_with_google")}
</Button>
)}
{isSAMLLoginEnabled && (
{displaySSOLogin && (
<SAMLLogin
samlTenantID={samlTenantID}
samlProductID={samlProductID}
@ -337,7 +348,6 @@ const _getServerSideProps = async function getServerSideProps(context: GetServer
};
};
Login.isThemeSupported = false;
Login.PageWrapper = PageWrapper;
export const getServerSideProps = withNonce(_getServerSideProps);

View File

@ -57,7 +57,6 @@ export function Logout(props: Props) {
);
}
Logout.isThemeSupported = false;
Logout.PageWrapper = PageWrapper;
export default Logout;

View File

@ -28,6 +28,7 @@ test.describe("Availablity tests", () => {
await page.locator('[data-testid="day"][data-disabled="false"]').nth(0).click();
await page.locator('[data-testid="date-override-mark-unavailable"]').click();
await page.locator('[data-testid="add-override-submit-btn"]').click();
await page.locator('[data-testid="dialog-rejection"]').click();
await expect(page.locator('[data-testid="date-overrides-list"] > li')).toHaveCount(1);
await page.locator('[form="availability-form"][type="submit"]').click();
});

View File

@ -15,7 +15,8 @@ test("Should display SAML Login button", async ({ page }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(!IS_SAML_LOGIN_ENABLED, "It should only run if SAML Login is installed");
await page.goto(`/auth/login`);
await expect(page.locator(`[data-testid=saml]`)).toBeVisible();
// TODO: Fix this later
// Button is visible only if there is a SAML connection exists (self-hosted)
// await page.goto(`/auth/login`);
// await expect(page.locator(`[data-testid=saml]`)).toBeVisible();
});

View File

@ -288,6 +288,7 @@
"when": "Wann",
"where": "Wo",
"add_to_calendar": "Zum Kalender hinzufügen",
"add_to_calendar_description": "Legen Sie fest, wo neue Termine hinzugefügt werden sollen, wenn Sie gebucht werden.",
"add_another_calendar": "Einen weiteren Kalender hinzufügen",
"other": "Sonstige",
"email_sign_in_subject": "Ihr Anmelde-Link für {{appName}}",
@ -599,6 +600,7 @@
"hide_book_a_team_member": "„Ein Teammitglied buchen“-Schaltfläche ausblenden",
"hide_book_a_team_member_description": "Blendet die „Ein Teammitglied buchen“-Schaltflächen auf Ihren öffentlichen Seiten aus.",
"danger_zone": "Achtung",
"account_deletion_cannot_be_undone": "Vorsicht. Löschen eines Kontos kann nicht rückgängig gemacht werden.",
"back": "Zurück",
"cancel": "Absagen",
"cancel_all_remaining": "Alle verbleibenden absagen",
@ -688,6 +690,7 @@
"people": "Personen",
"your_email": "Ihre E-Mail-Adresse",
"change_avatar": "Profilbild ändern",
"upload_avatar": "Avatar hochladen",
"language": "Sprache",
"timezone": "Zeitzone",
"first_day_of_week": "Erster Tag der Woche",
@ -1276,6 +1279,7 @@
"personal_cal_url": "Meine persönliche {{appName}}-URL",
"bio_hint": "Schreiben Sie eine kurze Beschreibung, welche auf Ihrer persönlichen Profil-Seite erscheinen wird.",
"user_has_no_bio": "Dieser Benutzer hat noch keine Bio hinzugefügt.",
"bio": "Biografie",
"delete_account_modal_title": "Account löschen",
"confirm_delete_account_modal": "Sind Sie sicher, dass Sie Ihr {{appName}}-Konto löschen möchten?",
"delete_my_account": "Meinen Account löschen",
@ -1530,6 +1534,7 @@
"problem_registering_domain": "Es gab ein Problem bei der Registrierung der Subdomain, bitte versuchen Sie es erneut oder kontaktieren Sie einen Administrator",
"team_publish": "Team veröffentlichen",
"number_text_notifications": "Telefonnummer (Textbenachrichtigungen)",
"number_sms_notifications": "Telefonnummer (SMS-Benachrichtigungen)",
"attendee_email_variable": "Teilnehmer E-Mail",
"attendee_email_info": "Die E-Mail-Adresse der buchenden Person",
"kbar_search_placeholder": "Geben Sie einen Befehl ein oder suchen Sie ...",
@ -1639,6 +1644,7 @@
"minimum_round_robin_hosts_count": "Anzahl der Veranstalter, die teilnehmen müssen",
"hosts": "Veranstalter",
"upgrade_to_enable_feature": "Sie müssen ein Team erstellen, um diese Funktion zu aktivieren. Klicken Sie hier, um ein Team zu erstellen.",
"orgs_upgrade_to_enable_feature": "Sie müssen auf unsere Enterprise-Lizenz aktualisieren, um diese Funktion zu aktivieren.",
"new_attendee": "Neuer Teilnehmer",
"awaiting_approval": "Wartet auf Genehmigung",
"requires_google_calendar": "Diese App erfordert eine Verbindung mit Google Calendar",
@ -1743,6 +1749,7 @@
"show_on_booking_page": "Auf der Buchungsseite anzeigen",
"get_started_zapier_templates": "Legen Sie mit Zapier-Vorlagen los",
"team_is_unpublished": "{{team}} ist unveröffentlicht",
"org_is_unpublished_description": "Dieser Organisations-Link ist derzeit nicht verfügbar. Bitte kontaktieren Sie den Organisations-Besitzer oder fragen Sie ihn, ob er ihn veröffentlicht.",
"team_is_unpublished_description": "Dieser {{entity}}-Link ist derzeit nicht verfügbar. Bitte kontaktieren Sie den {{entity}}-Besitzer oder fragen Sie ihn, ob er ihn veröffentlicht.",
"team_member": "Teammitglied",
"a_routing_form": "Ein Weiterleitungsformular",
@ -1877,6 +1884,7 @@
"edit_invite_link": "Linkeinstellungen bearbeiten",
"invite_link_copied": "Einladungslink kopiert",
"invite_link_deleted": "Einladungslink gelöscht",
"api_key_deleted": "API-Schlüssel gelöscht",
"invite_link_updated": "Einladungslink-Einstellungen gespeichert",
"link_expires_after": "Links verfallen nach...",
"one_day": "1 Tag",
@ -2011,7 +2019,13 @@
"attendee_last_name_variable": "Nachname des Teilnehmers",
"attendee_first_name_info": "Vorname der buchenden Person",
"attendee_last_name_info": "Nachname der buchenden Person",
"your_monthly_digest": "Ihre monatliche Statistik",
"member_name": "Mitgliedsname",
"most_popular_events": "Beliebteste Termine",
"summary_of_events_for_your_team_for_the_last_30_days": "Hier ist Ihre Zusammenfassung der beliebten Termine für Ihr Team {{teamName}} der letzten 30 Tage",
"me": "Ich",
"monthly_digest_email": "Monatliche Statistik E-Mail",
"monthly_digest_email_for_teams": "Monatliche Statistik E-Mail für Teams",
"verify_team_tooltip": "Verifizieren Sie Ihr Team, um das Senden von Nachrichten an Teilnehmer zu aktivieren",
"member_removed": "Mitglied entfernt",
"my_availability": "Meine Verfügbarkeit",
@ -2041,12 +2055,28 @@
"team_no_event_types": "Dieses Team hat keine Ereignistypen",
"seat_options_doesnt_multiple_durations": "Platzoption unterstützt mehrere Dauern nicht",
"include_calendar_event": "Kalenderereignis hinzufügen",
"oAuth": "OAuth",
"recently_added": "Kürzlich hinzugefügt",
"no_members_found": "Keine Mitglieder gefunden",
"event_setup_length_error": "Ereignis-Einrichtung: Die Dauer muss mindestens 1 Minute betragen.",
"availability_schedules": "Verfügbarkeitspläne",
"unauthorized": "Nicht authorisiert",
"access_cal_account": "{{clientName}} möchte auf Ihr {{appName}} Konto zugreifen",
"select_account_team": "Konto oder Team auswählen",
"allow_client_to": "Dies wird {{clientName}} erlauben",
"see_personal_info": "Ihre persönlichen Daten einzusehen, einschließlich persönlicher Informationen, die Sie öffentlich zugänglich gemacht haben",
"see_primary_email_address": "Ihre primäre E-Mail-Adresse einzusehen",
"connect_installed_apps": "Sich mit Ihren installierten Apps zu verbinden",
"access_event_type": "Lesen, Bearbeiten, Löschen Ihrer Ereignis-Typen",
"access_availability": "Lesen, Bearbeiten, Löschen Ihrer Verfügbarkeiten",
"access_bookings": "Lesen, Bearbeiten, Löschen Ihrer Termine",
"allow_client_to_do": "{{clientName}} zulassen, dies zu tun?",
"allow": "Zulassen",
"view_only_edit_availability_not_onboarded": "Dieser Benutzer hat das Onboarding noch nicht abgeschlossen. Sie können seine Verfügbarkeit erst festlegen, wenn er das Onboarding abgeschlossen hat.",
"view_only_edit_availability": "Sie sehen die Verfügbarkeit dieses Benutzers. Sie können nur Ihre eigene Verfügbarkeit bearbeiten.",
"edit_users_availability": "Benutzerverfügbarkeit bearbeiten: {{username}}",
"resend_invitation": "Einladung erneut senden",
"invitation_resent": "Die Einladung wurde erneut gesendet.",
"this_app_is_not_setup_already": "Diese App wurde noch nicht eingerichtet",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -1619,6 +1619,7 @@
"date_overrides_mark_all_day_unavailable_other": "Mark unavailable on selected dates",
"date_overrides_add_btn": "Add Override",
"date_overrides_update_btn": "Update Override",
"date_successfully_added": "Date override added successfully",
"event_type_duplicate_copy_text": "{{slug}}-copy",
"set_as_default": "Set as default",
"hide_eventtype_details": "Hide event type details",

View File

@ -0,0 +1 @@
{}

View File

@ -122,9 +122,9 @@ function AlbySetupPage(props: IAlbySetupProps) {
const albyIcon = (
<>
<img className="h-16 w-16 dark:hidden" src="/api/app-store/alby/icon-borderless.svg" alt="Alby Icon" />
<img className="h-12 w-12 dark:hidden" src="/api/app-store/alby/icon-borderless.svg" alt="Alby Icon" />
<img
className="hidden h-16 w-16 dark:block"
className="hidden h-12 w-12 dark:block"
src="/api/app-store/alby/icon-borderless-dark.svg"
alt="Alby Icon"
/>

View File

@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { defaultResponder } from "@calcom/lib/server";
import { createContext } from "@calcom/trpc/server/createContext";
import { viewerRouter } from "@calcom/trpc/server/routers/viewer/_router";
import { apiKeysRouter } from "@calcom/trpc/server/routers/viewer/apiKeys/_router";
import checkSession from "../../_utils/auth";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
@ -15,9 +15,9 @@ export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const appType = appConfig.type;
const ctx = await createContext({ req, res });
const caller = viewerRouter.createCaller(ctx);
const caller = apiKeysRouter.createCaller(ctx);
const apiKey = await caller.apiKeys.create({
const apiKey = await caller.create({
note: "Cal.ai",
expiresAt: null,
appId: "cal-ai",

View File

@ -3,7 +3,6 @@ import { useForm } from "react-hook-form";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { classNames } from "@calcom/lib";
import { yyyymmdd } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
@ -15,6 +14,7 @@ import {
DialogHeader,
DialogClose,
Switch,
showToast,
Form,
Button,
} from "@calcom/ui";
@ -23,15 +23,11 @@ import DatePicker from "../../calendars/DatePicker";
import type { TimeRange } from "./Schedule";
import { DayRanges } from "./Schedule";
// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {};
const DateOverrideForm = ({
value,
workingHours,
excludedDates,
onChange,
onClose = noop,
}: {
workingHours?: WorkingHours[];
onChange: (newValue: TimeRange[]) => void;
@ -137,14 +133,10 @@ const DateOverrideForm = ({
})
: datesInRanges
);
onClose();
setSelectedDates([]);
}}
className="p-6 sm:flex sm:p-0 md:flex-col lg:flex-col xl:flex-row">
<div
className={classNames(
selectedDates[0] && "sm:border-subtle w-full sm:border-r sm:pr-6",
"sm:p-4 md:p-8"
)}>
<div className="sm:border-subtle w-full sm:border-r sm:p-4 sm:pr-6 md:p-8">
<DialogHeader title={t("date_overrides_dialog_title")} />
<DatePicker
excludedDates={excludedDates}
@ -160,39 +152,48 @@ const DateOverrideForm = ({
locale={isLocaleReady ? i18n.language : "en"}
/>
</div>
{selectedDates[0] && (
<div className="relative mt-8 flex w-full flex-col sm:mt-0 sm:p-4 md:p-8">
<div className="mb-4 flex-grow space-y-4">
<p className="text-medium text-emphasis text-sm">{t("date_overrides_dialog_which_hours")}</p>
<div>
{datesUnavailable ? (
<p className="text-subtle border-default rounded border p-2 text-sm">
{t("date_overrides_unavailable")}
</p>
) : (
<DayRanges name="range" />
)}
<div className="relative mt-8 flex w-full flex-col sm:mt-0 sm:p-4 md:p-8">
{selectedDates[0] ? (
<>
<div className="mb-4 flex-grow space-y-4">
<p className="text-medium text-emphasis text-sm">{t("date_overrides_dialog_which_hours")}</p>
<div>
{datesUnavailable ? (
<p className="text-subtle border-default rounded border p-2 text-sm">
{t("date_overrides_unavailable")}
</p>
) : (
<DayRanges name="range" />
)}
</div>
<Switch
label={t("date_overrides_mark_all_day_unavailable_one")}
checked={datesUnavailable}
onCheckedChange={setDatesUnavailable}
data-testid="date-override-mark-unavailable"
/>
</div>
<Switch
label={t("date_overrides_mark_all_day_unavailable_one")}
checked={datesUnavailable}
onCheckedChange={setDatesUnavailable}
data-testid="date-override-mark-unavailable"
/>
<div className="mt-4 flex flex-row-reverse sm:mt-0">
<Button
className="ml-2"
color="primary"
type="submit"
onClick={() => {
showToast(t("date_successfully_added"), "success");
}}
disabled={selectedDates.length === 0}
data-testid="add-override-submit-btn">
{value ? t("date_overrides_update_btn") : t("date_overrides_add_btn")}
</Button>
<DialogClose />
</div>
</>
) : (
<div className="bottom-7 right-8 flex flex-row-reverse sm:absolute">
<DialogClose />
</div>
<div className="mt-4 flex flex-row-reverse sm:mt-0">
<Button
className="ml-2"
color="primary"
type="submit"
disabled={selectedDates.length === 0}
data-testid="add-override-submit-btn">
{value ? t("date_overrides_update_btn") : t("date_overrides_add_btn")}
</Button>
<DialogClose onClick={onClose} />
</div>
</div>
)}
)}
</div>
</Form>
);
};
@ -220,7 +221,7 @@ const DateOverrideInputDialog = ({
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{Trigger}</DialogTrigger>
<DialogContent enableOverflow={enableOverflow} size="md" className="p-0 md:w-auto">
<DialogContent enableOverflow={enableOverflow} size="md" className="p-0">
<DateOverrideForm
excludedDates={excludedDates}
{...passThroughProps}

View File

@ -1008,7 +1008,7 @@ function MainContainer({
<main className="bg-default relative z-0 flex-1 focus:outline-none">
{/* show top navigation for md and smaller (tablet and phones) */}
{TopNavContainerProp}
<div className="max-w-full px-2 py-4 md:py-12 lg:px-6">
<div className="max-w-full px-2 py-4 lg:px-6">
<ErrorBoundary>
{!props.withoutMain ? <ShellMain {...props}>{props.children}</ShellMain> : props.children}
</ErrorBoundary>

View File

@ -7,7 +7,7 @@ export const useUrlMatchesCurrentUrl = (url: string) => {
// It can certainly have null value https://nextjs.org/docs/app/api-reference/functions/use-pathname#:~:text=usePathname%20can%20return%20null%20when%20a%20fallback%20route%20is%20being%20rendered%20or%20when%20a%20pages%20directory%20page%20has%20been%20automatically%20statically%20optimized%20by%20Next.js%20and%20the%20router%20is%20not%20ready.
const pathname = usePathname() as null | string;
const searchParams = useSearchParams();
const query = searchParams.toString();
const query = searchParams?.toString();
let pathnameWithQuery;
if (query) {
pathnameWithQuery = `${pathname}?${query}`;

View File

@ -49,4 +49,11 @@ export const publicViewerRouter = router({
const handler = await importHandler(namespaced("event"), () => import("./event.handler"));
return handler(opts);
}),
ssoConnections: publicProcedure.query(async () => {
const handler = await importHandler(
namespaced("ssoConnections"),
() => import("./ssoConnections.handler")
);
return handler();
}),
});

View File

@ -0,0 +1,31 @@
import jackson from "@calcom/features/ee/sso/lib/jackson";
import { samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
import { TRPCError } from "@trpc/server";
export const handler = async () => {
try {
if (HOSTED_CAL_FEATURES) {
return {
connectionExists: null,
};
}
const { connectionController } = await jackson();
const connections = await connectionController.getConnections({
tenant: samlTenantID,
product: samlProductID,
});
return {
connectionExists: connections.length > 0,
};
} catch (err) {
console.error("Error getting SSO connections", err);
throw new TRPCError({ code: "BAD_REQUEST", message: "Fetching SSO connections failed." });
}
};
export default handler;

View File

@ -187,7 +187,7 @@ export function DialogClose(
return (
<DialogPrimitive.Close asChild {...props.dialogCloseProps}>
{/* This will require the i18n string passed in */}
<Button color={props.color || "minimal"} {...props}>
<Button data-testid="dialog-rejection" color={props.color || "minimal"} {...props}>
{props.children ? props.children : t("Close")}
</Button>
</DialogPrimitive.Close>

View File

@ -1,22 +1,20 @@
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
import { Canvas, Meta, Story } from "@storybook/addon-docs";
import {
Examples,
Example,
Note,
Title,
CustomArgsTable,
VariantRow,
VariantsTable,
} from "@calcom/storybook/components";
import { Select, UnstyledSelect } from "../select";
import { InputFieldWithSelect } from "./Input";
import { InputField } from "./Input";
import { InputFieldWithSelect } from "./InputFieldWithSelect";
import { InputField } from "./TextField";
<Meta title="UI/Form/Input Field" component={InputField} />
<Title title="Inputs" suffix="Brief" subtitle="Version 2.0 — Last Update: 22 Aug 2022" />
<Title title="Inputs" suffix="Brief" subtitle="Version 2.0 — Last Update: 24 Aug 2023" />
## Definition

View File

@ -0,0 +1,134 @@
import { Canvas, Meta, Story } from "@storybook/addon-docs/blocks";
import {
Title,
VariantRow,
VariantsTable,
CustomArgsTable,
Examples,
Example,
} from "@calcom/storybook/components";
import { Plus } from "@calcom/ui/components/icon";
import HorizontalTabs from "../HorizontalTabs";
<Meta title="UI/Navigation/HorizontalTabs" component={HorizontalTabs} />
<Title title="Horizontal Tabs" suffix="Brief" subtitle="Version 1.0 — Last Update: 17 Aug 2023" />
## Definition
The HorizontalTabs component is a user interface element used for displaying a horizontal set of tabs, often employed for navigation or organization purposes within a web application.
## Structure
The HorizontalTabs component is designed to work alongside the HorizontalTabItem component, which represents individual tabs within the tab bar.
export const tabs = [
{
name: "Tab 1",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab1",
disabled: false,
linkShallow: true,
linkScroll: true,
icon: Plus,
},
{
name: "Tab 2",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab2",
disabled: false,
linkShallow: true,
linkScroll: true,
avatar: "Avatar",
},
{
name: "Tab 3",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab3",
disabled: true,
linkShallow: true,
linkScroll: true,
},
];
<CustomArgsTable of={HorizontalTabs} />
<Examples title="Default">
<Example title="Default">
<HorizontalTabs
tabs={[
{
name: "tab 1",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab1",
},
]}
/>
</Example>
<Example title="With avatar">
<HorizontalTabs
tabs={[
{
name: "Tab 1",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab1",
avatar: "Avatar",
},
]}
/>
</Example>
<Example title="With icon">
<HorizontalTabs
tabs={[
{
name: "Tab 1",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab1",
icon: Plus,
},
]}
/>
</Example>
<Example title="Disabled">
<HorizontalTabs
tabs={[
{
name: "Tab 1",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab1",
disabled: true,
},
]}
/>
</Example>
</Examples>
## HorizontalTabs Story
<Canvas>
<Story
name="Horizontal Tabs"
args={{
name: "Tab 1",
href: "/tab1",
disabled: false,
className: "",
linkShallow: true,
linkScroll: true,
icon: "",
avatar: "",
}}
argTypes={{
name: { control: "text", description: "Tab name" },
href: { control: "text", description: "Tab link" },
disabled: { control: "boolean", description: "Whether the tab is disabled" },
className: { control: "text", description: "Additional CSS class" },
linkShallow: { control: "boolean", description: "Whether to use shallow links" },
linkScroll: { control: "boolean", description: "Whether to scroll to links" },
icon: { control: "text", description: "SVGComponent icon" },
avatar: { control: "text", description: "Avatar image URL" },
}}>
{(...props) => (
<VariantsTable titles={["Default"]} columnMinWidth={150}>
<VariantRow>
<HorizontalTabs tabs={tabs} className="overflow-hidden" actions={<button>Click me</button>} />
</VariantRow>
</VariantsTable>
)}
</Story>
</Canvas>

View File

@ -0,0 +1,158 @@
import { Meta, Story } from "@storybook/addon-docs/blocks";
import {
Title,
CustomArgsTable,
Examples,
Example,
VariantsTable,
VariantsRow,
} from "@calcom/storybook/components";
import { Plus } from "@calcom/ui/components/icon";
import VerticalTabs from "../VerticalTabs";
<Meta title="UI/Navigation/VerticalTabs" component={VerticalTabs} />
<Title title="Vertical Tabs Brief" subtitle="Version 1.0 — Last Update: 17 Aug 2023" />
## Definition
The VerticalTabs component is a user interface element utilized to present a vertical set of tabs, commonly employed for navigation or organizing content within a web application.
## Structure
The VerticalTabs component is designed to complement the HorizontalTabItem component, which represents individual tabs within the tab bar. This combination allows for creating intuitive navigation experiences and organized content presentation.
export const tabs = [
{
name: "Tab 1",
href: "/tab1",
disabled: false,
linkShallow: true,
linkScroll: true,
disableChevron: true,
icon: Plus,
},
{
name: "Tab 2",
href: "/tab2",
disabled: false,
linkShallow: true,
linkScroll: true,
avatar: "Avatar",
},
{
name: "Tab 3",
href: "/tab3",
disabled: true,
linkShallow: true,
linkScroll: true,
},
];
<CustomArgsTable of={VerticalTabs} />
<Examples title="Default">
<Example title="Default">
<VerticalTabs
tabs={[
{
name: "tab 1",
href: "/tab1",
},
]}
/>
</Example>
<Example title="Disabled chevron">
<VerticalTabs
tabs={[
{
name: "Tab 1",
href: "/tab1",
disableChevron: true,
},
]}
/>
</Example>
<Example title="With icon">
<VerticalTabs
tabs={[
{
name: "Tab 1",
href: "/tab1",
icon: Plus,
},
]}
/>
</Example>
<Example title="Disabled">
<VerticalTabs
tabs={[
{
name: "Tab 1",
href: "/tab1",
disabled: true,
},
]}
/>
</Example>
</Examples>
## VerticalTabs Story
<Canvas>
<Story
name="Vertical Tabs"
args={{
name: "Tab 1",
info: "Tab information",
icon: Plus,
disabled: false,
children: [
{
name: "Sub Tab 1",
href: "/sub-tab1",
disabled: false,
className: "sub-tab",
},
],
textClassNames: "",
className: "",
isChild: false,
hidden: false,
disableChevron: true,
href: "/tab1",
isExternalLink: true,
linkShallow: true,
linkScroll: true,
avatar: "",
iconClassName: "",
}}
argTypes={{
name: { control: "text", description: "Tab name" },
info: { control: "text", description: "Tab information" },
icon: { control: "object", description: "SVGComponent icon" },
disabled: { control: "boolean", description: "Whether the tab is disabled" },
children: { control: "object", description: "Array of child tabs" },
textClassNames: { control: "text", description: "Additional text class names" },
className: { control: "text", description: "Additional CSS class" },
isChild: { control: "boolean", description: "Whether the tab is a child tab" },
hidden: { control: "boolean", description: "Whether the tab is hidden" },
disableChevron: { control: "boolean", description: "Whether to disable the chevron" },
href: { control: "text", description: "Tab link" },
isExternalLink: { control: "boolean", description: "Whether the link is external" },
linkShallow: { control: "boolean", description: "Whether to use shallow links" },
linkScroll: { control: "boolean", description: "Whether to scroll to links" },
avatar: { control: "text", description: "Avatar image URL" },
iconClassName: { control: "text", description: "Additional icon class name" },
}}>
{(...props) => (
<VariantsTable titles={["Default"]} columnMinWidth={150}>
<VariantRow>
<VerticalTabs tabs={tabs} className="overflow-hidden" />
</VariantRow>
</VariantsTable>
)}
</Story>
</Canvas>