Compare commits

..
33 Commits
Author SHA1 Message Date
Omar López 95be16c358 v3.2.9
release.yml / release (push) Canceled after 0s
2023-09-07 13:14:59 -07:00
Peer Richelsen 5621507f7e chore: fixed an i18n string (#11200) 2023-09-07 17:27:22 +00:00
Mehul bc6d58fd47 fix: bad setState call at datePicker component (#11175) 2023-09-07 17:11:59 +00:00
WesleyandCarinaWolli db29cc1251 feat: send new booking uid on reschedule webhook (#10654)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-09-07 16:27:46 +00:00
Patel Divyesh 69888c57b0 fix: Fixed SVG icons on light and dark mode (#11173) 2023-09-07 17:34:11 +02:00
5c0da23b97 feat: User Avatar and Org Logo (#10700)
* Pass organization name & logo

* Overflow hidden

* Show org icon on public page

* Add org logo to large user avatars

* Clean up

* Add org name and logo to context

* Get org logo from /avatar.png endpoint

* Do not query for logo

* Remove name and logo from session middleware

* Type fix

* Set user onboarding org logo

* feat: organization avatar component (#10788)

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

* Type fixes

* Type fix

* Transition to org slug for organization avatar

* Address feedback

* Clean up

* Clean up

* Type fix

* fix: set avatar cache control (#11163)

* test: Integration tests for handleNewBooking (#11044)

Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>

* fix: booking_paid webhook and added new payment metadata (#11093)

* app store improvements, logos, dark mode, added screenshots, fixed author names (#11164)

* fix: mobile event types and avatars (#11184)

* New Crowdin translations by Github Action

* fix: updateProfile metadata overwrite (#11188)

Co-authored-by: alannnc <alannnc@gmail.com>

* New Crowdin translations by Github Action

---------

Co-authored-by: Sean Brydon <sean@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-09-07 17:26:40 +02:00
Hariom BalharaandCarina Wollendorfer 5324ec8051 fix: Routing Form - Show responses in the order of fields (#11143)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-09-07 15:22:39 +00:00
Crowdin Bot b946f89ffb New Crowdin translations by Github Action 2023-09-07 12:33:00 +00:00
Crowdin Bot 3e3581fabc New Crowdin translations by Github Action 2023-09-07 12:29:44 +00:00
Crowdin Bot 05ac339233 New Crowdin translations by Github Action 2023-09-07 12:26:19 +00:00
Crowdin Bot ef4675ff44 New Crowdin translations by Github Action 2023-09-07 12:22:42 +00:00
sean-brydon 47dc85ee1b chore: fixing darkmode data table (#11195) 2023-09-07 14:18:52 +02:00
Crowdin Bot 50342e33ed New Crowdin translations by Github Action 2023-09-06 23:43:28 +00:00
Omar Lópezandalannnc eebcb636f1 fix: updateProfile metadata overwrite (#11188)
Co-authored-by: alannnc <alannnc@gmail.com>
2023-09-06 16:40:50 -07:00
Crowdin Bot ed2ced899a New Crowdin translations by Github Action 2023-09-06 21:43:37 +00:00
Leo Giovanetti b7ae8c9594 fix: mobile event types and avatars (#11184) 2023-09-06 23:41:03 +02:00
Peer Richelsen fce98ebb13 app store improvements, logos, dark mode, added screenshots, fixed author names (#11164) 2023-09-07 01:17:04 +05:30
alannnc b81b221bf5 fix: booking_paid webhook and added new payment metadata (#11093) 2023-09-06 12:40:27 -07:00
f9eb335d0b test: Integration tests for handleNewBooking (#11044)
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-09-06 12:23:53 -07:00
Omar López d7d7bcd651 fix: set avatar cache control (#11163) 2023-09-06 12:22:13 -07:00
Crowdin Bot 4c861b9e67 New Crowdin translations by Github Action 2023-09-06 18:32:09 +00:00
Carina WollendorferandCarinaWolli 2fa704c39b fix: add make invite link to setup page (#11177)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-09-06 14:29:13 -04:00
Crowdin Bot 9ccdf21d75 New Crowdin translations by Github Action 2023-09-06 15:25:19 +00:00
GitStart-Cal.comandgitstart-calcom 9d6148da42 chore: add Wizard in storybook (CALCOM-10760) (#11166)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-09-06 17:22:02 +02:00
MehulandUdit Takkar bf437b7d6b fix: event duration can be 0 (#11145)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-09-06 17:21:24 +02:00
Udit Takkar 3701fcd68b fix: teams page error (#11171) 2023-09-06 11:38:21 +02:00
GitStart-Cal.comandgitstart-calcom 2f90dc7cb6 fix: DataTable storybook file is not rendering (#10984)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-09-06 09:18:06 +01:00
alannnc 829f90b82f chore: remove unused queries on user event types (#11159)
* remove unused queries on user event types

* Fix types
2023-09-06 05:00:06 -03:00
Crowdin Bot 3539693729 New Crowdin translations by Github Action 2023-09-06 05:11:51 +00:00
Joe Au-Yeung 868ba4396d fix: Installing Zoho CRM (#11161)
* Fix installing Zoho CRM

* Clean up
2023-09-06 05:08:59 +00:00
zomars d06e36a5cd Update yarn.lock 2023-09-05 14:46:14 -07:00
Crowdin Bot f61c9d9bbf New Crowdin translations by Github Action 2023-09-05 21:39:15 +00:00
Leo Giovanetti a2d521e001 fix: i18n issues reported by crowdin (#11160) 2023-09-05 18:36:17 -03:00
97 changed files with 2389 additions and 1802 deletions
@@ -13,7 +13,6 @@ import type { EventLocationType } 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 { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import cx from "@calcom/lib/classNames";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
@@ -302,13 +301,7 @@ export const EventSetupTab = (
<div className="flex items-center">
<img
src={eventLocationType.iconUrl}
className={cx(
"h-4 w-4",
// invert all the icons except app icons
eventLocationType.iconUrl &&
eventLocationType.iconUrl.includes("-dark") &&
"dark:invert"
)}
className="h-4 w-4 dark:invert-[.65]"
alt={`${eventLocationType.label} logo`}
/>
<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 { useForm } from "react-hook-form";
import OrganizationAvatar from "@calcom/features/ee/organizations/components/OrganizationAvatar";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import turndown from "@calcom/lib/turndownService";
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";
type FormData = {
@@ -98,7 +99,14 @@ const UserProfile = () => {
return (
<form onSubmit={onSubmit}>
<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
ref={avatarRef}
type="hidden"
@@ -22,14 +22,7 @@ export type GroupOptionType = GroupBase<LocationOption>;
const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => {
return (
<div className="flex items-center gap-3">
{icon && (
<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")}
/>
)}
{icon && <img src={icon} alt="cover" className="h-3.5 w-3.5 dark:invert-[.65]" />}
<span className={classNames("text-sm font-medium")}>{label}</span>
</div>
);
+4
View File
@@ -254,6 +254,10 @@ const nextConfig = {
source: "/org/:slug",
destination: "/team/:slug",
},
{
source: "/org/:orgSlug/avatar.png",
destination: "/api/user/avatar?orgSlug=:orgSlug",
},
{
source: "/team/:teamname/avatar.png",
destination: "/api/user/avatar?teamname=:teamname",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.2.8",
"version": "3.2.9",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+10 -2
View File
@@ -11,6 +11,7 @@ import {
useEmbedStyles,
useIsEmbed,
} 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 { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
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 { baseEventTypeSelect } from "@calcom/prisma/selects";
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 type { EmbedProps } from "@lib/withEmbedSsr";
@@ -96,7 +97,12 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
"max-w-3xl px-4 py-24"
)}>
<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">
{profile.name}
{user.verified && (
@@ -218,6 +224,7 @@ export type UserPageProps = {
theme: string | null;
brandColor: string;
darkBrandColor: string;
organizationSlug: string | null;
allowSEOIndexing: boolean;
};
users: Pick<User, "away" | "name" | "username" | "bio" | "verified">[];
@@ -321,6 +328,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
theme: user.theme,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
organizationSlug: user.organization?.slug ?? null,
allowSEOIndexing: user.allowSEOIndexing ?? true,
};
+1 -1
View File
@@ -187,7 +187,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
res.setHeader("Content-Type", response.headers.get("content-type") as string);
res.setHeader("Cache-Control", "s-maxage=86400");
res.setHeader("Cache-Control", "s-maxage=86400, stale-while-revalidate=60");
res.send(buffer);
} catch (error) {
res.statusCode = 404;
+21 -2
View File
@@ -10,6 +10,7 @@ const querySchema = z
.object({
username: z.string(),
teamname: z.string(),
orgSlug: z.string(),
/**
* Allow fetching avatar of a particular organization
* Avatars being public, we need not worry about others accessing it.
@@ -19,7 +20,7 @@ const querySchema = z
.partial();
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 org = isValidOrgDomain ? currentOrgDomain : null;
@@ -59,7 +60,23 @@ async function getIdentityData(req: NextApiRequest) {
org,
name: teamname,
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),
};
}
}
@@ -67,6 +84,8 @@ async function getIdentityData(req: NextApiRequest) {
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const identity = await getIdentityData(req);
const img = identity?.avatar;
// We cache for one day
res.setHeader("Cache-Control", "s-maxage=86400, stale-while-revalidate=60");
// If image isn't set or links to this route itself, use default avatar
if (!img) {
if (identity?.org) {
+10 -2
View File
@@ -376,9 +376,12 @@ const EventTypePage = (props: EventTypeSetupProps) => {
bookerLayouts,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
multipleDurationEnabled,
length,
...input
} = values;
if (!Number(length)) throw new Error(t("event_setup_length_error"));
if (bookingLimits) {
const isValid = validateIntervalLimitOrder(bookingLimits);
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
@@ -396,7 +399,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
if (metadata?.multipleDuration.length < 1) {
throw new Error(t("event_setup_multiple_duration_error"));
} else {
if (!input.length && !metadata?.multipleDuration?.includes(input.length)) {
if (!length && !metadata?.multipleDuration?.includes(length)) {
throw new Error(t("event_setup_multiple_duration_default_error"));
}
}
@@ -410,6 +413,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
const { availability, ...rest } = input;
updateMutation.mutate({
...rest,
length,
locations,
recurringEvent,
periodStartDate: periodDates.startDate,
@@ -467,9 +471,12 @@ const EventTypePage = (props: EventTypeSetupProps) => {
seatsPerTimeSlotEnabled,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
multipleDurationEnabled,
length,
...input
} = values;
if (!Number(length)) throw new Error(t("event_setup_length_error"));
if (bookingLimits) {
const isValid = validateIntervalLimitOrder(bookingLimits);
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
@@ -487,7 +494,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
if (metadata?.multipleDuration.length < 1) {
throw new Error(t("event_setup_multiple_duration_error"));
} else {
if (!input.length && !metadata?.multipleDuration?.includes(input.length)) {
if (!length && !metadata?.multipleDuration?.includes(length)) {
throw new Error(t("event_setup_multiple_duration_default_error"));
}
}
@@ -496,6 +503,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
const { availability, ...rest } = input;
updateMutation.mutate({
...rest,
length,
locations,
recurringEvent,
periodStartDate: periodDates.startDate,
+39 -16
View File
@@ -108,8 +108,10 @@ const MobileTeamsTab: FC<MobileTeamsTabProps> = (props) => {
const orgBranding = useOrgBranding();
const tabs = eventTypeGroups.map((item) => ({
name: item.profile.name ?? "",
href: item.teamId ? `/event-types?teamId=${item.teamId}` : "/event-types",
avatar: item.profile.image ?? `${orgBranding?.fullDomain ?? WEBAPP_URL}/${item.profile.slug}/avatar.png`,
href: item.teamId ? `/event-types?teamId=${item.teamId}` : "/event-types?noTeam",
avatar: orgBranding
? `${orgBranding.fullDomain}${item.teamId ? "/team" : ""}/${item.profile.slug}/avatar.png`
: item.profile.image ?? `${WEBAPP_URL + (item.teamId && "/team")}/${item.profile.slug}/avatar.png`,
}));
const { data } = useTypedQuery(querySchema);
const events = eventTypeGroups.filter((item) => item.teamId === data.teamId);
@@ -117,13 +119,15 @@ const MobileTeamsTab: FC<MobileTeamsTabProps> = (props) => {
return (
<div>
<HorizontalTabs tabs={tabs} />
{events.length && (
{events.length > 0 ? (
<EventTypeList
types={events[0].eventTypes}
group={events[0]}
groupIndex={0}
readOnly={events[0].metadata.readOnly}
/>
) : (
<CreateFirstEventTypeView />
)}
</div>
);
@@ -358,6 +362,10 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
}
}, []);
if (!types.length) {
return group.teamId ? <EmptyEventTypeList group={group} /> : <CreateFirstEventTypeView />;
}
const firstItem = types[0];
const lastItem = types[types.length - 1];
const isManagedEventPrefix = () => {
@@ -391,23 +399,27 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
className="relative right-3 top-1"
size="sm"
truncateAfter={4}
items={type.users.map(
(organizer: { name: string | null; username: string | null }) => ({
alt: organizer.name || "",
image: `${orgBranding?.fullDomain ?? WEBAPP_URL}/${
organizer.username
}/avatar.png`,
title: organizer.name || "",
})
)}
items={
type?.users
? type.users.map(
(organizer: { name: string | null; username: string | null }) => ({
alt: organizer.name || "",
image: `${orgBranding?.fullDomain ?? WEBAPP_URL}/${
organizer.username
}/avatar.png`,
title: organizer.name || "",
})
)
: []
}
/>
)}
{isManagedEventType && type.children && (
{isManagedEventType && type?.children && type.children?.length > 0 && (
<AvatarGroup
className="relative right-3 top-1"
size="sm"
truncateAfter={4}
items={type.children
items={type?.children
.flatMap((ch) => ch.users)
.map((user: Pick<User, "name" | "username">) => ({
alt: user.name || "",
@@ -708,7 +720,10 @@ const EventTypeListHeading = ({
<Avatar
alt={profile?.name || ""}
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
imageSrc={`${orgBranding?.fullDomain ?? WEBAPP_URL}/${profile.slug}/avatar.png` || undefined}
imageSrc={
`${orgBranding?.fullDomain ?? WEBAPP_URL}${teamId ? "/team" : ""}/${profile.slug}/avatar.png` ||
undefined
}
size="md"
className="mt-1 inline-flex justify-center"
/>
@@ -755,6 +770,12 @@ const CreateFirstEventTypeView = () => {
Icon={LinkIcon}
headline={t("new_event_type_heading")}
description={t("new_event_type_description")}
className="mb-16"
buttonRaw={
<Button href="?dialog=new" variant="button">
{t("create")}
</Button>
}
/>
);
};
@@ -889,8 +910,10 @@ const Main = ({
groupIndex={index}
readOnly={group.metadata.readOnly}
/>
) : (
) : group.teamId ? (
<EmptyEventTypeList group={group} />
) : (
<CreateFirstEventTypeView />
)}
</div>
))
+11 -2
View File
@@ -6,6 +6,7 @@ import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
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 { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -14,10 +15,10 @@ import turndown from "@calcom/lib/turndownService";
import { IdentityProvider } from "@calcom/prisma/enums";
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react";
import type { RouterOutputs } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import {
Alert,
Avatar,
Button,
Dialog,
DialogClose,
@@ -223,6 +224,7 @@ const ProfileView = () => {
key={JSON.stringify(defaultValues)}
defaultValues={defaultValues}
isLoading={updateProfileMutation.isLoading}
userOrganization={user.organization}
onSubmit={(values) => {
if (values.email !== user.email && isCALIdentityProvider) {
setTempFormValues(values);
@@ -364,11 +366,13 @@ const ProfileForm = ({
onSubmit,
extraField,
isLoading = false,
userOrganization,
}: {
defaultValues: FormValues;
onSubmit: (values: FormValues) => void;
extraField?: React.ReactNode;
isLoading: boolean;
userOrganization: RouterOutputs["viewer"]["me"]["organization"];
}) => {
const { t } = useLocale();
const [firstRender, setFirstRender] = useState(true);
@@ -406,7 +410,12 @@ const ProfileForm = ({
name="avatar"
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">
<ImageUploader
target="avatar"
+1 -1
View File
@@ -142,7 +142,7 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
<TextField
addOnLeading={
orgSlug
? getOrgFullDomain(orgSlug, { protocol: true })
? `${getOrgFullDomain(orgSlug, { protocol: true })}/`
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/`
}
{...register("username")}
+1 -40
View File
@@ -1,60 +1,21 @@
import { expect } from "@playwright/test";
import type { Prisma } from "@prisma/client";
import { uuid } from "short-uuid";
import { v4 as uuidv4 } from "uuid";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import type { Fixtures } from "./lib/fixtures";
import { test } from "./lib/fixtures";
import {
bookTimeSlot,
createNewSeatedEventType,
selectFirstAvailableTimeSlotNextMonth,
createUserWithSeatedEventAndAttendees,
} from "./lib/testUtils";
test.describe.configure({ mode: "parallel" });
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("User can create a seated event (2 seats as example)", async ({ users, page }) => {
const user = await users.create({ name: "Seated event" });
+46
View File
@@ -6,6 +6,10 @@ import { createServer } from "http";
import { noop } from "lodash";
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";
export function todo(title: string) {
@@ -192,6 +196,7 @@ export async function installAppleCalendar(page: Page) {
await page.waitForURL("/apps/apple-calendar");
await page.click('[data-testid="install-app-button"]');
}
export async function getEmailsReceivedByUser({
emails,
userEmail,
@@ -228,3 +233,44 @@ export async function expectEmailsToHaveSubject({
expect(organizerFirstEmail.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 };
}
+169
View File
@@ -1,5 +1,9 @@
import type { Page } 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 {
@@ -8,6 +12,7 @@ import {
selectFirstAvailableTimeSlotNextMonth,
waitFor,
gotoRoutingLink,
createUserWithSeatedEventAndAttendees,
} from "./lib/testUtils";
// 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());
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("add webhook & test that creating an event triggers a webhook call", async ({
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("on submitting user form, triggers user webhook", async ({ page, users, routingForms }) => {
const webhookReceiver = createHttpServer();
+8 -10
View File
@@ -13,7 +13,7 @@
"reset_password_subject": "{{appName}}: إرشادات إعادة تعيين كلمة المرور",
"verify_email_subject": "{{appName}}: تأكيد حسابك",
"check_your_email": "تحقق من بريدك الإلكتروني",
"verify_email_page_body": "أرسلنا رسالة إلكترونية إلى {{email}}. من المهم تأكيد عنوان بريدك الإلكتروني لضمان أفضل تسليم للبريد الإلكتروني والتقويم من {{appName}}.",
"verify_email_page_body": "أرسلنا رسالة إلكترونية إلى {{email}}. من المهم تأكيد عنوان بريدك الإلكتروني لضمان تسليم البريد الإلكتروني والتقويم من {{appName}}.",
"verify_email_banner_body": "قم بتأكيد عنوان بريدك الإلكتروني لضمان أفضل تسليم للبريد الإلكتروني والتقويم",
"verify_email_email_header": "تأكيد عنوان بريدك الإلكتروني",
"verify_email_email_button": "تأكيد البريد الإلكتروني",
@@ -130,7 +130,7 @@
"upgrade_banner_action": "قم بالترقية هنا",
"team_upgraded_successfully": "تمت ترقية فريقك بنجاح!",
"org_upgrade_banner_description": "شكرًا لك على تجربة خطة Organization الجديدة التي نقدمها. لقد لاحظنا أن منظمتك \"{{teamName}}\" بحاجة إلى الترقية.",
"org_upgraded_successfully": "تمت ترقية منظمتك بنجاح!",
"org_upgraded_successfully": "تمت ترقية خطة Organization بنجاح!",
"use_link_to_reset_password": "استخدم الرابط أدناه لإعادة تعيين كلمة المرور",
"hey_there": "مرحبًا،",
"forgot_your_password_calcom": "نسيت كلمة المرور الخاصة بك؟ - {{appName}}",
@@ -550,7 +550,7 @@
"team_description": "بضع جمل عن فريقك. ستظهر على صفحة رابط فريقك.",
"org_description": "بضع جمل عن منظمتك. ستظهر على صفحة رابط منظمتك.",
"members": "الأعضاء",
"organization_members": "أعضاء المنظمة",
"organization_members": "أعضاء Organization",
"member": "العضو",
"number_member_one": "{{count}} عضو",
"number_member_other": "{{count}} من الأعضاء",
@@ -1078,7 +1078,7 @@
"url_start_with_https": "يجب أن يبدأ العنوان بـ http:// أو https://",
"number_provided": "سيتم توفير رقم الهاتف",
"before_event_trigger": "قبل بدء الحدث",
"event_cancelled_trigger": "متى تم إلغاء هذا الحدث",
"event_cancelled_trigger": "عند إلغاء هذا الحدث",
"new_event_trigger": "متى تم حجز الحدث الجديد",
"email_host_action": "إرسال رسالة إلكترونية إلى المضيف",
"email_attendee_action": "إرسال رسالة إلكترونية إلى الحضور",
@@ -1652,9 +1652,8 @@
"delete_sso_configuration_confirmation_description": "هل تريد بالتأكيد حذف تكوين {{connectionType}}؟ لن يتمكن أعضاء فريقك الذين يستخدمون معلومات تسجيل الدخول إلى {{connectionType}} من الوصول إلى Cal.com بعد الآن.",
"organizer_timezone": "منظم المناطق الزمنية",
"email_user_cta": "عرض الدعوة",
"email_no_user_invite_heading": "تمت دعوتك للانضمام إلى {{appName}} {{entity}}",
"email_no_user_invite_subheading": "دعاك {{invitedBy}} للانضمام إلى فريقه على {{appName}}. إن {{appName}} هو برنامج جدولة الأحداث الذي يمكّنك أنت وفريقك من جدولة الاجتماعات دون الحاجة إلى المراسلة عبر البريد الإلكتروني.",
"email_user_invite_subheading": "لقد دعاك {{invitedBy}} للانضمام إلى {{entity}} `{{teamName}}` على {{appName}}. إن {{appName}} هو برنامج جدولة الأحداث الذي يمكّنك أنت و{{entity}} من جدولة الاجتماعات دون الحاجة إلى المراسلة عبر البريد الإلكتروني.",
"email_user_invite_subheading_team": "دعاك {{invitedBy}} للانضمام إلى فريقه {{teamName}} على {{appName}}. إن {{appName}} هو برنامج جدولة الأحداث الذي يمكّنك أنت وفريقك من جدولة الاجتماعات دون الحاجة إلى المراسلة عبر البريد الإلكتروني.",
"email_no_user_invite_steps_intro": "سنرشدك خلال بضع خطوات قصيرة وستستمتع بجدول زمني خالٍ من التوتر مع {{entity}} في لمح البصر.",
"email_no_user_step_one": "اختر اسم المستخدم الخاص بك",
"email_no_user_step_two": "ربط حساب التقويم الخاص بك",
@@ -1834,7 +1833,7 @@
"invite_link_copied": "تم نسخ رابط الدعوة",
"invite_link_deleted": "تم حذف رابط الدعوة",
"invite_link_updated": "تم حفظ إعدادات رابط الدعوة",
"link_expires_after": "تم تعيين الروابط للانتهاء بعد...",
"link_expires_after": "تم تعيين انتهاء صلاحية الروابط بعد...",
"one_day": "1 يوم",
"seven_days": "7 أيام",
"thirty_days": "30 يومًا",
@@ -1871,7 +1870,6 @@
"first_event_type_webhook_description": "قم بإنشاء أول شبكة ويب هوك لهذا النوع من الأحداث",
"install_app_on": "تثبيت التطبيق على",
"create_for": "إنشاء من أجل",
"setup_organization": "إعداد منظمة",
"organization_banner_description": "إنشاء بيئات حيث يمكن لفرقك إنشاء تطبيقات مشتركة ومهام سير العمل وأنواع الأحداث باستخدام الجدولة الدوارة والجماعية.",
"organization_banner_title": "إدارة المنظمات ذات الفرق المتعددة",
"set_up_your_organization": "إعداد منظمتك",
@@ -1886,7 +1884,7 @@
"organization_verify_email_body": "الرجاء استخدام الرمز أدناه لتأكيد عنوان بريدك الإلكتروني لمواصلة إعداد منظمتك.",
"additional_url_parameters": "معلمات الرابط الإضافية",
"about_your_organization": "حول منظمتك",
"about_your_organization_description": "المنظمات هي بيئات مشتركة حيث يمكنك إنشاء فرق متعددة مع أعضاء مشتركين وأنواع الأحداث والتطبيقات ومهام سير العمل والمزيد.",
"about_your_organization_description": "المنظمات هي بيئات مشتركة حيث يمكنك إنشاء فرق متعددة مع أعضاء مشتركين وأنواع الأحداث والتطبيقات ومهام سير العمل المشتركة والمزيد.",
"create_your_teams": "إنشاء فرقك",
"create_your_teams_description": "ابدأوا الجدولة معًا عن طريق إضافة أعضاء فريقك إلى منظمتك",
"invite_organization_admins": "دعوة مشرفي منظمتك",
@@ -1927,7 +1925,7 @@
"404_the_org": "المنظمة",
"404_the_team": "الفريق",
"404_claim_entity_org": "المطالبة بنطاقك الفرعي لمنظمتك",
"404_claim_entity_team": "المطالبة بهذا الفريق والبدء في إدارة الجداول الزمنية بشكل جماعي",
"404_claim_entity_team": "انضم لهذا الفريق وابدأ في إدارة الجداول الزمنية بشكل جماعي",
"insights_all_org_filter": "الكل",
"insights_team_filter": "الفريق: {{teamName}}",
"insights_user_filter": "المستخدم: {{userName}}",
+9 -11
View File
@@ -118,7 +118,7 @@
"team_info": "Informace o týmu",
"request_another_invitation_email": "Pokud si nepřejete použít e-mail {{toEmail}} pro {{appName}} nebo už účet na {{appName}} máte, požádejte prosím o další pozvánku na jiný e-mail.",
"you_have_been_invited": "Byli jste pozváni do týmu {{teamName}}",
"user_invited_you": "Uživatel {{user}} vás pozval, abyste se připojili k subjektu {{entity}} {{team}} v aplikaci {{appName}}",
"user_invited_you": "Uživatel {{user}} vás pozval, abyste se připojili k {{entity}} {{team}} v aplikaci {{appName}}",
"hidden_team_member_title": "V tomto týmu nejste viditelní",
"hidden_team_member_message": "Vaše místo není zaplacené. Buď přejděte na tarif Pro, nebo informujte vlastníka týmu, že za vaše místo může zaplatit.",
"hidden_team_owner_message": "Pro využívání týmů potřebujete tarif Pro, budete skrytí, než přejdete na vyšší verzi.",
@@ -130,7 +130,7 @@
"upgrade_banner_action": "Upgradovat zde",
"team_upgraded_successfully": "Váš tým byl úspěšně přešel na vyšší verzi!",
"org_upgrade_banner_description": "Děkujeme, že jste vyzkoušeli náš tarif Organizace. Všimli jsme si, že vaše organizace „{{teamName}}“ vyžaduje upgrade.",
"org_upgraded_successfully": "Vaše organizace byla upgradována!",
"org_upgraded_successfully": "Váš upgrade na tarif Organization byl úspěšně dokončen!",
"use_link_to_reset_password": "Pro obnovení hesla použijte odkaz níž",
"hey_there": "Zdravíme,",
"forgot_your_password_calcom": "Zapomněli jste heslo? - {{appName}}",
@@ -307,7 +307,7 @@
"layout": "Rozvržení",
"bookerlayout_default_title": "Výchozí zobrazení",
"bookerlayout_description": "Můžete si jich vybrat více a vaši rezervující si mohou zobrazení přepínat.",
"bookerlayout_user_settings_title": "Rozvržení rezervace",
"bookerlayout_user_settings_title": "Rozvržení rezervací",
"bookerlayout_user_settings_description": "Můžete si jich vybrat více a vaši rezervující si mohou zobrazení přepínat. Lze nadefinovat pro každou událost zvlášť.",
"bookerlayout_month_view": "Měsíční",
"bookerlayout_week_view": "Týdenní",
@@ -403,7 +403,7 @@
"recording_ready": "Odkaz ke stažení záznamu je připraven",
"booking_created": "Rezervace vytvořena",
"booking_rejected": "Rezervace byla zamítnuta",
"booking_requested": "Byla vyžádána rezervace",
"booking_requested": "Váš požadavek na rezervaci byl úspěšně odeslán",
"meeting_ended": "Schůzka skončila",
"form_submitted": "Formulář byl odeslán",
"event_triggers": "Eventy na základě akce",
@@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "Opravdu chcete odstranit konfiguraci {{connectionType}}? Členové vašeho týmu, kteří používají přihlášení {{connectionType}}, ztratí přístup k webu Cal.com.",
"organizer_timezone": "Časové pásmo organizátora",
"email_user_cta": "Zobrazit pozvánku",
"email_no_user_invite_heading": "Byli jste pozváni, abyste se připojili k subjektu {{entity}} v aplikaci {{appName}}",
"email_no_user_invite_subheading": "Uživatel {{invitedBy}} vás pozval, abyste se připojili k jeho týmu v aplikaci {{appName}}. {{appName}} je plánovač událostí, který vám a vašemu týmu umožňuje plánovat schůzky bez e-mailového ping-pongu.",
"email_user_invite_subheading": "Uživatel {{invitedBy}} vás pozval, abyste se připojili k týmu {{teamName}} subjektu {{entity}} v aplikaci {{appName}}. {{appName}} je plánovač událostí, který vám a vašemu subjektu {{entity}} umožňuje plánovat schůzky bez e-mailového pingpongu.",
"email_user_invite_subheading_team": "Uživatel {{invitedBy}} vás pozval, abyste se připojili k jeho týmu „{{teamName}}“ v aplikaci {{appName}}. {{appName}} je plánovač událostí, který vám a vašemu týmu umožňuje plánovat schůzky bez e-mailového pingpongu.",
"email_no_user_invite_steps_intro": "Provedeme vás několika krátkými kroky a za malou chvíli si budete se svým subjektem {{entity}} užívat plánování bez stresu.",
"email_no_user_step_one": "Vyberte si uživatelské jméno",
"email_no_user_step_two": "Připojte svůj účet kalendáře",
@@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "Vytvořte svůj první webhook pro tento typ události",
"install_app_on": "Nainstalovat aplikaci v",
"create_for": "Vytvořit pro",
"setup_organization": "Nastavení organizace",
"organization_banner_description": "Vytvořte prostředí, ve kterém budou moct vaše týmy vytvářet sdílené aplikace, pracovní postupy a typy událostí s kolektivním plánováním nebo plánováním typu round-robin.",
"organization_banner_title": "Správa organizace s více týmy",
"set_up_your_organization": "Nastavení organizace",
@@ -1889,12 +1887,12 @@
"admin_username": "Uživatelské jméno správce",
"organization_name": "Název organizace",
"organization_url": "Adresa URL organizace",
"organization_verify_header": "Ověřte e-mailovou adresu organizace",
"organization_verify_header": "Ověřte svou e-mailovou adresu organizace",
"organization_verify_email_body": "Pomocí uvedeného kódu ověřte vaši e-mailovou adresu, abyste mohli pokračovat v nastavení vaší organizace.",
"additional_url_parameters": "Další parametry adresy URL",
"about_your_organization": "Informace o vaší organizaci",
"about_your_organization_description": "Organizace jsou sdílená prostředí, ve kterých můžete vytvořit několik týmů se sdílenými členy, typy událostí, aplikacemi, pracovními postupy a mnoha dalšími věcmi.",
"create_your_teams": "Vytvoření týmů",
"create_your_teams": "Vytvořit tým",
"create_your_teams_description": "Přidejte do své organizace členy a pusťte se do společného plánování",
"invite_organization_admins": "Pozvěte správce své organizace",
"invite_organization_admins_description": "Tito správci budou mít přístup ke všem týmům ve vaší organizaci. Správce a členy týmů můžete přidat později.",
@@ -1934,11 +1932,11 @@
"404_the_org": "Organizace",
"404_the_team": "Tým",
"404_claim_entity_org": "Vyžádejte si subdoménu pro svou organizaci",
"404_claim_entity_team": "Vyžádejte si tento tým a začněte kolektivně spravovat rozvrhy",
"404_claim_entity_team": "Vyžádejte si přístup k tomuto týmu a začněte kolektivně spravovat rozvrhy",
"insights_all_org_filter": "Všechny aplikace",
"insights_team_filter": "Tým: {{teamName}}",
"insights_user_filter": "Uživatel: {{userName}}",
"insights_subtitle": "Zobrazte si přehled o rezervacích napříč vašimi událostmi",
"insights_subtitle": "Zobrazte si Insight rezervací napříč vašimi událostmi",
"custom_plan": "Vlastní plán",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1679,9 +1679,8 @@
"delete_sso_configuration_confirmation_description": "Sind Sie sicher, dass Sie die {{connectionType}}-Konfiguration löschen möchten? Ihre Teammitglieder, die sich über {{connectionType}} anmelden, werden keinen Zugriff mehr auf Cal.com haben.",
"organizer_timezone": "Zeitzone des Veranstalters",
"email_user_cta": "Einladung anzeigen",
"email_no_user_invite_heading": "Sie wurden eingeladen, einem {{entity}} in {{appName}} beizutreten",
"email_no_user_invite_subheading": "{{invitedBy}} hat Sie eingeladen, dem Team in {{appName}} beizutreten. {{appName}} ist der Event-Planer, der es Ihnen und Ihrem Team ermöglicht, Meetings ohne ständiges hin und her zu planen.",
"email_user_invite_subheading": "{{invitedBy}} hat Sie eingeladen, dem {{entity}} „{{teamName}}“ in {{appName}} beizutreten. {{appName}} ist der Terminplaner, der es Ihnen und Ihrem {{entity}} ermöglicht, Meetings ohne ständiges hin und her zu planen.",
"email_user_invite_subheading_team": "{{invitedBy}} hat Sie eingeladen, dem Team „{{teamName}}“ beizutreten. {{appName}} ist der Event-Planer, der es Ihnen und Ihrem Team ermöglicht, Meetings ohne Hin und Her zu planen.",
"email_no_user_invite_steps_intro": "Wir begleiten Sie durch ein paar Schritte, danach können Sie in kürzester Zeit stressfreie Planung mit Ihrem {{entity}} genießen.",
"email_no_user_step_one": "Wählen Sie Ihren Benutzernamen",
"email_no_user_step_two": "Verbinden Sie Ihr Kalenderkonto",
@@ -1902,7 +1901,6 @@
"install_app_on": "App installieren auf",
"create_for": "Erstellen für",
"currency": "Währung",
"setup_organization": "Eine Organization einrichten",
"organization_banner_description": "Schaffen Sie Umgebungen, in der Teams gemeinsame Apps, Workflows und Termintypen mit Round Robin und kollektiver Terminplanung erstellen können.",
"organization_banner_title": "Verwalten Sie Organizations mit mehreren Teams",
"set_up_your_organization": "Ihre Organization einrichten",
@@ -1060,7 +1060,7 @@
"your_unique_api_key": "Your unique API key",
"copy_safe_api_key": "Copy this API key and save it somewhere safe. If you lose this key you have to generate a new one.",
"zapier_setup_instructions": "<0>Log into your Zapier account and create a new Zap.</0><1>Select Cal.com as your Trigger app. Also choose a Trigger event.</1><2>Choose your account and then enter your Unique API Key.</2><3>Test your Trigger.</3><4>You're set!</4>",
"make_setup_instructions": "<0>Log into your Make account and create a new Scenario.</0><1>Select Cal.com as your Trigger app. Also choose a Trigger event.</1><2>Choose your account and then enter your Unique API Key.</2><3>Test your Trigger.</3><4>You're set!</4>",
"make_setup_instructions": "<0>Go to <1><0>Make Invite Link</0></1> and install the Cal.com app.</0><1>Log into your Make account and create a new Scenario.</1><2>Select Cal.com as your Trigger app. Also choose a Trigger event.</2><3>Choose your account and then enter your Unique API Key.</3><4>Test your Trigger.</4><5>You're set!</5>",
"install_zapier_app": "Please first install the Zapier App in the app store.",
"install_make_app": "Please first install the Make App in the app store.",
"connect_apple_server": "Connect to Apple Server",
@@ -1093,7 +1093,6 @@
"or_lowercase": "or",
"nevermind": "Nevermind",
"go_to": "Go to: ",
"invite_link_make": "<li>Go to<a>Make Invite Link</a> and install Cal.com app</li>",
"zapier_invite_link": "Zapier Invite Link",
"meeting_url_provided_after_confirmed": "A Meeting URL will be created once the event is confirmed.",
"dynamically_display_attendee_or_organizer": "Dynamically display the name of your attendee for you, or your name if it's viewed by your attendee",
@@ -1689,9 +1688,11 @@
"delete_sso_configuration_confirmation_description": "Are you sure you want to delete the {{connectionType}} configuration? Your team members who use {{connectionType}} login will no longer be able to access Cal.com.",
"organizer_timezone": "Organizer timezone",
"email_user_cta": "View Invitation",
"email_no_user_invite_heading": "You’ve been invited to join a {{appName}} {{entity}}",
"email_no_user_invite_heading_team": "You’ve been invited to join a {{appName}} team",
"email_no_user_invite_heading_org": "You’ve been invited to join a {{appName}} organization",
"email_no_user_invite_subheading": "{{invitedBy}} has invited you to join their team on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.",
"email_user_invite_subheading": "{{invitedBy}} has invited you to join their {{entity}} `{{teamName}}` on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your {{entity}} to schedule meetings without the email tennis.",
"email_user_invite_subheading_team": "{{invitedBy}} has invited you to join their team `{{teamName}}` on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.",
"email_user_invite_subheading_org": "{{invitedBy}} has invited you to join their organization `{{teamName}}` on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your organization to schedule meetings without the email tennis.",
"email_no_user_invite_steps_intro": "We’ll walk you through a few short steps and you’ll be enjoying stress free scheduling with your {{entity}} in no time.",
"email_no_user_step_one": "Choose your username",
"email_no_user_step_two": "Connect your calendar account",
@@ -1914,7 +1915,6 @@
"install_app_on": "Install app on",
"create_for": "Create for",
"currency": "Currency",
"setup_organization": "Setup an Organization",
"organization_banner_description": "Create an environments where your teams can create shared apps, workflows and event types with round-robin and collective scheduling.",
"organization_banner_title": "Manage organizations with multiple teams",
"set_up_your_organization": "Set up your organization",
@@ -2044,5 +2044,7 @@
"include_calendar_event": "Include calendar event",
"recently_added":"Recently added",
"no_members_found": "No members found",
"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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -129,7 +129,7 @@
"team_upgrade_banner_description": "Gracias por probar nuestro nuevo plan de equipo. Notamos que su equipo \"{{teamName}}\" necesita actualizarse.",
"upgrade_banner_action": "Actualizar aquí",
"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.",
"use_link_to_reset_password": "Utilice el enlace de abajo para restablecer su contraseña",
"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.",
"layout": "Diseño",
"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_description": "Puede seleccionar varios y quienes le reservan pueden cambiar de vista. Esto se puede anular por evento.",
"bookerlayout_month_view": "Mes",
@@ -315,7 +315,7 @@
"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_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.",
"sunday_time_error": "Hora inválida del domingo",
"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.",
"org_description": "Algunas frases sobre su organización. Esto aparecerá en la página de la URL de su organización.",
"members": "Miembros",
"organization_members": "Miembros de la organización",
"organization_members": "Miembros de Organization",
"member": "Miembro",
"number_member_one": "{{count}} miembro",
"number_member_other": "{{count}} miembros",
@@ -699,7 +699,7 @@
"create_team_to_get_started": "Crea un equipo para empezar",
"teams": "Equipos",
"team": "Equipo",
"organization": "Organización",
"organization": "Organization",
"team_billing": "Facturación del equipo",
"team_billing_description": "Gestione la facturación para su equipo",
"upgrade_to_flexible_pro_title": "Hemos cambiado la facturación de los equipos",
@@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "¿Está seguro de que desea eliminar la configuración de {{connectionType}}? Los miembros de su equipo que utilicen el inicio de sesión {{connectionType}} ya no podrán acceder a Cal.com.",
"organizer_timezone": "Zona horaria del organizador",
"email_user_cta": "Ver la invitación",
"email_no_user_invite_heading": "Lo invitaron a unirse a {{appName}} {{entity}}",
"email_no_user_invite_subheading": "{{invitedBy}} lo invitó a unirse a su equipo en {{appName}}. {{appName}} es el programador de eventos que les permite a usted y a su equipo programar reuniones sin necesidad de enviar correos electrónicos.",
"email_user_invite_subheading": "{{invitedBy}} lo ha invitado a unirse a su {{entity}} \"{{teamName}}\" en {{appName}}. {{appName}} es el planificador de eventos que le permite a usted y a su {{entity}} programar reuniones sin necesidad de correos electrónicos.",
"email_user_invite_subheading_team": "{{invitedBy}} lo ha invitado a unirse a su equipo \"{{teamName}}\" en {{appName}}. {{appName}} es el planificador de eventos que le permite a usted y a su equipo programar reuniones sin correos electrónicos de ida y vuelta.",
"email_no_user_invite_steps_intro": "Lo guiaremos a través de unos pocos pasos y disfrutará de una programación sin estrés con su {{entity}} en muy poco tiempo.",
"email_no_user_step_one": "Elija su nombre de usuario",
"email_no_user_step_two": "Conecte su cuenta de calendario",
@@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "Cree su primer webhook para este tipo de evento",
"install_app_on": "Instalar aplicación en",
"create_for": "Crear para",
"setup_organization": "Configurar una organización",
"organization_banner_description": "Cree entornos en los que sus equipos puedan crear aplicaciones, flujos de trabajo y tipos de eventos compartidos con programación por turnos y colectiva.",
"organization_banner_title": "Gestione organizaciones con varios equipos",
"set_up_your_organization": "Configure su organización",
@@ -1918,7 +1916,7 @@
"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_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_settings": "Mi configuración",
"crm": "CRM",
@@ -1060,7 +1060,7 @@
"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.",
"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_make_app": "Veuillez d'abord installer l'application Make dans l'App Store.",
"connect_apple_server": "Se connecter au serveur d'Apple",
@@ -1093,7 +1093,6 @@
"or_lowercase": "ou",
"nevermind": "Annuler",
"go_to": "Aller à : ",
"invite_link_make": "<li>Accédez<a>au lien d'invitation Make</a> et installez l'application Cal.com</li>",
"zapier_invite_link": "Lien d'invitation Zapier",
"meeting_url_provided_after_confirmed": "Un lien de rendez-vous sera créé une fois l'événement confirmé.",
"dynamically_display_attendee_or_organizer": "Affiche dynamiquement le nom du participant pour vous ou votre nom s'il est vu par le participant",
@@ -1689,9 +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.",
"organizer_timezone": "Fuseau horaire de l'organisateur",
"email_user_cta": "Voir l'invitation",
"email_no_user_invite_heading": "Vous avez été invité à rejoindre une {{entity}} {{appName}}",
"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_user_invite_subheading": "{{invitedBy}} vous a invité à rejoindre son {{entity}} « {{teamName}} » sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre {{entity}} 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_step_one": "Choisissez votre nom d'utilisateur",
"email_no_user_step_two": "Connectez votre compte de calendrier",
@@ -1914,7 +1915,6 @@
"install_app_on": "Installer l’application sur",
"create_for": "Créer pour",
"currency": "Devise",
"setup_organization": "Configurer une organisation",
"organization_banner_description": "Créez un environnement où vos équipes peuvent créer des applications partagées, des workflows et des types d'événements avec une planification round-robin et collective.",
"organization_banner_title": "Gérer les organisations avec plusieurs équipes",
"set_up_your_organization": "Configurer votre organisation",
@@ -2044,5 +2044,7 @@
"include_calendar_event": "Inclure l'événement du calendrier",
"recently_added": "Ajouté récemment",
"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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "אתה בטוח שאתה רוצה למחוק את הגדרות {{connectionType}}? חברי הצוות שלך שמשתמשים ב- {{connectionType}} להזדהות לא יוכלו להשתמש בו להכנס ל- Cal.com.",
"organizer_timezone": "מארגן אזורי זמן",
"email_user_cta": "צפה בהזמנה",
"email_no_user_invite_heading": "הוזמנת להצטרף ל{{entity}} של {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} הזמין אותך להצטרף לצוות שלו ב- {{appName}}. {{appName}} הינה מתזמן זימונים שמאפשר לך ולצוות שלך לזמן פגישות בלי כל הפינג פונג במיילים.",
"email_user_invite_subheading": "{{invitedBy}} הזמין/ה אותך להצטרף ל-{{entity}} שלו/ה בשם ״{{teamName}}״ באפליקציה {{appName}}. אפליקציית {{appName}} היא כלי לקביעת מועדים לאירועים שמאפשר לך ול-{{entity}} שלך לתזמן פגישות בלי הצורך לנהל התכתבויות ארוכות בדוא״ל.",
"email_user_invite_subheading_team": "{{invitedBy}} הזמין/ה אותך להצטרף לצוות שלו/ה בשם '{{teamName}}' באפליקציה {{appName}}. אפליקציית {{appName}} היא כלי לקביעת מועדים לאירועים שמאפשר לך ולצוות שלך לתזמן פגישות בלי כל הפינג פונג במיילים.",
"email_no_user_invite_steps_intro": "נדריך אותך במספר קטן של צעדים ותוכל/י להתחיל ליהנות מקביעת מועדים עם ה-{{entity}} שלך במהירות ובלי בעיות.",
"email_no_user_step_one": "בחר שם משתמש",
"email_no_user_step_two": "קשר את לוח השנה שלך",
@@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "צור/צרי את ה-Webhook הראשון שלך עבור סוג האירוע הזה",
"install_app_on": "התקנת האפליקציה ב-",
"create_for": "צור/צרי עבור",
"setup_organization": "הגדרת Organization",
"organization_banner_description": "צור/צרי סביבות שבהן הצוותים שלך יוכלו ליצור אפליקציות, תהליכי עבודה וסוגי אירועים משותפים, עם תכונות כמו סבב וקביעת מועדים שיתופית.",
"organization_banner_title": "ניהול ארגונים עם צוותים מרובים",
"set_up_your_organization": "הגדרת הארגון שלך",
@@ -403,7 +403,7 @@
"recording_ready": "Il link per scaricare la registrazione è pronto",
"booking_created": "Prenotazione Creata",
"booking_rejected": "Prenotazione Rifiutata",
"booking_requested": "Prenotazione Richiesta",
"booking_requested": "Richiesta di prenotazione inviata",
"meeting_ended": "Riunione terminata",
"form_submitted": "Modulo inviato",
"event_triggers": "Attivatori Evento",
@@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "Eliminare la configurazione {{connectionType}}? I membri del tuo team che utilizzano l'accesso {{connectionType}} non saranno più in grado di accedere a Cal.com.",
"organizer_timezone": "Fuso orario organizzatore",
"email_user_cta": "Visualizza invito",
"email_no_user_invite_heading": "Hai ricevuto un invito a partecipare a un/una {{entity}} di {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} ti ha invitato a unirti al suo team su {{appName}}. {{appName}} è uno strumento di pianificazione di eventi che permette al tuo team di pianificare le riunioni senza scambiare decine di e-mail.",
"email_user_invite_subheading": "{{invitedBy}} ti ha invitato a partecipare al/alla {{entity}} `{{teamName}}` su {{appName}}. {{appName}} è uno strumento di pianificazione di eventi che permette a te e al tuo/tua {{entity}} di pianificare le riunioni senza scambiarvi decine di e-mail.",
"email_user_invite_subheading_team": "{{invitedBy}} ti ha invitato a unirti al suo team `{{teamName}}` su {{appName}}. {{appName}} è uno strumento di pianificazione di eventi che permette a te e al tuo team di pianificare le riunioni senza scambiare decine di e-mail.",
"email_no_user_invite_steps_intro": "Ti assisteremo nei passaggi iniziali e potrai rapidamente pianificare eventi per il/la {{entity}} senza fatica.",
"email_no_user_step_one": "Scegli il tuo nome utente",
"email_no_user_step_two": "Collega il tuo account di calendario",
@@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "Crea il primo webhook per questo tipo di evento",
"install_app_on": "Installa l'app su",
"create_for": "Crea per",
"setup_organization": "Imposta un'organizzazione",
"organization_banner_description": "Crea ambienti dove i tuoi team potranno creare e condividere applicazioni, flussi di lavoro e tipi di eventi con pianificazioni di gruppo e round robin.",
"organization_banner_title": "Gestisci organizzazioni con più team",
"set_up_your_organization": "Imposta la tua organizzazione",
@@ -1889,7 +1887,7 @@
"admin_username": "Nome utente dell'amministratore",
"organization_name": "Nome dell'organizzazione",
"organization_url": "URL dell'organizzazione",
"organization_verify_header": "Verifica l'e-mail della tua organizzazione",
"organization_verify_header": "Verifica il tuo indirizzo e-mail",
"organization_verify_email_body": "Usa il codice sottostante per verificare il tuo indirizzo e-mail e proseguire la configurazione dell'organizzazione.",
"additional_url_parameters": "Parametri aggiuntivi dell'URL",
"about_your_organization": "Informazioni sulla tua organizzazione",
+10 -12
View File
@@ -306,7 +306,7 @@
"password_has_been_reset_login": "パスワードがリセットされました。新しく作成したパスワードでログインできるようになりました。",
"layout": "レイアウト",
"bookerlayout_default_title": "デフォルトの表示",
"bookerlayout_description": "複数のものを選択することができ、予約者は表示を切り替えることができます。",
"bookerlayout_description": "複数のビューを選択することができ、予約者は表示を切り替えることができます。",
"bookerlayout_user_settings_title": "予約のレイアウト",
"bookerlayout_user_settings_description": "複数のものを選択することができ、予約者は表示を切り替えることができます。これはイベントごとに上書きが可能です。",
"bookerlayout_month_view": "月",
@@ -391,7 +391,7 @@
"user_dynamic_booking_disabled": "グループ内の一部のユーザーは、現在動的なグループ予約を無効にしています",
"allow_dynamic_booking_tooltip": "\"+\" を使って複数のユーザー名を追加することで動的に作成できるグループ予約リンク。例: \"{{appName}}/bailey+peer\"",
"allow_dynamic_booking": "出席者が動的なグループ予約を通じてあなたを予約できるようにする",
"dynamic_booking": "動的なグループリンク",
"dynamic_booking": "ダイナミックグループリンク",
"email": "Eメールアドレス",
"email_placeholder": "jdoe@example.com",
"full_name": "フルネーム",
@@ -555,7 +555,7 @@
"member": "メンバー",
"number_member_one": "{{count}} 人のメンバー",
"number_member_other": "{{count}} 人のメンバー",
"number_selected": "{{count}} 件が選択されました",
"number_selected": "{{count}} が選択されました",
"owner": "所有者",
"admin": "管理者",
"administrator_user": "管理者ユーザー",
@@ -1150,8 +1150,8 @@
"choose_template": "テンプレートを選択する",
"custom": "カスタム",
"reminder": "リマインダー",
"rescheduled": "スケジュール変更済み",
"completed": "完了",
"rescheduled": "スケジュールが変更されました",
"completed": "完了しました",
"reminder_email": "リマインダー:{{date}} の {{name}} との {{eventType}}",
"not_triggering_existing_bookings": "イベントの予約時に電話番号の入力を求められるため、すでにある予約はトリガーされません。",
"minute_one": "{{count}} 分",
@@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "{{connectionType}} の構成を削除してもよろしいですか?{{connectionType}} でのログインを使用しているチームメンバーは、Cal.com にアクセスできなくなります。",
"organizer_timezone": "主催者のタイムゾーン",
"email_user_cta": "招待を表示",
"email_no_user_invite_heading": "{{appName}} の {{entity}} への参加に招待されました",
"email_no_user_invite_subheading": "{{invitedBy}} は {{appName}} のチームに参加するようあなたを招待しました。{{appName}} は、イベント調整スケジューラーです。チームと延々とメールのやりとりをすることなくミーティングのスケジュール設定を行うことができます。",
"email_user_invite_subheading": "{{invitedBy}} から {{appName}} の {{entity}} \"{{teamName}}\" に参加するよう招待されました。{{appName}} はイベント調整スケジューラーであり、{{entity}} 内で延々とメールのやりとりを続けることなくミーティングのスケジュールを設定できます。",
"email_user_invite_subheading_team": "{{invitedBy}}から{{appName}}の「{{teamName}}」に参加するよう招待されました。{{appName}}はイベント調整スケジューラーで、チーム内で延々とメールのやりとりをすることなく、ミーティングのスケジュールを設定できます。",
"email_no_user_invite_steps_intro": "いくつかの短い手順を踏むだけで、すぐに {{entity}} のストレスフリーなスケジュール設定をお楽しみいただけます。",
"email_no_user_step_one": "ユーザー名を選択",
"email_no_user_step_two": "カレンダーアカウントを接続",
@@ -1673,7 +1672,7 @@
"scheduler": "{Scheduler}",
"no_workflows": "ワークフローがありません",
"change_filter": "個人やチームのワークフローを表示するためのフィルターを変更します。",
"change_filter_common": "結果を表示するフィルターを変更します。",
"change_filter_common": "フィルターを変更して結果を表示します。",
"no_results_for_filter": "このフィルターに該当する結果はありません",
"recommended_next_steps": "推奨される次のステップ",
"create_a_managed_event": "管理されたイベントの種類を作成",
@@ -1697,7 +1696,7 @@
"booking_questions_title": "予約の質問",
"booking_questions_description": "予約ページで尋ねる質問をカスタマイズする",
"add_a_booking_question": "質問を追加",
"identifier": "識別子",
"identifier": "ID",
"duplicate_email": "メールが重複しています",
"booking_with_payment_cancelled": "このイベントの支払いはもうできません",
"booking_with_payment_cancelled_already_paid": "この予約に関するお支払いの払い戻しについては、現在処理中です。",
@@ -1841,7 +1840,7 @@
"invite_link_copied": "招待リンクをコピーしました",
"invite_link_deleted": "招待リンクを削除しました",
"invite_link_updated": "招待リンクの設定を保存しました",
"link_expires_after": "リンクが期限切れとなるまで、あと...",
"link_expires_after": "リンクの期限切れまで...",
"one_day": "1 日",
"seven_days": "7 日",
"thirty_days": "30 日",
@@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "このイベントの種類の最初の Webhook を作成",
"install_app_on": "次のアカウントにアプリをインストール",
"create_for": "作成対象",
"setup_organization": "組織をセットアップする",
"organization_banner_description": "チームがラウンドロビンスケジューリングや一括スケジューリングを活用して、共有アプリ、ワークフロー、イベントの種類を作成できる環境を作成します。",
"organization_banner_title": "複数のチームを持つ組織を管理する",
"set_up_your_organization": "組織をセットアップする",
@@ -1938,7 +1936,7 @@
"insights_all_org_filter": "すべて",
"insights_team_filter": "チーム: {{teamName}}",
"insights_user_filter": "ユーザー: {{userName}}",
"insights_subtitle": "イベント全体での予約に関する分析情報を表示する",
"insights_subtitle": "イベント全体での予約に関する Insights を表示する",
"custom_plan": "カスタムプラン",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -21,7 +21,7 @@
"verify_email_email_link_text": "버튼 클릭이 마음에 들지 않는 경우 다음 링크를 사용하세요.",
"email_sent": "이메일이 성공적으로 전송되었습니다",
"event_declined_subject": "거절됨: {{date}} {{title}}",
"event_cancelled_subject": "취소됨: {{title}} 날짜 {{date}}",
"event_cancelled_subject": "취소됨: {{title}}, 날짜 {{date}}",
"event_request_declined": "회의 요청이 거절되었습니다.",
"event_request_declined_recurring": "되풀이 이벤트 요청이 거부되었습니다",
"event_request_cancelled": "예약된 일정이 취소되었습니다",
@@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "{{connectionType}} 구성을 삭제하시겠습니까? {{connectionType}} 로그인을 사용하는 팀원은 더 이상 Cal.com에 액세스할 수 없습니다.",
"organizer_timezone": "주최자 시간대",
"email_user_cta": "초대 보기",
"email_no_user_invite_heading": "{{appName}} {{entity}}에 참여 초대를 받으셨습니다",
"email_no_user_invite_subheading": "{{invitedBy}} 님이 귀하를 {{appName}}에서 자신의 팀에 초대했습니다. {{appName}} 앱은 귀하와 귀하의 팀이 이메일을 주고 받지 않고 회의 일정을 잡을 수 있게 해주는 이벤트 정리 스케줄러입니다.",
"email_user_invite_subheading": "{{invitedBy}} 님이 {{appName}}에서 자신의 {{entity}} `{{teamName}}`에 귀하의 참여를 초대했습니다. {{appName}}은(는) 사용자와 {{entity}}이(가) 이메일을 주고 받지 않고도 회의 일정을 잡을 수 있게 해주는 이벤트 조정 스케줄러입니다.",
"email_user_invite_subheading_team": "{{invitedBy}}님이 {{appName}}에서 자신의 `{{teamName}}` 팀에 가입하도록 당신을 초대했습니다. {{appName}}은 유저와 팀이 이메일을 주고 받지 않고도 회의 일정을 잡을 수 있게 하는 이벤트 조율 스케줄러입니다.",
"email_no_user_invite_steps_intro": "안내해 드릴 몇 가지 간단한 단계만 거치면 곧 스트레스 없이 편리하게 {{entity}} 일정을 관리할 수 있게 될 것입니다.",
"email_no_user_step_one": "사용자 이름 선택",
"email_no_user_step_two": "캘린더 계정 연결",
@@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "이 이벤트 유형에 대한 첫 번째 웹훅 만들기",
"install_app_on": "앱 설치 대상",
"create_for": "작성 대상",
"setup_organization": "조직 설정",
"organization_banner_description": "팀이 라운드 로빈 및 공동 예약을 통해 공유 앱, 워크플로 및 이벤트 유형을 생성할 수 있는 환경을 만듭니다.",
"organization_banner_title": "여러 팀으로 조직 관리",
"set_up_your_organization": "조직 설정",
@@ -1663,9 +1663,8 @@
"delete_sso_configuration_confirmation_description": "Weet u zeker dat u de {{connectionType}}-configuratie wilt verwijderen? Uw teamleden die {{connectionType}}-aanmelding gebruiken hebben niet langer toegang tot Cal.com.",
"organizer_timezone": "Tijdzone organisator",
"email_user_cta": "Uitnodiging weergeven",
"email_no_user_invite_heading": "U bent uitgenodigd om lid te worden van {{appName}} {{entity}}",
"email_no_user_invite_subheading": "{{invitedBy}} heeft u uitgenodigd om lid te worden van zijn team op {{appName}}. {{appName}} is de gebeurtenissenplanner die u en uw team in staat stelt vergaderingen te plannen zonder heen en weer te e-mailen.",
"email_user_invite_subheading": "{{invitedBy}} heeft u uitgenodigd om lid te worden van zijn {{entity}} '{{teamName}}' op {{appName}}. {{appName}} is de gebeurtenissenplanner die u en uw {{entity}} in staat stelt afspraken te plannen zonder heen en weer te e-mailen.",
"email_user_invite_subheading_team": "{{invitedBy}} heeft u uitgenodigd om lid te worden van zijn team '{{teamName}}' op {{appName}}. {{appName}} is de gebeurtenissenplanner die u en uw team in staat stelt afspraken te plannen zonder heen en weer te e-mailen.",
"email_no_user_invite_steps_intro": "We doorlopen samen een paar korte stappen en in een mum van tijd kunt u genieten van een stressvrije planning met uw {{entity}}.",
"email_no_user_step_one": "Kies uw gebruikersnaam",
"email_no_user_step_two": "Koppel uw agenda-account",
@@ -1885,7 +1884,6 @@
"first_event_type_webhook_description": "Maak uw eerste webhook voor dit gebeurtenistype",
"install_app_on": "App installeren op",
"create_for": "Maken voor",
"setup_organization": "Een organisatie instellen",
"organization_banner_description": "Maak een omgeving waarin uw teams gedeelde apps, werkstromen en gebeurtenistypen kunnen maken met round-robin en collectieve planning.",
"organization_banner_title": "Beheer organisaties met meerdere teams",
"set_up_your_organization": "Stel uw organisatie in",
@@ -12,7 +12,7 @@
"have_any_questions": "Masz pytania? Jesteśmy tutaj, aby pomóc.",
"reset_password_subject": "{{appName}}: Instrukcje resetowania hasła",
"verify_email_subject": "{{appName}}: zweryfikuj swoje konto",
"check_your_email": "Sprawdź swój adres e-mail",
"check_your_email": "Sprawdź swoją pocztę e-mail",
"verify_email_page_body": "Wysłaliśmy wiadomość e-mail na adres {{email}}. Weryfikacja adresu e-mail jest ważna, ponieważ umożliwia zagwarantowanie prawidłowego działania kalendarza i dostarczania wiadomości e-mail z aplikacji {{appName}}.",
"verify_email_banner_body": "Potwierdź adres e-mail, aby upewnić się, że wiadomości e-mail i powiadomienia z kalendarza będą do Ciebie docierać.",
"verify_email_email_header": "Zweryfikuj swój adres e-mail",
@@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "Czy na pewno chcesz usunąć konfigurację {{connectionType}}? Członkowie zespołu, którzy używają logowania {{connectionType}} nie będą już mogli uzyskać dostępu do Cal.com.",
"organizer_timezone": "Strefa czasowa organizatora",
"email_user_cta": "Wyświetl zaproszenie",
"email_no_user_invite_heading": "Zaproszono Cię do dołączenia do aplikacji {{appName}} ({{entity}})",
"email_no_user_invite_subheading": "Użytkownik {{invitedBy}} zaprasza Cię do dołączenia do jego zespołu w aplikacji {{appName}}. Aplikacja {{appName}} to terminarz do planowania wydarzeń, który umożliwi Tobie i Twojemu zespołowi planowanie spotkań bez czasochłonnej wymiany wiadomości e-mail.",
"email_user_invite_subheading": "{{invitedBy}} zaprasza Cię do dołączenia do: {{entity}} „{{teamName}}” w aplikacji {{appName}}. Aplikacja {{appName}} to terminarz do planowania wydarzeń, który umożliwi {{entity}} planowanie spotkań bez czasochłonnej wymiany wiadomości e-mail.",
"email_user_invite_subheading_team": "Użytkownik {{invitedBy}} zaprasza Cię do dołączenia do jego zespołu „{{teamName}}” w aplikacji {{appName}}. Aplikacja {{appName}} to terminarz do planowania wydarzeń, który umożliwi Tobie i Twojemu zespołowi planowanie spotkań bez czasochłonnej wymiany wiadomości e-mail.",
"email_no_user_invite_steps_intro": "Przeprowadzimy Cię przez kilka krótkich kroków i wkrótce w ramach {{entity}} zaczniesz z przyjemnością korzystać z bezstresowego planowania.",
"email_no_user_step_one": "Wybierz nazwę użytkownika",
"email_no_user_step_two": "Połącz swoje konto kalendarza",
@@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "Utwórz swój pierwszy element webhook dla tego typu wydarzenia",
"install_app_on": "Zainstaluj aplikację na koncie",
"create_for": "Utwórz dla",
"setup_organization": "Skonfiguruj organizację",
"organization_banner_description": "Utwórz środowisko, w którym zespoły będą mogły tworzyć wspólne aplikacje, przepływy pracy i typy wydarzeń przy użyciu algorytmu karuzelowego lub zbiorowego ustalania harmonogramu.",
"organization_banner_title": "Zarządzaj organizacjami z wieloma zespołami",
"set_up_your_organization": "Skonfiguruj swoją organizację",
@@ -1889,7 +1887,7 @@
"admin_username": "Nazwa użytkownika administratora",
"organization_name": "Nazwa organizacji",
"organization_url": "Adres URL organizacji",
"organization_verify_header": "Zweryfikuj adres e-mail swojej organizacji",
"organization_verify_header": "Zweryfikuj swój adres e-mail w organizacji",
"organization_verify_email_body": "Użyj poniższego kodu, aby zweryfikować adres e-mail i kontynuować konfigurację swojej organizacji.",
"additional_url_parameters": "Dodatkowe parametry adresu URL",
"about_your_organization": "Informacje o Twojej organizacji",
@@ -1675,9 +1675,8 @@
"delete_sso_configuration_confirmation_description": "Você tem certeza que deseja remover a configuração de {{connectionType}}? Os membros do seu time que utilizam {{connectionType}} para fazer login não conseguirão acessar o Cal.com.",
"organizer_timezone": "Fuso horário do organizador",
"email_user_cta": "Ver convite",
"email_no_user_invite_heading": "Você recebeu um convite para ingressar em {{entity}} de {{appName}}",
"email_no_user_invite_subheading": "Você recebeu um convite de {{invitedBy}} para ingressar em sua equipe em {{appName}}. {{appName}} é um agendador que concilia eventos e permite que sua equipe agende reuniões sem precisar trocar e-mails.",
"email_user_invite_subheading": "Você recebeu um convite de {{invitedBy}} para ingressar na equipe \"{{teamName}}\" de {{entity}} em {{appName}}. {{appName}} é um agendador que concilia eventos e permite que {{entity}} agende reuniões sem precisar trocar e-mails.",
"email_user_invite_subheading_team": "Você recebeu um convite de {{invitedBy}} para ingressar na equipe \"{{teamName}}\" em {{appName}}. {{appName}} é um agendador que concilia eventos e permite que sua equipe agende reuniões sem precisar trocar e-mails.",
"email_no_user_invite_steps_intro": "Orientaremos ao longo de alguns passos rápidos para que você comece logo a agendar eventos com {{entity}} sem preocupação.",
"email_no_user_step_one": "Escolha seu nome de usuário",
"email_no_user_step_two": "Conecte com sua conta de calendário",
@@ -1898,7 +1897,6 @@
"install_app_on": "Instalar aplicativo em",
"create_for": "Criar para",
"currency": "Moeda",
"setup_organization": "Definir uma Organização",
"organization_banner_description": "Crie um ambiente onde suas equipes podem criar tipos de evento, fluxos de trabalho e aplicativos compartilhados com agendamento coletivo e round robin.",
"organization_banner_title": "Gerenciar organizações com múltiplas equipes",
"set_up_your_organization": "Configurar sua organização",
@@ -12,7 +12,7 @@
"have_any_questions": "Tem perguntas? Estamos disponíveis para ajudar.",
"reset_password_subject": "{{appName}}: Instruções de redefinição da senha",
"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_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",
@@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "Tem a certeza que pretende eliminar a configuração {{connectionType}}? Os membros da sua equipa que utilizem {{connectionType}} para o início de sessão deixarão de poder aceder a Cal.com.",
"organizer_timezone": "Fuso horário do organizador",
"email_user_cta": "Ver convite",
"email_no_user_invite_heading": "Foi convidado para se juntar a um {{entity}} {{appName}}",
"email_no_user_invite_subheading": "Recebeu um convite de {{invitedBy}} para fazer parte da respetiva equipa em {{appName}}. {{appName}} é um ferramenta de conciliação e agendamento de eventos que lhe permite a si e à sua equipa agendar reuniões sem o pingue-pongue de e-mails.",
"email_user_invite_subheading": "Recebeu um convite de {{invitedBy}} para fazer parte da sua {{entity}} `{{teamName}}` em {{appName}}. {{appName}} é uma ferramenta de conciliação e agendamento de eventos que lhe permite a si e à sua {{entity}} agendar reuniões sem o pingue-pongue de mensagens eletrónicas.",
"email_user_invite_subheading_team": "Recebeu um convite de {{invitedBy}} para fazer parte da equipa `{{teamName}}` em {{appName}}. {{appName}} é uma ferramenta de conciliação e agendamento de eventos que lhe permite a si e à sua equipa agendar reuniões sem o pingue-pongue de mensagens eletrónicas.",
"email_no_user_invite_steps_intro": "Vamos ajudar com alguns pequenos passos para que você e a sua {{entity}} possam desfrutar rapidamente de uma gestão de agendamentos sem complicações.",
"email_no_user_step_one": "Escolha o seu nome de utilizador",
"email_no_user_step_two": "Associe a sua conta de calendário",
@@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "Crie o seu primeiro webhook para este tipo de evento",
"install_app_on": "Instalar aplicação em",
"create_for": "Criar para",
"setup_organization": "Configurar uma Organização",
"organization_banner_description": "Crie um ambiente onde as suas equipas possam criar aplicações partilhadas, fluxos de trabalho e tipos de eventos com distribuição equilibrada e agendamento coletivo.",
"organization_banner_title": "Faça a gestão de múltiplas organizações com múltiplas equipas",
"set_up_your_organization": "Configurar a sua organização",
@@ -118,7 +118,7 @@
"team_info": "Informații echipă",
"request_another_invitation_email": "Dacă preferați să nu utilizați {{toEmail}} ca e-mail {{appName}} sau aveți deja un cont {{appName}}, vă rugăm să solicitați o altă invitație la acel e-mail.",
"you_have_been_invited": "Ați fost invitat să vă alăturați echipei {{teamName}}",
"user_invited_you": "{{user}} v-a invitat să faceți parte din {{entity}} {{team}} de pe {{appName}}",
"user_invited_you": "{{user}} v-a invitat să faceți parte din {{entity}} {{team}} pe {{appName}}",
"hidden_team_member_title": "Sunteți ascuns în această echipă",
"hidden_team_member_message": "Licența dvs. nu este plătită. Fie faceți upgrade la Pro, fie anunțați proprietarul echipei că vă poate plăti licența.",
"hidden_team_owner_message": "Aveți nevoie de un cont Pro pentru a utiliza echipe. Sunteți ascuns până când faceți upgrade.",
@@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "Sigur doriți să ștergeți configurația {{connectionType}}? Membrii echipei dvs. care utilizează conectarea prin {{connectionType}} nu vor mai putea accesa Cal.com.",
"organizer_timezone": "Fusul orar al organizatorului",
"email_user_cta": "Vizualizare invitație",
"email_no_user_invite_heading": "Ați fost invitat să faceți parte dintr-o {{entity}} {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} v-a invitat să faceți parte din echipa sa de pe {{appName}}. {{appName}} este un instrument de planificare a evenimentelor, care vă permite dvs. și echipei dvs. să programați ședințe fără a face ping-pong prin e-mail.",
"email_user_invite_subheading": "{{invitedBy}} v-a invitat să faceți parte din {{entity}} „{{teamName}}” de pe {{appName}}. {{appName}} este un instrument de planificare a evenimentelor, prin care dvs. și {{entity}} dvs. puteți să programați ședințe fără a face ping-pong prin e-mail.",
"email_user_invite_subheading_team": "{{invitedBy}} v-a invitat să faceți parte din echipa sa „{{teamName}}” de pe {{appName}}. {{appName}} este un instrument de planificare a evenimentelor, care vă permite dvs. și echipei dvs. să programați ședințe fără a face ping-pong prin e-mail.",
"email_no_user_invite_steps_intro": "Vom parcurge împreună câțiva pași simpli și vă veți bucura alături de {{entity}} dvs. de programări fără probleme, în cel mai scurt timp.",
"email_no_user_step_one": "Alegeți-vă numele de utilizator",
"email_no_user_step_two": "Conectați-vă contul de calendar",
@@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "Creați primul dvs. webhook pentru acest tip de eveniment",
"install_app_on": "Instalați aplicația în",
"create_for": "Creare pentru",
"setup_organization": "Configurarați o organizație",
"organization_banner_description": "Dezvoltați medii în care echipele dvs. să poată crea aplicații, fluxuri de lucru și tipuri de evenimente comune, folosind programarea colectivă sau cu alocare prin rotație.",
"organization_banner_title": "Gestionați organizații cu mai multe echipe",
"set_up_your_organization": "Configurați organizația",
@@ -118,7 +118,7 @@
"team_info": "Информация о команде",
"request_another_invitation_email": "Если вы не хотите использовать {{toEmail}} как ваш {{appName}} адрес электронной почты или уже есть аккаунт {{appName}}, пожалуйста, запросите другое приглашение на это письмо.",
"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_message": "Ваше место не оплачено. Перейдите на аккаунт PRO или свяжитесь с руководителем команды, чтобы сообщить ему, что он может оплатить ваше место.",
"hidden_team_owner_message": "Чтобы работать с командами, необходим аккаунт Pro. До перехода на этот тариф Вы остаетесь скрытым пользователем.",
@@ -403,7 +403,7 @@
"recording_ready": "Ссылка для скачивания записи готова",
"booking_created": "Бронирование создано",
"booking_rejected": "Бронирование отклонено",
"booking_requested": "Поступил запрос на бронирование",
"booking_requested": "Запрос на бронирование отправлен",
"meeting_ended": "Встреча завершилась",
"form_submitted": "Форма отправлена",
"event_triggers": "Триггеры событий",
@@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "Удалить конфигурацию {{connectionType}}? Участники команды, входящие в Cal.com через {{connectionType}}, больше не смогут получить к нему доступ.",
"organizer_timezone": "Часовой пояс организатора",
"email_user_cta": "Посмотреть приглашение",
"email_no_user_invite_heading": "Вас пригласили в {{entity}} {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} пригласил(-а) вас в команду в {{appName}}. {{appName}} — это гибкий планировщик событий, с помощью которого пользователи и целые команды могут планировать встречи без утомительной переписки по электронной почте.",
"email_user_invite_subheading": "{{invitedBy}} пригласил(-а) вас в {{entity}} `{{teamName}}` в {{appName}}. {{appName}} — это гибкий планировщик событий, с помощью которого вы и ваша {{entity}} можете планировать встречи без утомительной переписки по электронной почте.",
"email_user_invite_subheading_team": "{{invitedBy}} пригласил(а) вас в команду в `{{teamName}}` в приложении {{appName}}. {{appName}} — это гибкий планировщик событий, с помощью которого пользователи и целые команды могут планировать встречи без утомительной переписки по электронной почте.",
"email_no_user_invite_steps_intro": "Всего несколько шагов — и вы сможете оперативно и без стресса планировать встречи в рамках вашей {{entity}}.",
"email_no_user_step_one": "Выберите имя пользователя",
"email_no_user_step_two": "Подключите аккаунт календаря",
@@ -1878,10 +1877,9 @@
"first_event_type_webhook_description": "Создайте первый вебхук для этого типа событий",
"install_app_on": "Установить приложение в",
"create_for": "Создать для",
"setup_organization": "Настроите организацию",
"organization_banner_description": "Создавайте рабочие среды, в рамках которых ваши команды смогут создавать общие приложения, рабочие процессы и типы событий с назначением участников по очереди и коллективным планированием.",
"organization_banner_title": "Управляйте организациями с несколькими командами",
"set_up_your_organization": "Настройте организацию",
"set_up_your_organization": "Настройка профиля организации",
"organizations_description": "Организация — это общая рабочая среда, в которой команды могут создавать общие типы событий, приложения, рабочие процессы и многое другое.",
"must_enter_organization_name": "Необходимо ввести название организации",
"must_enter_organization_admin_email": "Необходимо ввести ваш адрес электронной почты в организации",
@@ -1889,7 +1887,7 @@
"admin_username": "Имя пользователя администратора",
"organization_name": "Название организации",
"organization_url": "URL-адрес организации",
"organization_verify_header": "Подтвердите адрес электронной почты вашей организации",
"organization_verify_header": "Подтвердите свой адрес электронной почты в организации",
"organization_verify_email_body": "С помощью кода ниже подтвердите свой адрес электронной почты, чтобы продолжить настройку организации.",
"additional_url_parameters": "Дополнительные параметры URL-адреса",
"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.",
"upgrade_banner_action": "Nadogradite ovde",
"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!",
"use_link_to_reset_password": "Resetujte lozinku koristeći link ispod",
"hey_there": "Zdravo,",
@@ -1660,9 +1660,8 @@
"delete_sso_configuration_confirmation_description": "Da li ste sigurni da želite da izbrišete {{connectionType}} konfiguraciju? Članovi vašeg tima koji koriste {{connectionType}} prijavljivanje više neće moći da pristupe Cal.com-u.",
"organizer_timezone": "Vremenska zona organizatora",
"email_user_cta": "Prikaži pozivnicu",
"email_no_user_invite_heading": "Pozvani ste da se pridružite {{appName}} {{entity}}",
"email_no_user_invite_subheading": "{{invitedBy}} vas je pozvao/la da se pridružite njihovom timu u {{appName}}. {{appName}} je planer za koordinaciju događaja koji omogućava vama i vašem timu da zakazujete sastanke bez dopisivanja imejlovima.",
"email_user_invite_subheading": "{{invitedBy}} vas je pozvao/la da se pridružite {{entity}} `{{teamName}}` na {{appName}}. {{appName}} je uređivač rasporeda koji omogućava vama i vašem {{entity}} da zakažete sastanke bez razmene imejlova.",
"email_user_invite_subheading_team": "{{invitedBy}} vas je pozvao/la da se pridružite njihovom timu `{{teamName}}` u {{appName}}. {{appName}} je planer za koordinaciju događaja koji omogućava vama i vašem timu da zakazujete sastanke bez dopisivanja imejlovima.",
"email_no_user_invite_steps_intro": "Provešćemo vas u nekoliko kratkiih koraka i za tren ćete uživati u zakazivanju bez stresa sa vašim {{entity}}.",
"email_no_user_step_one": "Izaberite korisničko ime",
"email_no_user_step_two": "Povežite se sa nalogom kalendara",
@@ -1879,7 +1878,6 @@
"first_event_type_webhook_description": "Napravite svoj prvi webhook za ovaj tip događaja",
"install_app_on": "Instaliraj aplikaciju",
"create_for": "Napravi za",
"setup_organization": "Postavi organizaciju",
"organization_banner_description": "Kreirajte okruženje gde vaši timovi mogu da postave deljene aplikacije, radne tokove i vrste događaja sa kružnom dodelom i zajedničko zakazivanje.",
"organization_banner_title": "Upravljajte organizacijama sa više timova",
"set_up_your_organization": "Postavite svoju organizaciju",
+8 -10
View File
@@ -21,7 +21,7 @@
"verify_email_email_link_text": "Här är länken om du inte gillar att klicka på knappar:",
"email_sent": "E-postmeddelandet har skickats",
"event_declined_subject": "Avvisades: {{title}} kl. {{date}}",
"event_cancelled_subject": "Avbröts: {{title}} {{date}}",
"event_cancelled_subject": "Avbokad: {{title}} {{date}}",
"event_request_declined": "Din bokningsförfrågan har avbjöts",
"event_request_declined_recurring": "Din återkommande händelse har avböjts",
"event_request_cancelled": "Din schemalagda bokning ställdes in",
@@ -306,7 +306,7 @@
"password_has_been_reset_login": "Ditt lösenord har återställts. Du kan nu logga in med ditt nyskapade lösenord.",
"layout": "Layout",
"bookerlayout_default_title": "Standardvy",
"bookerlayout_description": "Du kan välja flera och dina bokare kan byta vy.",
"bookerlayout_description": "Du kan välja flera och dina bokare kan byta vyer.",
"bookerlayout_user_settings_title": "Bokningslayout",
"bookerlayout_user_settings_description": "Du kan välja flera och bokare kan byta vy. Detta kan åsidosättas för varje enskild händelse.",
"bookerlayout_month_view": "Månad",
@@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "Vill du verkligen ta bort {{connectionType}}-konfigurationen? Dina teammedlemmar som använder {{connectionType}}-inloggning kan inte längre komma åt Cal.com.",
"organizer_timezone": "Organisatörens tidszon",
"email_user_cta": "Visa inbjudan",
"email_no_user_invite_heading": "Du har blivit inbjuden att gå med i en/ett {{appName}}-{{entity}}",
"email_no_user_invite_subheading": "{{invitedBy}} har bjudit in dig att gå med i sitt team på {{appName}}. {{appName}} är en händelsejonglerande schemaläggare som du och ditt team kan använda för att planera möten utan att skicka e-post fram och tillbaka.",
"email_user_invite_subheading": "{{invitedBy}} har bjudit in dig att gå med i deras {{entity}} '{{teamName}}' på {{appName}}. {{appName}} är schemaläggaren som gör det möjligt för dig och din/ditt {{entity}} att schemalägga möten utan att skicka e-post fram och tillbaka.",
"email_user_invite_subheading_team": "{{invitedBy}} har bjudit in dig att gå med i sitt team {{teamName}} den {{appName}}. {{appName}} är en händelsejonglerande schemaläggare som du och ditt team kan använda för att planera möten utan att skicka e-post fram och tillbaka.",
"email_no_user_invite_steps_intro": "Vi guidar dig genom några korta steg och du kommer att kunna schemalägga med din/ditt {{entity}} på nolltid utan stress.",
"email_no_user_step_one": "Välj ditt användarnamn",
"email_no_user_step_two": "Anslut ditt kalenderkonto",
@@ -1687,7 +1686,7 @@
"attendee_no_longer_attending": "En deltagare deltar inte längre i din händelse",
"attendee_no_longer_attending_subtitle": "{{name}} har avbokat. Detta innebär att en plats har blivit ledig för denna tid",
"create_event_on": "Skapa händelse i",
"create_routing_form_on": "Skapa omdirigeringsformulär",
"create_routing_form_on": "Skapa omdirigeringsformulär på",
"default_app_link_title": "Skapa en standardapplänk",
"default_app_link_description": "Om du skapar en standardapplänk kan alla händelsetyper som nyligen skapats använda den applänk som du har angett.",
"organizer_default_conferencing_app": "Arrangörens standardapp",
@@ -1878,18 +1877,17 @@
"first_event_type_webhook_description": "Skapa din första webhook för denna händelsetyp",
"install_app_on": "Installera appen på",
"create_for": "Skapa för",
"setup_organization": "Konfigurera en organisation",
"organization_banner_description": "Skapa en miljö där dina team kan skapa delade appar, arbetsflöden och händelsetyper med round-robin och kollektiv schemaläggning.",
"organization_banner_description": "Skapa miljöer där dina team kan skapa delade appar, arbetsflöden och händelsetyper med round-robin och kollektiv schemaläggning.",
"organization_banner_title": "Hantera organisationer med flera team",
"set_up_your_organization": "Konfigurera din organisation",
"organizations_description": "Organisationer är delade miljöer där team kan skapa delade händelsetyper, appar, arbetsflöden och mycket mer.",
"must_enter_organization_name": "Ett organisationsnamn måste anges",
"must_enter_organization_admin_email": "En organisations e-postadress måste anges",
"admin_email": "Din organisations e-postadress",
"admin_email": "Din e-postadress i organisationen",
"admin_username": "Administratörens användarnamn",
"organization_name": "Organisationens namn",
"organization_url": "Organisationens URL",
"organization_verify_header": "Verifiera din organisations e-postadress",
"organization_verify_header": "Verifiera din e-postadress i organisationen",
"organization_verify_email_body": "Använd koden nedan för att verifiera din e-postadress så att du kan fortsätta att konfigurera din organisation.",
"additional_url_parameters": "Ytterligare URL-parametrar",
"about_your_organization": "Om din organisation",
@@ -1940,5 +1938,5 @@
"insights_user_filter": "Användare: {{userName}}",
"insights_subtitle": "Visa Insights-bokningar för dina händelser",
"custom_plan": "Anpassad plan",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Lägg till dina nya strängar här ovanför ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -130,7 +130,7 @@
"upgrade_banner_action": "Buradan yükseltin",
"team_upgraded_successfully": "Ekibiniz başarıyla yükseltildi!",
"org_upgrade_banner_description": "Yeni Organization planımızı denediğiniz için teşekkür ederiz. \"{{teamName}}\" adlı Kuruluşunuzun planının yükseltilmesi gerektiğini fark ettik.",
"org_upgraded_successfully": "Kuruluşunuz başarıyla yükseltildi!",
"org_upgraded_successfully": "Organization'ınız başarıyla yükseltildi!",
"use_link_to_reset_password": "Şifrenizi sıfırlamak için aşağıdaki bağlantıyı kullanın",
"hey_there": "Selam,",
"forgot_your_password_calcom": "Şifrenizi mi unuttunuz? - {{appName}}",
@@ -551,7 +551,7 @@
"team_description": "Birkaç cümleyle ekibinizden bahsedin. Bu, ekibinizin URL sayfasında görünecektir.",
"org_description": "Birkaç cümleyle kuruluşunuzdan bahsedin. Bu, kuruluşunuzun URL sayfasında görünecektir.",
"members": "Üyeler",
"organization_members": "Kuruluş üyeleri",
"organization_members": "Organization üyeleri",
"member": "Üye",
"number_member_one": "{{count}} üye",
"number_member_other": "{{count}} üye",
@@ -662,7 +662,7 @@
"event_type_updated_successfully": "{{eventTypeTitle}} etkinlik türü başarıyla güncellendi",
"event_type_deleted_successfully": "Etkinlik türü başarıyla silindi",
"hours": "Saat",
"people": "İnsan",
"people": "İnsanlar",
"your_email": "E-postanız",
"change_avatar": "Avatarı değiştir",
"language": "Dil",
@@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "{{connectionType}} yapılandırmasını silmek istediğinizden emin misiniz? {{connectionType}} girişini kullanan ekip üyeleriniz artık Cal.com'a erişemeyecek.",
"organizer_timezone": "Organizatörün saat dilimi",
"email_user_cta": "Daveti görüntüle",
"email_no_user_invite_heading": "{{appName}} {{entity}} grubuna katılmaya davet edildiniz",
"email_no_user_invite_subheading": "{{invitedBy}}, sizi {{appName}} ekibine katılmaya davet etti. {{appName}}, size ve ekibinize e-posta iletişimine ihtiyaç duymadan toplantı planlama yapma olanağı sağlayan bir etkinlik planlayıcıdır.",
"email_user_invite_subheading": "{{invitedBy}}, sizi {{appName}} uygulamasındaki {{entity}} `{{teamName}}` ekibine katılmaya davet etti. {{appName}}, size ve {{entity}} ekibinize e-posta iletişimine ihtiyaç duymadan toplantı planlama yapma olanağı sağlayan bir etkinlik planlayıcıdır.",
"email_user_invite_subheading_team": "{{invitedBy}}, sizi {{appName}} uygulamasındaki `{{teamName}}` ekibine katılmaya davet etti. {{appName}}, size ve ekibinize e-posta iletişimine ihtiyaç duymadan toplantı planlama yapma olanağı sağlayan bir etkinlik planlayıcıdır.",
"email_no_user_invite_steps_intro": "{{entity}} ekibinizle birlikte kısa sürede ve sorunsuz planlama yapmanın keyfini çıkarmanız için birkaç adımda size rehberlik edeceğiz.",
"email_no_user_step_one": "Kullanıcı adınızı seçin",
"email_no_user_step_two": "Takvim hesabınızı bağlayın",
@@ -1876,9 +1875,8 @@
"connect_google_workspace": "Google Workspace'i bağlayın",
"google_workspace_admin_tooltip": "Bu özelliği kullanmak için Çalışma Alanı Yöneticisi olmalısınız",
"first_event_type_webhook_description": "Bu etkinlik türü için ilk web kancanızı oluşturun",
"install_app_on": "Uygulamayı şurada yükle:",
"install_app_on": "Uygulamayı şuraya yükle:",
"create_for": "Oluşturun",
"setup_organization": "Kuruluş oluşturun",
"organization_banner_description": "Ekiplerinizin, döngüsel ve toplu planlama ile paylaşılan uygulamalar, iş akışları ve olay türleri oluşturabileceği bir ortam oluşturun.",
"organization_banner_title": "Kuruluşları birden çok ekiple yönetin",
"set_up_your_organization": "Kuruluşunuzu düzenleyin",
@@ -551,7 +551,7 @@
"team_description": "Кілька речень про вашу команду. Ця інформація з’явиться на сторінці за URL-адресою вашої команди.",
"org_description": "Кілька речень про вашу організацію. Ця інформація з’явиться на сторінці за URL-адресою вашої організації.",
"members": "Учасники",
"organization_members": "Учасники організації",
"organization_members": "Учасники Organization",
"member": "Учасник",
"number_member_one": "{{count}} учасник",
"number_member_other": "Учасників: {{count}}",
@@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "Справді видалити конфігурацію {{connectionType}}? Учасники вашої команди, які входять через {{connectionType}}, не зможуть ввійти в Cal.com.",
"organizer_timezone": "Часовий пояс організатора",
"email_user_cta": "Переглянути запрошення",
"email_no_user_invite_heading": "Вас запрошено приєднатися: {{entity}} в застосунку {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} запрошує вас приєднатися до команди в {{appName}}. {{appName}} — планувальник подій, який дає змогу вам і вашій команді планувати зустрічі без тривалої переписки електронною поштою.",
"email_user_invite_subheading": "{{invitedBy}} запрошує вас приєднатися до {{entity}} «{{teamName}}» в застосунку {{appName}}. {{appName}} — планувальник подій, завдяки якому ви й ваша {{entity}} можете планувати наради без довгого листування.",
"email_user_invite_subheading_team": "{{invitedBy}} запрошує вас приєднатися до команди «{{teamName}}» в {{appName}}. {{appName}} — планувальник подій, який дає змогу вам і вашій команді планувати зустрічі без тривалої переписки електронною поштою.",
"email_no_user_invite_steps_intro": "Ми ознайомимо вас із застосунком, і вже скоро ви й ваша {{entity}} насолоджуватиметеся плануванням без жодного клопоту.",
"email_no_user_step_one": "Виберіть ім’я користувача",
"email_no_user_step_two": "Підключіть обліковий запис календаря",
@@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "Створіть свій перший вебгук для цього типу заходів",
"install_app_on": "Установити застосунок у",
"create_for": "Створити для",
"setup_organization": "Налаштувати організацію",
"organization_banner_description": "Підготуйте середовища, де ваші команди можуть створювати спільні застосунки, робочі процеси й типи заходів із циклічним і колективним плануванням.",
"organization_banner_title": "Керування організаціями з кількома командами",
"set_up_your_organization": "Налаштуйте свою організацію",
@@ -1938,7 +1936,7 @@
"insights_all_org_filter": "Усі",
"insights_team_filter": "Команда: {{teamName}}",
"insights_user_filter": "Користувач: {{userName}}",
"insights_subtitle": "Переглядайте аналітичні дані про бронювання для своїх заходів",
"insights_subtitle": "Переглядайте дані Insights про бронювання для своїх заходів",
"custom_plan": "Користувацький план",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Додайте нові рядки вище ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "Bạn có chắc chắn muốn xóa cấu hình {{connectionType}} không? Các thành viên trong nhóm của bạn sử dụng thông tin đăng nhập {{connectionType}} sẽ không thể truy cập vào Cal.com được nữa.",
"organizer_timezone": "Múi giờ của người tổ chức",
"email_user_cta": "Xem lời mời",
"email_no_user_invite_heading": "Bạn đã được mời gia nhập {{entity}} {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} đã mời bạn gia nhập nhóm của họ trên {{appName}}. {{appName}} là công cụ lên lịch sắp xếp sự kiện cho phép bạn và nhóm bạn lên lịch các cuộc gặp mà không cần trao đổi email nhiều.",
"email_user_invite_subheading": "{{invitedBy}} đã mời bạn gia nhập {{entity}} '{{teamName}}' của họ trên {{appName}}. {{appName}} là công cụ lên lịch sắp xếp sự kiện cho phép bạn và {{entity}} của bạn lên lịch các cuộc gặp mà không cần trao đổi email nhiều.",
"email_user_invite_subheading_team": "{{invitedBy}} đã mời bạn gia nhập nhóm '{{teamName}}' của họ trên {{appName}}. {{appName}} là công cụ lên lịch sắp xếp sự kiện cho phép bạn và nhóm bạn lên lịch các cuộc gặp mà không cần trao đổi email nhiều.",
"email_no_user_invite_steps_intro": "Chúng tôi sẽ hướng dẫn cho bạn qua vài bước nhỏ và bạn sẽ tận hưởng được ngay cảm giác thoải mái không căng thẳng trong việc lên lịch cùng {{entity}} của mình.",
"email_no_user_step_one": "Chọn tên người dùng của bạn",
"email_no_user_step_two": "Kết nối tài khoản lịch của bạn",
@@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "Tạo webhook đầu tiên của bạn cho loại sự kiện này",
"install_app_on": "Cài đặt ứng dụng cho",
"create_for": "Tạo cho",
"setup_organization": "Thiết lập một Tổ chức",
"organization_banner_description": "Tạo môi trường tại đó các nhóm của bạn có thể tạo những ứng dụng, tiến độ công việc và loại sự kiện mà có thể chia sẻ được, với chức năng đặt lịch dạng round-robin và tập thể.",
"organization_banner_title": "Quản lý các tổ chức có nhiều nhóm",
"set_up_your_organization": "Thiết lập tổ chức của bạn",
@@ -1663,9 +1663,8 @@
"delete_sso_configuration_confirmation_description": "确定要删除 {{connectionType}} 配置吗?使用 {{connectionType}} 登录的团队成员将无法再访问 Cal.com。",
"organizer_timezone": "组织者时区",
"email_user_cta": "查看邀请",
"email_no_user_invite_heading": "您已被邀请加入 {{appName}} {{entity}}",
"email_no_user_invite_subheading": "{{invitedBy}} 已邀请您加入他们在 {{appName}} 上的团队。{{appName}} 是一个活动安排调度程序,让您和您的团队无需通过电子邮件沟通即可安排会议。",
"email_user_invite_subheading": "{{invitedBy}} 已邀请您加入他们在 {{appName}} 上的 {{entity}}“{{teamName}}”。{{appName}} 是一个活动日程安排程序,让您和您的 {{entity}} 无需通过电子邮件沟通即可安排会议。",
"email_user_invite_subheading_team": "{{invitedBy}} 已邀请您加入他们在 {{appName}} 上的团队“{{teamName}}”。{{appName}} 是一个活动安排调度程序,让您和您的团队无需通过电子邮件沟通即可安排会议。",
"email_no_user_invite_steps_intro": "我们将引导您完成几个简短步骤,您将立即与您的 {{entity}} 一起享受无压力的日程安排。",
"email_no_user_step_one": "选择您的用户名",
"email_no_user_step_two": "连接您的日历账户",
@@ -1882,7 +1881,6 @@
"first_event_type_webhook_description": "为此活动类型创建第一个 Webhook",
"install_app_on": "安装应用的账户",
"create_for": "创建",
"setup_organization": "设置组织",
"organization_banner_description": "创建环境,以便让您的团队可以在其中通过轮流和集体日程安排来创建共享的应用、工作流程和活动类型。",
"organization_banner_title": "管理具有多个团队的组织",
"set_up_your_organization": "设置您的组织",
@@ -89,7 +89,7 @@
"event_has_been_rescheduled": "已更新 - 已經重新預定活動時間",
"request_reschedule_subtitle": "{{organizer}} 已取消預約,並要求您選擇其他時間。",
"request_reschedule_title_organizer": "您已要求 {{attendee}} 重新預約",
"request_reschedule_subtitle_organizer": "您已取消預約,{{attendee}} 應和您重新挑選新的預約時間。",
"request_reschedule_subtitle_organizer": "您已取消預約,{{attendee}} 應和您重新挑選預約時間。",
"rescheduled_event_type_subject": "已送出重新預定的申請:在 {{date}} 與 {{name}} 的 {{eventType}}",
"requested_to_reschedule_subject_attendee": "需進行重新預約操作:請為與 {{name}} 的 {{eventType}} 預訂新時間",
"hi_user_name": "哈囉 {{name}}",
@@ -306,7 +306,7 @@
"password_has_been_reset_login": "密碼已經重置。現在即可使用新建立的密碼登入。",
"layout": "版面",
"bookerlayout_default_title": "預設檢視",
"bookerlayout_description": "您可以選取多個版面,以便預約者切換檢視。",
"bookerlayout_description": "您可以選取多種檢視方式供預約者切換。",
"bookerlayout_user_settings_title": "預約版面",
"bookerlayout_user_settings_description": "您可以選取多個版面,以便預約者切換檢視。您可根據不同活動來覆寫此設定。",
"bookerlayout_month_view": "月",
@@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "確定要刪除 {{connectionType}} 設定?使用 {{connectionType}} 登入的團隊成員將再也無法存取 Cal.com。",
"organizer_timezone": "主辦者時區",
"email_user_cta": "檢視邀請",
"email_no_user_invite_heading": "您已獲邀加入 {{appName}} {{entity}}",
"email_no_user_invite_subheading": "{{invitedBy}} 已邀請您加入對方在 {{appName}} 上的團隊。{{appName}} 是活動多工排程工具,可讓您和您的團隊無須透過繁複的電子郵件往來就能輕鬆預定會議。",
"email_user_invite_subheading": "{{invitedBy}} 已邀請您加入對方在 {{appName}} 上的「{{teamName}}」{{entity}}。{{appName}} 為一款靈活的日程管理工具,可讓您和 {{entity}} 輕鬆預定會議,而無須透過繁複的電子郵件往來。",
"email_user_invite_subheading_team": "{{invitedBy}} 已邀請您加入對方在 {{appName}} 上的「{{teamName}}」團隊。{{appName}} 為一款活動多功能排程工具,可讓您和團隊無須透過繁複的電子郵件往來,就能輕鬆預定會議。",
"email_no_user_invite_steps_intro": "我們會逐步引導您完成幾個簡短的步驟,然後您就能馬上與 {{entity}} 享受沒有壓力的排程預定功能。",
"email_no_user_step_one": "選擇使用者名稱",
"email_no_user_step_two": "連結行事曆帳號",
@@ -1687,7 +1686,7 @@
"attendee_no_longer_attending": "一位與會者不再參加您的活動",
"attendee_no_longer_attending_subtitle": "{{name}} 已取消,這代表此時段空出了一個座位",
"create_event_on": "活動建立日期",
"create_routing_form_on": "引導表單的建立位置",
"create_routing_form_on": "引導表單的建立帳戶",
"default_app_link_title": "設定預設應用程式連結",
"default_app_link_description": "設定預設應用程式連結,即可讓所有全新建立的活動類型使用您設定的應用程式連結。",
"organizer_default_conferencing_app": "主辦者的預設應用程式",
@@ -1841,7 +1840,7 @@
"invite_link_copied": "邀請連結已複製",
"invite_link_deleted": "邀請連結已刪除",
"invite_link_updated": "邀請連結設定已儲存",
"link_expires_after": "連結設定效期…",
"link_expires_after": "將連結效期設為…",
"one_day": "1 天",
"seven_days": "7 天",
"thirty_days": "30 天",
@@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "為此活動類型建立第一個 Webhook",
"install_app_on": "應用程式安裝帳號",
"create_for": "建立",
"setup_organization": "設定組織",
"organization_banner_description": "建立環境,供團隊利用輪流制和全體預定功能建立共用的應用程式、工作流程和活動類型。",
"organization_banner_title": "管理有多個團隊的組織",
"set_up_your_organization": "設定組織",
@@ -1938,7 +1936,7 @@
"insights_all_org_filter": "所有應用程式",
"insights_team_filter": "團隊:{{teamName}}",
"insights_user_filter": "使用者:{{userName}}",
"insights_subtitle": "查看所有活動的預約洞見",
"insights_subtitle": "查看所有活動的預約 Insight",
"custom_plan": "自訂方案",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 請在此處新增您的字串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+6
View File
@@ -93,6 +93,12 @@
--cal-brand-text: black;
}
@layer base {
* {
@apply border-default
}
}
::-moz-selection {
color: var(--cal-brand-text);
background: var(--cal-brand);
+20
View File
@@ -0,0 +1,20 @@
// my-test.ts
import { test as base } from "vitest";
import { getTestEmails } from "@calcom/lib/testEmails";
export interface Fixtures {
emails: ReturnType<typeof getEmailsFixture>;
}
export const test = base.extend<Fixtures>({
emails: async ({}, use) => {
await use(getEmailsFixture());
},
});
function getEmailsFixture() {
return {
get: getTestEmails,
};
}
+2 -302
View File
@@ -1,23 +1,14 @@
import type {
EventType as PrismaEventType,
User as PrismaUser,
Booking as PrismaBooking,
App as PrismaApp,
} from "@prisma/client";
import CalendarManagerMock from "../../../../tests/libs/__mocks__/CalendarManager";
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
import { diff } from "jest-diff";
import { v4 as uuidv4 } from "uuid";
import { describe, expect, vi, beforeEach, afterEach, test } from "vitest";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type { SchedulingType } from "@calcom/prisma/enums";
import type { BookingStatus } from "@calcom/prisma/enums";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util";
import { getDate, getGoogleCalendarCredential, createBookingScenario} from "../utils/bookingScenario";
// TODO: Mock properly
prismaMock.eventType.findUnique.mockResolvedValue(null);
@@ -129,6 +120,7 @@ const TestData = {
},
users: {
example: {
name: "Example",
username: "example",
defaultScheduleId: 1,
email: "example@example.com",
@@ -151,63 +143,6 @@ const TestData = {
},
};
type App = {
slug: string;
dirName: string;
};
type InputCredential = typeof TestData.credentials.google;
type InputSelectedCalendar = typeof TestData.selectedCalendars.google;
type InputUser = typeof TestData.users.example & { id: number } & {
credentials?: InputCredential[];
selectedCalendars?: InputSelectedCalendar[];
schedules: {
id: number;
name: string;
availability: {
userId: number | null;
eventTypeId: number | null;
days: number[];
startTime: Date;
endTime: Date;
date: string | null;
}[];
timeZone: string;
}[];
};
type InputEventType = {
id: number;
title?: string;
length?: number;
offsetStart?: number;
slotInterval?: number;
minimumBookingNotice?: number;
users?: { id: number }[];
hosts?: { id: number }[];
schedulingType?: SchedulingType;
beforeEventBuffer?: number;
afterEventBuffer?: number;
};
type InputBooking = {
userId?: number;
eventTypeId: number;
startTime: string;
endTime: string;
title?: string;
status: BookingStatus;
attendees?: { email: string }[];
};
type InputHost = {
id: number;
userId: number;
eventTypeId: number;
isFixed: boolean;
};
const cleanup = async () => {
await prisma.eventType.deleteMany();
@@ -241,7 +176,6 @@ describe("getSchedule", () => {
]);
const scenarioData = {
hosts: [],
eventTypes: [
{
id: 1,
@@ -350,7 +284,6 @@ describe("getSchedule", () => {
endTime: `${plus2DateString}T06:15:00.000Z`,
},
],
hosts: [],
});
// Day Plus 2 is completely free - It only has non accepted bookings
@@ -449,7 +382,6 @@ describe("getSchedule", () => {
schedules: [TestData.schedules.IstWorkHours],
},
],
hosts: [],
});
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
@@ -550,7 +482,6 @@ describe("getSchedule", () => {
schedules: [TestData.schedules.IstWorkHours],
},
],
hosts: [],
});
const { dateString: todayDateString } = getDate();
const { dateString: minus1DateString } = getDate({ dateIncrement: -1 });
@@ -634,7 +565,6 @@ describe("getSchedule", () => {
selectedCalendars: [TestData.selectedCalendars.google],
},
],
hosts: [],
apps: [TestData.apps.googleCalendar],
};
@@ -710,7 +640,6 @@ describe("getSchedule", () => {
},
],
apps: [TestData.apps.googleCalendar],
hosts: [],
};
createBookingScenario(scenarioData);
@@ -768,7 +697,6 @@ describe("getSchedule", () => {
selectedCalendars: [TestData.selectedCalendars.google],
},
],
hosts: [],
apps: [TestData.apps.googleCalendar],
};
@@ -834,7 +762,6 @@ describe("getSchedule", () => {
schedules: [TestData.schedules.IstWorkHoursWithDateOverride(plus2DateString)],
},
],
hosts: [],
};
createBookingScenario(scenarioData);
@@ -913,15 +840,6 @@ describe("getSchedule", () => {
endTime: `${plus2DateString}T04:15:00.000Z`,
},
],
hosts: [
// This user is a host of our Collective event
{
id: 1,
eventTypeId: 1,
userId: 101,
isFixed: true,
},
],
});
// Requesting this user's availability for their
@@ -1022,7 +940,6 @@ describe("getSchedule", () => {
endTime: `${plus2DateString}T05:45:00.000Z`,
},
],
hosts: [],
});
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule({
@@ -1162,7 +1079,6 @@ describe("getSchedule", () => {
endTime: `${plus3DateString}T04:15:00.000Z`,
},
],
hosts: [],
});
const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule({
input: {
@@ -1224,219 +1140,3 @@ describe("getSchedule", () => {
});
});
function getGoogleCalendarCredential() {
return {
type: "google_calendar",
key: {
scope:
"https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/calendar.readonly",
token_type: "Bearer",
expiry_date: 1656999025367,
access_token: "ACCESS_TOKEN",
refresh_token: "REFRESH_TOKEN",
},
};
}
function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser[]) {
const baseEventType = {
title: "Base EventType Title",
slug: "base-event-type-slug",
timeZone: null,
beforeEventBuffer: 0,
afterEventBuffer: 0,
schedulingType: null,
//TODO: What is the purpose of periodStartDate and periodEndDate? Test these?
periodStartDate: new Date("2022-01-21T09:03:48.000Z"),
periodEndDate: new Date("2022-01-21T09:03:48.000Z"),
periodCountCalendarDays: false,
periodDays: 30,
seatsPerTimeSlot: null,
metadata: {},
minimumBookingNotice: 0,
offsetStart: 0,
};
const foundEvents: Record<number, boolean> = {};
const eventTypesWithUsers = eventTypes.map((eventType) => {
if (!eventType.slotInterval && !eventType.length) {
throw new Error("eventTypes[number]: slotInterval or length must be defined");
}
if (foundEvents[eventType.id]) {
throw new Error(`eventTypes[number]: id ${eventType.id} is not unique`);
}
foundEvents[eventType.id] = true;
const users =
eventType.users?.map((userWithJustId) => {
return usersStore.find((user) => user.id === userWithJustId.id);
}) || [];
return {
...baseEventType,
...eventType,
users,
};
});
logger.silly("TestData: Creating EventType", eventTypes);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.eventType.findUnique.mockImplementation(({ where }) => {
return new Promise((resolve) => {
const eventType = eventTypesWithUsers.find((e) => e.id === where.id) as unknown as PrismaEventType & {
users: PrismaUser[];
};
resolve(eventType);
});
});
}
async function addBookings(bookings: InputBooking[], eventTypes: InputEventType[]) {
logger.silly("TestData: Creating Bookings", bookings);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.booking.findMany.mockImplementation((findManyArg) => {
const where = findManyArg?.where || {};
return new Promise((resolve) => {
resolve(
bookings
// We can improve this filter to support the entire where clause but that isn't necessary yet. So, handle what we know we pass to `findMany` and is needed
.filter((booking) => {
/**
* A user is considered busy within a given time period if there
* is a booking they own OR host. This function mocks some of the logic
* for each condition. For details see the following ticket:
* https://github.com/calcom/cal.com/issues/6374
*/
// ~~ FIRST CONDITION ensures that this booking is owned by this user
// and that the status is what we want
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const statusIn = where.OR[0].status?.in || [];
const firstConditionMatches =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
statusIn.includes(booking.status) && booking.userId === where.OR[0].userId;
// We return this booking if either condition is met
return firstConditionMatches;
})
.map((booking) => ({
uid: uuidv4(),
title: "Test Booking Title",
...booking,
eventType: eventTypes.find((eventType) => eventType.id === booking.eventTypeId),
})) as unknown as PrismaBooking[]
);
});
});
}
function addUsers(users: InputUser[]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.user.findUniqueOrThrow.mockImplementation((findUniqueArgs) => {
return new Promise((resolve) => {
resolve({
email: `IntegrationTestUser${findUniqueArgs?.where.id}@example.com`,
} as unknown as PrismaUser);
});
});
prismaMock.user.findMany.mockResolvedValue(
users.map((user) => {
return {
...user,
username: `IntegrationTestUser${user.id}`,
email: `IntegrationTestUser${user.id}@example.com`,
};
}) as unknown as PrismaUser[]
);
}
type ScenarioData = {
// TODO: Support multiple bookings and add tests with that.
bookings?: InputBooking[];
users: InputUser[];
hosts: InputHost[];
credentials?: InputCredential[];
apps?: App[];
selectedCalendars?: InputSelectedCalendar[];
eventTypes: InputEventType[];
calendarBusyTimes?: {
start: string;
end: string;
}[];
};
function createBookingScenario(data: ScenarioData) {
logger.silly("TestData: Creating Scenario", data);
addUsers(data.users);
const eventType = addEventTypes(data.eventTypes, data.users);
if (data.apps) {
prismaMock.app.findMany.mockResolvedValue(data.apps as PrismaApp[]);
// FIXME: How do we know which app to return?
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.app.findUnique.mockImplementation(({ where: { slug: whereSlug } }) => {
return new Promise((resolve) => {
if (!data.apps) {
resolve(null);
return;
}
resolve((data.apps.find(({ slug }) => slug == whereSlug) as PrismaApp) || null);
});
});
}
data.bookings = data.bookings || [];
addBookings(data.bookings, data.eventTypes);
return {
eventType,
};
}
/**
* This fn indents to dynamically compute day, month, year for the purpose of testing.
* We are not using DayJS because that's actually being tested by this code.
* - `dateIncrement` adds the increment to current day
* - `monthIncrement` adds the increment to current month
* - `yearIncrement` adds the increment to current year
*/
const getDate = (param: { dateIncrement?: number; monthIncrement?: number; yearIncrement?: number } = {}) => {
let { dateIncrement, monthIncrement, yearIncrement } = param;
dateIncrement = dateIncrement || 0;
monthIncrement = monthIncrement || 0;
yearIncrement = yearIncrement || 0;
let _date = new Date().getDate() + dateIncrement;
let year = new Date().getFullYear() + yearIncrement;
// Make it start with 1 to match with DayJS requiremet
let _month = new Date().getMonth() + monthIncrement + 1;
// If last day of the month(As _month is plus 1 already it is going to be the 0th day of next month which is the last day of current month)
const lastDayOfMonth = new Date(year, _month, 0).getDate();
const numberOfDaysForNextMonth = +_date - +lastDayOfMonth;
if (numberOfDaysForNextMonth > 0) {
_date = numberOfDaysForNextMonth;
_month = _month + 1;
}
if (_month === 13) {
_month = 1;
year = year + 1;
}
const date = _date < 10 ? "0" + _date : _date;
const month = _month < 10 ? "0" + _month : _month;
return {
date,
month,
year,
dateString: `${year}-${month}-${date}`,
};
};
+742
View File
@@ -0,0 +1,742 @@
import type {
EventType as PrismaEventType,
User as PrismaUser,
Booking as PrismaBooking,
App as PrismaApp,
} from "@prisma/client";
import type { Prisma } from "@prisma/client";
import type { WebhookTriggerEvents } from "@prisma/client";
import { v4 as uuidv4 } from "uuid";
import { expect } from "vitest";
import "vitest-fetch-mock";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import logger from "@calcom/lib/logger";
import type { SchedulingType } from "@calcom/prisma/enums";
import type { BookingStatus } from "@calcom/prisma/enums";
import type { EventBusyDate } from "@calcom/types/Calendar";
import type { Fixtures } from "@calcom/web/test/fixtures/fixtures";
import appStoreMock from "../../../../tests/libs/__mocks__/app-store";
import i18nMock from "../../../../tests/libs/__mocks__/libServerI18n";
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
type App = {
slug: string;
dirName: string;
};
type InputWebhook = {
appId: string | null;
userId?: number | null;
teamId?: number | null;
eventTypeId?: number;
active: boolean;
eventTriggers: WebhookTriggerEvents[];
subscriberUrl: string;
};
/**
* Data to be mocked
*/
type ScenarioData = {
// hosts: { id: number; eventTypeId?: number; userId?: number; isFixed?: boolean }[];
/**
* Prisma would return these eventTypes
*/
eventTypes: InputEventType[];
/**
* Prisma would return these users
*/
users: InputUser[];
/**
* Prisma would return these apps
*/
apps?: App[];
bookings?: InputBooking[];
webhooks?: InputWebhook[];
};
type InputCredential = typeof TestData.credentials.google;
type InputSelectedCalendar = typeof TestData.selectedCalendars.google;
type InputUser = typeof TestData.users.example & { id: number } & {
credentials?: InputCredential[];
selectedCalendars?: InputSelectedCalendar[];
schedules: {
id: number;
name: string;
availability: {
userId: number | null;
eventTypeId: number | null;
days: number[];
startTime: Date;
endTime: Date;
date: string | null;
}[];
timeZone: string;
}[];
};
type InputEventType = {
id: number;
title?: string;
length?: number;
offsetStart?: number;
slotInterval?: number;
minimumBookingNotice?: number;
/**
* These user ids are `ScenarioData["users"]["id"]`
*/
users?: { id: number }[];
hosts?: { id: number }[];
schedulingType?: SchedulingType;
beforeEventBuffer?: number;
afterEventBuffer?: number;
requiresConfirmation?: boolean;
};
type InputBooking = {
userId?: number;
eventTypeId: number;
startTime: string;
endTime: string;
title?: string;
status: BookingStatus;
attendees?: { email: string }[];
};
const Timezones = {
"+5:30": "Asia/Kolkata",
"+6:00": "Asia/Dhaka",
};
function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser[]) {
const baseEventType = {
title: "Base EventType Title",
slug: "base-event-type-slug",
timeZone: null,
beforeEventBuffer: 0,
afterEventBuffer: 0,
schedulingType: null,
//TODO: What is the purpose of periodStartDate and periodEndDate? Test these?
periodStartDate: new Date("2022-01-21T09:03:48.000Z"),
periodEndDate: new Date("2022-01-21T09:03:48.000Z"),
periodCountCalendarDays: false,
periodDays: 30,
seatsPerTimeSlot: null,
metadata: {},
minimumBookingNotice: 0,
offsetStart: 0,
};
const foundEvents: Record<number, boolean> = {};
const eventTypesWithUsers = eventTypes.map((eventType) => {
if (!eventType.slotInterval && !eventType.length) {
throw new Error("eventTypes[number]: slotInterval or length must be defined");
}
if (foundEvents[eventType.id]) {
throw new Error(`eventTypes[number]: id ${eventType.id} is not unique`);
}
foundEvents[eventType.id] = true;
const users =
eventType.users?.map((userWithJustId) => {
return usersStore.find((user) => user.id === userWithJustId.id);
}) || [];
return {
...baseEventType,
...eventType,
workflows: [],
users,
};
});
logger.silly("TestData: Creating EventType", eventTypes);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const eventTypeMock = ({ where }) => {
return new Promise((resolve) => {
const eventType = eventTypesWithUsers.find((e) => e.id === where.id) as unknown as PrismaEventType & {
users: PrismaUser[];
};
resolve(eventType);
});
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.eventType.findUnique.mockImplementation(eventTypeMock);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.eventType.findUniqueOrThrow.mockImplementation(eventTypeMock);
}
async function addBookings(bookings: InputBooking[], eventTypes: InputEventType[]) {
logger.silly("TestData: Creating Bookings", bookings);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.booking.findMany.mockImplementation((findManyArg) => {
const where = findManyArg?.where || {};
return new Promise((resolve) => {
resolve(
bookings
// We can improve this filter to support the entire where clause but that isn't necessary yet. So, handle what we know we pass to `findMany` and is needed
.filter((booking) => {
/**
* A user is considered busy within a given time period if there
* is a booking they own OR host. This function mocks some of the logic
* for each condition. For details see the following ticket:
* https://github.com/calcom/cal.com/issues/6374
*/
// ~~ FIRST CONDITION ensures that this booking is owned by this user
// and that the status is what we want
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const statusIn = where.OR[0].status?.in || [];
const firstConditionMatches =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
statusIn.includes(booking.status) && booking.userId === where.OR[0].userId;
// We return this booking if either condition is met
return firstConditionMatches;
})
.map((booking) => ({
uid: uuidv4(),
title: "Test Booking Title",
...booking,
eventType: eventTypes.find((eventType) => eventType.id === booking.eventTypeId),
})) as unknown as PrismaBooking[]
);
});
});
}
async function addWebhooks(webhooks: InputWebhook[]) {
prismaMock.webhook.findMany.mockResolvedValue(
webhooks.map((webhook) => {
return {
...webhook,
payloadTemplate: null,
secret: null,
id: uuidv4(),
createdAt: new Date(),
userId: webhook.userId || null,
eventTypeId: webhook.eventTypeId || null,
teamId: webhook.teamId || null,
};
})
);
}
function addUsers(users: InputUser[]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.user.findUniqueOrThrow.mockImplementation((findUniqueArgs) => {
return new Promise((resolve) => {
resolve({
email: `IntegrationTestUser${findUniqueArgs?.where.id}@example.com`,
} as unknown as PrismaUser);
});
});
prismaMock.user.findMany.mockResolvedValue(
users.map((user) => {
return {
...user,
username: `IntegrationTestUser${user.id}`,
email: `IntegrationTestUser${user.id}@example.com`,
};
}) as unknown as PrismaUser[]
);
}
export async function createBookingScenario(data: ScenarioData) {
logger.silly("TestData: Creating Scenario", data);
addUsers(data.users);
const eventType = addEventTypes(data.eventTypes, data.users);
if (data.apps) {
prismaMock.app.findMany.mockResolvedValue(data.apps as PrismaApp[]);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
const appMock = ({ where: { slug: whereSlug } }) => {
return new Promise((resolve) => {
if (!data.apps) {
resolve(null);
return;
}
const foundApp = data.apps.find(({ slug }) => slug == whereSlug);
//TODO: Pass just the app name in data.apps and maintain apps in a separate object or load them dyamically
resolve(
({
...foundApp,
...(foundApp?.slug ? TestData.apps[foundApp.slug as keyof typeof TestData.apps] || {} : {}),
enabled: true,
createdAt: new Date(),
updatedAt: new Date(),
categories: [],
} as PrismaApp) || null
);
});
};
// FIXME: How do we know which app to return?
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.app.findUnique.mockImplementation(appMock);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.app.findFirst.mockImplementation(appMock);
}
data.bookings = data.bookings || [];
allowSuccessfulBookingCreation();
addBookings(data.bookings, data.eventTypes);
// mockBusyCalendarTimes([]);
addWebhooks(data.webhooks || []);
return {
eventType,
};
}
/**
* This fn indents to /ally compute day, month, year for the purpose of testing.
* We are not using DayJS because that's actually being tested by this code.
* - `dateIncrement` adds the increment to current day
* - `monthIncrement` adds the increment to current month
* - `yearIncrement` adds the increment to current year
*/
export const getDate = (
param: { dateIncrement?: number; monthIncrement?: number; yearIncrement?: number } = {}
) => {
let { dateIncrement, monthIncrement, yearIncrement } = param;
dateIncrement = dateIncrement || 0;
monthIncrement = monthIncrement || 0;
yearIncrement = yearIncrement || 0;
let _date = new Date().getDate() + dateIncrement;
let year = new Date().getFullYear() + yearIncrement;
// Make it start with 1 to match with DayJS requiremet
let _month = new Date().getMonth() + monthIncrement + 1;
// If last day of the month(As _month is plus 1 already it is going to be the 0th day of next month which is the last day of current month)
const lastDayOfMonth = new Date(year, _month, 0).getDate();
const numberOfDaysForNextMonth = +_date - +lastDayOfMonth;
if (numberOfDaysForNextMonth > 0) {
_date = numberOfDaysForNextMonth;
_month = _month + 1;
}
if (_month === 13) {
_month = 1;
year = year + 1;
}
const date = _date < 10 ? "0" + _date : _date;
const month = _month < 10 ? "0" + _month : _month;
return {
date,
month,
year,
dateString: `${year}-${month}-${date}`,
};
};
export function getMockedCredential({
metadataLookupKey,
key,
}: {
metadataLookupKey: string;
key: {
expiry_date?: number;
token_type?: string;
access_token?: string;
refresh_token?: string;
scope: string;
};
}) {
return {
type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type,
appId: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].slug,
key: {
expiry_date: Date.now() + 1000000,
token_type: "Bearer",
access_token: "ACCESS_TOKEN",
refresh_token: "REFRESH_TOKEN",
...key,
},
};
}
export function getGoogleCalendarCredential() {
return getMockedCredential({
metadataLookupKey: "googlecalendar",
key: {
scope:
"https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/calendar.readonly",
},
});
}
export function getZoomAppCredential() {
return getMockedCredential({
metadataLookupKey: "zoomvideo",
key: {
scope: "meeting:writed",
},
});
}
export const TestData = {
selectedCalendars: {
google: {
integration: "google_calendar",
externalId: "john@example.com",
},
},
credentials: {
google: getGoogleCalendarCredential(),
},
schedules: {
IstWorkHours: {
id: 1,
name: "9:30AM to 6PM in India - 4:00AM to 12:30PM in GMT",
availability: [
{
userId: null,
eventTypeId: null,
days: [0, 1, 2, 3, 4, 5, 6],
startTime: new Date("1970-01-01T09:30:00.000Z"),
endTime: new Date("1970-01-01T18:00:00.000Z"),
date: null,
},
],
timeZone: Timezones["+5:30"],
},
IstWorkHoursWithDateOverride: (dateString: string) => ({
id: 1,
name: "9:30AM to 6PM in India - 4:00AM to 12:30PM in GMT but with a Date Override for 2PM to 6PM IST(in GST time it is 8:30AM to 12:30PM)",
availability: [
{
userId: null,
eventTypeId: null,
days: [0, 1, 2, 3, 4, 5, 6],
startTime: new Date("1970-01-01T09:30:00.000Z"),
endTime: new Date("1970-01-01T18:00:00.000Z"),
date: null,
},
{
userId: null,
eventTypeId: null,
days: [0, 1, 2, 3, 4, 5, 6],
startTime: new Date(`1970-01-01T14:00:00.000Z`),
endTime: new Date(`1970-01-01T18:00:00.000Z`),
date: dateString,
},
],
timeZone: Timezones["+5:30"],
}),
},
users: {
example: {
name: "Example",
email: "example@example.com",
username: "example",
defaultScheduleId: 1,
timeZone: Timezones["+5:30"],
},
},
apps: {
"google-calendar": {
slug: "google-calendar",
dirName: "whatever",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
keys: {
expiry_date: Infinity,
client_id: "client_id",
client_secret: "client_secret",
redirect_uris: ["http://localhost:3000/auth/callback"],
},
},
"daily-video": {
slug: "daily-video",
dirName: "whatever",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
keys: {
expiry_date: Infinity,
api_key: "",
scale_plan: "false",
client_id: "client_id",
client_secret: "client_secret",
redirect_uris: ["http://localhost:3000/auth/callback"],
},
},
},
};
function allowSuccessfulBookingCreation() {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
prismaMock.booking.create.mockImplementation(function (booking) {
return booking.data;
});
}
export class MockError extends Error {
constructor(message: string) {
super(message);
this.name = "MockError";
}
}
export function getOrganizer({
name,
email,
id,
schedules,
credentials,
selectedCalendars,
}: {
name: string;
email: string;
id: number;
schedules: InputUser["schedules"];
credentials?: InputCredential[];
selectedCalendars?: InputSelectedCalendar[];
}) {
return {
...TestData.users.example,
name,
email,
id,
schedules,
credentials,
selectedCalendars,
};
}
export function getScenarioData({
organizer,
eventTypes,
usersApartFromOrganizer = [],
apps = [],
webhooks,
}: // hosts = [],
{
organizer: ReturnType<typeof getOrganizer>;
eventTypes: ScenarioData["eventTypes"];
apps: ScenarioData["apps"];
usersApartFromOrganizer?: ScenarioData["users"];
webhooks?: ScenarioData["webhooks"];
// hosts?: ScenarioData["hosts"];
}) {
const users = [organizer, ...usersApartFromOrganizer];
eventTypes.forEach((eventType) => {
if (
eventType.users?.filter((eventTypeUser) => {
return !users.find((userToCreate) => userToCreate.id === eventTypeUser.id);
}).length
) {
throw new Error(`EventType ${eventType.id} has users that are not present in ScenarioData["users"]`);
}
});
return {
// hosts: [...hosts],
eventTypes: [...eventTypes],
users,
apps: [...apps],
webhooks,
};
}
export function mockEnableEmailFeature() {
prismaMock.feature.findMany.mockResolvedValue([
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
{
slug: "emails",
// It's a kill switch
enabled: false,
},
]);
}
export function mockNoTranslations() {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
i18nMock.getTranslation.mockImplementation(() => {
return new Promise((resolve) => {
const identityFn = (key: string) => key;
resolve(identityFn);
});
});
}
export function mockCalendarToHaveNoBusySlots(metadataLookupKey: keyof typeof appStoreMetadata) {
const appStoreLookupKey = metadataLookupKey;
appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default].mockResolvedValue({
lib: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
CalendarService: function MockCalendarService() {
return {
createEvent: () => {
return Promise.resolve({
type: "daily_video",
id: "dailyEventName",
password: "dailyvideopass",
url: "http://dailyvideo.example.com",
});
},
getAvailability: (): Promise<EventBusyDate[]> => {
return new Promise((resolve) => {
resolve([]);
});
},
};
},
},
});
}
export function mockSuccessfulVideoMeetingCreation({
metadataLookupKey,
appStoreLookupKey,
}: {
metadataLookupKey: string;
appStoreLookupKey?: string;
}) {
appStoreLookupKey = appStoreLookupKey || metadataLookupKey;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default].mockImplementation(() => {
return new Promise((resolve) => {
resolve({
lib: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
VideoApiAdapter: () => ({
createMeeting: () => {
return Promise.resolve({
type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type,
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-${metadataLookupKey}.example.com`,
});
},
}),
},
});
});
});
}
export function mockErrorOnVideoMeetingCreation({
metadataLookupKey,
appStoreLookupKey,
}: {
metadataLookupKey: string;
appStoreLookupKey?: string;
}) {
appStoreLookupKey = appStoreLookupKey || metadataLookupKey;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
appStoreMock.default[appStoreLookupKey].mockImplementation(() => {
return new Promise((resolve) => {
resolve({
lib: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
VideoApiAdapter: () => ({
createMeeting: () => {
throw new MockError("Error creating Video meeting");
},
}),
},
});
});
});
}
export function expectWebhookToHaveBeenCalledWith(
subscriberUrl: string,
data: {
triggerEvent: WebhookTriggerEvents;
payload: { metadata: Record<string, unknown>; responses: Record<string, unknown> };
}
) {
const fetchCalls = fetchMock.mock.calls;
const webhookFetchCall = fetchCalls.find((call) => call[0] === subscriberUrl);
if (!webhookFetchCall) {
throw new Error(`Webhook not called with ${subscriberUrl}`);
}
expect(webhookFetchCall[0]).toBe(subscriberUrl);
const body = webhookFetchCall[1]?.body;
const parsedBody = JSON.parse((body as string) || "{}");
console.log({ payload: parsedBody.payload });
expect(parsedBody.triggerEvent).toBe(data.triggerEvent);
parsedBody.payload.metadata.videoCallUrl = parsedBody.payload.metadata.videoCallUrl
? parsedBody.payload.metadata.videoCallUrl.replace(/\/video\/[a-zA-Z0-9]{22}/, "/video/DYNAMIC_UID")
: parsedBody.payload.metadata.videoCallUrl;
expect(parsedBody.payload.metadata).toContain(data.payload.metadata);
expect(parsedBody.payload.responses).toEqual(data.payload.responses);
}
export function expectWorkflowToBeTriggered() {
// TODO: Implement this.
}
export function expectBookingToBeInDatabase(booking: Partial<Prisma.BookingCreateInput>) {
const createBookingCalledWithArgs = prismaMock.booking.create.mock.calls[0];
expect(createBookingCalledWithArgs[0].data).toEqual(expect.objectContaining(booking));
}
export function getBooker({ name, email }: { name: string; email: string }) {
return {
name,
email,
};
}
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace jest {
interface Matchers<R> {
toHaveEmail(expectedEmail: { htmlToContain?: string; to: string }): R;
}
}
}
expect.extend({
toHaveEmail(
testEmail: ReturnType<Fixtures["emails"]["get"]>[number],
expectedEmail: {
//TODO: Support email HTML parsing to target specific elements
htmlToContain?: string;
to: string;
}
) {
let isHtmlContained = true;
let isToAddressExpected = true;
if (expectedEmail.htmlToContain) {
isHtmlContained = testEmail.html.includes(expectedEmail.htmlToContain);
}
isToAddressExpected = expectedEmail.to === testEmail.to;
return {
pass: isHtmlContained && isToAddressExpected,
message: () => {
if (!isHtmlContained) {
return `Email HTML is not as expected. Expected:"${expectedEmail.htmlToContain}" isn't contained in "${testEmail.html}"`;
}
return `Email To address is not as expected. Expected:${expectedEmail.to} isn't contained in ${testEmail.to}`;
},
};
},
});
+2 -1
View File
@@ -89,7 +89,8 @@
"prettier": "^2.8.6",
"tsc-absolute": "^1.0.0",
"typescript": "^4.9.4",
"vitest": "^0.31.1",
"vitest": "^0.34.3",
"vitest-fetch-mock": "^0.2.2",
"vitest-mock-extended": "^1.1.3"
},
"dependencies": {
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "Basecamp3",
"slug": "basecamp3",
"type": "basecamp3_other_calendar",
"logo": "logo.svg",
"logo": "icon-dark.svg",
"url": "https://basecamp.com/",
"variant": "other",
"categories": ["other"],

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

+28 -16
View File
@@ -1,26 +1,38 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_117_4)">
<rect width="128" height="128" fill="white"/>
<circle cx="64" cy="64" r="64" fill="#04BF8A"/>
<mask id="path-2-inside-1_117_4" fill="white">
<path d="M32.7099 74.7566C29.6119 75.9781 26.0314 74.4473 25.5846 71.1473C25.1747 68.1192 25.3114 65.0002 26.0042 61.9169C27.1525 56.8057 29.7838 51.9924 33.6065 48.0106C37.4291 44.0289 42.2937 41.0341 47.6612 39.3584C50.9977 38.3167 54.4475 37.8099 57.8599 37.8459C61.1841 37.8809 63.1528 41.2619 62.1562 44.4334V44.4334C61.2866 47.2009 58.433 48.7691 55.5372 48.9409C53.9259 49.0365 52.3131 49.3307 50.7418 49.8213C47.4989 50.8338 44.5597 52.6432 42.2501 55.0489C39.9405 57.4547 38.3507 60.3628 37.6569 63.4509C37.418 64.5141 37.2885 65.5844 37.2675 66.6474C37.2014 69.9769 35.8078 73.5351 32.7099 74.7566V74.7566Z"/>
<g clip-path="url(#clip0_21_40)">
<mask id="mask0_21_40" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="128" height="128">
<path d="M128 0H0V128H128V0Z" fill="white"/>
</mask>
<path d="M32.7099 74.7566C29.6119 75.9781 26.0314 74.4473 25.5846 71.1473C25.1747 68.1192 25.3114 65.0002 26.0042 61.9169C27.1525 56.8057 29.7838 51.9924 33.6065 48.0106C37.4291 44.0289 42.2937 41.0341 47.6612 39.3584C50.9977 38.3167 54.4475 37.8099 57.8599 37.8459C61.1841 37.8809 63.1528 41.2619 62.1562 44.4334V44.4334C61.2866 47.2009 58.433 48.7691 55.5372 48.9409C53.9259 49.0365 52.3131 49.3307 50.7418 49.8213C47.4989 50.8338 44.5597 52.6432 42.2501 55.0489C39.9405 57.4547 38.3507 60.3628 37.6569 63.4509C37.418 64.5141 37.2885 65.5844 37.2675 66.6474C37.2014 69.9769 35.8078 73.5351 32.7099 74.7566V74.7566Z" fill="white" stroke="white" stroke-width="2" mask="url(#path-2-inside-1_117_4)"/>
<mask id="path-3-inside-2_117_4" fill="white">
<path d="M52.9036 31.2935C51.7371 28.1744 53.331 24.6215 56.6383 24.2332C59.6732 23.8768 62.7894 24.0688 65.8599 24.8159C70.95 26.0545 75.716 28.7705 79.6295 32.663C83.543 36.5554 86.4513 41.4723 88.0318 46.8685C89.0144 50.2229 89.46 53.6812 89.3637 57.0924C89.2699 60.4155 85.8546 62.3241 82.7012 61.2715V61.2715C79.9496 60.353 78.4321 57.4722 78.3115 54.5738C78.2444 52.9611 77.9788 51.3433 77.5161 49.7636C76.5611 46.5033 74.804 43.5326 72.4395 41.1808C70.0749 38.829 67.1954 37.188 64.12 36.4396C63.0612 36.182 61.9934 36.0336 60.9309 35.9938C57.6031 35.8689 54.0701 34.4126 52.9036 31.2935V31.2935Z"/>
<g mask="url(#mask0_21_40)">
<path d="M64 128C99.3462 128 128 99.3462 128 64C128 28.6538 99.3462 0 64 0C28.6538 0 0 28.6538 0 64C0 99.3462 28.6538 128 64 128Z" fill="#04BF8A"/>
<mask id="mask1_21_40" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="24" y="36" width="40" height="41">
<path d="M32.7098 74.7567C29.6118 75.9782 26.0313 74.4474 25.5845 71.1474C25.1746 68.1193 25.3113 65.0003 26.0041 61.917C27.1524 56.8058 29.7837 51.9925 33.6064 48.0107C37.429 44.029 42.2936 41.0342 47.6611 39.3585C50.9976 38.3168 54.4474 37.81 57.8598 37.846C61.184 37.881 63.1527 41.262 62.1561 44.4335C61.2865 47.201 58.4329 48.7692 55.5371 48.941C53.9258 49.0366 52.313 49.3308 50.7417 49.8214C47.4988 50.8339 44.5596 52.6433 42.25 55.049C39.9404 57.4548 38.3506 60.3629 37.6568 63.451C37.4179 64.5142 37.2884 65.5845 37.2674 66.6475C37.2013 69.977 35.8077 73.5352 32.7098 74.7567Z" fill="white" stroke="white" stroke-width="2"/>
</mask>
<path d="M52.9036 31.2935C51.7371 28.1744 53.331 24.6215 56.6383 24.2332C59.6732 23.8768 62.7894 24.0688 65.8599 24.8159C70.95 26.0545 75.716 28.7705 79.6295 32.663C83.543 36.5554 86.4513 41.4723 88.0318 46.8685C89.0144 50.2229 89.46 53.6812 89.3637 57.0924C89.2699 60.4155 85.8546 62.3241 82.7012 61.2715V61.2715C79.9496 60.353 78.4321 57.4722 78.3115 54.5738C78.2444 52.9611 77.9788 51.3433 77.5161 49.7636C76.5611 46.5033 74.804 43.5326 72.4395 41.1808C70.0749 38.829 67.1954 37.188 64.12 36.4396C63.0612 36.182 61.9934 36.0336 60.9309 35.9938C57.6031 35.8689 54.0701 34.4126 52.9036 31.2935V31.2935Z" fill="white" stroke="white" stroke-width="2" mask="url(#path-3-inside-2_117_4)"/>
<mask id="path-4-inside-3_117_4" fill="white">
<path d="M97.7468 52.0457C100.912 51.0099 104.395 52.7501 104.646 56.0708C104.876 59.1178 104.554 62.2233 103.68 65.2601C102.231 70.2944 99.3192 74.9433 95.2674 78.6916C91.2156 82.4399 86.1821 85.1412 80.7248 86.4961C77.3324 87.3383 73.8587 87.6398 70.4544 87.4017C67.1381 87.1698 65.3731 83.6782 66.5559 80.5713V80.5713C67.5879 77.8602 70.5294 76.4638 73.4303 76.4638C75.0444 76.4638 76.6718 76.2656 78.2694 75.869C81.5666 75.0504 84.6078 73.4183 87.0559 71.1536C89.5039 68.8889 91.2632 66.0801 92.1388 63.0385C92.4402 61.9913 92.6328 60.9306 92.7168 59.8706C92.98 56.551 94.5819 53.0816 97.7468 52.0457V52.0457Z"/>
<g mask="url(#mask1_21_40)">
<path d="M32.7098 74.7567C29.6118 75.9782 26.0313 74.4474 25.5845 71.1474C25.1746 68.1193 25.3113 65.0003 26.0041 61.917C27.1524 56.8058 29.7837 51.9925 33.6064 48.0107C37.429 44.029 42.2936 41.0342 47.6611 39.3585C50.9976 38.3168 54.4474 37.81 57.8598 37.846C61.184 37.881 63.1527 41.262 62.1561 44.4335C61.2865 47.201 58.4329 48.7692 55.5371 48.941C53.9258 49.0366 52.313 49.3308 50.7417 49.8214C47.4988 50.8339 44.5596 52.6433 42.25 55.049C39.9404 57.4548 38.3506 60.3629 37.6568 63.451C37.4179 64.5142 37.2884 65.5845 37.2674 66.6475C37.2013 69.977 35.8077 73.5352 32.7098 74.7567Z" fill="white" stroke="white" stroke-width="2"/>
</g>
<mask id="mask2_21_40" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="51" y="23" width="40" height="40">
<path d="M52.9037 31.2934C51.7372 28.1743 53.3311 24.6214 56.6384 24.2331C59.6733 23.8767 62.7895 24.0687 65.86 24.8158C70.9501 26.0544 75.7161 28.7704 79.6296 32.6629C83.5431 36.5553 86.4514 41.4722 88.0319 46.8684C89.0145 50.2228 89.4601 53.6811 89.3638 57.0923C89.27 60.4154 85.8547 62.324 82.7013 61.2714C79.9497 60.3529 78.4322 57.4721 78.3116 54.5737C78.2445 52.961 77.9789 51.3432 77.5162 49.7635C76.5612 46.5032 74.8041 43.5325 72.4396 41.1807C70.075 38.8289 67.1955 37.1879 64.1201 36.4395C63.0613 36.1819 61.9935 36.0335 60.931 35.9937C57.6032 35.8688 54.0702 34.4125 52.9037 31.2934Z" fill="white" stroke="white" stroke-width="2"/>
</mask>
<path d="M97.7468 52.0457C100.912 51.0099 104.395 52.7501 104.646 56.0708C104.876 59.1178 104.554 62.2233 103.68 65.2601C102.231 70.2944 99.3192 74.9433 95.2674 78.6916C91.2156 82.4399 86.1821 85.1412 80.7248 86.4961C77.3324 87.3383 73.8587 87.6398 70.4544 87.4017C67.1381 87.1698 65.3731 83.6782 66.5559 80.5713V80.5713C67.5879 77.8602 70.5294 76.4638 73.4303 76.4638C75.0444 76.4638 76.6718 76.2656 78.2694 75.869C81.5666 75.0504 84.6078 73.4183 87.0559 71.1536C89.5039 68.8889 91.2632 66.0801 92.1388 63.0385C92.4402 61.9913 92.6328 60.9306 92.7168 59.8706C92.98 56.551 94.5819 53.0816 97.7468 52.0457V52.0457Z" fill="white" stroke="white" stroke-width="2" mask="url(#path-4-inside-3_117_4)"/>
<mask id="path-5-inside-4_117_4" fill="white">
<path d="M75.895 95.1969C77.127 98.2907 75.6083 101.876 72.3099 102.334C69.2832 102.755 66.1637 102.628 63.0781 101.946C57.963 100.815 53.1408 98.2 49.1462 94.3909C45.1515 90.5817 42.1403 85.7273 40.4464 80.3655C39.3934 77.0326 38.8749 73.5845 38.8994 70.172C38.9232 66.8477 42.2974 64.8675 45.4723 65.8534V65.8534C48.2427 66.7137 49.8205 69.5619 50.0022 72.4571C50.1033 74.0681 50.4029 75.6799 50.8988 77.2495C51.9223 80.489 53.7416 83.422 56.1551 85.7234C58.5687 88.0249 61.4822 89.6048 64.5727 90.2882C65.6367 90.5235 66.7073 90.6493 67.7705 90.6668C71.1001 90.7215 74.663 92.1031 75.895 95.1969V95.1969Z"/>
<g mask="url(#mask2_21_40)">
<path d="M52.9037 31.2934C51.7372 28.1743 53.3311 24.6214 56.6384 24.2331C59.6733 23.8767 62.7895 24.0687 65.86 24.8158C70.9501 26.0544 75.7161 28.7704 79.6296 32.6629C83.5431 36.5553 86.4514 41.4722 88.0319 46.8684C89.0145 50.2228 89.4601 53.6811 89.3638 57.0923C89.27 60.4154 85.8547 62.324 82.7013 61.2714C79.9497 60.3529 78.4322 57.4721 78.3116 54.5737C78.2445 52.961 77.9789 51.3432 77.5162 49.7635C76.5612 46.5032 74.8041 43.5325 72.4396 41.1807C70.075 38.8289 67.1955 37.1879 64.1201 36.4395C63.0613 36.1819 61.9935 36.0335 60.931 35.9937C57.6032 35.8688 54.0702 34.4125 52.9037 31.2934Z" fill="white" stroke="white" stroke-width="2"/>
</g>
<mask id="mask3_21_40" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="65" y="50" width="41" height="39">
<path d="M97.747 52.0457C100.912 51.0099 104.395 52.7501 104.646 56.0708C104.876 59.1178 104.554 62.2233 103.68 65.2601C102.231 70.2944 99.3194 74.9433 95.2676 78.6916C91.2158 82.4399 86.1823 85.1412 80.725 86.4961C77.3326 87.3383 73.8589 87.6398 70.4546 87.4017C67.1383 87.1698 65.3733 83.6782 66.5561 80.5713C67.5881 77.8602 70.5296 76.4638 73.4305 76.4638C75.0446 76.4638 76.672 76.2656 78.2696 75.869C81.5668 75.0504 84.608 73.4183 87.0561 71.1536C89.5041 68.8889 91.2634 66.0801 92.139 63.0385C92.4404 61.9913 92.633 60.9306 92.717 59.8706C92.9802 56.551 94.5821 53.0816 97.747 52.0457Z" fill="white" stroke="white" stroke-width="2"/>
</mask>
<path d="M75.895 95.1969C77.127 98.2907 75.6083 101.876 72.3099 102.334C69.2832 102.755 66.1637 102.628 63.0781 101.946C57.963 100.815 53.1408 98.2 49.1462 94.3909C45.1515 90.5817 42.1403 85.7273 40.4464 80.3655C39.3934 77.0326 38.8749 73.5845 38.8994 70.172C38.9232 66.8477 42.2974 64.8675 45.4723 65.8534V65.8534C48.2427 66.7137 49.8205 69.5619 50.0022 72.4571C50.1033 74.0681 50.4029 75.6799 50.8988 77.2495C51.9223 80.489 53.7416 83.422 56.1551 85.7234C58.5687 88.0249 61.4822 89.6048 64.5727 90.2882C65.6367 90.5235 66.7073 90.6493 67.7705 90.6668C71.1001 90.7215 74.663 92.1031 75.895 95.1969V95.1969Z" fill="white" stroke="white" stroke-width="2" mask="url(#path-5-inside-4_117_4)"/>
<g mask="url(#mask3_21_40)">
<path d="M97.747 52.0457C100.912 51.0099 104.395 52.7501 104.646 56.0708C104.876 59.1178 104.554 62.2233 103.68 65.2601C102.231 70.2944 99.3194 74.9433 95.2676 78.6916C91.2158 82.4399 86.1823 85.1412 80.725 86.4961C77.3326 87.3383 73.8589 87.6398 70.4546 87.4017C67.1383 87.1698 65.3733 83.6782 66.5561 80.5713C67.5881 77.8602 70.5296 76.4638 73.4305 76.4638C75.0446 76.4638 76.672 76.2656 78.2696 75.869C81.5668 75.0504 84.608 73.4183 87.0561 71.1536C89.5041 68.8889 91.2634 66.0801 92.139 63.0385C92.4404 61.9913 92.633 60.9306 92.717 59.8706C92.9802 56.551 94.5821 53.0816 97.747 52.0457Z" fill="white" stroke="white" stroke-width="2"/>
</g>
<mask id="mask4_21_40" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="37" y="64" width="41" height="40">
<path d="M75.8949 95.1969C77.1269 98.2907 75.6082 101.876 72.3098 102.334C69.2831 102.755 66.1636 102.628 63.078 101.946C57.9629 100.815 53.1407 98.2 49.1461 94.3909C45.1514 90.5817 42.1402 85.7273 40.4463 80.3655C39.3933 77.0326 38.8748 73.5845 38.8993 70.172C38.9231 66.8477 42.2973 64.8675 45.4722 65.8534C48.2426 66.7137 49.8204 69.5619 50.0021 72.4571C50.1032 74.0681 50.4028 75.6799 50.8987 77.2495C51.9222 80.489 53.7415 83.422 56.155 85.7234C58.5686 88.0249 61.4821 89.6048 64.5726 90.2882C65.6366 90.5235 66.7072 90.6493 67.7704 90.6668C71.1 90.7215 74.6629 92.1031 75.8949 95.1969Z" fill="white" stroke="white" stroke-width="2"/>
</mask>
<g mask="url(#mask4_21_40)">
<path d="M75.8949 95.1969C77.1269 98.2907 75.6082 101.876 72.3098 102.334C69.2831 102.755 66.1636 102.628 63.078 101.946C57.9629 100.815 53.1407 98.2 49.1461 94.3909C45.1514 90.5817 42.1402 85.7273 40.4463 80.3655C39.3933 77.0326 38.8748 73.5845 38.8993 70.172C38.9231 66.8477 42.2973 64.8675 45.4722 65.8534C48.2426 66.7137 49.8204 69.5619 50.0021 72.4571C50.1032 74.0681 50.4028 75.6799 50.8987 77.2495C51.9222 80.489 53.7415 83.422 56.155 85.7234C58.5686 88.0249 61.4821 89.6048 64.5726 90.2882C65.6366 90.5235 66.7072 90.6493 67.7704 90.6668C71.1 90.7215 74.6629 92.1031 75.8949 95.1969Z" fill="white" stroke="white" stroke-width="2"/>
</g>
</g>
</g>
<defs>
<clipPath id="clip0_117_4">
<clipPath id="clip0_21_40">
<rect width="128" height="128" fill="white"/>
</clipPath>
</defs>

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

+3 -3
View File
@@ -4,12 +4,12 @@
"slug": "make",
"type": "make_automation",
"logo": "icon.svg",
"url": "https://cal.com/apps/make",
"url": "https://github.com/aar2dee2",
"variant": "automation",
"categories": ["automation"],
"publisher": "aa2dee2",
"publisher": "aar2dee2",
"email": "support@cal.com",
"description": "Automate workflows",
"description": "From tasks and workflows to apps and systems, build and automate anything in one powerful visual platform.",
"isTemplate": false,
"__createdUsingCli": true,
"__template": "basic",
@@ -112,7 +112,7 @@ export default function MakeSetup({ inviteLink }: InferGetStaticPropsType<typeof
</>
<ol className="mb-5 ml-5 mt-5 list-decimal ltr:mr-5 rtl:ml-5">
<Trans i18nKey="invite_link_make">
<Trans i18nKey="make_setup_instructions">
<li>
Go to
<a href={inviteLink} className="ml-1 mr-1 text-orange-600 underline">
@@ -120,9 +120,6 @@ export default function MakeSetup({ inviteLink }: InferGetStaticPropsType<typeof
</a>
and install the Cal.com app.
</li>
</Trans>
<Trans i18nKey="make_setup_instructions">
<li>Log into your Make account and create a new Scenario.</li>
<li>Select Cal.com as your Trigger app. Also choose a Trigger event.</li>
<li>Choose your account and then enter your Unique API Key.</li>
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 33 KiB

@@ -3,15 +3,15 @@ import type { App_RoutingForms_Form } from "@prisma/client";
import { BaseEmailHtml, Info } from "@calcom/emails/src/components";
import { WEBAPP_URL } from "@calcom/lib/constants";
import type { Response } from "../../types/types";
import type { OrderedResponses } from "../../types/types";
export const ResponseEmail = ({
form,
response,
orderedResponses,
...props
}: {
form: Pick<App_RoutingForms_Form, "id" | "name">;
response: Response;
orderedResponses: OrderedResponses;
subject: string;
} & Partial<React.ComponentProps<typeof BaseEmailHtml>>) => {
return (
@@ -36,11 +36,11 @@ export const ResponseEmail = ({
title={form.name}
subtitle="New Response Received"
{...props}>
{Object.entries(response).map(([fieldId, fieldResponse]) => {
{orderedResponses.map((fieldResponse, index) => {
return (
<Info
withSpacer
key={fieldId}
key={index}
label={fieldResponse.label}
description={
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 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">;
export default class ResponseEmail extends BaseEmail {
response: Response;
orderedResponses: OrderedResponses;
toAddresses: string[];
form: Form;
constructor({ toAddresses, response, form }: { form: Form; toAddresses: string[]; response: Response }) {
constructor({
toAddresses,
orderedResponses,
form,
}: {
form: Form;
toAddresses: string[];
orderedResponses: OrderedResponses;
}) {
super();
this.form = form;
this.response = response;
this.orderedResponses = orderedResponses;
this.toAddresses = toAddresses;
}
@@ -26,7 +34,7 @@ export default class ResponseEmail extends BaseEmail {
subject,
html: renderEmail("ResponseEmail", {
form: this.form,
response: this.response,
orderedResponses: this.orderedResponses,
subject,
}),
};
@@ -6,6 +6,7 @@ import logger from "@calcom/lib/logger";
import { WebhookTriggerEvents } from "@calcom/prisma/client";
import type { Ensure } from "@calcom/types/utils";
import type { OrderedResponses } from "../types/types";
import type { Response, SerializableForm } from "../types/types";
export async function onFormSubmission(
@@ -61,23 +62,28 @@ export async function onFormSubmission(
});
await Promise.all(promises);
const orderedResponses = form.fields.reduce((acc, field) => {
acc.push(response[field.id]);
return acc;
}, [] as OrderedResponses);
if (form.settings?.emailOwnerOnSubmission) {
logger.debug(
`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 (
form: Pick<App_RoutingForms_Form, "id" | "name">,
response: Response,
orderedResponses: OrderedResponses,
ownerEmail: string
) => {
try {
if (typeof window === "undefined") {
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();
}
} catch (e) {
+2
View File
@@ -45,3 +45,5 @@ export type SerializableRoute =
isFallback?: LocalRoute["isFallback"];
})
| GlobalRoute;
export type OrderedResponses = Response[string][];
+4
View File
@@ -1,3 +1,7 @@
---
items:
- 1.jpg
---
Vital App is an app that can can help you combine your health peripherals with your calendar.
#### Supported Actions:
+1 -1
View File
@@ -8,7 +8,7 @@ export const metadata = {
installed: true,
category: "automation",
categories: ["automation"],
logo: "icon.svg",
logo: "icon-dark.svg",
label: "Vital",
publisher: "Vital",
slug: "vital-automation",
Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Before

Width:  |  Height:  |  Size: 571 B

After

Width:  |  Height:  |  Size: 571 B

+14 -2
View File
@@ -1,4 +1,5 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { stringify } from "querystring";
import { WEBAPP_URL } from "@calcom/lib/constants";
@@ -13,7 +14,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (!client_id) return res.status(400).json({ message: "zohocrm client id missing." });
const state = encodeOAuthState(req);
const redirectUri = WEBAPP_URL + "/api/integrations/zohocrm/callback";
const url = `https://accounts.zoho.com/oauth/v2/auth?scope=ZohoCRM.modules.ALL,ZohoCRM.users.READ,AaaServer.profile.READ&client_id=${client_id}&response_type=code&access_type=offline&redirect_uri=${redirectUri}&state=${state}`;
const params = {
client_id,
response_type: "code",
redirect_uri: WEBAPP_URL + "/api/integrations/zohocrm/callback",
scope: ["ZohoCRM.modules.ALL", "ZohoCRM.users.READ", "AaaServer.profile.READ"],
access_type: "offline",
state,
prompt: "consent",
};
const query = stringify(params);
const url = `https://accounts.zoho.com/oauth/v2/auth?${query}`;
res.status(200).json({ url });
}
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "ZohoCRM",
"slug": "zohocrm",
"type": "zohocrm_other_calendar",
"logo": "icon.png",
"logo": "icon.svg",
"url": "https://github.com/jatinsandilya",
"variant": "other",
"categories": ["crm"],
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.2 KiB

+9 -1
View File
@@ -10,6 +10,7 @@ import { appKeysSchema as calVideoKeysSchema } from "@calcom/app-store/dailyvide
import { getEventLocationTypeFromApp } from "@calcom/app-store/locations";
import { MeetLocationType } from "@calcom/app-store/locations";
import getApps from "@calcom/app-store/utils";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import { createdEventSchema } from "@calcom/prisma/zod-utils";
import type { NewCalendarEventType } from "@calcom/types/Calendar";
@@ -436,7 +437,14 @@ export default class EventManager {
* This might happen if someone tries to use a location with a missing credential, so we fallback to Cal Video.
* @todo remove location from event types that has missing credentials
* */
if (!videoCredential) videoCredential = { ...FAKE_DAILY_CREDENTIAL, appName: "FAKE" };
if (!videoCredential) {
logger.warn(
'Falling back to "daily" video integration for event with location: ' +
event.location +
" because credential is missing for the app"
);
videoCredential = { ...FAKE_DAILY_CREDENTIAL, appName: "FAKE" };
}
return videoCredential;
}
@@ -33,7 +33,7 @@ export const SlugReplacementEmail = (
</Trans>
<Trans i18nKey="email_body_slug_replacement_info">
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
Your link will continue to work but somesettings for it may have changed. You can review it in
Your link will continue to work but some settings for it may have changed. You can review it in
event types.
</p>
</Trans>
@@ -27,9 +27,8 @@ export const TeamInviteEmail = (
})}>
<p style={{ fontSize: "24px", marginBottom: "16px", textAlign: "center" }}>
<>
{props.language("email_no_user_invite_heading", {
{props.language(`email_no_user_invite_heading_${props.isOrg ? "org" : "team"}`, {
appName: APP_NAME,
entity: props.language(props.isOrg ? "organization" : "team").toLowerCase(),
})}
</>
</p>
@@ -57,11 +56,10 @@ export const TeamInviteEmail = (
lineHeightStep: "24px",
}}>
<>
{props.language("email_user_invite_subheading", {
{props.language(`email_user_invite_subheading_${props.isOrg ? "org" : "team"}`, {
invitedBy: props.from,
appName: APP_NAME,
teamName: props.teamName,
entity: props.language(props.isOrg ? "organization" : "team").toLowerCase(),
})}
</>
</p>
+11
View File
@@ -6,6 +6,7 @@ import dayjs from "@calcom/dayjs";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { serverConfig } from "@calcom/lib/serverConfig";
import { setTestEmail } from "@calcom/lib/testEmails";
import prisma from "@calcom/prisma";
export default class BaseEmail {
@@ -34,6 +35,16 @@ export default class BaseEmail {
return new Promise((r) => r("Skipped Sending Email due to active Kill Switch"));
}
if (process.env.INTEGRATION_TEST_MODE === "true") {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-expect-error
setTestEmail(this.getNodeMailerPayload());
console.log(
"Skipped Sending Email as process.env.NEXT_PUBLIC_UNIT_TESTS is set. Emails are available in globalThis.testEmails"
);
return new Promise((r) => r("Skipped sendEmail for Unit Tests"));
}
const payload = this.getNodeMailerPayload();
const parseSubject = z.string().safeParse(payload?.subject);
const payloadWithUnEscapedSubject = {
@@ -21,11 +21,7 @@ function RenderIcon({
return (
<img
src={eventLocationType.iconUrl}
className={classNames(
"me-[10px] h-4 w-4 opacity-70 dark:opacity-100",
eventLocationType.iconUrl?.includes("-dark") ? "dark:invert-[.65]" : "",
eventLocationType.iconUrl?.includes("-dark") && isTooltip && "invert"
)}
className="me-[10px] h-4 w-4 opacity-70 invert-[.65] dark:invert-0"
alt={`${eventLocationType.label} icon`}
/>
);
@@ -338,7 +338,67 @@ export async function handleConfirmation(args: {
);
})
);
await Promise.all(promises);
if (paid) {
let paymentExternalId: string | undefined;
const subscriberMeetingPaid = await getWebhooks({
userId: triggerForUser ? booking.userId : null,
eventTypeId: booking.eventTypeId,
triggerEvent: WebhookTriggerEvents.BOOKING_PAID,
teamId: booking.eventType?.teamId,
});
const bookingWithPayment = await prisma.booking.findFirst({
where: {
id: bookingId,
},
select: {
payment: {
select: {
id: true,
success: true,
externalId: true,
},
},
},
});
const successPayment = bookingWithPayment?.payment?.find((item) => item.success);
if (successPayment) {
paymentExternalId = successPayment.externalId;
}
const paymentMetadata = {
identifier: "cal.com",
bookingId,
eventTypeId: booking.eventType?.id,
bookerEmail: evt.attendees[0].email,
eventTitle: booking.eventType?.title,
externalId: paymentExternalId,
};
const bookingPaidSubscribers = subscriberMeetingPaid.map((sub) =>
sendPayload(sub.secret, WebhookTriggerEvents.BOOKING_PAID, new Date().toISOString(), sub, {
...evt,
...eventTypeInfo,
bookingId,
eventTypeId: booking.eventType?.id,
status: "ACCEPTED",
smsReminderNumber: booking.smsReminderNumber || undefined,
paymentId: bookingWithPayment?.payment?.[0].id,
metadata: {
...(paid ? paymentMetadata : {}),
},
}).catch((e) => {
console.error(
`Error executing webhook for event: ${WebhookTriggerEvents.BOOKING_PAID}, URL: ${sub.subscriberUrl}`,
e
);
})
);
// I don't need to await for this
Promise.all(bookingPaidSubscribers);
}
} catch (error) {
// Silently fail
console.error(error);
@@ -0,0 +1,511 @@
/**
* How to ensure that unmocked prisma queries aren't called?
*/
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, expect, beforeEach } from "vitest";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { BookingStatus } from "@calcom/prisma/enums";
import { test } from "@calcom/web/test/fixtures/fixtures";
import {
createBookingScenario,
getDate,
expectWorkflowToBeTriggered,
getGoogleCalendarCredential,
TestData,
getOrganizer,
getBooker,
getScenarioData,
expectBookingToBeInDatabase,
getZoomAppCredential,
mockEnableEmailFeature,
mockNoTranslations,
mockErrorOnVideoMeetingCreation,
mockSuccessfulVideoMeetingCreation,
mockCalendarToHaveNoBusySlots,
expectWebhookToHaveBeenCalledWith,
MockError,
} from "@calcom/web/test/utils/bookingScenario";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
// Local test runs sometime gets too slow
const timeout = process.env.CI ? 5000 : 20000;
describe.sequential("handleNewBooking", () => {
beforeEach(() => {
// Required to able to generate token in email in some cases
process.env.CALENDSO_ENCRYPTION_KEY="abcdefghjnmkljhjklmnhjklkmnbhjui"
mockNoTranslations();
mockEnableEmailFeature();
globalThis.testEmails = [];
fetchMock.resetMocks();
});
describe.sequential("Frontend:", () => {
test(
`should create a successful booking with Cal Video(Daily Video) if no explicit location is provided
1. Should create a booking in the database
2. Should send emails to the booker as well as organizer
3. Should trigger BOOKING_CREATED webhook
`,
async ({ emails }) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
},
});
const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});
const scenarioData = getScenarioData({
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CREATED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 1,
appId: null,
},
],
eventTypes: [
{
id: 1,
slotInterval: 45,
length: 45,
users: [
{
id: 101,
},
],
},
],
organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
});
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
});
mockCalendarToHaveNoBusySlots("googlecalendar");
createBookingScenario(scenarioData);
const createdBooking = await handleNewBooking(req);
expect(createdBooking.responses).toContain({
email: booker.email,
name: booker.name,
});
expect(createdBooking).toContain({
location: "integrations:daily",
});
expectBookingToBeInDatabase({
description: "",
eventType: {
connect: {
id: mockBookingData.eventTypeId,
},
},
status: BookingStatus.ACCEPTED,
});
expectWorkflowToBeTriggered();
const testEmails = emails.get();
expect(testEmails[0]).toHaveEmail({
htmlToContain: "<title>confirmed_event_type_subject</title>",
to: `${organizer.email}`,
});
expect(testEmails[1]).toHaveEmail({
htmlToContain: "<title>confirmed_event_type_subject</title>",
to: `${booker.name} <${booker.email}>`,
});
expect(testEmails[1].html).toContain("<title>confirmed_event_type_subject</title>");
expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", {
triggerEvent: "BOOKING_CREATED",
payload: {
metadata: {
videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`,
},
responses: {
name: { label: "your_name", value: "Booker" },
email: { label: "email_address", value: "booker@example.com" },
location: {
label: "location",
value: { optionValue: "", value: "integrations:daily" },
},
title: { label: "what_is_this_meeting_about" },
notes: { label: "additional_notes" },
guests: { label: "additional_guests" },
rescheduleReason: { label: "reason_for_reschedule" },
},
},
});
},
timeout
);
test(
`should submit a booking request for event requiring confirmation
1. Should create a booking in the database with status PENDING
2. Should send emails to the booker as well as organizer for booking request and awaiting approval
3. Should trigger BOOKING_REQUESTED webhook
`,
async ({ emails }) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
},
});
const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});
const scenarioData = getScenarioData({
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CREATED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 1,
appId: null,
},
],
eventTypes: [
{
id: 1,
slotInterval: 45,
requiresConfirmation: true,
length: 45,
users: [
{
id: 101,
},
],
},
],
organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
});
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
});
mockCalendarToHaveNoBusySlots("googlecalendar");
createBookingScenario(scenarioData);
const createdBooking = await handleNewBooking(req);
expect(createdBooking.responses).toContain({
email: booker.email,
name: booker.name,
});
expect(createdBooking).toContain({
location: "integrations:daily",
});
expectBookingToBeInDatabase({
description: "",
eventType: {
connect: {
id: mockBookingData.eventTypeId,
},
},
status: BookingStatus.PENDING,
});
expectWorkflowToBeTriggered();
const testEmails = emails.get();
expect(testEmails[0]).toHaveEmail({
htmlToContain: "<title>event_awaiting_approval_subject</title>",
to: `${organizer.email}`,
});
expect(testEmails[1]).toHaveEmail({
htmlToContain: "<title>booking_submitted_subject</title>",
to: `${booker.email}`,
});
expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", {
triggerEvent: "BOOKING_REQUESTED",
payload: {
metadata: {
// In a Pending Booking Request, we don't send the video call url
videoCallUrl: undefined,
},
responses: {
name: { label: "your_name", value: "Booker" },
email: { label: "email_address", value: "booker@example.com" },
location: {
label: "location",
value: { optionValue: "", value: "integrations:daily" },
},
title: { label: "what_is_this_meeting_about" },
notes: { label: "additional_notes" },
guests: { label: "additional_guests" },
rescheduleReason: { label: "reason_for_reschedule" },
},
},
});
},
timeout
);
test(
`if booking with Cal Video(Daily Video) fails, booking creation fails with uncaught error`,
async ({}) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const booker = getBooker({
email: "booker@example.org",
name: "Booker",
});
const organizer = TestData.users.example;
const { req } = createMockNextJsRequest({
method: "POST",
body: getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
},
}),
});
const scenarioData = {
hosts: [],
eventTypes: [
{
id: 1,
slotInterval: 45,
length: 45,
users: [
{
id: 101,
},
],
},
],
users: [
{
...organizer,
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
},
],
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
};
mockErrorOnVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
});
mockCalendarToHaveNoBusySlots("googlecalendar");
createBookingScenario(scenarioData);
try {
await handleNewBooking(req);
} catch (e) {
expect(e).toBeInstanceOf(MockError);
expect((e as { message: string }).message).toBe("Error creating Video meeting");
}
},
timeout
);
test(
`should create a successful booking with Zoom if used`,
async ({ emails }) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getZoomAppCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const { req } = createMockNextJsRequest({
method: "POST",
body: getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:zoom" },
},
},
}),
});
const bookingScenario = getScenarioData({
organizer,
eventTypes: [
{
id: 1,
slotInterval: 45,
length: 45,
users: [
{
id: 101,
},
],
},
],
apps: [TestData.apps["daily-video"]],
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CREATED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 1,
appId: null,
},
],
});
createBookingScenario(bookingScenario);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "zoomvideo",
});
await handleNewBooking(req);
const testEmails = emails.get();
expect(testEmails[0]).toHaveEmail({
htmlToContain: "<title>confirmed_event_type_subject</title>",
to: `${organizer.email}`,
});
expect(testEmails[1]).toHaveEmail({
htmlToContain: "<title>confirmed_event_type_subject</title>",
to: `${booker.name} <${booker.email}>`,
});
expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", {
triggerEvent: "BOOKING_CREATED",
payload: {
metadata: {
videoCallUrl: "http://mock-zoomvideo.example.com",
},
responses: {
name: { label: "your_name", value: "Booker" },
email: { label: "email_address", value: "booker@example.com" },
location: {
label: "location",
value: { optionValue: "", value: "integrations:zoom" },
},
title: { label: "what_is_this_meeting_about" },
notes: { label: "additional_notes" },
guests: { label: "additional_guests" },
rescheduleReason: { label: "reason_for_reschedule" },
},
},
});
},
timeout
);
});
});
function createMockNextJsRequest(...args: Parameters<typeof createMocks>) {
return createMocks<CustomNextApiRequest, CustomNextApiResponse>(...args);
}
function getBasicMockRequestDataForBooking() {
return {
start: `${getDate({ dateIncrement: 1 }).dateString}T04:00:00.000Z`,
end: `${getDate({ dateIncrement: 1 }).dateString}T04:30:00.000Z`,
eventTypeSlug: "no-confirmation",
timeZone: "Asia/Calcutta",
language: "en",
bookingUid: "bvCmP5rSquAazGSA7hz7ZP",
user: "teampro",
metadata: {},
hasHashedBookingLink: false,
hashedLink: null,
};
}
function getMockRequestDataForBooking({
data,
}: {
data: Partial<ReturnType<typeof getBasicMockRequestDataForBooking>> & {
eventTypeId: number;
responses: {
email: string;
name: string;
location: { optionValue: ""; value: string };
};
};
}) {
return {
...getBasicMockRequestDataForBooking(),
...data,
};
}
@@ -1756,6 +1756,7 @@ async function handler(
const webhookData = {
...evt,
...eventTypeInfo,
uid: resultBooking?.uid || uid,
bookingId: booking?.id,
rescheduleUid,
rescheduleStartTime: originalRescheduledBooking?.startTime
+29 -39
View File
@@ -1,3 +1,4 @@
import { useEffect } from "react";
import { shallow } from "zustand/shallow";
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 = ({
minDate = dayjs.utc(),
excludedDates = [],
@@ -218,11 +185,34 @@ const Days = ({
};
});
useHandleInitialDateSelection({
daysToRenderForTheMonth,
selected,
onChange: props.onChange,
});
/**
* Takes care of selecting a valid date in the month if the selected date is not available in the month
*/
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 (
<>
@@ -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;
@@ -61,7 +61,9 @@ export default function WorkflowDetailsPage(props: Props) {
...options,
...group.eventTypes.map((eventType) => ({
value: String(eventType.id),
label: `${eventType.title} ${eventType.children.length ? `(+${eventType.children.length})` : ``}`,
label: `${eventType.title} ${
eventType.children && eventType.children.length ? `(+${eventType.children.length})` : ``
}`,
})),
];
}, [] as Option[]) || [],
@@ -103,7 +103,7 @@ export function AvailabilitySliderTable() {
.padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
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-subtle text-xs leading-none">GMT {offsetFormatted}</span>
</div>
@@ -5,7 +5,7 @@ import { useOrgBranding } from "@calcom/ee/organizations/context/provider";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
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 { 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("timezone")} value={loadedUser?.timeZone ?? ""} />
<DisplayInfo
label={t("availability_schedules")}
value={
schedulesNames && schedulesNames?.length === 0
? [t("user_has_no_schedules")]
: schedulesNames ?? "" // TS wtf
}
/>
<div className="flex flex-col">
<Label className="text-subtle mb-1 text-xs font-semibold uppercase leading-none">
{t("availability_schedules")}
</Label>
<div className="flex flex-col">
{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
label={t("teams")}
displayCount={teamNames?.length ?? 0}
@@ -18,7 +18,7 @@ export type EventTypeInfo = {
export type WebhookDataType = CalendarEvent &
EventTypeInfo & {
metadata?: { [key: string]: string };
metadata?: { [key: string]: string | number | boolean | null };
bookingId?: number;
status?: string;
smsReminderNumber?: string;
@@ -28,6 +28,7 @@ export type WebhookDataType = CalendarEvent &
triggerEvent: string;
createdAt: string;
downloadLink?: string;
paymentId?: number;
};
function getZapierPayload(
+6 -8
View File
@@ -136,7 +136,6 @@ export async function getTeamWithMembers(args: {
// This should improve performance saving already app data found.
const appDataMap = new Map();
const members = team.members.map((obj) => {
return {
...obj.user,
@@ -144,7 +143,7 @@ export async function getTeamWithMembers(args: {
accepted: obj.accepted,
disableImpersonation: obj.disableImpersonation,
avatar: `${WEBAPP_URL}/${obj.user.username}/avatar.png`,
connectedApps: obj.user.credentials.map((cred) => {
connectedApps: obj?.user?.credentials?.map((cred) => {
const appSlug = cred.app?.slug;
let appData = appDataMap.get(appSlug);
@@ -153,14 +152,13 @@ export async function getTeamWithMembers(args: {
appDataMap.set(appSlug, appData);
}
const isCalendar = cred?.app?.categories.includes("calendar");
const externalId = isCalendar ? cred.destinationCalendars[0]?.externalId : undefined;
const isCalendar = cred?.app?.categories?.includes("calendar") ?? false;
const externalId = isCalendar ? cred.destinationCalendars?.[0]?.externalId : null;
return {
name: appData?.name,
logo: appData?.logo,
name: appData?.name ?? null,
logo: appData?.logo ?? null,
app: cred.app,
externalId,
externalId: externalId ?? null,
};
}),
};
+18
View File
@@ -0,0 +1,18 @@
declare global {
// eslint-disable-next-line no-var
var testEmails: {
to: string;
from: string;
subject: string;
html: string;
}[];
}
export const setTestEmail = (email: (typeof globalThis.testEmails)[number]) => {
globalThis.testEmails = globalThis.testEmails || [];
globalThis.testEmails.push(email);
};
export const getTestEmails = () => {
return globalThis.testEmails;
};
+1
View File
@@ -56,6 +56,7 @@ model EventType {
position Int @default(0)
/// @zod.custom(imports.eventTypeLocations)
locations Json?
/// @zod.min(1)
length Int
offsetStart Int @default(0)
hidden Boolean @default(false)
@@ -64,6 +64,7 @@ export async function getUserFromSession(ctx: TRPCContextInner, session: Maybe<S
id: true,
slug: true,
metadata: true,
name: true,
members: {
select: { userId: true },
where: {
@@ -14,7 +14,7 @@ import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/Syn
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
import { prisma } from "@calcom/prisma";
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 { TRPCError } from "@trpc/server";
@@ -31,11 +31,10 @@ type UpdateProfileOptions = {
export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) => {
const { user } = ctx;
const { metadata: metadataFromInput } = input;
const cleanMetadata = cleanMetadataAllowedUpdateKeys(metadataFromInput);
const userMetadata = handleUserMetadata({ ctx, input });
const data: Prisma.UserUpdateInput = {
...input,
metadata: cleanMetadata,
metadata: userMetadata,
};
// some actions can invalidate a user session.
@@ -65,26 +64,9 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
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) {
const stripeCustomerId = metadata?.stripeCustomerId;
const stripeCustomerId = userMetadata?.stripeCustomerId;
const isPremium = userMetadata?.isPremium;
if (!isPremium || !stripeCustomerId) {
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"]) => {
if (!metadata) {
return {} as Prisma.InputJsonValue;
return {};
}
const cleanedMetadata = updateUserMetadataAllowedKeys.safeParse(metadata);
if (!cleanedMetadata.success) {
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 };
};
@@ -52,14 +52,6 @@ const userEventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
users: {
select: userSelect,
},
// Temporarily putting this back for testing
children: {
include: {
users: {
select: userSelect,
},
},
},
parentId: true,
hosts: {
select: {
@@ -74,13 +66,13 @@ const userEventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
const teamEventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
...userEventTypeSelect,
// children: {
// include: {
// users: {
// select: userSelect,
// },
// },
// },
children: {
include: {
users: {
select: userSelect,
},
},
},
});
export const compareMembership = (mship1: MembershipRole, mship2: MembershipRole) => {
@@ -167,11 +159,15 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
const mapEventType = (eventType: (typeof user.eventTypes)[number]) => ({
type UserEventTypes = (typeof user.eventTypes)[number];
type TeamEventTypeChildren = (typeof user.teams)[number]["team"]["eventTypes"][number];
const mapEventType = (eventType: UserEventTypes & Partial<TeamEventTypeChildren>) => ({
...eventType,
safeDescription: markdownToSafeHTML(eventType.description),
users: !!eventType.hosts?.length ? eventType.hosts.map((host) => host.user) : eventType.users,
safeDescription: eventType?.description ? markdownToSafeHTML(eventType.description) : undefined,
users: !!eventType?.hosts?.length ? eventType?.hosts.map((host) => host.user) : eventType.users,
metadata: eventType.metadata ? EventTypeMetaDataSchema.parse(eventType.metadata) : undefined,
children: eventType.children,
});
const userEventTypes = user.eventTypes.map(mapEventType);
@@ -279,8 +275,7 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
const bookerUrl = await getBookerUrl(user);
return {
// don't display event teams without event types,
eventTypeGroups: eventTypeGroups.filter((groupBy) => groupBy.parentId || !!groupBy.eventTypes?.length),
eventTypeGroups,
// so we can show a dropdown when the user has teams
profiles: eventTypeGroups.map((group) => ({
...group.profile,
@@ -241,13 +241,13 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
}
}
if (input?.price || input.metadata?.apps?.stripe?.price) {
data.price = input.price || input.metadata?.apps?.stripe?.price;
if (input.metadata?.apps?.stripe?.price) {
data.price = input.metadata?.apps?.stripe?.price;
const paymentCredential = await ctx.prisma.credential.findFirst({
where: {
userId: ctx.user.id,
type: {
contains: "_payment",
equals: "stripe_payment",
},
},
select: {
@@ -260,6 +260,22 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
const { default_currency } = stripeDataSchema.parse(paymentCredential.key);
data.currency = default_currency;
}
}
if (input.metadata?.apps?.paypal?.price) {
data.price = input.metadata?.apps?.paypal?.price;
const paymentCredential = await ctx.prisma.credential.findFirst({
where: {
userId: ctx.user.id,
type: {
equals: "paypal_payment",
},
},
select: {
type: true,
key: true,
},
});
if (paymentCredential?.type === "paypal_payment" && input.metadata?.apps?.paypal?.currency) {
data.currency = input.metadata?.apps?.paypal?.currency.toLowerCase();
}
+5 -14
View File
@@ -7,7 +7,6 @@ import { AVATAR_FALLBACK } from "@calcom/lib/constants";
import type { Maybe } from "@trpc/server";
import { Check } from "../icon";
import { Tooltip } from "../tooltip";
export type AvatarProps = {
@@ -20,6 +19,7 @@ export type AvatarProps = {
fallback?: React.ReactNode;
accepted?: boolean;
asChild?: boolean; // Added to ignore the outer span on the fallback component - messes up styling
indicator?: React.ReactNode;
};
const sizesPropsBySize = {
@@ -34,12 +34,13 @@ const sizesPropsBySize = {
} as const;
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]);
let avatar = (
<AvatarPrimitive.Root
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,
sizesPropsBySize[size]
)}>
@@ -57,17 +58,7 @@ export function Avatar(props: AvatarProps) {
{props.fallback ? props.fallback : <img src={AVATAR_FALLBACK} alt={alt} className={rootClass} />}
</>
</AvatarPrimitive.Fallback>
{props.accepted && (
<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>
)}
{indicator}
</>
</AvatarPrimitive.Root>
);
@@ -11,7 +11,6 @@ export type AvatarGroupProps = {
href?: string;
}[];
className?: string;
accepted?: boolean;
truncateAfter?: number;
};
@@ -36,7 +35,6 @@ export const AvatarGroup = function AvatarGroup(props: AvatarGroupProps) {
imageSrc={item.image}
title={item.title}
alt={item.alt || ""}
accepted={props.accepted}
size={props.size}
href={item.href}
/>
+1 -1
View File
@@ -108,7 +108,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item
ref={ref}
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
)}
{...props}
@@ -14,20 +14,54 @@ import { DataTable } from "../";
import { columns, filterableItems } from "./columns";
import { dataTableDemousers, dataTableSelectionActions } from "./data";
<Meta title="UI/table/data-table" component={DataTable} />
<Meta title="UI/table/DataTable" component={DataTable} />
<Title title="Data-table" suffix="Brief" subtitle="Version 2.0 — Last Update: 06 Jan 2023" />
<Title title="DataTable" suffix="Brief" subtitle="Version 3.0 — Last Update: 28 Aug 2023" />
## Definition
The `DataTable` component facilitates tabular data display with configurable columns, virtual scrolling, filtering, and interactive features for seamless dynamic table creation.
## Structure
The `DataTable` setup for tabular data, with columns, virtual scroll, sticky headers, and interactive features like filtering and row selection.
<CustomArgsTable of={DataTable} />
## Dialog Story
<Canvas>
<Story name="Datatable">
<DataTable
columns={columns}
data={dataTableDemousers}
filterableItems={filterableItems}
searchKey="username"
selectionOptions={dataTableSelectionActions}
/>
<Story
name="DataTable"
args={{
columns: columns,
data: dataTableDemousers,
isLoading: false,
searchKey: "username",
filterableItems: filterableItems,
tableContainerRef: { current: null },
selectionOptions: dataTableSelectionActions,
}}
argTypes={{
tableContainerRef: { table: { disable: true } },
searchKey: {
control: {
type: "select",
options: ["username", "email"],
},
},
selectionOptions: { control: { type: "object" } },
onScroll: { table: { disable: true } },
tableOverlay: { table: { disable: true } },
CTA: { table: { disable: true } },
tableCTA: { table: { disable: true } },
}}>
{(args) => (
<VariantsTable titles={["Default"]} columnMinWidth={1000}>
<VariantRow>
<DataTable {...args} />
</VariantRow>
</VariantsTable>
)}
</Story>
</Canvas>
@@ -48,7 +48,13 @@ export function EmptyScreen({
</div>
)}
<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 && (
<div className="text-default mb-8 mt-3 text-center text-sm font-normal leading-6">
{description}
@@ -0,0 +1,59 @@
import { Canvas, Meta, Story } from "@storybook/addon-docs";
import { useRouter } from "next/router";
import { CustomArgsTable, Title, VariantsTable, VariantRow } from "@calcom/storybook/components";
import WizardForm from "./WizardForm";
<Meta title="UI/Form/WizardForm" component={WizardForm} />
<Title title="WizardForm" subtitle="Version 1.0 — Last Update: 5 Sep 2023" />
## Definition
The `WizardForm` component provides a structure for creating multi-step forms or wizards.
## Structure
<CustomArgsTable of={WizardForm} />
## Note on Navigation
Please be aware that the steps navigation is managed internally within the `WizardForm` component. As such, when viewing this component in Storybook, clicking the "Next" button will not showcase the transition to the subsequent step.
To observe the actual step navigation behavior, please refer to the Storybook stories for the individual `Steps` component.
## WizardForm Story
<Canvas>
<Story
name="Basic"
args={{
href: "/wizard",
steps: [
{ title: "Step 1", description: "Description for Step 1" },
{ title: "Step 2", description: "Description for Step 2" },
{ title: "Step 3", description: "Description for Step 3" },
],
}}
argTypes={{
href: {
control: {
type: "text",
},
},
steps: {
control: {
type: "object",
},
},
}}>
{({ href, steps }) => (
<VariantsTable titles={["Basic Wizard Form"]} columnMinWidth={150}>
<VariantRow>
<WizardForm href={href} steps={steps} />
</VariantRow>
</VariantsTable>
)}
</Story>
</Canvas>
+1 -4
View File
@@ -36,10 +36,7 @@ const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTML
({ className, ...props }, ref) => (
<tr
ref={ref}
className={classNames(
"hover:bg-subtle data-[state=selected]:bg-subtle border-subtle border-b",
className
)}
className={classNames("hover:muted data-[state=selected]:bg-muted border-subtle border-b", className)}
{...props}
/>
)
+7
View File
@@ -0,0 +1,7 @@
import { vi } from "vitest";
import createFetchMock from "vitest-fetch-mock";
const fetchMocker = createFetchMock(vi);
// sets globalThis.fetch and globalThis.fetchMock to our mocked version
fetchMocker.enableMocks();
+17
View File
@@ -0,0 +1,17 @@
import { beforeEach, vi } from "vitest";
import { mockReset, mockDeep } from "vitest-mock-extended";
import type * as appStore from "@calcom/app-store";
vi.mock("@calcom/app-store", () => appStoreMock);
beforeEach(() => {
mockReset(appStoreMock);
});
const appStoreMock = mockDeep<typeof appStore>({
fallbackMockImplementation: () => {
throw new Error("Unimplemented");
},
});
export default appStoreMock;
+13
View File
@@ -0,0 +1,13 @@
import { beforeEach, vi } from "vitest";
import { mockReset, mockDeep } from "vitest-mock-extended";
import type * as i18n from "@calcom/lib/server/i18n";
vi.mock("@calcom/lib/server/i18n", () => i18nMock);
beforeEach(() => {
mockReset(i18nMock);
});
const i18nMock = mockDeep<typeof i18n>();
export default i18nMock;
+1
View File
@@ -5,6 +5,7 @@ import type { PrismaClient } from "@calcom/prisma";
vi.mock("@calcom/prisma", () => ({
default: prisma,
prisma,
availabilityUserSelect: vi.fn(),
userSelect: vi.fn(),
}));
+13
View File
@@ -0,0 +1,13 @@
import { beforeEach, vi } from "vitest";
import { mockReset, mockDeep } from "vitest-mock-extended";
import type * as reminderScheduler from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
vi.mock("@calcom/features/ee/workflows/lib/reminders/reminderScheduler", () => reminderSchedulerMock);
beforeEach(() => {
mockReset(reminderSchedulerMock);
});
const reminderSchedulerMock = mockDeep<typeof reminderScheduler>();
export default reminderSchedulerMock;
+13
View File
@@ -0,0 +1,13 @@
import { beforeEach, vi } from "vitest";
import { mockReset, mockDeep } from "vitest-mock-extended";
import type * as videoClient from "@calcom/core/videoClient";
vi.mock("@calcom/core/videoClient", () => videoClientMock);
beforeEach(() => {
mockReset(videoClientMock);
});
const videoClientMock = mockDeep<typeof videoClient>();
export default videoClientMock;
+4 -1
View File
@@ -1,9 +1,12 @@
import { defineConfig } from "vitest/config";
process.env.INTEGRATION_TEST_MODE = "true";
export default defineConfig({
test: {
coverage: {
provider: "c8",
provider: "v8",
},
testTimeout: 500000,
},
});
+1
View File
@@ -17,6 +17,7 @@ const workspaces = packagedEmbedTestsOnly
include: ["packages/**/*.{test,spec}.{ts,js}", "apps/**/*.{test,spec}.{ts,js}"],
// TODO: Ignore the api until tests are fixed
exclude: ["apps/api/**/*", "**/node_modules/**/*", "packages/embeds/**/*"],
setupFiles: ["setupVitest.ts"],
},
},
{
+153 -137
View File
@@ -3746,6 +3746,16 @@ __metadata:
languageName: unknown
linkType: soft
"@calcom/make@workspace:packages/app-store/make":
version: 0.0.0-use.local
resolution: "@calcom/make@workspace:packages/app-store/make"
dependencies:
"@calcom/lib": "*"
"@calcom/types": "*"
"@types/node-schedule": ^2.1.0
languageName: unknown
linkType: soft
"@calcom/metapixel@workspace:packages/app-store/metapixel":
version: 0.0.0-use.local
resolution: "@calcom/metapixel@workspace:packages/app-store/metapixel"
@@ -6975,7 +6985,7 @@ __metadata:
languageName: node
linkType: hard
"@jridgewell/sourcemap-codec@npm:^1.4.13, @jridgewell/sourcemap-codec@npm:^1.4.14":
"@jridgewell/sourcemap-codec@npm:^1.4.13, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15":
version: 1.4.15
resolution: "@jridgewell/sourcemap-codec@npm:1.4.15"
checksum: b881c7e503db3fc7f3c1f35a1dd2655a188cc51a3612d76efc8a6eb74728bef5606e6758ee77423e564092b4a518aba569bbb21c9bac5ab7a35b0c6ae7e344c8
@@ -13205,57 +13215,56 @@ __metadata:
languageName: node
linkType: hard
"@vitest/expect@npm:0.31.1":
version: 0.31.1
resolution: "@vitest/expect@npm:0.31.1"
"@vitest/expect@npm:0.34.3":
version: 0.34.3
resolution: "@vitest/expect@npm:0.34.3"
dependencies:
"@vitest/spy": 0.31.1
"@vitest/utils": 0.31.1
"@vitest/spy": 0.34.3
"@vitest/utils": 0.34.3
chai: ^4.3.7
checksum: 0d1e135ae753d913231eae830da00ee42afca53d354898fb43f97e82398dcf17298c02e9989dd6b19b9b2909989248ef76d203d63f6af6f9159dc96959ea654b
checksum: 79afaa37d2efb7bb5503332caf389860b2261f198dbe61557e8061262b628d18658e59eb51d1808ecd35fc59f4bb4d04c0e0f97a27c7db02584ab5b424147b8d
languageName: node
linkType: hard
"@vitest/runner@npm:0.31.1":
version: 0.31.1
resolution: "@vitest/runner@npm:0.31.1"
"@vitest/runner@npm:0.34.3":
version: 0.34.3
resolution: "@vitest/runner@npm:0.34.3"
dependencies:
"@vitest/utils": 0.31.1
concordance: ^5.0.4
"@vitest/utils": 0.34.3
p-limit: ^4.0.0
pathe: ^1.1.0
checksum: cc8702e21b799d5e941409cb2afe6d0e576b4f3ac99df4a1393a8cd11b57f6b0b06e756cc24e2739812d095fbfd0824e22e861dbd6a71769ca387d485ade6fb5
pathe: ^1.1.1
checksum: 945580eaa58e8edbe29a64059bc2a524a9e85117b6d600fdb457cfe84cbfb81bf6d7e98e1227e7cb4e7399992c8fe8d83d0791d0385ff005dc1a4d9da125443b
languageName: node
linkType: hard
"@vitest/snapshot@npm:0.31.1":
version: 0.31.1
resolution: "@vitest/snapshot@npm:0.31.1"
"@vitest/snapshot@npm:0.34.3":
version: 0.34.3
resolution: "@vitest/snapshot@npm:0.34.3"
dependencies:
magic-string: ^0.30.0
pathe: ^1.1.0
pretty-format: ^27.5.1
checksum: de05fa9136864f26f0804baf3ae8068f67de28015f29047329c84e67fb33be7305c9e52661b016e834d30f4081c136b3b6d8d4054c024a5d52b22a7f90fc4be0
magic-string: ^0.30.1
pathe: ^1.1.1
pretty-format: ^29.5.0
checksum: 234893e91a1efd4bdbbde047a68de40975e02ead8407724ce8ca4a24edf0fb2d725f8a3efceb104965388407b598faf22407aadfbf4164cc74b3cf1e0e9f4543
languageName: node
linkType: hard
"@vitest/spy@npm:0.31.1":
version: 0.31.1
resolution: "@vitest/spy@npm:0.31.1"
"@vitest/spy@npm:0.34.3":
version: 0.34.3
resolution: "@vitest/spy@npm:0.34.3"
dependencies:
tinyspy: ^2.1.0
checksum: 8b06cf25fcc028c16106ec82f4ceb84d6dfa04d06f651bca4738ce2b99796d1fc4e0c10319767240755eff8ede2bff9d31d5a901fe92828d319c65001581137b
tinyspy: ^2.1.1
checksum: a2b64b9c357a56ad2f2340ecd225ffe787e61afba4ffb24a6670aad3fc90ea2606ed48daa188ed62b3ef67d55c0259fda6b101143d6c91b58c9ac4298d8be4f9
languageName: node
linkType: hard
"@vitest/utils@npm:0.31.1":
version: 0.31.1
resolution: "@vitest/utils@npm:0.31.1"
"@vitest/utils@npm:0.34.3":
version: 0.34.3
resolution: "@vitest/utils@npm:0.34.3"
dependencies:
concordance: ^5.0.4
diff-sequences: ^29.4.3
loupe: ^2.3.6
pretty-format: ^27.5.1
checksum: 58016c185455e3814632cb77e37368c846bde5e342f8b4a66fa229bde64f455ca39abebc9c12e2483696ee38bc17b3c4300379f7a3b18d1087f24f474448a8d8
pretty-format: ^29.5.0
checksum: aeb8ef7fd98b32cb6c403796880d0aa8f5411bbdb249bb23b3301a70e1b7d1ee025ddb204aae8c1db5756f6ac428c49ebbb8e2ed23ce185c8a659b67413efa85
languageName: node
linkType: hard
@@ -13854,6 +13863,15 @@ __metadata:
languageName: node
linkType: hard
"acorn@npm:^8.10.0, acorn@npm:^8.9.0":
version: 8.10.0
resolution: "acorn@npm:8.10.0"
bin:
acorn: bin/acorn
checksum: 538ba38af0cc9e5ef983aee196c4b8b4d87c0c94532334fa7e065b2c8a1f85863467bb774231aae91613fcda5e68740c15d97b1967ae3394d20faddddd8af61d
languageName: node
linkType: hard
"acorn@npm:^8.5.0":
version: 8.7.1
resolution: "acorn@npm:8.7.1"
@@ -15266,13 +15284,6 @@ __metadata:
languageName: node
linkType: hard
"blueimp-md5@npm:^2.10.0":
version: 2.19.0
resolution: "blueimp-md5@npm:2.19.0"
checksum: 28095dcbd2c67152a2938006e8d7c74c3406ba6556071298f872505432feb2c13241b0476644160ee0a5220383ba94cb8ccdac0053b51f68d168728f9c382530
languageName: node
linkType: hard
"bmp-js@npm:^0.1.0":
version: 0.1.0
resolution: "bmp-js@npm:0.1.0"
@@ -15962,7 +15973,8 @@ __metadata:
tsc-absolute: ^1.0.0
turbo: ^1.10.1
typescript: ^4.9.4
vitest: ^0.31.1
vitest: ^0.34.3
vitest-fetch-mock: ^0.2.2
vitest-mock-extended: ^1.1.3
languageName: unknown
linkType: soft
@@ -17068,22 +17080,6 @@ __metadata:
languageName: node
linkType: hard
"concordance@npm:^5.0.4":
version: 5.0.4
resolution: "concordance@npm:5.0.4"
dependencies:
date-time: ^3.1.0
esutils: ^2.0.3
fast-diff: ^1.2.0
js-string-escape: ^1.0.1
lodash: ^4.17.15
md5-hex: ^3.0.1
semver: ^7.3.2
well-known-symbols: ^2.0.0
checksum: 749153ba711492feb7c3d2f5bb04c107157440b3e39509bd5dd19ee7b3ac751d1e4cd75796d9f702e0a713312dbc661421c68aa4a2c34d5f6d91f47e3a1c64a6
languageName: node
linkType: hard
"concurrently@npm:^7.6.0":
version: 7.6.0
resolution: "concurrently@npm:7.6.0"
@@ -17504,6 +17500,15 @@ __metadata:
languageName: node
linkType: hard
"cross-fetch@npm:^3.0.6":
version: 3.1.8
resolution: "cross-fetch@npm:3.1.8"
dependencies:
node-fetch: ^2.6.12
checksum: 78f993fa099eaaa041122ab037fe9503ecbbcb9daef234d1d2e0b9230a983f64d645d088c464e21a247b825a08dc444a6e7064adfa93536d3a9454b4745b3632
languageName: node
linkType: hard
"cross-spawn@npm:7.0.3, cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3":
version: 7.0.3
resolution: "cross-spawn@npm:7.0.3"
@@ -17926,15 +17931,6 @@ __metadata:
languageName: node
linkType: hard
"date-time@npm:^3.1.0":
version: 3.1.0
resolution: "date-time@npm:3.1.0"
dependencies:
time-zone: ^1.0.0
checksum: f9cfcd1b15dfeabab15c0b9d18eb9e4e2d9d4371713564178d46a8f91ad577a290b5178b80050718d02d9c0cf646f8a875011e12d1ed05871e9f72c72c8a8fe6
languageName: node
linkType: hard
"datocms-listen@npm:^0.1.9":
version: 0.1.15
resolution: "datocms-listen@npm:0.1.15"
@@ -19936,7 +19932,7 @@ __metadata:
languageName: node
linkType: hard
"esutils@npm:^2.0.2, esutils@npm:^2.0.3":
"esutils@npm:^2.0.2":
version: 2.0.3
resolution: "esutils@npm:2.0.3"
checksum: 22b5b08f74737379a840b8ed2036a5fb35826c709ab000683b092d9054e5c2a82c27818f12604bfc2a9a76b90b6834ef081edbc1c7ae30d1627012e067c6ec87
@@ -20391,13 +20387,6 @@ __metadata:
languageName: node
linkType: hard
"fast-diff@npm:^1.2.0":
version: 1.3.0
resolution: "fast-diff@npm:1.3.0"
checksum: d22d371b994fdc8cce9ff510d7b8dc4da70ac327bcba20df607dd5b9cae9f908f4d1028f5fe467650f058d1e7270235ae0b8230809a262b4df587a3b3aa216c3
languageName: node
linkType: hard
"fast-equals@npm:^1.6.3":
version: 1.6.3
resolution: "fast-equals@npm:1.6.3"
@@ -26516,12 +26505,12 @@ __metadata:
languageName: node
linkType: hard
"magic-string@npm:^0.30.0":
version: 0.30.0
resolution: "magic-string@npm:0.30.0"
"magic-string@npm:^0.30.1":
version: 0.30.3
resolution: "magic-string@npm:0.30.3"
dependencies:
"@jridgewell/sourcemap-codec": ^1.4.13
checksum: 7bdf22e27334d8a393858a16f5f840af63a7c05848c000fd714da5aa5eefa09a1bc01d8469362f25cc5c4a14ec01b46557b7fff8751365522acddf21e57c488d
"@jridgewell/sourcemap-codec": ^1.4.15
checksum: a5a9ddf9bd3bf49a2de1048bf358464f1bda7b3cc1311550f4a0ba8f81a4070e25445d53a5ee28850161336f1bff3cf28aa3320c6b4aeff45ce3e689f300b2f3
languageName: node
linkType: hard
@@ -26715,15 +26704,6 @@ __metadata:
languageName: node
linkType: hard
"md5-hex@npm:^3.0.1":
version: 3.0.1
resolution: "md5-hex@npm:3.0.1"
dependencies:
blueimp-md5: ^2.10.0
checksum: 6799a19e8bdd3e0c2861b94c1d4d858a89220488d7885c1fa236797e367d0c2e5f2b789e05309307083503f85be3603a9686a5915568a473137d6b4117419cc2
languageName: node
linkType: hard
"md5.js@npm:^1.3.4":
version: 1.3.5
resolution: "md5.js@npm:1.3.5"
@@ -27786,6 +27766,18 @@ __metadata:
languageName: node
linkType: hard
"mlly@npm:^1.4.0":
version: 1.4.1
resolution: "mlly@npm:1.4.1"
dependencies:
acorn: ^8.10.0
pathe: ^1.1.1
pkg-types: ^1.0.3
ufo: ^1.3.0
checksum: b2b59ab3d70196127be4e54609d2a442bd252345727138940fb245672a238b2fbdd431e8c75ec5c741ff90410ce488c5fd6446d5d3e6476d21dbf4c3fa35d4a0
languageName: node
linkType: hard
"mock-fs@npm:^4.1.0":
version: 4.14.0
resolution: "mock-fs@npm:4.14.0"
@@ -28436,6 +28428,20 @@ __metadata:
languageName: node
linkType: hard
"node-fetch@npm:^2.6.12":
version: 2.7.0
resolution: "node-fetch@npm:2.7.0"
dependencies:
whatwg-url: ^5.0.0
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
checksum: d76d2f5edb451a3f05b15115ec89fc6be39de37c6089f1b6368df03b91e1633fd379a7e01b7ab05089a25034b2023d959b47e59759cb38d88341b2459e89d6e5
languageName: node
linkType: hard
"node-forge@npm:1.3.1, node-forge@npm:^1.0.0":
version: 1.3.1
resolution: "node-forge@npm:1.3.1"
@@ -29879,6 +29885,13 @@ __metadata:
languageName: node
linkType: hard
"pathe@npm:^1.1.1":
version: 1.1.1
resolution: "pathe@npm:1.1.1"
checksum: 34ab3da2e5aa832ebc6a330ffe3f73d7ba8aec6e899b53b8ec4f4018de08e40742802deb12cf5add9c73b7bf719b62c0778246bd376ca62b0fb23e0dde44b759
languageName: node
linkType: hard
"pathval@npm:^1.1.1":
version: 1.1.1
resolution: "pathval@npm:1.1.1"
@@ -30708,7 +30721,7 @@ __metadata:
languageName: node
linkType: hard
"pretty-format@npm:^27.0.2, pretty-format@npm:^27.5.1":
"pretty-format@npm:^27.0.2":
version: 27.5.1
resolution: "pretty-format@npm:27.5.1"
dependencies:
@@ -34498,10 +34511,10 @@ __metadata:
languageName: node
linkType: hard
"std-env@npm:^3.3.2":
version: 3.3.3
resolution: "std-env@npm:3.3.3"
checksum: 6665f6d8bd63aae432d3eb9abbd7322847ad0d902603e6dce1e8051b4f42ceeb4f7f96a4faf70bb05ce65ceee2dc982502b701575c8a58b1bfad29f3dbb19f81
"std-env@npm:^3.3.3":
version: 3.4.3
resolution: "std-env@npm:3.4.3"
checksum: bef186fb2baddda31911234b1e58fa18f181eb6930616aaec3b54f6d5db65f2da5daaa5f3b326b98445a7d50ca81d6fe8809ab4ebab85ecbe4a802f1b40921bf
languageName: node
linkType: hard
@@ -35801,13 +35814,6 @@ __metadata:
languageName: node
linkType: hard
"time-zone@npm:^1.0.0":
version: 1.0.0
resolution: "time-zone@npm:1.0.0"
checksum: e46f5a69b8c236dcd8e91e29d40d4e7a3495ed4f59888c3f84ce1d9678e20461421a6ba41233509d47dd94bc18f1a4377764838b21b584663f942b3426dcbce8
languageName: node
linkType: hard
"timed-out@npm:^4.0.0, timed-out@npm:^4.0.1":
version: 4.0.1
resolution: "timed-out@npm:4.0.1"
@@ -35893,17 +35899,17 @@ __metadata:
languageName: node
linkType: hard
"tinypool@npm:^0.5.0":
version: 0.5.0
resolution: "tinypool@npm:0.5.0"
checksum: 4e0dfd8f28666d541c1d92304222edc4613f05d74fe2243c8520d466a2cc6596011a7072c1c41a7de7522351b82fda07e8038198e8f43673d8d69401c5903f8c
"tinypool@npm:^0.7.0":
version: 0.7.0
resolution: "tinypool@npm:0.7.0"
checksum: fdcccd5c750574fce51f8801a877f8284e145d12b79cd5f2d72bfbddfe20c895e915555bc848e122bb6aa968098e7ac4fe1e8e88104904d518dc01cccd18a510
languageName: node
linkType: hard
"tinyspy@npm:^2.1.0":
version: 2.1.0
resolution: "tinyspy@npm:2.1.0"
checksum: cb83c1f74a79dd5934018bad94f60a304a29d98a2d909ea45fc367f7b80b21b0a7d8135a2ce588deb2b3ba56c7c607258b2a03e6001d89e4d564f9a95cc6a81f
"tinyspy@npm:^2.1.1":
version: 2.1.1
resolution: "tinyspy@npm:2.1.1"
checksum: cfe669803a7f11ca912742b84c18dcc4ceecaa7661c69bc5eb608a8a802d541c48aba220df8929f6c8cd09892ad37cb5ba5958ddbbb57940e91d04681d3cee73
languageName: node
linkType: hard
@@ -36850,6 +36856,13 @@ __metadata:
languageName: node
linkType: hard
"ufo@npm:^1.3.0":
version: 1.3.0
resolution: "ufo@npm:1.3.0"
checksum: 01f0be86cd5c205ad1b49ebea985e000a4542c503ee75398302b0f5e4b9a6d9cd8e77af2dc614ab7bea08805fdfd9a85191fb3b5ee3df383cb936cf65e9db30d
languageName: node
linkType: hard
"uglify-js@npm:^3.1.4":
version: 3.15.3
resolution: "uglify-js@npm:3.15.3"
@@ -37830,19 +37843,19 @@ __metadata:
languageName: node
linkType: hard
"vite-node@npm:0.31.1":
version: 0.31.1
resolution: "vite-node@npm:0.31.1"
"vite-node@npm:0.34.3":
version: 0.34.3
resolution: "vite-node@npm:0.34.3"
dependencies:
cac: ^6.7.14
debug: ^4.3.4
mlly: ^1.2.0
pathe: ^1.1.0
mlly: ^1.4.0
pathe: ^1.1.1
picocolors: ^1.0.0
vite: ^3.0.0 || ^4.0.0
bin:
vite-node: vite-node.mjs
checksum: f70ffa3f6dcb4937cdc99f59bf360d42de83c556ba9a19eb1c3504ef20db4c1d1afa644d9a8e63240e851c0c95773b64c526bdb3eb4794b5e941ddcd57124aa9
checksum: 366c4f3fb7c038e2180abc6b18cfbac3b8684cd878eaf7ebf1ffb07d95d2ea325713fc575a7949a13bb00cfe264acbc28c02e2836b8647e1f443fe631c17805a
languageName: node
linkType: hard
@@ -37920,6 +37933,17 @@ __metadata:
languageName: node
linkType: hard
"vitest-fetch-mock@npm:^0.2.2":
version: 0.2.2
resolution: "vitest-fetch-mock@npm:0.2.2"
dependencies:
cross-fetch: ^3.0.6
peerDependencies:
vitest: ">=0.16.0"
checksum: fa160f301171cd45dbf7d782880b6b6063fc74b9dd1965ef9206545e812ca8696e6be76662afbac822c6bf850fbb66cf8fb066af646e0e159f5a87ab25c97a02
languageName: node
linkType: hard
"vitest-mock-extended@npm:^1.1.3":
version: 1.1.3
resolution: "vitest-mock-extended@npm:1.1.3"
@@ -37932,34 +37956,33 @@ __metadata:
languageName: node
linkType: hard
"vitest@npm:^0.31.1":
version: 0.31.1
resolution: "vitest@npm:0.31.1"
"vitest@npm:^0.34.3":
version: 0.34.3
resolution: "vitest@npm:0.34.3"
dependencies:
"@types/chai": ^4.3.5
"@types/chai-subset": ^1.3.3
"@types/node": "*"
"@vitest/expect": 0.31.1
"@vitest/runner": 0.31.1
"@vitest/snapshot": 0.31.1
"@vitest/spy": 0.31.1
"@vitest/utils": 0.31.1
acorn: ^8.8.2
"@vitest/expect": 0.34.3
"@vitest/runner": 0.34.3
"@vitest/snapshot": 0.34.3
"@vitest/spy": 0.34.3
"@vitest/utils": 0.34.3
acorn: ^8.9.0
acorn-walk: ^8.2.0
cac: ^6.7.14
chai: ^4.3.7
concordance: ^5.0.4
debug: ^4.3.4
local-pkg: ^0.4.3
magic-string: ^0.30.0
pathe: ^1.1.0
magic-string: ^0.30.1
pathe: ^1.1.1
picocolors: ^1.0.0
std-env: ^3.3.2
std-env: ^3.3.3
strip-literal: ^1.0.1
tinybench: ^2.5.0
tinypool: ^0.5.0
tinypool: ^0.7.0
vite: ^3.0.0 || ^4.0.0
vite-node: 0.31.1
vite-node: 0.34.3
why-is-node-running: ^2.2.2
peerDependencies:
"@edge-runtime/vm": "*"
@@ -37989,7 +38012,7 @@ __metadata:
optional: true
bin:
vitest: vitest.mjs
checksum: b3f64a36102edc5b8594c085da648c838c0d275c620bd3b780624f936903b9c06579d6ef137fe9859e468f16deb8f154a50f009093119f9adb8b60ff1b7597ee
checksum: 4535d080feede94db5015eb60c6ed5f7b0d8cd67f12072de5ae1faded133cc640043c0c2646ef51ab9b61c2f885589da57458a65e82cf91a25cf954470018a40
languageName: node
linkType: hard
@@ -38693,13 +38716,6 @@ __metadata:
languageName: node
linkType: hard
"well-known-symbols@npm:^2.0.0":
version: 2.0.0
resolution: "well-known-symbols@npm:2.0.0"
checksum: 4f54bbc3012371cb4d228f436891b8e7536d34ac61a57541890257e96788608e096231e0121ac24d08ef2f908b3eb2dc0adba35023eaeb2a7df655da91415402
languageName: node
linkType: hard
"whatwg-encoding@npm:^2.0.0":
version: 2.0.0
resolution: "whatwg-encoding@npm:2.0.0"
@@ -39535,4 +39551,4 @@ __metadata:
resolution: "zwitch@npm:2.0.4"
checksum: f22ec5fc2d5f02c423c93d35cdfa83573a3a3bd98c66b927c368ea4d0e7252a500df2a90a6b45522be536a96a73404393c958e945fdba95e6832c200791702b6
languageName: node
linkType: hard
linkType: hard