merge with remote main
This commit is contained in:
commit
1c2211e2bf
|
@ -13,7 +13,6 @@ import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
|
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
|
||||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||||
import cx from "@calcom/lib/classNames";
|
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { md } from "@calcom/lib/markdownIt";
|
import { md } from "@calcom/lib/markdownIt";
|
||||||
|
@ -302,13 +301,7 @@ export const EventSetupTab = (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<img
|
<img
|
||||||
src={eventLocationType.iconUrl}
|
src={eventLocationType.iconUrl}
|
||||||
className={cx(
|
className="h-4 w-4 dark:invert-[.65]"
|
||||||
"h-4 w-4",
|
|
||||||
// invert all the icons except app icons
|
|
||||||
eventLocationType.iconUrl &&
|
|
||||||
eventLocationType.iconUrl.includes("-dark") &&
|
|
||||||
"dark:invert"
|
|
||||||
)}
|
|
||||||
alt={`${eventLocationType.label} logo`}
|
alt={`${eventLocationType.label} logo`}
|
||||||
/>
|
/>
|
||||||
<span className="ms-1 line-clamp-1 text-sm">{`${eventLabel} ${
|
<span className="ms-1 line-clamp-1 text-sm">{`${eventLabel} ${
|
||||||
|
|
|
@ -3,12 +3,13 @@ import type { FormEvent } from "react";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import OrganizationAvatar from "@calcom/features/ee/organizations/components/OrganizationAvatar";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { md } from "@calcom/lib/markdownIt";
|
import { md } from "@calcom/lib/markdownIt";
|
||||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import turndown from "@calcom/lib/turndownService";
|
import turndown from "@calcom/lib/turndownService";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Avatar, Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
||||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
type FormData = {
|
type FormData = {
|
||||||
|
@ -98,7 +99,14 @@ const UserProfile = () => {
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<div className="flex flex-row items-center justify-start rtl:justify-end">
|
<div className="flex flex-row items-center justify-start rtl:justify-end">
|
||||||
{user && <Avatar alt={user.username || "user avatar"} size="lg" imageSrc={imageSrc} />}
|
{user && (
|
||||||
|
<OrganizationAvatar
|
||||||
|
alt={user.username || "user avatar"}
|
||||||
|
size="lg"
|
||||||
|
imageSrc={imageSrc}
|
||||||
|
organizationSlug={user.organization?.slug}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
ref={avatarRef}
|
ref={avatarRef}
|
||||||
type="hidden"
|
type="hidden"
|
||||||
|
|
|
@ -22,14 +22,7 @@ export type GroupOptionType = GroupBase<LocationOption>;
|
||||||
const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => {
|
const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{icon && (
|
{icon && <img src={icon} alt="cover" className="h-3.5 w-3.5 dark:invert-[.65]" />}
|
||||||
<img
|
|
||||||
src={icon}
|
|
||||||
alt="cover"
|
|
||||||
// invert all the icons except app icons
|
|
||||||
className={classNames(icon.includes("-dark") && "dark:invert", "h-3.5 w-3.5")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<span className={classNames("text-sm font-medium")}>{label}</span>
|
<span className={classNames("text-sm font-medium")}>{label}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -254,6 +254,10 @@ const nextConfig = {
|
||||||
source: "/org/:slug",
|
source: "/org/:slug",
|
||||||
destination: "/team/:slug",
|
destination: "/team/:slug",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: "/org/:orgSlug/avatar.png",
|
||||||
|
destination: "/api/user/avatar?orgSlug=:orgSlug",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
source: "/team/:teamname/avatar.png",
|
source: "/team/:teamname/avatar.png",
|
||||||
destination: "/api/user/avatar?teamname=:teamname",
|
destination: "/api/user/avatar?teamname=:teamname",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "3.2.8",
|
"version": "3.2.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
useEmbedStyles,
|
useEmbedStyles,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
|
import OrganizationAvatar from "@calcom/features/ee/organizations/components/OrganizationAvatar";
|
||||||
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||||
|
@ -25,7 +26,7 @@ import prisma from "@calcom/prisma";
|
||||||
import type { EventType, User } from "@calcom/prisma/client";
|
import type { EventType, User } from "@calcom/prisma/client";
|
||||||
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { Avatar, HeadSeo, UnpublishedEntity } from "@calcom/ui";
|
import { HeadSeo, UnpublishedEntity } from "@calcom/ui";
|
||||||
import { Verified, ArrowRight } from "@calcom/ui/components/icon";
|
import { Verified, ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||||
|
@ -96,7 +97,12 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
||||||
"max-w-3xl px-4 py-24"
|
"max-w-3xl px-4 py-24"
|
||||||
)}>
|
)}>
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
<Avatar imageSrc={profile.image} size="xl" alt={profile.name} />
|
<OrganizationAvatar
|
||||||
|
imageSrc={profile.image}
|
||||||
|
size="xl"
|
||||||
|
alt={profile.name}
|
||||||
|
organizationSlug={profile.organizationSlug}
|
||||||
|
/>
|
||||||
<h1 className="font-cal text-emphasis mb-1 text-3xl" data-testid="name-title">
|
<h1 className="font-cal text-emphasis mb-1 text-3xl" data-testid="name-title">
|
||||||
{profile.name}
|
{profile.name}
|
||||||
{user.verified && (
|
{user.verified && (
|
||||||
|
@ -218,6 +224,7 @@ export type UserPageProps = {
|
||||||
theme: string | null;
|
theme: string | null;
|
||||||
brandColor: string;
|
brandColor: string;
|
||||||
darkBrandColor: string;
|
darkBrandColor: string;
|
||||||
|
organizationSlug: string | null;
|
||||||
allowSEOIndexing: boolean;
|
allowSEOIndexing: boolean;
|
||||||
};
|
};
|
||||||
users: Pick<User, "away" | "name" | "username" | "bio" | "verified">[];
|
users: Pick<User, "away" | "name" | "username" | "bio" | "verified">[];
|
||||||
|
@ -321,6 +328,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||||
theme: user.theme,
|
theme: user.theme,
|
||||||
brandColor: user.brandColor,
|
brandColor: user.brandColor,
|
||||||
darkBrandColor: user.darkBrandColor,
|
darkBrandColor: user.darkBrandColor,
|
||||||
|
organizationSlug: user.organization?.slug ?? null,
|
||||||
allowSEOIndexing: user.allowSEOIndexing ?? true,
|
allowSEOIndexing: user.allowSEOIndexing ?? true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ const querySchema = z
|
||||||
.object({
|
.object({
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
teamname: z.string(),
|
teamname: z.string(),
|
||||||
|
orgSlug: z.string(),
|
||||||
/**
|
/**
|
||||||
* Allow fetching avatar of a particular organization
|
* Allow fetching avatar of a particular organization
|
||||||
* Avatars being public, we need not worry about others accessing it.
|
* Avatars being public, we need not worry about others accessing it.
|
||||||
|
@ -19,7 +20,7 @@ const querySchema = z
|
||||||
.partial();
|
.partial();
|
||||||
|
|
||||||
async function getIdentityData(req: NextApiRequest) {
|
async function getIdentityData(req: NextApiRequest) {
|
||||||
const { username, teamname, orgId } = querySchema.parse(req.query);
|
const { username, teamname, orgId, orgSlug } = querySchema.parse(req.query);
|
||||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req.headers.host ?? "");
|
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req.headers.host ?? "");
|
||||||
|
|
||||||
const org = isValidOrgDomain ? currentOrgDomain : null;
|
const org = isValidOrgDomain ? currentOrgDomain : null;
|
||||||
|
@ -59,7 +60,23 @@ async function getIdentityData(req: NextApiRequest) {
|
||||||
org,
|
org,
|
||||||
name: teamname,
|
name: teamname,
|
||||||
email: null,
|
email: null,
|
||||||
avatar: team?.logo || getPlaceholderAvatar(null, teamname),
|
avatar: getPlaceholderAvatar(team?.logo, teamname),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (orgSlug) {
|
||||||
|
const org = await prisma.team.findFirst({
|
||||||
|
where: getSlugOrRequestedSlug(orgSlug),
|
||||||
|
select: {
|
||||||
|
slug: true,
|
||||||
|
logo: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
org: org?.slug,
|
||||||
|
name: org?.name,
|
||||||
|
email: null,
|
||||||
|
avatar: getPlaceholderAvatar(org?.logo, org?.name),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -781,6 +781,12 @@ const CreateFirstEventTypeView = () => {
|
||||||
Icon={LinkIcon}
|
Icon={LinkIcon}
|
||||||
headline={t("new_event_type_heading")}
|
headline={t("new_event_type_heading")}
|
||||||
description={t("new_event_type_description")}
|
description={t("new_event_type_description")}
|
||||||
|
className="mb-16"
|
||||||
|
buttonRaw={
|
||||||
|
<Button href="?dialog=new" variant="button">
|
||||||
|
{t("create")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { Controller, useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||||
|
import OrganizationAvatar from "@calcom/features/ee/organizations/components/OrganizationAvatar";
|
||||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
import { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
|
import { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
@ -14,10 +15,10 @@ import turndown from "@calcom/lib/turndownService";
|
||||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||||
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Avatar,
|
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
|
@ -223,6 +224,7 @@ const ProfileView = () => {
|
||||||
key={JSON.stringify(defaultValues)}
|
key={JSON.stringify(defaultValues)}
|
||||||
defaultValues={defaultValues}
|
defaultValues={defaultValues}
|
||||||
isLoading={updateProfileMutation.isLoading}
|
isLoading={updateProfileMutation.isLoading}
|
||||||
|
userOrganization={user.organization}
|
||||||
onSubmit={(values) => {
|
onSubmit={(values) => {
|
||||||
if (values.email !== user.email && isCALIdentityProvider) {
|
if (values.email !== user.email && isCALIdentityProvider) {
|
||||||
setTempFormValues(values);
|
setTempFormValues(values);
|
||||||
|
@ -364,11 +366,13 @@ const ProfileForm = ({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
extraField,
|
extraField,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
|
userOrganization,
|
||||||
}: {
|
}: {
|
||||||
defaultValues: FormValues;
|
defaultValues: FormValues;
|
||||||
onSubmit: (values: FormValues) => void;
|
onSubmit: (values: FormValues) => void;
|
||||||
extraField?: React.ReactNode;
|
extraField?: React.ReactNode;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
userOrganization: RouterOutputs["viewer"]["me"]["organization"];
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [firstRender, setFirstRender] = useState(true);
|
const [firstRender, setFirstRender] = useState(true);
|
||||||
|
@ -406,7 +410,12 @@ const ProfileForm = ({
|
||||||
name="avatar"
|
name="avatar"
|
||||||
render={({ field: { value } }) => (
|
render={({ field: { value } }) => (
|
||||||
<>
|
<>
|
||||||
<Avatar alt="" imageSrc={value} size="lg" />
|
<OrganizationAvatar
|
||||||
|
alt={formMethods.getValues("username")}
|
||||||
|
imageSrc={value}
|
||||||
|
size="lg"
|
||||||
|
organizationSlug={userOrganization.slug}
|
||||||
|
/>
|
||||||
<div className="ms-4">
|
<div className="ms-4">
|
||||||
<ImageUploader
|
<ImageUploader
|
||||||
target="avatar"
|
target="avatar"
|
||||||
|
|
|
@ -142,7 +142,7 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
|
||||||
<TextField
|
<TextField
|
||||||
addOnLeading={
|
addOnLeading={
|
||||||
orgSlug
|
orgSlug
|
||||||
? getOrgFullDomain(orgSlug, { protocol: true })
|
? `${getOrgFullDomain(orgSlug, { protocol: true })}/`
|
||||||
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/`
|
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/`
|
||||||
}
|
}
|
||||||
{...register("username")}
|
{...register("username")}
|
||||||
|
|
|
@ -1,60 +1,21 @@
|
||||||
import { expect } from "@playwright/test";
|
import { expect } from "@playwright/test";
|
||||||
import type { Prisma } from "@prisma/client";
|
|
||||||
import { uuid } from "short-uuid";
|
import { uuid } from "short-uuid";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { BookingStatus } from "@calcom/prisma/enums";
|
import { BookingStatus } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import type { Fixtures } from "./lib/fixtures";
|
|
||||||
import { test } from "./lib/fixtures";
|
import { test } from "./lib/fixtures";
|
||||||
import {
|
import {
|
||||||
bookTimeSlot,
|
bookTimeSlot,
|
||||||
createNewSeatedEventType,
|
createNewSeatedEventType,
|
||||||
selectFirstAvailableTimeSlotNextMonth,
|
selectFirstAvailableTimeSlotNextMonth,
|
||||||
|
createUserWithSeatedEventAndAttendees,
|
||||||
} from "./lib/testUtils";
|
} from "./lib/testUtils";
|
||||||
|
|
||||||
test.describe.configure({ mode: "parallel" });
|
test.describe.configure({ mode: "parallel" });
|
||||||
test.afterEach(({ users }) => users.deleteAll());
|
test.afterEach(({ users }) => users.deleteAll());
|
||||||
|
|
||||||
async function createUserWithSeatedEvent(users: Fixtures["users"]) {
|
|
||||||
const slug = "seats";
|
|
||||||
const user = await users.create({
|
|
||||||
eventTypes: [
|
|
||||||
{
|
|
||||||
title: "Seated event",
|
|
||||||
slug,
|
|
||||||
seatsPerTimeSlot: 10,
|
|
||||||
requiresConfirmation: true,
|
|
||||||
length: 30,
|
|
||||||
disableGuests: true, // should always be true for seated events
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const eventType = user.eventTypes.find((e) => e.slug === slug)!;
|
|
||||||
return { user, eventType };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createUserWithSeatedEventAndAttendees(
|
|
||||||
fixtures: Pick<Fixtures, "users" | "bookings">,
|
|
||||||
attendees: Prisma.AttendeeCreateManyBookingInput[]
|
|
||||||
) {
|
|
||||||
const { user, eventType } = await createUserWithSeatedEvent(fixtures.users);
|
|
||||||
const booking = await fixtures.bookings.create(user.id, user.username, eventType.id, {
|
|
||||||
status: BookingStatus.ACCEPTED,
|
|
||||||
// startTime with 1 day from now and endTime half hour after
|
|
||||||
startTime: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
|
||||||
endTime: new Date(Date.now() + 24 * 60 * 60 * 1000 + 30 * 60 * 1000),
|
|
||||||
attendees: {
|
|
||||||
createMany: {
|
|
||||||
data: attendees,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return { user, eventType, booking };
|
|
||||||
}
|
|
||||||
|
|
||||||
test.describe("Booking with Seats", () => {
|
test.describe("Booking with Seats", () => {
|
||||||
test("User can create a seated event (2 seats as example)", async ({ users, page }) => {
|
test("User can create a seated event (2 seats as example)", async ({ users, page }) => {
|
||||||
const user = await users.create({ name: "Seated event" });
|
const user = await users.create({ name: "Seated event" });
|
||||||
|
|
|
@ -6,6 +6,10 @@ import { createServer } from "http";
|
||||||
import { noop } from "lodash";
|
import { noop } from "lodash";
|
||||||
import type { API, Messages } from "mailhog";
|
import type { API, Messages } from "mailhog";
|
||||||
|
|
||||||
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
|
import { BookingStatus } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
|
import type { Fixtures } from "./fixtures";
|
||||||
import { test } from "./fixtures";
|
import { test } from "./fixtures";
|
||||||
|
|
||||||
export function todo(title: string) {
|
export function todo(title: string) {
|
||||||
|
@ -192,6 +196,7 @@ export async function installAppleCalendar(page: Page) {
|
||||||
await page.waitForURL("/apps/apple-calendar");
|
await page.waitForURL("/apps/apple-calendar");
|
||||||
await page.click('[data-testid="install-app-button"]');
|
await page.click('[data-testid="install-app-button"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEmailsReceivedByUser({
|
export async function getEmailsReceivedByUser({
|
||||||
emails,
|
emails,
|
||||||
userEmail,
|
userEmail,
|
||||||
|
@ -228,3 +233,44 @@ export async function expectEmailsToHaveSubject({
|
||||||
expect(organizerFirstEmail.subject).toBe(emailSubject);
|
expect(organizerFirstEmail.subject).toBe(emailSubject);
|
||||||
expect(bookerFirstEmail.subject).toBe(emailSubject);
|
expect(bookerFirstEmail.subject).toBe(emailSubject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this method is not used anywhere else
|
||||||
|
// but I'm keeping it here in case we need in the future
|
||||||
|
async function createUserWithSeatedEvent(users: Fixtures["users"]) {
|
||||||
|
const slug = "seats";
|
||||||
|
const user = await users.create({
|
||||||
|
eventTypes: [
|
||||||
|
{
|
||||||
|
title: "Seated event",
|
||||||
|
slug,
|
||||||
|
seatsPerTimeSlot: 10,
|
||||||
|
requiresConfirmation: true,
|
||||||
|
length: 30,
|
||||||
|
disableGuests: true, // should always be true for seated events
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const eventType = user.eventTypes.find((e) => e.slug === slug)!;
|
||||||
|
return { user, eventType };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createUserWithSeatedEventAndAttendees(
|
||||||
|
fixtures: Pick<Fixtures, "users" | "bookings">,
|
||||||
|
attendees: Prisma.AttendeeCreateManyBookingInput[]
|
||||||
|
) {
|
||||||
|
const { user, eventType } = await createUserWithSeatedEvent(fixtures.users);
|
||||||
|
|
||||||
|
const booking = await fixtures.bookings.create(user.id, user.username, eventType.id, {
|
||||||
|
status: BookingStatus.ACCEPTED,
|
||||||
|
// startTime with 1 day from now and endTime half hour after
|
||||||
|
startTime: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
||||||
|
endTime: new Date(Date.now() + 24 * 60 * 60 * 1000 + 30 * 60 * 1000),
|
||||||
|
attendees: {
|
||||||
|
createMany: {
|
||||||
|
data: attendees,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { user, eventType, booking };
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import type { Page } from "@playwright/test";
|
import type { Page } from "@playwright/test";
|
||||||
import { expect } from "@playwright/test";
|
import { expect } from "@playwright/test";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import { BookingStatus } from "@calcom/prisma/client";
|
||||||
|
|
||||||
import { test } from "./lib/fixtures";
|
import { test } from "./lib/fixtures";
|
||||||
import {
|
import {
|
||||||
|
@ -8,6 +12,7 @@ import {
|
||||||
selectFirstAvailableTimeSlotNextMonth,
|
selectFirstAvailableTimeSlotNextMonth,
|
||||||
waitFor,
|
waitFor,
|
||||||
gotoRoutingLink,
|
gotoRoutingLink,
|
||||||
|
createUserWithSeatedEventAndAttendees,
|
||||||
} from "./lib/testUtils";
|
} from "./lib/testUtils";
|
||||||
|
|
||||||
// remove dynamic properties that differs depending on where you run the tests
|
// remove dynamic properties that differs depending on where you run the tests
|
||||||
|
@ -15,6 +20,29 @@ const dynamic = "[redacted/dynamic]";
|
||||||
|
|
||||||
test.afterEach(({ users }) => users.deleteAll());
|
test.afterEach(({ users }) => users.deleteAll());
|
||||||
|
|
||||||
|
async function createWebhookReceiver(page: Page) {
|
||||||
|
const webhookReceiver = createHttpServer();
|
||||||
|
|
||||||
|
await page.goto(`/settings/developer/webhooks`);
|
||||||
|
|
||||||
|
// --- add webhook
|
||||||
|
await page.click('[data-testid="new_webhook"]');
|
||||||
|
|
||||||
|
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
|
||||||
|
|
||||||
|
await page.fill('[name="secret"]', "secret");
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
page.click("[type=submit]"),
|
||||||
|
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// page contains the url
|
||||||
|
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
|
||||||
|
|
||||||
|
return webhookReceiver;
|
||||||
|
}
|
||||||
|
|
||||||
test.describe("BOOKING_CREATED", async () => {
|
test.describe("BOOKING_CREATED", async () => {
|
||||||
test("add webhook & test that creating an event triggers a webhook call", async ({
|
test("add webhook & test that creating an event triggers a webhook call", async ({
|
||||||
page,
|
page,
|
||||||
|
@ -388,6 +416,147 @@ test.describe("BOOKING_REQUESTED", async () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe("BOOKING_RESCHEDULED", async () => {
|
||||||
|
test("can reschedule a booking and get a booking rescheduled event", async ({ page, users, bookings }) => {
|
||||||
|
const user = await users.create();
|
||||||
|
const [eventType] = user.eventTypes;
|
||||||
|
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
const webhookReceiver = await createWebhookReceiver(page);
|
||||||
|
|
||||||
|
const booking = await bookings.create(user.id, user.username, eventType.id, {
|
||||||
|
status: BookingStatus.ACCEPTED,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`/${user.username}/${eventType.slug}?rescheduleUid=${booking.uid}`);
|
||||||
|
|
||||||
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||||
|
|
||||||
|
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/.*booking/);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking?.uid } })!;
|
||||||
|
expect(newBooking).not.toBeNull();
|
||||||
|
|
||||||
|
// --- check that webhook was called
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(webhookReceiver.requestList.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [request] = webhookReceiver.requestList;
|
||||||
|
|
||||||
|
expect(request.body).toMatchObject({
|
||||||
|
triggerEvent: "BOOKING_RESCHEDULED",
|
||||||
|
payload: {
|
||||||
|
uid: newBooking?.uid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("when rescheduling to a booking that already exists, should send a booking rescheduled event with the existant booking uid", async ({
|
||||||
|
page,
|
||||||
|
users,
|
||||||
|
bookings,
|
||||||
|
}) => {
|
||||||
|
const { user, eventType, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
|
||||||
|
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||||
|
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
await prisma.eventType.update({
|
||||||
|
where: { id: eventType.id },
|
||||||
|
data: { requiresConfirmation: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
const webhookReceiver = await createWebhookReceiver(page);
|
||||||
|
|
||||||
|
const bookingAttendees = await prisma.attendee.findMany({
|
||||||
|
where: { bookingId: booking.id },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const bookingSeats = bookingAttendees.map((attendee) => ({
|
||||||
|
bookingId: booking.id,
|
||||||
|
attendeeId: attendee.id,
|
||||||
|
referenceUid: uuidv4(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
await prisma.bookingSeat.createMany({
|
||||||
|
data: bookingSeats,
|
||||||
|
});
|
||||||
|
|
||||||
|
const references = await prisma.bookingSeat.findMany({
|
||||||
|
where: { bookingId: booking.id },
|
||||||
|
include: { attendee: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`/reschedule/${references[0].referenceUid}`);
|
||||||
|
|
||||||
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||||
|
|
||||||
|
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/.*booking/);
|
||||||
|
|
||||||
|
const newBooking = await prisma.booking.findFirst({
|
||||||
|
where: {
|
||||||
|
attendees: {
|
||||||
|
some: {
|
||||||
|
email: bookingAttendees[0].email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- ensuring that new booking was created
|
||||||
|
expect(newBooking).not.toBeNull();
|
||||||
|
|
||||||
|
// --- check that webhook was called
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(webhookReceiver.requestList.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [firstRequest] = webhookReceiver.requestList;
|
||||||
|
|
||||||
|
expect(firstRequest?.body).toMatchObject({
|
||||||
|
triggerEvent: "BOOKING_RESCHEDULED",
|
||||||
|
payload: {
|
||||||
|
uid: newBooking?.uid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`/reschedule/${references[1].referenceUid}`);
|
||||||
|
|
||||||
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||||
|
|
||||||
|
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/.*booking/);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(webhookReceiver.requestList.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [_, secondRequest] = webhookReceiver.requestList;
|
||||||
|
|
||||||
|
expect(secondRequest?.body).toMatchObject({
|
||||||
|
triggerEvent: "BOOKING_RESCHEDULED",
|
||||||
|
payload: {
|
||||||
|
// in the current implementation, it is the same as the first booking
|
||||||
|
uid: newBooking?.uid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test.describe("FORM_SUBMITTED", async () => {
|
test.describe("FORM_SUBMITTED", async () => {
|
||||||
test("on submitting user form, triggers user webhook", async ({ page, users, routingForms }) => {
|
test("on submitting user form, triggers user webhook", async ({ page, users, routingForms }) => {
|
||||||
const webhookReceiver = createHttpServer();
|
const webhookReceiver = createHttpServer();
|
||||||
|
|
|
@ -1078,7 +1078,7 @@
|
||||||
"url_start_with_https": "يجب أن يبدأ العنوان بـ http:// أو https://",
|
"url_start_with_https": "يجب أن يبدأ العنوان بـ http:// أو https://",
|
||||||
"number_provided": "سيتم توفير رقم الهاتف",
|
"number_provided": "سيتم توفير رقم الهاتف",
|
||||||
"before_event_trigger": "قبل بدء الحدث",
|
"before_event_trigger": "قبل بدء الحدث",
|
||||||
"event_cancelled_trigger": "متى تم إلغاء هذا الحدث",
|
"event_cancelled_trigger": "عند إلغاء هذا الحدث",
|
||||||
"new_event_trigger": "متى تم حجز الحدث الجديد",
|
"new_event_trigger": "متى تم حجز الحدث الجديد",
|
||||||
"email_host_action": "إرسال رسالة إلكترونية إلى المضيف",
|
"email_host_action": "إرسال رسالة إلكترونية إلى المضيف",
|
||||||
"email_attendee_action": "إرسال رسالة إلكترونية إلى الحضور",
|
"email_attendee_action": "إرسال رسالة إلكترونية إلى الحضور",
|
||||||
|
@ -1833,7 +1833,7 @@
|
||||||
"invite_link_copied": "تم نسخ رابط الدعوة",
|
"invite_link_copied": "تم نسخ رابط الدعوة",
|
||||||
"invite_link_deleted": "تم حذف رابط الدعوة",
|
"invite_link_deleted": "تم حذف رابط الدعوة",
|
||||||
"invite_link_updated": "تم حفظ إعدادات رابط الدعوة",
|
"invite_link_updated": "تم حفظ إعدادات رابط الدعوة",
|
||||||
"link_expires_after": "تم تعيين الروابط للانتهاء بعد...",
|
"link_expires_after": "تم تعيين انتهاء صلاحية الروابط بعد...",
|
||||||
"one_day": "1 يوم",
|
"one_day": "1 يوم",
|
||||||
"seven_days": "7 أيام",
|
"seven_days": "7 أيام",
|
||||||
"thirty_days": "30 يومًا",
|
"thirty_days": "30 يومًا",
|
||||||
|
@ -1884,7 +1884,7 @@
|
||||||
"organization_verify_email_body": "الرجاء استخدام الرمز أدناه لتأكيد عنوان بريدك الإلكتروني لمواصلة إعداد منظمتك.",
|
"organization_verify_email_body": "الرجاء استخدام الرمز أدناه لتأكيد عنوان بريدك الإلكتروني لمواصلة إعداد منظمتك.",
|
||||||
"additional_url_parameters": "معلمات الرابط الإضافية",
|
"additional_url_parameters": "معلمات الرابط الإضافية",
|
||||||
"about_your_organization": "حول منظمتك",
|
"about_your_organization": "حول منظمتك",
|
||||||
"about_your_organization_description": "المنظمات هي بيئات مشتركة حيث يمكنك إنشاء فرق متعددة مع أعضاء مشتركين وأنواع الأحداث والتطبيقات ومهام سير العمل والمزيد.",
|
"about_your_organization_description": "المنظمات هي بيئات مشتركة حيث يمكنك إنشاء فرق متعددة مع أعضاء مشتركين وأنواع الأحداث والتطبيقات ومهام سير العمل المشتركة والمزيد.",
|
||||||
"create_your_teams": "إنشاء فرقك",
|
"create_your_teams": "إنشاء فرقك",
|
||||||
"create_your_teams_description": "ابدأوا الجدولة معًا عن طريق إضافة أعضاء فريقك إلى منظمتك",
|
"create_your_teams_description": "ابدأوا الجدولة معًا عن طريق إضافة أعضاء فريقك إلى منظمتك",
|
||||||
"invite_organization_admins": "دعوة مشرفي منظمتك",
|
"invite_organization_admins": "دعوة مشرفي منظمتك",
|
||||||
|
@ -1925,7 +1925,7 @@
|
||||||
"404_the_org": "المنظمة",
|
"404_the_org": "المنظمة",
|
||||||
"404_the_team": "الفريق",
|
"404_the_team": "الفريق",
|
||||||
"404_claim_entity_org": "المطالبة بنطاقك الفرعي لمنظمتك",
|
"404_claim_entity_org": "المطالبة بنطاقك الفرعي لمنظمتك",
|
||||||
"404_claim_entity_team": "المطالبة بهذا الفريق والبدء في إدارة الجداول الزمنية بشكل جماعي",
|
"404_claim_entity_team": "انضم لهذا الفريق وابدأ في إدارة الجداول الزمنية بشكل جماعي",
|
||||||
"insights_all_org_filter": "الكل",
|
"insights_all_org_filter": "الكل",
|
||||||
"insights_team_filter": "الفريق: {{teamName}}",
|
"insights_team_filter": "الفريق: {{teamName}}",
|
||||||
"insights_user_filter": "المستخدم: {{userName}}",
|
"insights_user_filter": "المستخدم: {{userName}}",
|
||||||
|
|
|
@ -2045,5 +2045,6 @@
|
||||||
"recently_added":"Recently added",
|
"recently_added":"Recently added",
|
||||||
"no_members_found": "No members found",
|
"no_members_found": "No members found",
|
||||||
"event_setup_length_error":"Event Setup: The duration must be at least 1 minute.",
|
"event_setup_length_error":"Event Setup: The duration must be at least 1 minute.",
|
||||||
|
"availability_schedules":"Availability Schedules",
|
||||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||||
}
|
}
|
||||||
|
|
|
@ -129,7 +129,7 @@
|
||||||
"team_upgrade_banner_description": "Gracias por probar nuestro nuevo plan de equipo. Notamos que su equipo \"{{teamName}}\" necesita actualizarse.",
|
"team_upgrade_banner_description": "Gracias por probar nuestro nuevo plan de equipo. Notamos que su equipo \"{{teamName}}\" necesita actualizarse.",
|
||||||
"upgrade_banner_action": "Actualizar aquí",
|
"upgrade_banner_action": "Actualizar aquí",
|
||||||
"team_upgraded_successfully": "¡Tu equipo se actualizó con éxito!",
|
"team_upgraded_successfully": "¡Tu equipo se actualizó con éxito!",
|
||||||
"org_upgrade_banner_description": "Gracias por probar nuestro nuevo plan de equipo. Notamos que su equipo \"{{teamName}}\" necesita actualizarse.",
|
"org_upgrade_banner_description": "Gracias por probar nuestro nuevo plan Organization. Notamos que su Organization \"{{teamName}}\" necesita actualizarse.",
|
||||||
"org_upgraded_successfully": "Su Organization se actualizó con éxito.",
|
"org_upgraded_successfully": "Su Organization se actualizó con éxito.",
|
||||||
"use_link_to_reset_password": "Utilice el enlace de abajo para restablecer su contraseña",
|
"use_link_to_reset_password": "Utilice el enlace de abajo para restablecer su contraseña",
|
||||||
"hey_there": "Hola,",
|
"hey_there": "Hola,",
|
||||||
|
@ -306,7 +306,7 @@
|
||||||
"password_has_been_reset_login": "Su contraseña ha sido restablecida. Ahora puede iniciar sesión con su nueva contraseña.",
|
"password_has_been_reset_login": "Su contraseña ha sido restablecida. Ahora puede iniciar sesión con su nueva contraseña.",
|
||||||
"layout": "Diseño",
|
"layout": "Diseño",
|
||||||
"bookerlayout_default_title": "Vista predeterminada",
|
"bookerlayout_default_title": "Vista predeterminada",
|
||||||
"bookerlayout_description": "Puede seleccionar varios y quienes le reserven pueden cambiar de vista.",
|
"bookerlayout_description": "Puede seleccionar varias y quienes le reserven pueden cambiar de vista.",
|
||||||
"bookerlayout_user_settings_title": "Diseño de reserva",
|
"bookerlayout_user_settings_title": "Diseño de reserva",
|
||||||
"bookerlayout_user_settings_description": "Puede seleccionar varios y quienes le reservan pueden cambiar de vista. Esto se puede anular por evento.",
|
"bookerlayout_user_settings_description": "Puede seleccionar varios y quienes le reservan pueden cambiar de vista. Esto se puede anular por evento.",
|
||||||
"bookerlayout_month_view": "Mes",
|
"bookerlayout_month_view": "Mes",
|
||||||
|
@ -315,7 +315,7 @@
|
||||||
"bookerlayout_error_min_one_enabled": "Al menos un diseño tiene que estar habilitado.",
|
"bookerlayout_error_min_one_enabled": "Al menos un diseño tiene que estar habilitado.",
|
||||||
"bookerlayout_error_default_not_enabled": "El diseño que seleccionó como vista predeterminada no es parte de los diseños habilitados.",
|
"bookerlayout_error_default_not_enabled": "El diseño que seleccionó como vista predeterminada no es parte de los diseños habilitados.",
|
||||||
"bookerlayout_error_unknown_layout": "El diseño seleccionado no es un diseño válido.",
|
"bookerlayout_error_unknown_layout": "El diseño seleccionado no es un diseño válido.",
|
||||||
"bookerlayout_override_global_settings": "Puede gestionar esto para todos sus tipos de eventos en <2>configuración / apariencia</2> o <6>sobrescribir solo para este evento</6>.",
|
"bookerlayout_override_global_settings": "Puede gestionar esto para todos sus tipos de eventos en <2>configuración / apariencia</2> o <6>anular solo para este evento</6>.",
|
||||||
"unexpected_error_try_again": "Ocurrió un error inesperado. Inténtelo de nuevo.",
|
"unexpected_error_try_again": "Ocurrió un error inesperado. Inténtelo de nuevo.",
|
||||||
"sunday_time_error": "Hora inválida del domingo",
|
"sunday_time_error": "Hora inválida del domingo",
|
||||||
"monday_time_error": "Hora inválida del lunes",
|
"monday_time_error": "Hora inválida del lunes",
|
||||||
|
@ -551,7 +551,7 @@
|
||||||
"team_description": "Comentarios sobre tu equipo. Esta información aparecerá en la página de la URL de tu equipo.",
|
"team_description": "Comentarios sobre tu equipo. Esta información aparecerá en la página de la URL de tu equipo.",
|
||||||
"org_description": "Algunas frases sobre su organización. Esto aparecerá en la página de la URL de su organización.",
|
"org_description": "Algunas frases sobre su organización. Esto aparecerá en la página de la URL de su organización.",
|
||||||
"members": "Miembros",
|
"members": "Miembros",
|
||||||
"organization_members": "Miembros de la organización",
|
"organization_members": "Miembros de Organization",
|
||||||
"member": "Miembro",
|
"member": "Miembro",
|
||||||
"number_member_one": "{{count}} miembro",
|
"number_member_one": "{{count}} miembro",
|
||||||
"number_member_other": "{{count}} miembros",
|
"number_member_other": "{{count}} miembros",
|
||||||
|
@ -699,7 +699,7 @@
|
||||||
"create_team_to_get_started": "Crea un equipo para empezar",
|
"create_team_to_get_started": "Crea un equipo para empezar",
|
||||||
"teams": "Equipos",
|
"teams": "Equipos",
|
||||||
"team": "Equipo",
|
"team": "Equipo",
|
||||||
"organization": "Organización",
|
"organization": "Organization",
|
||||||
"team_billing": "Facturación del equipo",
|
"team_billing": "Facturación del equipo",
|
||||||
"team_billing_description": "Gestione la facturación para su equipo",
|
"team_billing_description": "Gestione la facturación para su equipo",
|
||||||
"upgrade_to_flexible_pro_title": "Hemos cambiado la facturación de los equipos",
|
"upgrade_to_flexible_pro_title": "Hemos cambiado la facturación de los equipos",
|
||||||
|
@ -1916,7 +1916,7 @@
|
||||||
"org_no_teams_yet_description": "Si usted es un administrador, asegúrese de crear equipos para que se muestren aquí.",
|
"org_no_teams_yet_description": "Si usted es un administrador, asegúrese de crear equipos para que se muestren aquí.",
|
||||||
"set_up": "Configurar",
|
"set_up": "Configurar",
|
||||||
"set_up_your_profile": "Configure su perfil",
|
"set_up_your_profile": "Configure su perfil",
|
||||||
"set_up_your_profile_description": "Informe a las personas quién es usted dentro de {{orgName}} cuándo interactúan con su enlace público.",
|
"set_up_your_profile_description": "Informe a las personas quién es usted dentro de {{orgName}} y cuándo interactúen con su enlace público.",
|
||||||
"my_profile": "Mi perfil",
|
"my_profile": "Mi perfil",
|
||||||
"my_settings": "Mi configuración",
|
"my_settings": "Mi configuración",
|
||||||
"crm": "CRM",
|
"crm": "CRM",
|
||||||
|
|
|
@ -1060,7 +1060,7 @@
|
||||||
"your_unique_api_key": "Votre clé API unique",
|
"your_unique_api_key": "Votre clé API unique",
|
||||||
"copy_safe_api_key": "Copiez cette clé API et conservez-la dans un endroit sûr. Si vous perdez cette clé, vous devrez en générer une nouvelle.",
|
"copy_safe_api_key": "Copiez cette clé API et conservez-la dans un endroit sûr. Si vous perdez cette clé, vous devrez en générer une nouvelle.",
|
||||||
"zapier_setup_instructions": "<0>Connectez-vous à votre compte Zapier et créez un nouveau Zap.</0><1>Sélectionnez Cal.com comme application déclencheur. Choisissez également un événement déclencheur.</1><2>Choisissez votre compte, puis saisissez votre clé API unique.</2><3>Testez votre déclencheur.</3><4>Vous êtes prêt !</4>",
|
"zapier_setup_instructions": "<0>Connectez-vous à votre compte Zapier et créez un nouveau Zap.</0><1>Sélectionnez Cal.com comme application déclencheur. Choisissez également un événement déclencheur.</1><2>Choisissez votre compte, puis saisissez votre clé API unique.</2><3>Testez votre déclencheur.</3><4>Vous êtes prêt !</4>",
|
||||||
"make_setup_instructions": "<0>Connectez-vous à votre compte Make et créez un nouveau Scénario.</0><1>Sélectionnez Cal.com comme application déclencheur. Choisissez également un événement déclencheur.</1><2>Choisissez votre compte, puis saisissez votre clé API unique.</2><3>Testez votre déclencheur.</3><4>Vous êtes prêt !</4>",
|
"make_setup_instructions": "<0>Accédez au <1><0>lien d'invitation Make</0></1> et installez l'application Cal.com.</0><1>Connectez-vous à votre compte Make et créez un nouveau scénario.</1><2>Sélectionnez Cal.com comme application déclencheur. Choisissez également un événement déclencheur.</2><3>Choisissez votre compte, puis saisissez votre clé API unique.</3><4>Testez votre déclencheur.</4><5>Vous êtes prêt !</5>",
|
||||||
"install_zapier_app": "Veuillez d'abord installer l'application Zapier dans l'App Store.",
|
"install_zapier_app": "Veuillez d'abord installer l'application Zapier dans l'App Store.",
|
||||||
"install_make_app": "Veuillez d'abord installer l'application Make dans l'App Store.",
|
"install_make_app": "Veuillez d'abord installer l'application Make dans l'App Store.",
|
||||||
"connect_apple_server": "Se connecter au serveur d'Apple",
|
"connect_apple_server": "Se connecter au serveur d'Apple",
|
||||||
|
@ -1688,8 +1688,11 @@
|
||||||
"delete_sso_configuration_confirmation_description": "Voulez-vous vraiment supprimer la configuration {{connectionType}} ? Les membres de votre équipe utilisant la connexion {{connectionType}} ne pourront plus accéder à Cal.com.",
|
"delete_sso_configuration_confirmation_description": "Voulez-vous vraiment supprimer la configuration {{connectionType}} ? Les membres de votre équipe utilisant la connexion {{connectionType}} ne pourront plus accéder à Cal.com.",
|
||||||
"organizer_timezone": "Fuseau horaire de l'organisateur",
|
"organizer_timezone": "Fuseau horaire de l'organisateur",
|
||||||
"email_user_cta": "Voir l'invitation",
|
"email_user_cta": "Voir l'invitation",
|
||||||
|
"email_no_user_invite_heading_team": "Vous avez été invité à rejoindre une équipe {{appName}}",
|
||||||
|
"email_no_user_invite_heading_org": "Vous avez été invité à rejoindre une organisation {{appName}}",
|
||||||
"email_no_user_invite_subheading": "{{invitedBy}} vous a invité à rejoindre son équipe sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre équipe d'organiser des rendez-vous sans échanges d'e-mails.",
|
"email_no_user_invite_subheading": "{{invitedBy}} vous a invité à rejoindre son équipe sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre équipe d'organiser des rendez-vous sans échanges d'e-mails.",
|
||||||
"email_user_invite_subheading_team": "{{invitedBy}} vous a invité à rejoindre son équipe « {{teamName}} » sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre équipe d'organiser des rendez-vous sans échanges d'e-mails.",
|
"email_user_invite_subheading_team": "{{invitedBy}} vous a invité à rejoindre son équipe « {{teamName}} » sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre équipe d'organiser des rendez-vous sans échanges d'e-mails.",
|
||||||
|
"email_user_invite_subheading_org": "{{invitedBy}} vous a invité à rejoindre son organisation « {{teamName}} » sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre organisation d'organiser des rendez-vous sans échanges d'e-mails.",
|
||||||
"email_no_user_invite_steps_intro": "Nous vous guiderons à travers quelques étapes courtes et vous profiterez d'une planification sans stress avec votre {{entity}} en un rien de temps.",
|
"email_no_user_invite_steps_intro": "Nous vous guiderons à travers quelques étapes courtes et vous profiterez d'une planification sans stress avec votre {{entity}} en un rien de temps.",
|
||||||
"email_no_user_step_one": "Choisissez votre nom d'utilisateur",
|
"email_no_user_step_one": "Choisissez votre nom d'utilisateur",
|
||||||
"email_no_user_step_two": "Connectez votre compte de calendrier",
|
"email_no_user_step_two": "Connectez votre compte de calendrier",
|
||||||
|
@ -2041,5 +2044,7 @@
|
||||||
"include_calendar_event": "Inclure l'événement du calendrier",
|
"include_calendar_event": "Inclure l'événement du calendrier",
|
||||||
"recently_added": "Ajouté récemment",
|
"recently_added": "Ajouté récemment",
|
||||||
"no_members_found": "Aucun membre trouvé",
|
"no_members_found": "Aucun membre trouvé",
|
||||||
|
"event_setup_length_error": "Configuration de l'événement : la durée doit être d'au moins 1 minute.",
|
||||||
|
"availability_schedules": "Horaires de disponibilité",
|
||||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||||
}
|
}
|
||||||
|
|
|
@ -306,7 +306,7 @@
|
||||||
"password_has_been_reset_login": "パスワードがリセットされました。新しく作成したパスワードでログインできるようになりました。",
|
"password_has_been_reset_login": "パスワードがリセットされました。新しく作成したパスワードでログインできるようになりました。",
|
||||||
"layout": "レイアウト",
|
"layout": "レイアウト",
|
||||||
"bookerlayout_default_title": "デフォルトの表示",
|
"bookerlayout_default_title": "デフォルトの表示",
|
||||||
"bookerlayout_description": "複数のものを選択することができ、予約者は表示を切り替えることができます。",
|
"bookerlayout_description": "複数のビューを選択することができ、予約者は表示を切り替えることができます。",
|
||||||
"bookerlayout_user_settings_title": "予約のレイアウト",
|
"bookerlayout_user_settings_title": "予約のレイアウト",
|
||||||
"bookerlayout_user_settings_description": "複数のものを選択することができ、予約者は表示を切り替えることができます。これはイベントごとに上書きが可能です。",
|
"bookerlayout_user_settings_description": "複数のものを選択することができ、予約者は表示を切り替えることができます。これはイベントごとに上書きが可能です。",
|
||||||
"bookerlayout_month_view": "月",
|
"bookerlayout_month_view": "月",
|
||||||
|
@ -391,7 +391,7 @@
|
||||||
"user_dynamic_booking_disabled": "グループ内の一部のユーザーは、現在動的なグループ予約を無効にしています",
|
"user_dynamic_booking_disabled": "グループ内の一部のユーザーは、現在動的なグループ予約を無効にしています",
|
||||||
"allow_dynamic_booking_tooltip": "\"+\" を使って複数のユーザー名を追加することで動的に作成できるグループ予約リンク。例: \"{{appName}}/bailey+peer\"",
|
"allow_dynamic_booking_tooltip": "\"+\" を使って複数のユーザー名を追加することで動的に作成できるグループ予約リンク。例: \"{{appName}}/bailey+peer\"",
|
||||||
"allow_dynamic_booking": "出席者が動的なグループ予約を通じてあなたを予約できるようにする",
|
"allow_dynamic_booking": "出席者が動的なグループ予約を通じてあなたを予約できるようにする",
|
||||||
"dynamic_booking": "動的なグループリンク",
|
"dynamic_booking": "ダイナミックグループリンク",
|
||||||
"email": "Eメールアドレス",
|
"email": "Eメールアドレス",
|
||||||
"email_placeholder": "jdoe@example.com",
|
"email_placeholder": "jdoe@example.com",
|
||||||
"full_name": "フルネーム",
|
"full_name": "フルネーム",
|
||||||
|
@ -555,7 +555,7 @@
|
||||||
"member": "メンバー",
|
"member": "メンバー",
|
||||||
"number_member_one": "{{count}} 人のメンバー",
|
"number_member_one": "{{count}} 人のメンバー",
|
||||||
"number_member_other": "{{count}} 人のメンバー",
|
"number_member_other": "{{count}} 人のメンバー",
|
||||||
"number_selected": "{{count}} 件が選択されました",
|
"number_selected": "{{count}} が選択されました",
|
||||||
"owner": "所有者",
|
"owner": "所有者",
|
||||||
"admin": "管理者",
|
"admin": "管理者",
|
||||||
"administrator_user": "管理者ユーザー",
|
"administrator_user": "管理者ユーザー",
|
||||||
|
@ -1150,8 +1150,8 @@
|
||||||
"choose_template": "テンプレートを選択する",
|
"choose_template": "テンプレートを選択する",
|
||||||
"custom": "カスタム",
|
"custom": "カスタム",
|
||||||
"reminder": "リマインダー",
|
"reminder": "リマインダー",
|
||||||
"rescheduled": "スケジュール変更済み",
|
"rescheduled": "スケジュールが変更されました",
|
||||||
"completed": "完了",
|
"completed": "完了しました",
|
||||||
"reminder_email": "リマインダー:{{date}} の {{name}} との {{eventType}}",
|
"reminder_email": "リマインダー:{{date}} の {{name}} との {{eventType}}",
|
||||||
"not_triggering_existing_bookings": "イベントの予約時に電話番号の入力を求められるため、すでにある予約はトリガーされません。",
|
"not_triggering_existing_bookings": "イベントの予約時に電話番号の入力を求められるため、すでにある予約はトリガーされません。",
|
||||||
"minute_one": "{{count}} 分",
|
"minute_one": "{{count}} 分",
|
||||||
|
@ -1672,7 +1672,7 @@
|
||||||
"scheduler": "{Scheduler}",
|
"scheduler": "{Scheduler}",
|
||||||
"no_workflows": "ワークフローがありません",
|
"no_workflows": "ワークフローがありません",
|
||||||
"change_filter": "個人やチームのワークフローを表示するためのフィルターを変更します。",
|
"change_filter": "個人やチームのワークフローを表示するためのフィルターを変更します。",
|
||||||
"change_filter_common": "結果を表示するフィルターを変更します。",
|
"change_filter_common": "フィルターを変更して結果を表示します。",
|
||||||
"no_results_for_filter": "このフィルターに該当する結果はありません",
|
"no_results_for_filter": "このフィルターに該当する結果はありません",
|
||||||
"recommended_next_steps": "推奨される次のステップ",
|
"recommended_next_steps": "推奨される次のステップ",
|
||||||
"create_a_managed_event": "管理されたイベントの種類を作成",
|
"create_a_managed_event": "管理されたイベントの種類を作成",
|
||||||
|
@ -1696,7 +1696,7 @@
|
||||||
"booking_questions_title": "予約の質問",
|
"booking_questions_title": "予約の質問",
|
||||||
"booking_questions_description": "予約ページで尋ねる質問をカスタマイズする",
|
"booking_questions_description": "予約ページで尋ねる質問をカスタマイズする",
|
||||||
"add_a_booking_question": "質問を追加",
|
"add_a_booking_question": "質問を追加",
|
||||||
"identifier": "識別子",
|
"identifier": "ID",
|
||||||
"duplicate_email": "メールが重複しています",
|
"duplicate_email": "メールが重複しています",
|
||||||
"booking_with_payment_cancelled": "このイベントの支払いはもうできません",
|
"booking_with_payment_cancelled": "このイベントの支払いはもうできません",
|
||||||
"booking_with_payment_cancelled_already_paid": "この予約に関するお支払いの払い戻しについては、現在処理中です。",
|
"booking_with_payment_cancelled_already_paid": "この予約に関するお支払いの払い戻しについては、現在処理中です。",
|
||||||
|
@ -1840,7 +1840,7 @@
|
||||||
"invite_link_copied": "招待リンクをコピーしました",
|
"invite_link_copied": "招待リンクをコピーしました",
|
||||||
"invite_link_deleted": "招待リンクを削除しました",
|
"invite_link_deleted": "招待リンクを削除しました",
|
||||||
"invite_link_updated": "招待リンクの設定を保存しました",
|
"invite_link_updated": "招待リンクの設定を保存しました",
|
||||||
"link_expires_after": "リンクが期限切れとなるまで、あと...",
|
"link_expires_after": "リンクの期限切れまで...",
|
||||||
"one_day": "1 日",
|
"one_day": "1 日",
|
||||||
"seven_days": "7 日",
|
"seven_days": "7 日",
|
||||||
"thirty_days": "30 日",
|
"thirty_days": "30 日",
|
||||||
|
@ -1936,7 +1936,7 @@
|
||||||
"insights_all_org_filter": "すべて",
|
"insights_all_org_filter": "すべて",
|
||||||
"insights_team_filter": "チーム: {{teamName}}",
|
"insights_team_filter": "チーム: {{teamName}}",
|
||||||
"insights_user_filter": "ユーザー: {{userName}}",
|
"insights_user_filter": "ユーザー: {{userName}}",
|
||||||
"insights_subtitle": "イベント全体での予約に関する分析情報を表示する",
|
"insights_subtitle": "イベント全体での予約に関する Insights を表示する",
|
||||||
"custom_plan": "カスタムプラン",
|
"custom_plan": "カスタムプラン",
|
||||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
"have_any_questions": "Tem perguntas? Estamos disponíveis para ajudar.",
|
"have_any_questions": "Tem perguntas? Estamos disponíveis para ajudar.",
|
||||||
"reset_password_subject": "{{appName}}: Instruções de redefinição da senha",
|
"reset_password_subject": "{{appName}}: Instruções de redefinição da senha",
|
||||||
"verify_email_subject": "{{appName}}: Verifique a sua conta",
|
"verify_email_subject": "{{appName}}: Verifique a sua conta",
|
||||||
"check_your_email": "Confirme o seu e-mail",
|
"check_your_email": "Verifique o seu e-mail",
|
||||||
"verify_email_page_body": "Enviámos um e-mail para {{email}}. É importante verificar o seu endereço de e-mail para garantir que receberá as comunicações de {{appName}}.",
|
"verify_email_page_body": "Enviámos um e-mail para {{email}}. É importante verificar o seu endereço de e-mail para garantir que receberá as comunicações de {{appName}}.",
|
||||||
"verify_email_banner_body": "Confirme o seu endereço de e-mail para garantir a melhor entrega possível de e-mail e de agenda",
|
"verify_email_banner_body": "Confirme o seu endereço de e-mail para garantir a melhor entrega possível de e-mail e de agenda",
|
||||||
"verify_email_email_header": "Confirme o seu endereço de e-mail",
|
"verify_email_email_header": "Confirme o seu endereço de e-mail",
|
||||||
|
|
|
@ -118,7 +118,7 @@
|
||||||
"team_info": "Информация о команде",
|
"team_info": "Информация о команде",
|
||||||
"request_another_invitation_email": "Если вы не хотите использовать {{toEmail}} как ваш {{appName}} адрес электронной почты или уже есть аккаунт {{appName}}, пожалуйста, запросите другое приглашение на это письмо.",
|
"request_another_invitation_email": "Если вы не хотите использовать {{toEmail}} как ваш {{appName}} адрес электронной почты или уже есть аккаунт {{appName}}, пожалуйста, запросите другое приглашение на это письмо.",
|
||||||
"you_have_been_invited": "Вас пригласили присоединиться к команде {{teamName}}",
|
"you_have_been_invited": "Вас пригласили присоединиться к команде {{teamName}}",
|
||||||
"user_invited_you": "{{user}} пригласил вас в команду {{entity}} {{team}} в {{appName}}",
|
"user_invited_you": "{{user}} пригласил вас в команду {{team}} {{entity}} в {{appName}}",
|
||||||
"hidden_team_member_title": "В этой команде вы скрытый пользователь",
|
"hidden_team_member_title": "В этой команде вы скрытый пользователь",
|
||||||
"hidden_team_member_message": "Ваше место не оплачено. Перейдите на аккаунт PRO или свяжитесь с руководителем команды, чтобы сообщить ему, что он может оплатить ваше место.",
|
"hidden_team_member_message": "Ваше место не оплачено. Перейдите на аккаунт PRO или свяжитесь с руководителем команды, чтобы сообщить ему, что он может оплатить ваше место.",
|
||||||
"hidden_team_owner_message": "Чтобы работать с командами, необходим аккаунт Pro. До перехода на этот тариф Вы остаетесь скрытым пользователем.",
|
"hidden_team_owner_message": "Чтобы работать с командами, необходим аккаунт Pro. До перехода на этот тариф Вы остаетесь скрытым пользователем.",
|
||||||
|
@ -403,7 +403,7 @@
|
||||||
"recording_ready": "Ссылка для скачивания записи готова",
|
"recording_ready": "Ссылка для скачивания записи готова",
|
||||||
"booking_created": "Бронирование создано",
|
"booking_created": "Бронирование создано",
|
||||||
"booking_rejected": "Бронирование отклонено",
|
"booking_rejected": "Бронирование отклонено",
|
||||||
"booking_requested": "Поступил запрос на бронирование",
|
"booking_requested": "Запрос на бронирование отправлен",
|
||||||
"meeting_ended": "Встреча завершилась",
|
"meeting_ended": "Встреча завершилась",
|
||||||
"form_submitted": "Форма отправлена",
|
"form_submitted": "Форма отправлена",
|
||||||
"event_triggers": "Триггеры событий",
|
"event_triggers": "Триггеры событий",
|
||||||
|
@ -1879,7 +1879,7 @@
|
||||||
"create_for": "Создать для",
|
"create_for": "Создать для",
|
||||||
"organization_banner_description": "Создавайте рабочие среды, в рамках которых ваши команды смогут создавать общие приложения, рабочие процессы и типы событий с назначением участников по очереди и коллективным планированием.",
|
"organization_banner_description": "Создавайте рабочие среды, в рамках которых ваши команды смогут создавать общие приложения, рабочие процессы и типы событий с назначением участников по очереди и коллективным планированием.",
|
||||||
"organization_banner_title": "Управляйте организациями с несколькими командами",
|
"organization_banner_title": "Управляйте организациями с несколькими командами",
|
||||||
"set_up_your_organization": "Настройте организацию",
|
"set_up_your_organization": "Настройка профиля организации",
|
||||||
"organizations_description": "Организация — это общая рабочая среда, в которой команды могут создавать общие типы событий, приложения, рабочие процессы и многое другое.",
|
"organizations_description": "Организация — это общая рабочая среда, в которой команды могут создавать общие типы событий, приложения, рабочие процессы и многое другое.",
|
||||||
"must_enter_organization_name": "Необходимо ввести название организации",
|
"must_enter_organization_name": "Необходимо ввести название организации",
|
||||||
"must_enter_organization_admin_email": "Необходимо ввести ваш адрес электронной почты в организации",
|
"must_enter_organization_admin_email": "Необходимо ввести ваш адрес электронной почты в организации",
|
||||||
|
@ -1887,7 +1887,7 @@
|
||||||
"admin_username": "Имя пользователя администратора",
|
"admin_username": "Имя пользователя администратора",
|
||||||
"organization_name": "Название организации",
|
"organization_name": "Название организации",
|
||||||
"organization_url": "URL-адрес организации",
|
"organization_url": "URL-адрес организации",
|
||||||
"organization_verify_header": "Подтвердите адрес электронной почты вашей организации",
|
"organization_verify_header": "Подтвердите свой адрес электронной почты в организации",
|
||||||
"organization_verify_email_body": "С помощью кода ниже подтвердите свой адрес электронной почты, чтобы продолжить настройку организации.",
|
"organization_verify_email_body": "С помощью кода ниже подтвердите свой адрес электронной почты, чтобы продолжить настройку организации.",
|
||||||
"additional_url_parameters": "Дополнительные параметры URL-адреса",
|
"additional_url_parameters": "Дополнительные параметры URL-адреса",
|
||||||
"about_your_organization": "О вашей организации",
|
"about_your_organization": "О вашей организации",
|
||||||
|
|
|
@ -130,7 +130,7 @@
|
||||||
"team_upgrade_banner_description": "Hvala vam što isprobavate naš novi plan za timove. Primetili smo da vaš tim „{{teamName}}“ treba da se nadogradi.",
|
"team_upgrade_banner_description": "Hvala vam što isprobavate naš novi plan za timove. Primetili smo da vaš tim „{{teamName}}“ treba da se nadogradi.",
|
||||||
"upgrade_banner_action": "Nadogradite ovde",
|
"upgrade_banner_action": "Nadogradite ovde",
|
||||||
"team_upgraded_successfully": "Vaš tim je uspešno pretplaćen!",
|
"team_upgraded_successfully": "Vaš tim je uspešno pretplaćen!",
|
||||||
"org_upgrade_banner_description": "Hvala što isprobavate plan naše organizacije. Primetili smo da tim vaše organizacije „{{teamName}}” treba da se nadogradi.",
|
"org_upgrade_banner_description": "Hvala što isprobavate naš Organization plan. Primetili smo da vaš Organization „{{teamName}}” treba da se nadogradi.",
|
||||||
"org_upgraded_successfully": "Vaš Organization je uspešno nadograđen!",
|
"org_upgraded_successfully": "Vaš Organization je uspešno nadograđen!",
|
||||||
"use_link_to_reset_password": "Resetujte lozinku koristeći link ispod",
|
"use_link_to_reset_password": "Resetujte lozinku koristeći link ispod",
|
||||||
"hey_there": "Zdravo,",
|
"hey_there": "Zdravo,",
|
||||||
|
|
|
@ -93,6 +93,12 @@
|
||||||
--cal-brand-text: black;
|
--cal-brand-text: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
::-moz-selection {
|
::-moz-selection {
|
||||||
color: var(--cal-brand-text);
|
color: var(--cal-brand-text);
|
||||||
background: var(--cal-brand);
|
background: var(--cal-brand);
|
||||||
|
|
|
@ -3,15 +3,15 @@ import type { App_RoutingForms_Form } from "@prisma/client";
|
||||||
import { BaseEmailHtml, Info } from "@calcom/emails/src/components";
|
import { BaseEmailHtml, Info } from "@calcom/emails/src/components";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
|
||||||
import type { Response } from "../../types/types";
|
import type { OrderedResponses } from "../../types/types";
|
||||||
|
|
||||||
export const ResponseEmail = ({
|
export const ResponseEmail = ({
|
||||||
form,
|
form,
|
||||||
response,
|
orderedResponses,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
form: Pick<App_RoutingForms_Form, "id" | "name">;
|
form: Pick<App_RoutingForms_Form, "id" | "name">;
|
||||||
response: Response;
|
orderedResponses: OrderedResponses;
|
||||||
subject: string;
|
subject: string;
|
||||||
} & Partial<React.ComponentProps<typeof BaseEmailHtml>>) => {
|
} & Partial<React.ComponentProps<typeof BaseEmailHtml>>) => {
|
||||||
return (
|
return (
|
||||||
|
@ -36,11 +36,11 @@ export const ResponseEmail = ({
|
||||||
title={form.name}
|
title={form.name}
|
||||||
subtitle="New Response Received"
|
subtitle="New Response Received"
|
||||||
{...props}>
|
{...props}>
|
||||||
{Object.entries(response).map(([fieldId, fieldResponse]) => {
|
{orderedResponses.map((fieldResponse, index) => {
|
||||||
return (
|
return (
|
||||||
<Info
|
<Info
|
||||||
withSpacer
|
withSpacer
|
||||||
key={fieldId}
|
key={index}
|
||||||
label={fieldResponse.label}
|
label={fieldResponse.label}
|
||||||
description={
|
description={
|
||||||
fieldResponse.value instanceof Array ? fieldResponse.value.join(",") : fieldResponse.value
|
fieldResponse.value instanceof Array ? fieldResponse.value.join(",") : fieldResponse.value
|
||||||
|
|
|
@ -3,17 +3,25 @@ import type { App_RoutingForms_Form } from "@prisma/client";
|
||||||
import { renderEmail } from "@calcom/emails";
|
import { renderEmail } from "@calcom/emails";
|
||||||
import BaseEmail from "@calcom/emails/templates/_base-email";
|
import BaseEmail from "@calcom/emails/templates/_base-email";
|
||||||
|
|
||||||
import type { Response } from "../../types/types";
|
import type { OrderedResponses } from "../../types/types";
|
||||||
|
|
||||||
type Form = Pick<App_RoutingForms_Form, "id" | "name">;
|
type Form = Pick<App_RoutingForms_Form, "id" | "name">;
|
||||||
export default class ResponseEmail extends BaseEmail {
|
export default class ResponseEmail extends BaseEmail {
|
||||||
response: Response;
|
orderedResponses: OrderedResponses;
|
||||||
toAddresses: string[];
|
toAddresses: string[];
|
||||||
form: Form;
|
form: Form;
|
||||||
constructor({ toAddresses, response, form }: { form: Form; toAddresses: string[]; response: Response }) {
|
constructor({
|
||||||
|
toAddresses,
|
||||||
|
orderedResponses,
|
||||||
|
form,
|
||||||
|
}: {
|
||||||
|
form: Form;
|
||||||
|
toAddresses: string[];
|
||||||
|
orderedResponses: OrderedResponses;
|
||||||
|
}) {
|
||||||
super();
|
super();
|
||||||
this.form = form;
|
this.form = form;
|
||||||
this.response = response;
|
this.orderedResponses = orderedResponses;
|
||||||
this.toAddresses = toAddresses;
|
this.toAddresses = toAddresses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +34,7 @@ export default class ResponseEmail extends BaseEmail {
|
||||||
subject,
|
subject,
|
||||||
html: renderEmail("ResponseEmail", {
|
html: renderEmail("ResponseEmail", {
|
||||||
form: this.form,
|
form: this.form,
|
||||||
response: this.response,
|
orderedResponses: this.orderedResponses,
|
||||||
subject,
|
subject,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,7 @@ import logger from "@calcom/lib/logger";
|
||||||
import { WebhookTriggerEvents } from "@calcom/prisma/client";
|
import { WebhookTriggerEvents } from "@calcom/prisma/client";
|
||||||
import type { Ensure } from "@calcom/types/utils";
|
import type { Ensure } from "@calcom/types/utils";
|
||||||
|
|
||||||
|
import type { OrderedResponses } from "../types/types";
|
||||||
import type { Response, SerializableForm } from "../types/types";
|
import type { Response, SerializableForm } from "../types/types";
|
||||||
|
|
||||||
export async function onFormSubmission(
|
export async function onFormSubmission(
|
||||||
|
@ -61,23 +62,28 @@ export async function onFormSubmission(
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
const orderedResponses = form.fields.reduce((acc, field) => {
|
||||||
|
acc.push(response[field.id]);
|
||||||
|
return acc;
|
||||||
|
}, [] as OrderedResponses);
|
||||||
|
|
||||||
if (form.settings?.emailOwnerOnSubmission) {
|
if (form.settings?.emailOwnerOnSubmission) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Preparing to send Form Response email for Form:${form.id} to form owner: ${form.user.email}`
|
`Preparing to send Form Response email for Form:${form.id} to form owner: ${form.user.email}`
|
||||||
);
|
);
|
||||||
await sendResponseEmail(form, response, form.user.email);
|
await sendResponseEmail(form, orderedResponses, form.user.email);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sendResponseEmail = async (
|
export const sendResponseEmail = async (
|
||||||
form: Pick<App_RoutingForms_Form, "id" | "name">,
|
form: Pick<App_RoutingForms_Form, "id" | "name">,
|
||||||
response: Response,
|
orderedResponses: OrderedResponses,
|
||||||
ownerEmail: string
|
ownerEmail: string
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
const { default: ResponseEmail } = await import("../emails/templates/response-email");
|
const { default: ResponseEmail } = await import("../emails/templates/response-email");
|
||||||
const email = new ResponseEmail({ form: form, toAddresses: [ownerEmail], response: response });
|
const email = new ResponseEmail({ form: form, toAddresses: [ownerEmail], orderedResponses });
|
||||||
await email.sendEmail();
|
await email.sendEmail();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -45,3 +45,5 @@ export type SerializableRoute =
|
||||||
isFallback?: LocalRoute["isFallback"];
|
isFallback?: LocalRoute["isFallback"];
|
||||||
})
|
})
|
||||||
| GlobalRoute;
|
| GlobalRoute;
|
||||||
|
|
||||||
|
export type OrderedResponses = Response[string][];
|
||||||
|
|
|
@ -57,8 +57,12 @@ export function getEventName(eventNameObj: EventNameObjectType, forAttendeeView
|
||||||
if (variable === bookingField) {
|
if (variable === bookingField) {
|
||||||
let fieldValue;
|
let fieldValue;
|
||||||
if (eventNameObj.bookingFields) {
|
if (eventNameObj.bookingFields) {
|
||||||
fieldValue =
|
const field = eventNameObj.bookingFields[bookingField as keyof typeof eventNameObj.bookingFields];
|
||||||
eventNameObj.bookingFields[bookingField as keyof typeof eventNameObj.bookingFields]?.toString();
|
if (field && typeof field === "object" && "value" in field) {
|
||||||
|
fieldValue = field?.value?.toString();
|
||||||
|
} else {
|
||||||
|
fieldValue = field?.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dynamicEventName = dynamicEventName.replace(`{${variable}}`, fieldValue || "");
|
dynamicEventName = dynamicEventName.replace(`{${variable}}`, fieldValue || "");
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,11 +21,7 @@ function RenderIcon({
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={eventLocationType.iconUrl}
|
src={eventLocationType.iconUrl}
|
||||||
className={classNames(
|
className="me-[10px] h-4 w-4 opacity-70 invert-[.65] dark:invert-0"
|
||||||
"me-[10px] h-4 w-4 opacity-70 dark:opacity-100",
|
|
||||||
eventLocationType.iconUrl?.includes("-dark") ? "dark:invert-[.65]" : "",
|
|
||||||
eventLocationType.iconUrl?.includes("-dark") && isTooltip && "invert"
|
|
||||||
)}
|
|
||||||
alt={`${eventLocationType.label} icon`}
|
alt={`${eventLocationType.label} icon`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1756,6 +1756,7 @@ async function handler(
|
||||||
const webhookData = {
|
const webhookData = {
|
||||||
...evt,
|
...evt,
|
||||||
...eventTypeInfo,
|
...eventTypeInfo,
|
||||||
|
uid: resultBooking?.uid || uid,
|
||||||
bookingId: booking?.id,
|
bookingId: booking?.id,
|
||||||
rescheduleUid,
|
rescheduleUid,
|
||||||
rescheduleStartTime: originalRescheduledBooking?.startTime
|
rescheduleStartTime: originalRescheduledBooking?.startTime
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
import { shallow } from "zustand/shallow";
|
import { shallow } from "zustand/shallow";
|
||||||
|
|
||||||
import type { Dayjs } from "@calcom/dayjs";
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
|
@ -100,40 +101,6 @@ const NoAvailabilityOverlay = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes care of selecting a valid date in the month if the selected date is not available in the month
|
|
||||||
*/
|
|
||||||
const useHandleInitialDateSelection = ({
|
|
||||||
daysToRenderForTheMonth,
|
|
||||||
selected,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
daysToRenderForTheMonth: { day: Dayjs | null; disabled: boolean }[];
|
|
||||||
selected: Dayjs | Dayjs[] | null | undefined;
|
|
||||||
onChange: (date: Dayjs | null) => void;
|
|
||||||
}) => {
|
|
||||||
// Let's not do something for now in case of multiple selected dates as behaviour is unclear and it's not needed at the moment
|
|
||||||
if (selected instanceof Array) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const firstAvailableDateOfTheMonth = daysToRenderForTheMonth.find((day) => !day.disabled)?.day;
|
|
||||||
|
|
||||||
const isSelectedDateAvailable = selected
|
|
||||||
? daysToRenderForTheMonth.some(({ day, disabled }) => {
|
|
||||||
if (day && yyyymmdd(day) === yyyymmdd(selected) && !disabled) return true;
|
|
||||||
})
|
|
||||||
: false;
|
|
||||||
|
|
||||||
if (!isSelectedDateAvailable && firstAvailableDateOfTheMonth) {
|
|
||||||
// If selected date not available in the month, select the first available date of the month
|
|
||||||
onChange(firstAvailableDateOfTheMonth);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!firstAvailableDateOfTheMonth) {
|
|
||||||
onChange(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Days = ({
|
const Days = ({
|
||||||
minDate = dayjs.utc(),
|
minDate = dayjs.utc(),
|
||||||
excludedDates = [],
|
excludedDates = [],
|
||||||
|
@ -218,11 +185,34 @@ const Days = ({
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
useHandleInitialDateSelection({
|
/**
|
||||||
daysToRenderForTheMonth,
|
* Takes care of selecting a valid date in the month if the selected date is not available in the month
|
||||||
selected,
|
*/
|
||||||
onChange: props.onChange,
|
|
||||||
});
|
const useHandleInitialDateSelection = () => {
|
||||||
|
// Let's not do something for now in case of multiple selected dates as behaviour is unclear and it's not needed at the moment
|
||||||
|
if (selected instanceof Array) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const firstAvailableDateOfTheMonth = daysToRenderForTheMonth.find((day) => !day.disabled)?.day;
|
||||||
|
|
||||||
|
const isSelectedDateAvailable = selected
|
||||||
|
? daysToRenderForTheMonth.some(({ day, disabled }) => {
|
||||||
|
if (day && yyyymmdd(day) === yyyymmdd(selected) && !disabled) return true;
|
||||||
|
})
|
||||||
|
: false;
|
||||||
|
|
||||||
|
if (!isSelectedDateAvailable && firstAvailableDateOfTheMonth) {
|
||||||
|
// If selected date not available in the month, select the first available date of the month
|
||||||
|
props.onChange(firstAvailableDateOfTheMonth);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firstAvailableDateOfTheMonth) {
|
||||||
|
props.onChange(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(useHandleInitialDateSelection);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import { Avatar } from "@calcom/ui";
|
||||||
|
import type { AvatarProps } from "@calcom/ui";
|
||||||
|
|
||||||
|
type OrganizationAvatarProps = AvatarProps & {
|
||||||
|
organizationSlug: string | null | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OrganizationAvatar = ({ size, imageSrc, alt, organizationSlug, ...rest }: OrganizationAvatarProps) => {
|
||||||
|
return (
|
||||||
|
<Avatar
|
||||||
|
size={size}
|
||||||
|
imageSrc={imageSrc}
|
||||||
|
alt={alt}
|
||||||
|
indicator={
|
||||||
|
organizationSlug ? (
|
||||||
|
<div
|
||||||
|
className={classNames("absolute bottom-0 right-0 z-10", size === "lg" ? "h-3 w-3" : "h-10 w-10")}>
|
||||||
|
<img
|
||||||
|
src={`/org/${organizationSlug}/avatar.png`}
|
||||||
|
alt={alt}
|
||||||
|
className="flex h-full items-center justify-center rounded-full ring-2 ring-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrganizationAvatar;
|
|
@ -103,7 +103,7 @@ export function AvailabilitySliderTable() {
|
||||||
.padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
|
.padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col text-center">
|
||||||
<span className="text-default text-sm font-medium">{time}</span>
|
<span className="text-default text-sm font-medium">{time}</span>
|
||||||
<span className="text-subtle text-xs leading-none">GMT {offsetFormatted}</span>
|
<span className="text-subtle text-xs leading-none">GMT {offsetFormatted}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useOrgBranding } from "@calcom/ee/organizations/context/provider";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Sheet, SheetContent, SheetFooter, Avatar, Skeleton, Loader } from "@calcom/ui";
|
import { Sheet, SheetContent, SheetFooter, Avatar, Skeleton, Loader, Label } from "@calcom/ui";
|
||||||
|
|
||||||
import type { State, Action } from "../UserListTable";
|
import type { State, Action } from "../UserListTable";
|
||||||
import { DisplayInfo } from "./DisplayInfo";
|
import { DisplayInfo } from "./DisplayInfo";
|
||||||
|
@ -69,14 +69,23 @@ export function EditUserSheet({ state, dispatch }: { state: State; dispatch: Dis
|
||||||
/>
|
/>
|
||||||
<DisplayInfo label={t("role")} value={loadedUser?.role ?? ""} asBadge badgeColor="blue" />
|
<DisplayInfo label={t("role")} value={loadedUser?.role ?? ""} asBadge badgeColor="blue" />
|
||||||
<DisplayInfo label={t("timezone")} value={loadedUser?.timeZone ?? ""} />
|
<DisplayInfo label={t("timezone")} value={loadedUser?.timeZone ?? ""} />
|
||||||
<DisplayInfo
|
<div className="flex flex-col">
|
||||||
label={t("availability_schedules")}
|
<Label className="text-subtle mb-1 text-xs font-semibold uppercase leading-none">
|
||||||
value={
|
{t("availability_schedules")}
|
||||||
schedulesNames && schedulesNames?.length === 0
|
</Label>
|
||||||
? [t("user_has_no_schedules")]
|
<div className="flex flex-col">
|
||||||
: schedulesNames ?? "" // TS wtf
|
{schedulesNames
|
||||||
}
|
? schedulesNames.map((scheduleName) => (
|
||||||
/>
|
<span
|
||||||
|
key={scheduleName}
|
||||||
|
className="text-emphasis inline-flex items-center gap-1 text-sm font-normal leading-5">
|
||||||
|
{scheduleName}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
: t("user_has_no_schedules")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DisplayInfo
|
<DisplayInfo
|
||||||
label={t("teams")}
|
label={t("teams")}
|
||||||
displayCount={teamNames?.length ?? 0}
|
displayCount={teamNames?.length ?? 0}
|
||||||
|
|
|
@ -64,6 +64,7 @@ export async function getUserFromSession(ctx: TRPCContextInner, session: Maybe<S
|
||||||
id: true,
|
id: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
|
name: true,
|
||||||
members: {
|
members: {
|
||||||
select: { userId: true },
|
select: { userId: true },
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/Syn
|
||||||
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
|
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||||
import { userMetadata } from "@calcom/prisma/zod-utils";
|
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
@ -31,11 +31,10 @@ type UpdateProfileOptions = {
|
||||||
|
|
||||||
export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) => {
|
export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) => {
|
||||||
const { user } = ctx;
|
const { user } = ctx;
|
||||||
const { metadata: metadataFromInput } = input;
|
const userMetadata = handleUserMetadata({ ctx, input });
|
||||||
const cleanMetadata = cleanMetadataAllowedUpdateKeys(metadataFromInput);
|
|
||||||
const data: Prisma.UserUpdateInput = {
|
const data: Prisma.UserUpdateInput = {
|
||||||
...input,
|
...input,
|
||||||
metadata: cleanMetadata,
|
metadata: userMetadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
// some actions can invalidate a user session.
|
// some actions can invalidate a user session.
|
||||||
|
@ -65,26 +64,9 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
|
||||||
data.avatar = await resizeBase64Image(input.avatar);
|
data.avatar = await resizeBase64Image(input.avatar);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchUserCurrentMetadata = await prisma.user.findUnique({
|
|
||||||
where: {
|
|
||||||
id: user.id,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
metadata: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const metadata = userMetadata.parse(fetchUserCurrentMetadata?.metadata);
|
|
||||||
|
|
||||||
// Required so we don't override and delete saved values
|
|
||||||
data.metadata = {
|
|
||||||
...metadata,
|
|
||||||
cleanMetadata,
|
|
||||||
};
|
|
||||||
|
|
||||||
const isPremium = metadata?.isPremium;
|
|
||||||
if (isPremiumUsername) {
|
if (isPremiumUsername) {
|
||||||
const stripeCustomerId = metadata?.stripeCustomerId;
|
const stripeCustomerId = userMetadata?.stripeCustomerId;
|
||||||
|
const isPremium = userMetadata?.isPremium;
|
||||||
if (!isPremium || !stripeCustomerId) {
|
if (!isPremium || !stripeCustomerId) {
|
||||||
throw new TRPCError({ code: "BAD_REQUEST", message: "User is not premium" });
|
throw new TRPCError({ code: "BAD_REQUEST", message: "User is not premium" });
|
||||||
}
|
}
|
||||||
|
@ -199,12 +181,21 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
|
||||||
|
|
||||||
const cleanMetadataAllowedUpdateKeys = (metadata: TUpdateProfileInputSchema["metadata"]) => {
|
const cleanMetadataAllowedUpdateKeys = (metadata: TUpdateProfileInputSchema["metadata"]) => {
|
||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
return {} as Prisma.InputJsonValue;
|
return {};
|
||||||
}
|
}
|
||||||
const cleanedMetadata = updateUserMetadataAllowedKeys.safeParse(metadata);
|
const cleanedMetadata = updateUserMetadataAllowedKeys.safeParse(metadata);
|
||||||
if (!cleanedMetadata.success) {
|
if (!cleanedMetadata.success) {
|
||||||
logger.error("Error cleaning metadata", cleanedMetadata.error);
|
logger.error("Error cleaning metadata", cleanedMetadata.error);
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return cleanedMetadata as Prisma.InputJsonValue;
|
return cleanedMetadata.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUserMetadata = ({ ctx, input }: UpdateProfileOptions) => {
|
||||||
|
const { user } = ctx;
|
||||||
|
const cleanMetadata = cleanMetadataAllowedUpdateKeys(input.metadata);
|
||||||
|
const userMetadata = userMetadataSchema.parse(user.metadata);
|
||||||
|
// Required so we don't override and delete saved values
|
||||||
|
return { ...userMetadata, ...cleanMetadata };
|
||||||
};
|
};
|
||||||
|
|
|
@ -275,8 +275,7 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
|
||||||
|
|
||||||
const bookerUrl = await getBookerUrl(user);
|
const bookerUrl = await getBookerUrl(user);
|
||||||
return {
|
return {
|
||||||
// don't display event teams without event types,
|
eventTypeGroups,
|
||||||
eventTypeGroups: eventTypeGroups.filter((groupBy) => groupBy.parentId || !!groupBy.eventTypes?.length),
|
|
||||||
// so we can show a dropdown when the user has teams
|
// so we can show a dropdown when the user has teams
|
||||||
profiles: eventTypeGroups.map((group) => ({
|
profiles: eventTypeGroups.map((group) => ({
|
||||||
...group.profile,
|
...group.profile,
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { AVATAR_FALLBACK } from "@calcom/lib/constants";
|
||||||
|
|
||||||
import type { Maybe } from "@trpc/server";
|
import type { Maybe } from "@trpc/server";
|
||||||
|
|
||||||
import { Check } from "../icon";
|
|
||||||
import { Tooltip } from "../tooltip";
|
import { Tooltip } from "../tooltip";
|
||||||
|
|
||||||
export type AvatarProps = {
|
export type AvatarProps = {
|
||||||
|
@ -20,6 +19,7 @@ export type AvatarProps = {
|
||||||
fallback?: React.ReactNode;
|
fallback?: React.ReactNode;
|
||||||
accepted?: boolean;
|
accepted?: boolean;
|
||||||
asChild?: boolean; // Added to ignore the outer span on the fallback component - messes up styling
|
asChild?: boolean; // Added to ignore the outer span on the fallback component - messes up styling
|
||||||
|
indicator?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizesPropsBySize = {
|
const sizesPropsBySize = {
|
||||||
|
@ -34,12 +34,13 @@ const sizesPropsBySize = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export function Avatar(props: AvatarProps) {
|
export function Avatar(props: AvatarProps) {
|
||||||
const { imageSrc, size = "md", alt, title, href } = props;
|
const { imageSrc, size = "md", alt, title, href, indicator } = props;
|
||||||
const rootClass = classNames("aspect-square rounded-full", sizesPropsBySize[size]);
|
const rootClass = classNames("aspect-square rounded-full", sizesPropsBySize[size]);
|
||||||
let avatar = (
|
let avatar = (
|
||||||
<AvatarPrimitive.Root
|
<AvatarPrimitive.Root
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"bg-emphasis item-center relative inline-flex aspect-square justify-center overflow-hidden rounded-full",
|
"bg-emphasis item-center relative inline-flex aspect-square justify-center rounded-full",
|
||||||
|
indicator ? "overflow-visible" : "overflow-hidden",
|
||||||
props.className,
|
props.className,
|
||||||
sizesPropsBySize[size]
|
sizesPropsBySize[size]
|
||||||
)}>
|
)}>
|
||||||
|
@ -57,17 +58,7 @@ export function Avatar(props: AvatarProps) {
|
||||||
{props.fallback ? props.fallback : <img src={AVATAR_FALLBACK} alt={alt} className={rootClass} />}
|
{props.fallback ? props.fallback : <img src={AVATAR_FALLBACK} alt={alt} className={rootClass} />}
|
||||||
</>
|
</>
|
||||||
</AvatarPrimitive.Fallback>
|
</AvatarPrimitive.Fallback>
|
||||||
{props.accepted && (
|
{indicator}
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
"text-inverted absolute bottom-0 right-0 block rounded-full bg-green-400 ring-2 ring-white",
|
|
||||||
size === "lg" ? "h-5 w-5" : "h-2 w-2"
|
|
||||||
)}>
|
|
||||||
<div className="flex h-full items-center justify-center p-[2px]">
|
|
||||||
{size === "lg" && <Check />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
</AvatarPrimitive.Root>
|
</AvatarPrimitive.Root>
|
||||||
);
|
);
|
||||||
|
|
|
@ -11,7 +11,6 @@ export type AvatarGroupProps = {
|
||||||
href?: string;
|
href?: string;
|
||||||
}[];
|
}[];
|
||||||
className?: string;
|
className?: string;
|
||||||
accepted?: boolean;
|
|
||||||
truncateAfter?: number;
|
truncateAfter?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -36,7 +35,6 @@ export const AvatarGroup = function AvatarGroup(props: AvatarGroupProps) {
|
||||||
imageSrc={item.image}
|
imageSrc={item.image}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
alt={item.alt || ""}
|
alt={item.alt || ""}
|
||||||
accepted={props.accepted}
|
|
||||||
size={props.size}
|
size={props.size}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -108,7 +108,7 @@ const CommandItem = React.forwardRef<
|
||||||
<CommandPrimitive.Item
|
<CommandPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"aria-selected:bg-subtle aria-selected:text-emphasis relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
"aria-selected:bg-muted aria-selected:text-emphasis relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
@ -48,7 +48,13 @@ export function EmptyScreen({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex max-w-[420px] flex-col items-center">
|
<div className="flex max-w-[420px] flex-col items-center">
|
||||||
<h2 className="text-semibold font-cal text-emphasis mt-6 text-center text-xl">{headline}</h2>
|
<h2
|
||||||
|
className={classNames(
|
||||||
|
"text-semibold font-cal text-emphasis text-center text-xl",
|
||||||
|
Icon && "mt-6"
|
||||||
|
)}>
|
||||||
|
{headline}
|
||||||
|
</h2>
|
||||||
{description && (
|
{description && (
|
||||||
<div className="text-default mb-8 mt-3 text-center text-sm font-normal leading-6">
|
<div className="text-default mb-8 mt-3 text-center text-sm font-normal leading-6">
|
||||||
{description}
|
{description}
|
||||||
|
|
|
@ -36,10 +36,7 @@ const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTML
|
||||||
({ className, ...props }, ref) => (
|
({ className, ...props }, ref) => (
|
||||||
<tr
|
<tr
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={classNames(
|
className={classNames("hover:muted data-[state=selected]:bg-muted border-subtle border-b", className)}
|
||||||
"hover:bg-subtle data-[state=selected]:bg-subtle border-subtle border-b",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user