Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75d5576e17 | ||
|
|
0a6dde4fcc | ||
|
|
73b5839284 | ||
|
|
1a42b94b13 | ||
|
|
acb910f680 | ||
|
|
38e857f9a9 | ||
|
|
b8149dee36 | ||
|
|
f583ef40f2 | ||
|
|
71574488c5 | ||
|
|
008e0e43a2 | ||
|
|
da22eda662 | ||
|
|
f94d696885 | ||
|
|
9e84eb1b2c | ||
|
|
4c549811a1 | ||
|
|
05502f0cfb | ||
|
|
e436b8e06b | ||
|
|
8146cf8c93 | ||
|
|
c05f595ef0 | ||
|
|
0a795f5363 | ||
|
|
5b50a469b2 | ||
|
|
c08365f09c | ||
|
|
477e102388 | ||
|
|
e1b10b4ec5 | ||
|
|
ff6db3c572 | ||
|
|
4fa3fdef97 | ||
|
|
f29f6632e8 | ||
|
|
36747988e6 | ||
|
|
3dd312b88b | ||
|
|
3c9e9329b0 | ||
|
|
8617f43a8e | ||
|
|
5f0eb652b0 | ||
|
|
26d7301885 | ||
|
|
ffc4d22ff1 | ||
|
|
ee286a600f | ||
|
|
72c493ee21 | ||
|
|
5bd0428d18 | ||
|
|
c7089b9db6 | ||
|
|
81ad00a816 | ||
|
|
b5730da56d | ||
|
|
9455d14ad5 | ||
|
|
a056160404 |
@@ -24,6 +24,7 @@
|
||||
"@types/node": "16.9.1",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import "highlight.js/styles/default.css";
|
||||
import { type AppProps } from "next/app";
|
||||
import "swagger-ui-react/swagger-ui.css";
|
||||
|
||||
import "../styles/globals.css";
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return <Component {...pageProps} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import type { SwaggerUI } from "swagger-ui-react";
|
||||
|
||||
import { SnippedGenerator, requestSnippets } from "@lib/snippets";
|
||||
|
||||
const SwaggerUIDynamic: SwaggerUI & { url: string } = dynamic(() => import("swagger-ui-react"), {
|
||||
const SwaggerUIDynamic = dynamic(() => import("swagger-ui-react"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
@@ -17,8 +16,6 @@ export default function APIDocs() {
|
||||
requestSnippets={requestSnippets}
|
||||
plugins={[SnippedGenerator]}
|
||||
tryItOutEnabled={true}
|
||||
syntaxHighlight={true}
|
||||
enableCORS={false} // Doesn't seem to work either
|
||||
docExpansion="list"
|
||||
filter={true}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
@@ -59,7 +60,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const selectedThemeIsDark =
|
||||
user?.theme === "dark" ||
|
||||
(!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark"));
|
||||
|
||||
formMethods.getValues().bookingFields.forEach(({ name }) => {
|
||||
bookingFields[name] = `${name} input`;
|
||||
});
|
||||
@@ -101,7 +101,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
};
|
||||
}
|
||||
return field;
|
||||
})
|
||||
}),
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
};
|
||||
|
||||
@@ -179,11 +180,20 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
{displayDestinationCalendarSelector && (
|
||||
<div className="w-full">
|
||||
<Switch
|
||||
label={t("display_add_to_calendar_organizer")}
|
||||
tooltip={t("reconnect_calendar_to_use")}
|
||||
label={
|
||||
<>
|
||||
{t("display_add_to_calendar_organizer")}
|
||||
<InfoIcon className="text-default hover:text-attention hover:bg-attention ms-1 inline h-4 w-4 rounded-md" />
|
||||
</>
|
||||
}
|
||||
checked={useEventTypeDestinationCalendarEmail}
|
||||
onCheckedChange={(val) => {
|
||||
setUseEventTypeDestinationCalendarEmail(val);
|
||||
formMethods.setValue("useEventTypeDestinationCalendarEmail", val);
|
||||
formMethods.setValue("useEventTypeDestinationCalendarEmail", val, { shouldDirty: true });
|
||||
if (val) {
|
||||
showToast(t("reconnect_calendar_to_use"), "warning");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -319,7 +329,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
description={t("private_link_description", { appName: APP_NAME })}
|
||||
checked={hashedLinkVisible}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("hashedLink", e ? hashedUrl : undefined);
|
||||
formMethods.setValue("hashedLink", e ? hashedUrl : undefined, { shouldDirty: true });
|
||||
setHashedLinkVisible(e);
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
@@ -383,10 +393,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
// Enabling seats will disable guests and requiring confirmation until fully supported
|
||||
if (e) {
|
||||
toggleGuests(false);
|
||||
formMethods.setValue("requiresConfirmation", false);
|
||||
formMethods.setValue("requiresConfirmation", false, { shouldDirty: true });
|
||||
setRequiresConfirmation(false);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined);
|
||||
formMethods.setValue("seatsPerTimeSlot", eventType.seatsPerTimeSlot ?? 2);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined, { shouldDirty: true });
|
||||
formMethods.setValue("seatsPerTimeSlot", eventType.seatsPerTimeSlot ?? 2, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
} else {
|
||||
formMethods.setValue("seatsPerTimeSlot", null);
|
||||
toggleGuests(true);
|
||||
@@ -506,7 +518,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
{showEventNameTip && (
|
||||
<CustomEventTypeModal
|
||||
close={closeEventNameTip}
|
||||
setValue={(val: string) => formMethods.setValue("eventName", val)}
|
||||
setValue={(val: string) => formMethods.setValue("eventName", val, { shouldDirty: true })}
|
||||
defaultValue={formMethods.getValues("eventName")}
|
||||
placeHolder={eventNamePlaceholder}
|
||||
event={eventNameObject}
|
||||
|
||||
@@ -31,10 +31,14 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
const allAppsData = formMethods.watch("metadata")?.apps || {};
|
||||
|
||||
const setAllAppsData = (_allAppsData: typeof allAppsData) => {
|
||||
formMethods.setValue("metadata", {
|
||||
...formMethods.getValues("metadata"),
|
||||
apps: _allAppsData,
|
||||
});
|
||||
formMethods.setValue(
|
||||
"metadata",
|
||||
{
|
||||
...formMethods.getValues("metadata"),
|
||||
apps: _allAppsData,
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
};
|
||||
|
||||
const getAppDataGetter = (appId: EventTypeAppsList): GetAppData => {
|
||||
|
||||
@@ -230,7 +230,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
|
||||
);
|
||||
|
||||
setValue("availability", value);
|
||||
setValue("availability", value, { shouldDirty: true });
|
||||
},
|
||||
[data]
|
||||
);
|
||||
@@ -238,7 +238,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!availabilityValue?.value) return;
|
||||
setValue("schedule", availabilityValue.value);
|
||||
setValue("schedule", availabilityValue.value, { shouldDirty: true });
|
||||
}, [availabilityValue, setValue]);
|
||||
|
||||
return (
|
||||
@@ -260,7 +260,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
isSearchable={false}
|
||||
onChange={(selected) => {
|
||||
field.onChange(selected?.value || null);
|
||||
if (selected?.value) setValue("availability", selected);
|
||||
if (selected?.value) setValue("availability", selected, { shouldDirty: true });
|
||||
}}
|
||||
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
||||
value={availabilityValue}
|
||||
@@ -298,7 +298,7 @@ const UseCommonScheduleSettingsToggle = ({ eventType }: { eventType: EventTypeSe
|
||||
onCheckedChange={(checked) => {
|
||||
onChange(!checked);
|
||||
if (!checked) {
|
||||
setValue("schedule", null);
|
||||
setValue("schedule", null, { shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
title={t("choose_common_schedule_team_event")}
|
||||
|
||||
@@ -61,7 +61,8 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
||||
minimumBookingNoticeDisplayValues.type,
|
||||
"minutes",
|
||||
minimumBookingNoticeDisplayValues.value
|
||||
)
|
||||
),
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}, [minimumBookingNoticeDisplayValues, setValue, passThroughProps.name]);
|
||||
|
||||
@@ -230,7 +231,9 @@ export const EventLimitsTab = () => {
|
||||
<Select
|
||||
isSearchable={false}
|
||||
onChange={(val) => {
|
||||
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
|
||||
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
defaultValue={
|
||||
slotIntervalOptions.find(
|
||||
@@ -258,11 +261,15 @@ export const EventLimitsTab = () => {
|
||||
checked={isChecked}
|
||||
onCheckedChange={(active) => {
|
||||
if (active) {
|
||||
formMethods.setValue("bookingLimits", {
|
||||
PER_DAY: 1,
|
||||
});
|
||||
formMethods.setValue(
|
||||
"bookingLimits",
|
||||
{
|
||||
PER_DAY: 1,
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
} else {
|
||||
formMethods.setValue("bookingLimits", {});
|
||||
formMethods.setValue("bookingLimits", {}, { shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
switchContainerClassName={classNames(
|
||||
@@ -279,7 +286,7 @@ export const EventLimitsTab = () => {
|
||||
/>
|
||||
<Controller
|
||||
name="onlyShowFirstAvailableSlot"
|
||||
render={({ field: { value } }) => {
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const isChecked = value;
|
||||
return (
|
||||
<SettingsToggle
|
||||
@@ -289,7 +296,7 @@ export const EventLimitsTab = () => {
|
||||
description={t("limit_booking_only_first_slot_description")}
|
||||
checked={isChecked}
|
||||
onCheckedChange={(active) => {
|
||||
formMethods.setValue("onlyShowFirstAvailableSlot", active ?? false);
|
||||
onChange(active ?? false);
|
||||
}}
|
||||
switchContainerClassName={classNames(
|
||||
"border-subtle mt-6 rounded-lg border py-6 px-4 sm:px-6",
|
||||
@@ -301,7 +308,7 @@ export const EventLimitsTab = () => {
|
||||
/>
|
||||
<Controller
|
||||
name="durationLimits"
|
||||
render={({ field: { value } }) => {
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const isChecked = Object.keys(value ?? {}).length > 0;
|
||||
return (
|
||||
<SettingsToggle
|
||||
@@ -317,11 +324,11 @@ export const EventLimitsTab = () => {
|
||||
checked={isChecked}
|
||||
onCheckedChange={(active) => {
|
||||
if (active) {
|
||||
formMethods.setValue("durationLimits", {
|
||||
onChange({
|
||||
PER_DAY: 60,
|
||||
});
|
||||
} else {
|
||||
formMethods.setValue("durationLimits", {});
|
||||
onChange({});
|
||||
}
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
@@ -338,7 +345,7 @@ export const EventLimitsTab = () => {
|
||||
/>
|
||||
<Controller
|
||||
name="periodType"
|
||||
render={({ field: { value } }) => {
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const isChecked = value && value !== "UNLIMITED";
|
||||
|
||||
return (
|
||||
@@ -353,11 +360,16 @@ export const EventLimitsTab = () => {
|
||||
title={t("limit_future_bookings")}
|
||||
description={t("limit_future_bookings_description")}
|
||||
checked={isChecked}
|
||||
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
|
||||
onCheckedChange={(bool) =>
|
||||
// formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED", { shouldDirty: true })
|
||||
onChange(bool ? "ROLLING" : "UNLIMITED")
|
||||
}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
<RadioGroup.Root
|
||||
value={watchPeriodType}
|
||||
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
|
||||
onValueChange={(val) =>
|
||||
formMethods.setValue("periodType", val as PeriodType, { shouldDirty: true })
|
||||
}>
|
||||
{PERIOD_TYPES.map((period) => {
|
||||
if (period.type === "UNLIMITED") return null;
|
||||
return (
|
||||
@@ -388,17 +400,21 @@ export const EventLimitsTab = () => {
|
||||
options={optionsPeriod}
|
||||
isSearchable={false}
|
||||
onChange={(opt) =>
|
||||
formMethods.setValue("periodCountCalendarDays", opt?.value === 1 ? "1" : "0")
|
||||
formMethods.setValue(
|
||||
"periodCountCalendarDays",
|
||||
opt?.value === 1 ? true : false,
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
name="periodCoundCalendarDays"
|
||||
value={optionsPeriod.find((opt) => {
|
||||
opt.value ===
|
||||
(formMethods.getValues("periodCountCalendarDays") === "1" ? 1 : 0);
|
||||
(formMethods.getValues("periodCountCalendarDays") === true ? 1 : 0);
|
||||
})}
|
||||
defaultValue={optionsPeriod.find(
|
||||
(opt) =>
|
||||
opt.value ===
|
||||
(formMethods.getValues("periodCountCalendarDays") === "1" ? 1 : 0)
|
||||
(formMethods.getValues("periodCountCalendarDays") === true ? 1 : 0)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -407,12 +423,12 @@ export const EventLimitsTab = () => {
|
||||
<div className="me-2 ms-2 inline-flex space-x-2 rtl:space-x-reverse">
|
||||
<Controller
|
||||
name="periodDates"
|
||||
render={() => (
|
||||
render={({ field: { onChange } }) => (
|
||||
<DateRangePicker
|
||||
startDate={formMethods.getValues("periodDates").startDate}
|
||||
endDate={formMethods.getValues("periodDates").endDate}
|
||||
onDatesChange={({ startDate, endDate }) => {
|
||||
formMethods.setValue("periodDates", {
|
||||
onChange({
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
@@ -446,7 +462,7 @@ export const EventLimitsTab = () => {
|
||||
onCheckedChange={(active) => {
|
||||
setOffsetToggle(active);
|
||||
if (!active) {
|
||||
formMethods.setValue("offsetStart", 0);
|
||||
formMethods.setValue("offsetStart", 0, { shouldDirty: true });
|
||||
}
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
@@ -577,11 +593,16 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
||||
);
|
||||
if (!rest || !currentKeys.length) return;
|
||||
//currentDurationLimits is always defined so can be casted
|
||||
// @ts-expect-error FIXME Fix these typings
|
||||
setValue(propertyName, {
|
||||
...watchIntervalLimits,
|
||||
[rest.value]: defaultLimit,
|
||||
});
|
||||
|
||||
setValue(
|
||||
propertyName,
|
||||
// @ts-expect-error FIXME Fix these typings
|
||||
{
|
||||
...watchIntervalLimits,
|
||||
[rest.value]: defaultLimit,
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -611,7 +632,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
||||
)}
|
||||
onLimitChange={(intervalLimitKey, val) =>
|
||||
// @ts-expect-error FIXME Fix these typings
|
||||
setValue(`${propertyName}.${intervalLimitKey}`, val)
|
||||
setValue(`${propertyName}.${intervalLimitKey}`, val, { shouldDirty: true })
|
||||
}
|
||||
onDelete={(intervalLimitKey) => {
|
||||
const current = currentIntervalLimits;
|
||||
|
||||
@@ -83,7 +83,7 @@ const DescriptionEditor = ({ isEditable }: { isEditable: boolean }) => {
|
||||
return mounted ? (
|
||||
<Editor
|
||||
getText={() => md.render(formMethods.getValues("description") || "")}
|
||||
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
||||
setText={(value: string) => formMethods.setValue("description", turndown(value), { shouldDirty: true })}
|
||||
excludedToolbarItems={["blockType"]}
|
||||
placeholder={t("quick_video_meeting")}
|
||||
editable={isEditable}
|
||||
@@ -512,16 +512,16 @@ export const EventSetupTab = (
|
||||
if (!newOptions.find((opt) => opt.value === defaultDuration?.value)) {
|
||||
if (newOptions.length > 0) {
|
||||
setDefaultDuration(newOptions[0]);
|
||||
formMethods.setValue("length", newOptions[0].value);
|
||||
formMethods.setValue("length", newOptions[0].value, { shouldDirty: true });
|
||||
} else {
|
||||
setDefaultDuration(null);
|
||||
}
|
||||
}
|
||||
if (newOptions.length === 1 && defaultDuration === null) {
|
||||
setDefaultDuration(newOptions[0]);
|
||||
formMethods.setValue("length", newOptions[0].value);
|
||||
formMethods.setValue("length", newOptions[0].value, { shouldDirty: true });
|
||||
}
|
||||
formMethods.setValue("metadata.multipleDuration", values);
|
||||
formMethods.setValue("metadata.multipleDuration", values, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -542,7 +542,7 @@ export const EventSetupTab = (
|
||||
setDefaultDuration(
|
||||
selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null
|
||||
);
|
||||
if (option) formMethods.setValue("length", option.value);
|
||||
if (option) formMethods.setValue("length", option.value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -571,12 +571,12 @@ export const EventSetupTab = (
|
||||
setMultipleDuration(undefined);
|
||||
setSelectedMultipleDuration([]);
|
||||
setDefaultDuration(null);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined);
|
||||
formMethods.setValue("length", eventType.length);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined, { shouldDirty: true });
|
||||
formMethods.setValue("length", eventType.length, { shouldDirty: true });
|
||||
} else {
|
||||
setMultipleDuration([]);
|
||||
formMethods.setValue("metadata.multipleDuration", []);
|
||||
formMethods.setValue("length", 0);
|
||||
formMethods.setValue("metadata.multipleDuration", [], { shouldDirty: true });
|
||||
formMethods.setValue("length", 0, { shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -127,7 +127,7 @@ const AssignAllTeamMembers = ({
|
||||
labelClassName="mt-0.5 font-normal"
|
||||
checked={assignAllTeamMembers}
|
||||
onCheckedChange={(active) => {
|
||||
setValue("assignAllTeamMembers", active);
|
||||
setValue("assignAllTeamMembers", active, { shouldDirty: true });
|
||||
setAssignAllTeamMembers(active);
|
||||
if (active) {
|
||||
onActive();
|
||||
@@ -250,7 +250,8 @@ const FixedHosts = ({
|
||||
isFixed: true,
|
||||
userId: parseInt(teamMember.value, 10),
|
||||
priority: 2,
|
||||
}))
|
||||
})),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -269,7 +270,7 @@ const FixedHosts = ({
|
||||
const rrHosts = getValues("hosts")
|
||||
.filter((host) => !host.isFixed)
|
||||
.sort((a, b) => (b.priority ?? 2) - (a.priority ?? 2));
|
||||
setValue("hosts", rrHosts);
|
||||
setValue("hosts", rrHosts, { shouldDirty: true });
|
||||
}
|
||||
setIsDisabled(checked);
|
||||
}}
|
||||
@@ -290,7 +291,8 @@ const FixedHosts = ({
|
||||
isFixed: true,
|
||||
userId: parseInt(teamMember.value, 10),
|
||||
priority: 2,
|
||||
}))
|
||||
})),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -332,7 +334,7 @@ const AddMembersWithSwitch = ({
|
||||
assignAllTeamMembers={assignAllTeamMembers}
|
||||
setAssignAllTeamMembers={setAssignAllTeamMembers}
|
||||
onActive={onActive}
|
||||
onInactive={() => setValue("hosts", [])}
|
||||
onInactive={() => setValue("hosts", [], { shouldDirty: true })}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -395,7 +397,8 @@ const RoundRobinHosts = ({
|
||||
userId: parseInt(teamMember.value, 10),
|
||||
priority: 2,
|
||||
}))
|
||||
.sort((a, b) => b.priority - a.priority)
|
||||
.sort((a, b) => b.priority - a.priority),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -420,7 +423,7 @@ const ChildrenEventTypes = ({
|
||||
<AssignAllTeamMembers
|
||||
assignAllTeamMembers={assignAllTeamMembers}
|
||||
setAssignAllTeamMembers={setAssignAllTeamMembers}
|
||||
onActive={() => setValue("children", childrenEventTypeOptions)}
|
||||
onActive={() => setValue("children", childrenEventTypeOptions, { shouldDirty: true })}
|
||||
/>
|
||||
{!assignAllTeamMembers ? (
|
||||
<Controller<FormValues>
|
||||
@@ -449,7 +452,7 @@ const Hosts = ({
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
control,
|
||||
resetField,
|
||||
setValue,
|
||||
getValues,
|
||||
formState: { submitCount },
|
||||
} = useFormContext<FormValues>();
|
||||
@@ -469,10 +472,12 @@ const Hosts = ({
|
||||
initialValue.current = { hosts: getValues("hosts"), schedulingType, submitCount };
|
||||
return;
|
||||
}
|
||||
resetField("hosts", {
|
||||
defaultValue: initialValue.current.schedulingType === schedulingType ? initialValue.current.hosts : [],
|
||||
});
|
||||
}, [schedulingType, resetField, getValues, submitCount]);
|
||||
setValue(
|
||||
"hosts",
|
||||
initialValue.current.schedulingType === schedulingType ? initialValue.current.hosts : [],
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}, [schedulingType, setValue, getValues, submitCount]);
|
||||
|
||||
return (
|
||||
<Controller<FormValues>
|
||||
@@ -586,7 +591,7 @@ export const EventTeamTab = ({
|
||||
className="w-full"
|
||||
onChange={(val) => {
|
||||
onChange(val?.value);
|
||||
setValue("assignAllTeamMembers", false);
|
||||
setValue("assignAllTeamMembers", false, { shouldDirty: true });
|
||||
setAssignAllTeamMembers(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||
import type { DialogProps } from "@calcom/ui";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
@@ -133,6 +134,60 @@ function getNavigation({
|
||||
];
|
||||
}
|
||||
|
||||
function DeleteDialog({
|
||||
isManagedEvent,
|
||||
eventTypeId,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: { isManagedEvent: string; eventTypeId: number } & Pick<DialogProps, "open" | "onOpenChange">) {
|
||||
const utils = trpc.useContext();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.viewer.eventTypes.invalidate();
|
||||
showToast(t("event_type_deleted_successfully"), "success");
|
||||
router.push("/event-types");
|
||||
onOpenChange?.(false);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
onOpenChange?.(false);
|
||||
} else if (err instanceof TRPCClientError) {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<ConfirmationDialogContent
|
||||
isPending={deleteMutation.isPending}
|
||||
variety="danger"
|
||||
title={t(`delete${isManagedEvent}_event_type`)}
|
||||
confirmBtnText={t(`confirm_delete_event_type`)}
|
||||
loadingText={t(`confirm_delete_event_type`)}
|
||||
onConfirm={(e) => {
|
||||
e.preventDefault();
|
||||
deleteMutation.mutate({ id: eventTypeId });
|
||||
}}>
|
||||
<p className="mt-5">
|
||||
<Trans
|
||||
i18nKey={`delete${isManagedEvent}_event_type_description`}
|
||||
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
|
||||
<ul>
|
||||
<li>Members assigned to this event type will also have their event types deleted.</li>
|
||||
<li>Anyone who they've shared their link with will no longer be able to book using it.</li>
|
||||
</ul>
|
||||
</Trans>
|
||||
</p>
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function EventTypeSingleLayout({
|
||||
children,
|
||||
eventType,
|
||||
@@ -149,9 +204,7 @@ function EventTypeSingleLayout({
|
||||
bookerUrl,
|
||||
activeWebhooksNumber,
|
||||
}: Props) {
|
||||
const utils = trpc.useContext();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
const hasPermsToDelete =
|
||||
@@ -160,24 +213,6 @@ function EventTypeSingleLayout({
|
||||
formMethods.getValues("schedulingType") === SchedulingType.MANAGED ||
|
||||
isUserOrganizationAdmin;
|
||||
|
||||
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.viewer.eventTypes.invalidate();
|
||||
showToast(t("event_type_deleted_successfully"), "success");
|
||||
router.push("/event-types");
|
||||
setDeleteDialogOpen(false);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
setDeleteDialogOpen(false);
|
||||
} else if (err instanceof TRPCClientError) {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||
formMethods.getValues(),
|
||||
t("locked_fields_admin_description"),
|
||||
@@ -308,7 +343,7 @@ function EventTypeSingleLayout({
|
||||
id="hiddenSwitch"
|
||||
checked={!formMethods.watch("hidden")}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("hidden", !e);
|
||||
formMethods.setValue("hidden", !e, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -424,7 +459,7 @@ function EventTypeSingleLayout({
|
||||
id="hiddenSwitch"
|
||||
checked={!formMethods.watch("hidden")}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("hidden", !e);
|
||||
formMethods.setValue("hidden", !e, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -435,6 +470,7 @@ function EventTypeSingleLayout({
|
||||
className="ml-4 lg:ml-0"
|
||||
type="submit"
|
||||
loading={isUpdateMutationLoading}
|
||||
disabled={!formMethods.formState.isDirty}
|
||||
data-testid="update-eventtype"
|
||||
form="event-type-form">
|
||||
{t("save")}
|
||||
@@ -467,31 +503,13 @@ function EventTypeSingleLayout({
|
||||
</div>
|
||||
</div>
|
||||
</Suspense>
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<ConfirmationDialogContent
|
||||
isPending={deleteMutation.isPending}
|
||||
variety="danger"
|
||||
title={t(`delete${isManagedEvent}_event_type`)}
|
||||
confirmBtnText={t(`confirm_delete_event_type`)}
|
||||
loadingText={t(`confirm_delete_event_type`)}
|
||||
onConfirm={(e) => {
|
||||
e.preventDefault();
|
||||
deleteMutation.mutate({ id: formMethods.getValues("id") });
|
||||
}}>
|
||||
<p className="mt-5">
|
||||
<Trans
|
||||
i18nKey={`delete${isManagedEvent}_event_type_description`}
|
||||
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
|
||||
<ul>
|
||||
<li>Members assigned to this event type will also have their event types deleted.</li>
|
||||
<li>
|
||||
Anyone who they've shared their link with will no longer be able to book using it.
|
||||
</li>
|
||||
</ul>
|
||||
</Trans>
|
||||
</p>
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
<DeleteDialog
|
||||
eventTypeId={eventType.id}
|
||||
isManagedEvent={isManagedEvent}
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
/>
|
||||
|
||||
<EventTypeEmbedDialog />
|
||||
</Shell>
|
||||
);
|
||||
|
||||
@@ -81,10 +81,10 @@ export default function InstantEventController({
|
||||
data-testid="instant-event-check"
|
||||
onCheckedChange={(e) => {
|
||||
if (!e) {
|
||||
formMethods.setValue("isInstantEvent", false);
|
||||
formMethods.setValue("isInstantEvent", false, { shouldDirty: true });
|
||||
setInstantEventState(false);
|
||||
} else {
|
||||
formMethods.setValue("isInstantEvent", true);
|
||||
formMethods.setValue("isInstantEvent", true, { shouldDirty: true });
|
||||
setInstantEventState(true);
|
||||
}
|
||||
}}>
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function RecurringEventController({
|
||||
data-testid="recurring-event-check"
|
||||
onCheckedChange={(e) => {
|
||||
if (!e) {
|
||||
formMethods.setValue("recurringEvent", null);
|
||||
formMethods.setValue("recurringEvent", null, { shouldDirty: true });
|
||||
setRecurringEventState(null);
|
||||
} else {
|
||||
const newVal = eventType.recurringEvent || {
|
||||
@@ -74,7 +74,7 @@ export default function RecurringEventController({
|
||||
count: 12,
|
||||
freq: Frequency.WEEKLY,
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
|
||||
setRecurringEventState(newVal);
|
||||
}
|
||||
}}>
|
||||
@@ -95,7 +95,7 @@ export default function RecurringEventController({
|
||||
...recurringEventState,
|
||||
interval: parseInt(event?.target.value),
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
|
||||
setRecurringEventState(newVal);
|
||||
}}
|
||||
/>
|
||||
@@ -110,7 +110,7 @@ export default function RecurringEventController({
|
||||
...recurringEventState,
|
||||
freq: parseInt(event?.value || `${Frequency.WEEKLY}`),
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
|
||||
setRecurringEventState(newVal);
|
||||
}}
|
||||
/>
|
||||
@@ -129,7 +129,7 @@ export default function RecurringEventController({
|
||||
...recurringEventState,
|
||||
count: parseInt(event?.target.value),
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
|
||||
setRecurringEventState(newVal);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function RequiresConfirmationController({
|
||||
|
||||
useEffect(() => {
|
||||
if (!requiresConfirmation) {
|
||||
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
|
||||
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined, { shouldDirty: true });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [requiresConfirmation]);
|
||||
@@ -82,7 +82,7 @@ export default function RequiresConfirmationController({
|
||||
checked={requiresConfirmation}
|
||||
LockedIcon={requiresConfirmationLockedProps.LockedIcon}
|
||||
onCheckedChange={(val) => {
|
||||
formMethods.setValue("requiresConfirmation", val);
|
||||
formMethods.setValue("requiresConfirmation", val, { shouldDirty: true });
|
||||
onRequiresConfirmation(val);
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
@@ -96,16 +96,19 @@ export default function RequiresConfirmationController({
|
||||
}
|
||||
onValueChange={(val) => {
|
||||
if (val === "always") {
|
||||
formMethods.setValue("requiresConfirmation", true);
|
||||
formMethods.setValue("requiresConfirmation", true, { shouldDirty: true });
|
||||
onRequiresConfirmation(true);
|
||||
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
|
||||
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
setRequiresConfirmationSetup(undefined);
|
||||
} else if (val === "notice") {
|
||||
formMethods.setValue("requiresConfirmation", true);
|
||||
formMethods.setValue("requiresConfirmation", true, { shouldDirty: true });
|
||||
onRequiresConfirmation(true);
|
||||
formMethods.setValue(
|
||||
"metadata.requiresConfirmationThreshold",
|
||||
requiresConfirmationSetup || defaultRequiresConfirmationSetup
|
||||
requiresConfirmationSetup || defaultRequiresConfirmationSetup,
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}
|
||||
}}>
|
||||
@@ -146,7 +149,8 @@ export default function RequiresConfirmationController({
|
||||
});
|
||||
formMethods.setValue(
|
||||
"metadata.requiresConfirmationThreshold.time",
|
||||
val
|
||||
val,
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}}
|
||||
className="border-default !m-0 block w-16 rounded-r-none border-r-0 text-sm [appearance:textfield] focus:z-10 focus:border-r"
|
||||
@@ -171,7 +175,8 @@ export default function RequiresConfirmationController({
|
||||
});
|
||||
formMethods.setValue(
|
||||
"metadata.requiresConfirmationThreshold.unit",
|
||||
opt?.value as UnitTypeLongPlural
|
||||
opt?.value as UnitTypeLongPlural,
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}}
|
||||
defaultValue={defaultValue}
|
||||
|
||||
@@ -425,7 +425,7 @@ export const EventTypeList = ({
|
||||
<MemoizedItem type={type} group={group} readOnly={readOnly} />
|
||||
<div className="mt-4 hidden sm:mt-0 sm:flex">
|
||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||
{type.team && !isManagedEventType && (
|
||||
{!!type.teamId && !isManagedEventType && (
|
||||
<UserAvatarGroup
|
||||
className="relative right-3"
|
||||
size="sm"
|
||||
|
||||
@@ -119,7 +119,7 @@ export type FormValues = {
|
||||
schedule: number | null;
|
||||
periodType: PeriodType;
|
||||
periodDays: number;
|
||||
periodCountCalendarDays: "1" | "0";
|
||||
periodCountCalendarDays: boolean;
|
||||
periodDates: { startDate: Date; endDate: Date };
|
||||
seatsPerTimeSlot: number | null;
|
||||
seatsShowAttendees: boolean | null;
|
||||
@@ -192,14 +192,17 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
|
||||
onSuccess: async () => {
|
||||
formMethods.setValue(
|
||||
"children",
|
||||
formMethods.getValues().children.map((child) => ({
|
||||
...child,
|
||||
created: true,
|
||||
}))
|
||||
);
|
||||
formMethods.setValue("assignAllTeamMembers", formMethods.getValues("assignAllTeamMembers") || false);
|
||||
const currentValues = formMethods.getValues();
|
||||
|
||||
currentValues.children = currentValues.children.map((child) => ({
|
||||
...child,
|
||||
created: true,
|
||||
}));
|
||||
currentValues.assignAllTeamMembers = currentValues.assignAllTeamMembers || false;
|
||||
|
||||
// Reset the form with these values as new default values to ensure the correct comparison for dirtyFields eval
|
||||
formMethods.reset(currentValues);
|
||||
|
||||
showToast(t("event_type_updated_successfully", { eventTypeTitle: eventType.title }), "success");
|
||||
},
|
||||
async onSettled() {
|
||||
@@ -253,7 +256,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
eventType.bookingFields.forEach(({ name }) => {
|
||||
bookingFields[name] = name;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const defaultValues: any = useMemo(() => {
|
||||
return {
|
||||
@@ -290,7 +292,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
offsetStart: eventType.offsetStart,
|
||||
bookingFields: eventType.bookingFields,
|
||||
periodType: eventType.periodType,
|
||||
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
|
||||
periodCountCalendarDays: eventType.periodCountCalendarDays ? true : false,
|
||||
schedulingType: eventType.schedulingType,
|
||||
requiresConfirmation: eventType.requiresConfirmation,
|
||||
slotInterval: eventType.slotInterval,
|
||||
@@ -316,7 +318,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
assignAllTeamMembers: eventType.assignAllTeamMembers,
|
||||
};
|
||||
}, [eventType, periodDates, metadata]);
|
||||
|
||||
const formMethods = useForm<FormValues>({
|
||||
defaultValues,
|
||||
resolver: zodResolver(
|
||||
@@ -405,14 +406,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
.passthrough()
|
||||
),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!formMethods.formState.isDirty) {
|
||||
//TODO: What's the best way to sync the form with backend
|
||||
formMethods.setValue("bookingFields", defaultValues.bookingFields);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultValues]);
|
||||
const {
|
||||
formState: { isDirty: isFormDirty, dirtyFields },
|
||||
} = formMethods;
|
||||
|
||||
const appsMetadata = formMethods.getValues("metadata")?.apps;
|
||||
const availability = formMethods.watch("availability");
|
||||
@@ -453,8 +449,100 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
),
|
||||
webhooks: <EventWebhooksTab eventType={eventType} />,
|
||||
} as const;
|
||||
const isObject = <T,>(value: T): boolean => {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
};
|
||||
|
||||
const isArray = <T,>(value: T): boolean => {
|
||||
return Array.isArray(value);
|
||||
};
|
||||
|
||||
const isFieldDirty = (fieldName: keyof FormValues) => {
|
||||
// If the field itself is directly marked as dirty
|
||||
if (dirtyFields[fieldName] === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the field is an object or an array
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const fieldValue: any = getNestedField(dirtyFields, fieldName);
|
||||
if (isObject(fieldValue)) {
|
||||
for (const key in fieldValue) {
|
||||
if (fieldValue[key] === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isObject(fieldValue[key]) || isArray(fieldValue[key])) {
|
||||
const nestedFieldName = `${fieldName}.${key}` as keyof FormValues;
|
||||
// Recursive call for nested objects or arrays
|
||||
if (isFieldDirty(nestedFieldName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isArray(fieldValue)) {
|
||||
for (const element of fieldValue) {
|
||||
// If element is an object, check each property of the object
|
||||
if (isObject(element)) {
|
||||
for (const key in element) {
|
||||
if (element[key] === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isObject(element[key]) || isArray(element[key])) {
|
||||
const nestedFieldName = `${fieldName}.${key}` as keyof FormValues;
|
||||
// Recursive call for nested objects or arrays within each element
|
||||
if (isFieldDirty(nestedFieldName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (element === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const getNestedField = (obj: typeof dirtyFields, path: string) => {
|
||||
const keys = path.split(".");
|
||||
let current = obj;
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
// @ts-expect-error /—— currentKey could be any deeply nested fields thanks to recursion
|
||||
const currentKey = current[keys[i]];
|
||||
if (currentKey === undefined) return undefined;
|
||||
current = currentKey;
|
||||
}
|
||||
|
||||
return current;
|
||||
};
|
||||
|
||||
const getDirtyFields = (values: FormValues): Partial<FormValues> => {
|
||||
if (!isFormDirty) {
|
||||
return {};
|
||||
}
|
||||
const updatedFields: Partial<FormValues> = {};
|
||||
Object.keys(dirtyFields).forEach((key) => {
|
||||
const typedKey = key as keyof typeof dirtyFields;
|
||||
updatedFields[typedKey] = undefined;
|
||||
const isDirty = isFieldDirty(typedKey);
|
||||
if (isDirty) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
updatedFields[typedKey] = values[typedKey];
|
||||
}
|
||||
});
|
||||
return updatedFields;
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: FormValues) => {
|
||||
const { children } = values;
|
||||
const dirtyValues = getDirtyFields(values);
|
||||
const dirtyFieldExists = Object.keys(dirtyValues).length !== 0;
|
||||
const {
|
||||
periodDates,
|
||||
periodCountCalendarDays,
|
||||
@@ -470,7 +558,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
locations,
|
||||
metadata,
|
||||
customInputs,
|
||||
children,
|
||||
assignAllTeamMembers,
|
||||
// We don't need to send send these values to the backend
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -483,8 +570,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
multipleDurationEnabled,
|
||||
length,
|
||||
...input
|
||||
} = values;
|
||||
|
||||
} = dirtyValues;
|
||||
if (!Number(length)) throw new Error(t("event_setup_length_error"));
|
||||
|
||||
if (bookingLimits) {
|
||||
@@ -504,8 +590,12 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
if (metadata?.multipleDuration.length < 1) {
|
||||
throw new Error(t("event_setup_multiple_duration_error"));
|
||||
} else {
|
||||
if (!length && !metadata?.multipleDuration?.includes(length)) {
|
||||
throw new Error(t("event_setup_multiple_duration_default_error"));
|
||||
// if length is unchanged, we skip this check
|
||||
if (length !== undefined) {
|
||||
if (!length && !metadata?.multipleDuration?.includes(length)) {
|
||||
//This would work but it leaves the potential of this check being useless. Need to check against length and not eventType.length, but length can be undefined
|
||||
throw new Error(t("event_setup_multiple_duration_default_error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -521,14 +611,14 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { availability, users, scheduleName, ...rest } = input;
|
||||
updateMutation.mutate({
|
||||
const payload = {
|
||||
...rest,
|
||||
length,
|
||||
locations,
|
||||
recurringEvent,
|
||||
periodStartDate: periodDates.startDate,
|
||||
periodEndDate: periodDates.endDate,
|
||||
periodCountCalendarDays: periodCountCalendarDays === "1",
|
||||
periodStartDate: periodDates?.startDate,
|
||||
periodEndDate: periodDates?.endDate,
|
||||
periodCountCalendarDays,
|
||||
id: eventType.id,
|
||||
beforeEventBuffer,
|
||||
afterEventBuffer,
|
||||
@@ -542,7 +632,19 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
customInputs,
|
||||
children,
|
||||
assignAllTeamMembers,
|
||||
});
|
||||
};
|
||||
// Filter out undefined values
|
||||
const filteredPayload = Object.entries(payload).reduce((acc, [key, value]) => {
|
||||
if (value !== undefined) {
|
||||
// @ts-expect-error Element implicitly has any type
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (dirtyFieldExists) {
|
||||
updateMutation.mutate({ ...filteredPayload, id: eventType.id });
|
||||
}
|
||||
};
|
||||
|
||||
const [slugExistsChildrenDialogOpen, setSlugExistsChildrenDialogOpen] = useState<ChildrenEventType[]>([]);
|
||||
@@ -595,6 +697,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
form={formMethods}
|
||||
id="event-type-form"
|
||||
handleSubmit={async (values) => {
|
||||
const { children } = values;
|
||||
const dirtyValues = getDirtyFields(values);
|
||||
const dirtyFieldExists = Object.keys(dirtyValues).length !== 0;
|
||||
const {
|
||||
periodDates,
|
||||
periodCountCalendarDays,
|
||||
@@ -617,9 +722,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
multipleDurationEnabled,
|
||||
length,
|
||||
...input
|
||||
} = values;
|
||||
} = dirtyValues;
|
||||
|
||||
if (!Number(length)) throw new Error(t("event_setup_length_error"));
|
||||
if (length && !Number(length)) throw new Error(t("event_setup_length_error"));
|
||||
|
||||
if (bookingLimits) {
|
||||
const isValid = validateIntervalLimitOrder(bookingLimits);
|
||||
@@ -638,8 +743,11 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
if (metadata?.multipleDuration.length < 1) {
|
||||
throw new Error(t("event_setup_multiple_duration_error"));
|
||||
} else {
|
||||
if (!length && !metadata?.multipleDuration?.includes(length)) {
|
||||
throw new Error(t("event_setup_multiple_duration_default_error"));
|
||||
if (length !== undefined) {
|
||||
if (!length && !metadata?.multipleDuration?.includes(length)) {
|
||||
//This would work but it leaves the potential of this check being useless. Need to check against length and not eventType.length, but length can be undefined
|
||||
throw new Error(t("event_setup_multiple_duration_default_error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -651,14 +759,15 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { availability, users, scheduleName, ...rest } = input;
|
||||
updateMutation.mutate({
|
||||
const payload = {
|
||||
...rest,
|
||||
children,
|
||||
length,
|
||||
locations,
|
||||
recurringEvent,
|
||||
periodStartDate: periodDates.startDate,
|
||||
periodEndDate: periodDates.endDate,
|
||||
periodCountCalendarDays: periodCountCalendarDays === "1",
|
||||
periodStartDate: periodDates?.startDate,
|
||||
periodEndDate: periodDates?.endDate,
|
||||
periodCountCalendarDays,
|
||||
id: eventType.id,
|
||||
beforeEventBuffer,
|
||||
afterEventBuffer,
|
||||
@@ -670,7 +779,19 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
seatsShowAvailabilityCount,
|
||||
metadata,
|
||||
customInputs,
|
||||
});
|
||||
};
|
||||
// Filter out undefined values
|
||||
const filteredPayload = Object.entries(payload).reduce((acc, [key, value]) => {
|
||||
if (value !== undefined) {
|
||||
// @ts-expect-error Element implicitly has any type
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (dirtyFieldExists) {
|
||||
updateMutation.mutate({ ...filteredPayload, id: eventType.id });
|
||||
}
|
||||
}}>
|
||||
<div ref={animationParentRef}>{tabMap[tabName]}</div>
|
||||
</Form>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { handleUserRedirection } from "@calcom/features/booking-redirect/handle-
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR } from "@calcom/lib/constants";
|
||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
@@ -206,7 +207,10 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
|
||||
const profile = {
|
||||
name: user.name || user.username || "",
|
||||
image: user.avatar,
|
||||
image: getUserAvatarUrl({
|
||||
...user,
|
||||
profile: user.profile,
|
||||
}),
|
||||
theme: user.theme,
|
||||
brandColor: user.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
|
||||
avatarUrl: user.avatarUrl,
|
||||
|
||||
@@ -155,7 +155,7 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
||||
<Link
|
||||
prefetch={false}
|
||||
href={{
|
||||
pathname: `/${user.username}/${type.slug}`,
|
||||
pathname: `/${user.profile.username}/${type.slug}`,
|
||||
query,
|
||||
}}
|
||||
passHref
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.7.19",
|
||||
"version": "3.8.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -42,8 +42,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
},
|
||||
data: {
|
||||
password: {
|
||||
update: {
|
||||
hash: hashedPassword,
|
||||
upsert: {
|
||||
create: { hash: hashedPassword },
|
||||
update: { hash: hashedPassword },
|
||||
},
|
||||
},
|
||||
emailVerified: new Date(),
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function Apps({ apps }: PageProps) {
|
||||
<div className="mb-16">
|
||||
<div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
{apps
|
||||
.sort((a, b) => (b.installCount || 0) - (a.installCount || 0))
|
||||
?.sort((a, b) => (b.installCount || 0) - (a.installCount || 0))
|
||||
.map((app) => {
|
||||
return <AppCard key={app.slug} app={app} />;
|
||||
})}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useEffect } from "react";
|
||||
|
||||
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getOrgAvatarUrl, getTeamAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
@@ -157,6 +157,11 @@ function TeamPage({
|
||||
</div>
|
||||
);
|
||||
|
||||
const profileImageSrc =
|
||||
isValidOrgDomain || team.metadata?.isOrganization
|
||||
? getOrgAvatarUrl({ slug: currentOrgDomain, logoUrl: team.logoUrl })
|
||||
: getTeamAvatarUrl({ slug: team.slug, logoUrl: team.logoUrl, organizationId: team.parent?.id });
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeadSeo
|
||||
@@ -166,22 +171,14 @@ function TeamPage({
|
||||
title: markdownStrippedBio,
|
||||
profile: {
|
||||
name: `${team.name}`,
|
||||
image: `${WEBAPP_URL}/${team.metadata?.isOrganization ? "org" : "team"}/${team.slug}/avatar.png`,
|
||||
image: profileImageSrc,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<main className="dark:bg-darkgray-50 bg-subtle mx-auto max-w-3xl rounded-md px-4 pb-12 pt-12">
|
||||
<div className="mx-auto mb-8 max-w-3xl text-center">
|
||||
<div className="relative">
|
||||
<Avatar
|
||||
alt={teamName}
|
||||
imageSrc={
|
||||
isValidOrgDomain
|
||||
? `/org/${currentOrgDomain}/avatar.png`
|
||||
: `${WEBAPP_URL}/${team.metadata?.isOrganization ? "org" : "team"}/${team.slug}/avatar.png`
|
||||
}
|
||||
size="lg"
|
||||
/>
|
||||
<Avatar alt={teamName} imageSrc={profileImageSrc} size="lg" />
|
||||
</div>
|
||||
<p className="font-cal text-emphasis mb-2 text-2xl tracking-wider" data-testid="team-name">
|
||||
{team.parent && `${team.parent.name} `}
|
||||
|
||||
@@ -143,8 +143,12 @@ test.describe("Update Profile", () => {
|
||||
const successLocator = await page.waitForSelector('[data-testId="toast-success"]');
|
||||
|
||||
expect(await successLocator.textContent()).toContain(email);
|
||||
|
||||
// After email verification is successfull. user is sent to /event-types
|
||||
await page.waitForURL("/event-types");
|
||||
|
||||
await page.goto("/settings/my-account/profile");
|
||||
const emailInputUpdated = page.getByTestId("profile-form-email");
|
||||
const emailInputUpdated = await page.getByTestId("profile-form-email");
|
||||
expect(await emailInputUpdated.inputValue()).toEqual(email);
|
||||
});
|
||||
test("Can update a users email (verification disabled)", async ({ page, users, prisma, features }) => {
|
||||
|
||||
@@ -13,10 +13,17 @@
|
||||
"reset_password_subject": "{{appName}}: Anleitung zum Zurücksetzen des Passworts",
|
||||
"verify_email_subject": "{{appName}}: Bestätigen Sie Ihr Konto",
|
||||
"check_your_email": "Schauen Sie in Ihrem E-Mail-Postfach nach",
|
||||
"old_email_address": "Alte E-Mail-Adresse",
|
||||
"new_email_address": "Neue E-Mail-Adresse",
|
||||
"verify_email_page_body": "Wir haben eine E-Mail an {{email}} gesendet. Es ist wichtig, Ihre E-Mail-Adresse zu bestätigen, um die Zustellbarkeit für E-Mails und Kalenderbenachrichtigungen von {{appName}} zu gewährleisten.",
|
||||
"verify_email_banner_body": "Bestätigen Sie Ihre E-Mail-Adresse, um die Zustellbarkeit von E-Mails und Kalenderbenachrichtigungen zu gewährleisten",
|
||||
"verify_email_email_header": "Bestätigen Sie Ihre E-Mail-Adresse",
|
||||
"verify_email_email_button": "E-Mail bestätigen",
|
||||
"verify_email_change_description": "Sie haben vor kurzem angefordert, die E-Mail-Adresse zu ändern, die Sie verwenden, um sich bei Ihrem {{appName}} Konto anzumelden. Bitte klicken Sie auf die Schaltfläche unten, um Ihre neue E-Mail-Adresse zu bestätigen.",
|
||||
"verify_email_change_success_toast": "Ihre E-Mail wurde auf {{email}} aktualisiert",
|
||||
"verify_email_change_failure_toast": "Fehler beim Aktualisieren der E-Mail.",
|
||||
"change_of_email": "Bestätigen Sie Ihre neue {{appName}} E-Mail-Adresse",
|
||||
"change_of_email_toast": "Wir haben einen Bestätigungslink an {{email}} gesendet. Wir werden Ihre E-Mail-Adresse aktualisieren, sobald Sie auf diesen Link klicken.",
|
||||
"copy_somewhere_safe": "Speichern Sie diesen API-Schlüssel an einem sicheren Ort. Sie können ihn nicht erneut ansehen.",
|
||||
"verify_email_email_body": "Bitte bestätigen Sie Ihre E-Mail-Adresse, indem Sie unten auf die Schaltfläche klicken.",
|
||||
"verify_email_by_code_email_body": "Bitte bestätigen Sie Ihre E-Mail-Adresse mit dem unten stehenden Code.",
|
||||
@@ -56,6 +63,17 @@
|
||||
"a_refund_failed": "Erstattung fehlgeschlagen",
|
||||
"awaiting_payment_subject": "Warte auf Zahlung: {{title}} am {{date}}",
|
||||
"meeting_awaiting_payment": "Zahlung für den Termin noch offen",
|
||||
"dark_theme_contrast_error": "Die Farbe des dunklen Designs hat die Kontrastprüfung nicht bestanden. Wir empfehlen Ihnen, diese Farbe zu ändern, damit Ihre Schaltflächen sichtbarer sind.",
|
||||
"light_theme_contrast_error": "Die Farbe des hellen Designs hat die Kontrastprüfung nicht bestanden. Wir empfehlen Ihnen, diese Farbe zu ändern, damit Ihre Schaltflächen sichtbarer sind.",
|
||||
"payment_not_created_error": "Zahlung konnte nicht erstellt werden",
|
||||
"couldnt_charge_card_error": "Karte konnte nicht für die Zahlung belastet werden",
|
||||
"no_available_users_found_error": "Keine verfügbaren Benutzer gefunden. Können Sie es mit einem anderen Zeitfenster versuchen?",
|
||||
"request_body_end_time_internal_error": "Interner Fehler. Request-Body enthält keinen Endzeitpunkt",
|
||||
"create_calendar_event_error": "Kalenderereignis konnte nicht im Kalender des Organisators erstellt werden",
|
||||
"update_calendar_event_error": "Kalenderereignis konnte nicht aktualisiert werden.",
|
||||
"delete_calendar_event_error": "Kalenderereignis konnte nicht gelöscht werden.",
|
||||
"already_signed_up_for_this_booking_error": "Sie sind bereits für diese Buchung registriert.",
|
||||
"hosts_unavailable_for_booking": "Einige der Veranstalter sind nicht für die Buchung verfügbar.",
|
||||
"help": "Hilfe",
|
||||
"price": "Preis",
|
||||
"paid": "Bezahlt",
|
||||
@@ -101,6 +119,7 @@
|
||||
"requested_to_reschedule_subject_attendee": "Aktion erforderlich - Neuplanung: Bitte buchen Sie eine neue Zeit für „{{eventType}}“ mit {{name}}",
|
||||
"hi_user_name": "Hallo, {{name}}",
|
||||
"ics_event_title": "{{eventType}} mit {{name}}",
|
||||
"please_book_a_time_sometime_later": "Ups, wir konnten Sie dieses Mal nicht verbinden. Bitte planen Sie stattdessen einen zukünftigen Anruf",
|
||||
"new_event_subject": "Neuer Termin: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"join_by_entrypoint": "{{entryPoint}} beitreten",
|
||||
"notes": "Notizen",
|
||||
@@ -117,15 +136,20 @@
|
||||
"meeting_id": "Termin-ID",
|
||||
"meeting_password": "Termin-Passwort",
|
||||
"meeting_url": "Termin-URL",
|
||||
"meeting_url_not_found": "Meeting-URL nicht gefunden",
|
||||
"token_not_found": "Token nicht gefunden",
|
||||
"some_other_host_already_accepted_the_meeting": "Ein anderer Veranstalter hat das Meeting bereits akzeptiert. Möchten Sie trotzdem beitreten? <1>Mit dem Meeting fortfahren</1>",
|
||||
"meeting_request_rejected": "Ihre Terminanfrage wurde abgelehnt",
|
||||
"rejected_event_type_with_organizer": "Abgelehnt: {{eventType}} mit {{organizer}} auf {{date}}",
|
||||
"hi": "Hi",
|
||||
"join_team": "Team beitreten",
|
||||
"manage_this_team": "Dieses Team verwalten",
|
||||
"team_info": "Informationen zum Team",
|
||||
"join_meeting": "Meeting beitreten",
|
||||
"request_another_invitation_email": "Falls Sie {{toEmail}} lieber nicht als Ihre {{appName}}-E-Mail verwenden möchten oder bereits über ein {{appName}}-Konto verfügen, fordern Sie bitte eine weitere Einladung zu dieser E-Mail an.",
|
||||
"you_have_been_invited": "Sie wurden eingeladen, dem Team {{teamName}} beizutreten",
|
||||
"user_invited_you": "{{user}} hat Sie eingeladen, dem {{entity}} {{team}} auf {{appName}} beizutreten",
|
||||
"user_invited_you_to_subteam": "{{user}} hat Sie eingeladen, dem Team {{team}} der Organisation {{parentTeamName}} auf {{appName}} beizutreten",
|
||||
"hidden_team_member_title": "Sie sind in diesem Team ausgeblendet",
|
||||
"hidden_team_member_message": "Ihre Teammitgliedschaft ist nicht bezahlt, entweder upgraden Sie Ihren Account auf Pro oder lassen den Team-Organisator wissen, dass er für Ihren Platz im Team bezahlen kann.",
|
||||
"hidden_team_owner_message": "Sie benötigen einen Pro-Account um die Teamfunktionen zu verweden. Sie werden dem Team nicht angezeigt, bis der Account upgegradet wurde.",
|
||||
@@ -229,6 +253,7 @@
|
||||
"reset_your_password": "Legen Sie Ihr neues Passwort mit den Anweisungen fest, die an Ihre E-Mail-Adresse gesendet wurden.",
|
||||
"email_change": "Melden Sie sich mit Ihrer neuen E-Mail-Adresse und Ihrem Passwort wieder an.",
|
||||
"create_your_account": "Erstellen Sie Ihr Konto",
|
||||
"create_your_calcom_account": "Erstellen Sie Ihr Cal.com-Konto",
|
||||
"sign_up": "Registrieren",
|
||||
"youve_been_logged_out": "Sie wurden abgemeldet",
|
||||
"hope_to_see_you_soon": "Wir hoffen, Sie bald wieder zu sehen!",
|
||||
@@ -266,6 +291,9 @@
|
||||
"nearly_there_instructions": "Zu guter Letzt: Ein Foto und eine Kurzbeschreibung zu Ihnen helfen, öfter gebucht zu werden, da Kunden dann genauer wissen, bei wem sie einen Termin buchen.",
|
||||
"set_availability_instructions": "Definieren Sie Zeiträume, wenn Sie wiederkehrend verfügbar sind. Sie können später weitere Zeiträume erstellen und verschiedenen Kalendern zuordnen.",
|
||||
"set_availability": "Verfügbarkeit festlegen",
|
||||
"set_availbility_description": "Legen Sie Zeitpläne für die Zeiten fest, in denen Sie gebucht werden möchten.",
|
||||
"share_a_link_or_embed": "Link oder Einbettung teilen",
|
||||
"share_a_link_or_embed_description": "Teilen Sie Ihren {{appName}} Link oder betten Sie ihn auf Ihrer Website ein.",
|
||||
"availability_settings": "Verfügbarkeitseinstellungen",
|
||||
"continue_without_calendar": "Ohne Kalender fortfahren",
|
||||
"continue_with": "Mit {{appName}} fortfahren",
|
||||
@@ -281,6 +309,7 @@
|
||||
"welcome_to_calcom": "Willkommen bei {{appName}}",
|
||||
"welcome_instructions": "Wie ist Ihr Name und in welcher Zeitzone sind Sie? Dies kann später bearbeitet werden.",
|
||||
"connect_caldav": "Mit CalDav-Server verbinden",
|
||||
"connect_ics_feed": "Mit einem ICS-Feed verbinden",
|
||||
"connect": "Verbinden",
|
||||
"try_for_free": "Kostenlos testen",
|
||||
"create_booking_link_with_calcom": "Erstellen Sie Ihren eigenen Buchungslink mit {{appName}}",
|
||||
@@ -419,6 +448,7 @@
|
||||
"browse_api_documentation": "Durchsuchen Sie unsere API Dokumentation",
|
||||
"leverage_our_api": "Nutzen Sie unsere API für die volle Kontrolle und Anpassbarkeit.",
|
||||
"create_webhook": "Webhook erstellen",
|
||||
"instant_meeting": "Sofortiges Meeting erstellt",
|
||||
"booking_cancelled": "Termin abgesagt",
|
||||
"booking_rescheduled": "Termin verlegt",
|
||||
"recording_ready": "Download-Link der Aufnahme ist nun verfügbar",
|
||||
@@ -533,6 +563,7 @@
|
||||
"enter_number_between_range": "Bitte geben Sie eine Zahl zwischen 1 und {{maxOccurences}} ein",
|
||||
"email_address": "E-Mail Adresse",
|
||||
"enter_valid_email": "Bitte geben Sie eine gültige Emaildresse ein",
|
||||
"please_schedule_future_call": " Bitte planen Sie einen Anruf in der Zukunft ein, falls wir nicht innerhalb von {{seconds}} Sekunden verfügbar sind",
|
||||
"location": "Ort",
|
||||
"address": "Adresse",
|
||||
"enter_address": "Geben Sie eine Adresse ein",
|
||||
@@ -592,6 +623,7 @@
|
||||
"new_member": "Neues Mitglied",
|
||||
"invite": "Einladen",
|
||||
"add_team_members": "Teammitglieder hinzufügen",
|
||||
"add_org_members": "Mitglieder hinzufügen",
|
||||
"add_team_members_description": "Lade andere ein, deinem Team beizutreten",
|
||||
"add_team_member": "Teammitglied hinzufügen",
|
||||
"invite_new_member": "Ein neues Mitglied einladen",
|
||||
@@ -606,8 +638,9 @@
|
||||
"hide_book_a_team_member_description": "Blendet die „Ein Teammitglied buchen“-Schaltflächen auf Ihren öffentlichen Seiten aus.",
|
||||
"danger_zone": "Achtung",
|
||||
"account_deletion_cannot_be_undone": "Vorsicht. Löschen eines Kontos kann nicht rückgängig gemacht werden.",
|
||||
"team_deletion_cannot_be_undone": "Seien Sie vorsichtig. Das Löschen eines Teams kann nicht rückgängig gemacht werden",
|
||||
"back": "Zurück",
|
||||
"cancel": "Absagen",
|
||||
"cancel": "Abbrechen",
|
||||
"cancel_all_remaining": "Alle verbleibenden absagen",
|
||||
"apply": "Anwenden",
|
||||
"cancel_event": "Termin absagen",
|
||||
@@ -656,6 +689,7 @@
|
||||
"default_duration": "Standardlaufzeit",
|
||||
"default_duration_no_options": "Bitte wählen Sie zuerst die verfügbaren Laufzeiten aus",
|
||||
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
|
||||
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
|
||||
"minutes": "Minuten",
|
||||
"round_robin": "Round Robin",
|
||||
"round_robin_description": "Treffen zwischen mehreren Teammitgliedern durchwechseln.",
|
||||
@@ -668,6 +702,7 @@
|
||||
"add_members": "Mitglieder hinzufügen...",
|
||||
"no_assigned_members": "Keine zugeordneten Mitglieder",
|
||||
"assigned_to": "Zugeordnet zu",
|
||||
"you_must_be_logged_in_to": "Sie müssen bei {{url}} angemeldet sein",
|
||||
"start_assigning_members_above": "Mit der Zuordnung von Mitgliedern oben beginnen",
|
||||
"locked_fields_admin_description": "Mitglieder werden dies nicht bearbeiten können",
|
||||
"locked_fields_member_description": "Diese Option wurde vom Team-Admin gesperrt",
|
||||
@@ -772,6 +807,7 @@
|
||||
"additional_input_description": "Erfordert die Eingabe zusätzlicher Eingaben durch den Planer vor der Terminbestätigung",
|
||||
"label": "Bezeichnung",
|
||||
"placeholder": "Platzhalter",
|
||||
"display_add_to_calendar_organizer": "„Zum Kalender hinzufügen“-E-Mail als Organisator anzeigen",
|
||||
"type": "Typ",
|
||||
"edit": "Bearbeiten",
|
||||
"add_input": "Eingabe hinzufügen",
|
||||
@@ -780,6 +816,9 @@
|
||||
"requires_confirmation_description": "Die Buchung muss manuell bestätigt werden, bevor die Buchungsinformationen an die Integrationen weitergeleitet werden und eine Bestätigungsmail gesendet wird.",
|
||||
"recurring_event": "Wiederkehrender Termin",
|
||||
"recurring_event_description": "Personen können sich für wiederkehrende Termine anmelden",
|
||||
"cannot_be_used_with_paid_event_types": "Es kann nicht mit bezahlten Ereignistypen verwendet werden",
|
||||
"warning_payment_instant_meeting_event": "Softige Meetings werden von wiederkehrenden Ereignissen und Zahlungs-Apps noch nicht unterstützt",
|
||||
"warning_instant_meeting_experimental": "Experimentell: „Sofortiges Meeting“-Ereignisse sind derzeit eine experimentelle Funktion.",
|
||||
"starting": "Beginnen",
|
||||
"disable_guests": "Gäste deaktivieren",
|
||||
"disable_guests_description": "Das Hinzufügen zusätzlicher Gäste deaktivieren.",
|
||||
@@ -788,6 +827,7 @@
|
||||
"private_link_label": "Privater Link",
|
||||
"private_link_hint": "Ihr privater Link wird nach jeder Nutzung neu generiert",
|
||||
"copy_private_link": "Privaten Link kopieren",
|
||||
"copy_private_link_to_event": "Privaten Link zum Ereignis kopieren",
|
||||
"private_link_description": "Erzeugen Sie einen privaten Link, den Sie teilen können, ohne Ihren {{appName}}-Benutzername preiszugeben",
|
||||
"invitees_can_schedule": "Teilnehmende können folgendes buchen",
|
||||
"date_range": "Datumszeitraum",
|
||||
@@ -847,6 +887,7 @@
|
||||
"next_step": "Schritt überspringen",
|
||||
"prev_step": "Vorheriger Schritt",
|
||||
"install": "Installieren",
|
||||
"start_paid_trial": "Kostenlose Testphase starten",
|
||||
"installed": "Installiert",
|
||||
"active_install_one": "{{count}} aktive Installation",
|
||||
"active_install_other": "{{count}} aktive Installationen",
|
||||
@@ -861,6 +902,7 @@
|
||||
"toggle_calendars_conflict": "Wechseln Sie die Kalender in denen nach Konflikten gesucht werden sollen, um Doppelbuchungen zu vermeiden.",
|
||||
"connect_additional_calendar": "Zusätzlichen Kalender verbinden",
|
||||
"calendar_updated_successfully": "Kalender erfolgreich aktualisiert",
|
||||
"check_here": "Hier auswählen",
|
||||
"conferencing": "Videokonferenzen",
|
||||
"calendar": "Kalender",
|
||||
"payments": "Zahlungen",
|
||||
@@ -1032,6 +1074,9 @@
|
||||
"user_impersonation_heading": "Benutzeridentitätswechsel",
|
||||
"user_impersonation_description": "Erlaubt es unserem Support-Team, sich vorübergehend mit Ihrem Konto anzumelden, um jegliche Probleme, die Sie gemeldet haben, schneller zu lösen.",
|
||||
"team_impersonation_description": "Ermöglicht den Team-Administratoren, sich vorübergehend als Sie einzuloggen.",
|
||||
"cal_signup_description": "Für Einzelpersonen kostenlos. Teampläne für kollaborative Funktionen.",
|
||||
"make_org_private": "Organisation privat stellen",
|
||||
"make_org_private_description": "Wenn diese Einstellung aktiv ist, können Ihre Organisationsmitglieder keine anderen Organisationsmitglieder sehen.",
|
||||
"make_team_private": "Team privat stellen",
|
||||
"make_team_private_description": "Wenn diese Einstellung aktiv ist, können deine Teammitglieder keine anderen Teammitglieder sehen.",
|
||||
"you_cannot_see_team_members": "Sie können nicht alle Teammitglieder eines privaten Teams sehen.",
|
||||
@@ -1091,6 +1136,7 @@
|
||||
"developer_documentation": "Entwickler-Dokumentation",
|
||||
"get_in_touch": "Kontakt aufnehmen",
|
||||
"contact_support": "Support kontaktieren",
|
||||
"premium_support": "Premium-Support",
|
||||
"community_support": "Community Support",
|
||||
"feedback": "Feedback",
|
||||
"submitted_feedback": "Vielen Dank für Ihr Feedback!",
|
||||
@@ -1254,6 +1300,7 @@
|
||||
"default_calendar_selected": "Standardkalender",
|
||||
"hide_from_profile": "Im Profil ausblenden",
|
||||
"event_setup_tab_title": "Termin-Einrichtung",
|
||||
"availability_not_found_in_schedule_error": "Keine Verfügbarkeit im Zeitplan gefunden",
|
||||
"event_limit_tab_title": "Limits",
|
||||
"event_limit_tab_description": "Wie oft können Sie gebucht werden",
|
||||
"event_advanced_tab_description": "Kalendereinstellungen & mehr...",
|
||||
@@ -1274,6 +1321,7 @@
|
||||
"do_this": "Mache dies",
|
||||
"turn_off": "Abschalten",
|
||||
"turn_on": "Einschalten",
|
||||
"cancelled_bookings_cannot_be_rescheduled": "Stornierte Buchungen können nicht verschoben werden",
|
||||
"settings_updated_successfully": "Einstellungen erfolgreich aktualisiert",
|
||||
"error_updating_settings": "Fehler beim Aktualisieren der Einstellungen",
|
||||
"personal_cal_url": "Meine persönliche {{appName}}-URL",
|
||||
@@ -1297,6 +1345,7 @@
|
||||
"customize_your_brand_colors": "Passen Sie Ihre eigene Markenfarbe auf Ihrer Buchungsseite an.",
|
||||
"pro": "Pro",
|
||||
"removes_cal_branding": "Entfernt alle {{appName}}-bezogenen Markenzeichen, z. B. „Powered by {{appName}}.“",
|
||||
"instant_meeting_with_title": "Sofortiges Meeting mit {{name}}",
|
||||
"profile_picture": "Profilbild",
|
||||
"upload": "Upload",
|
||||
"add_profile_photo": "Profilbild hinzufügen",
|
||||
@@ -1352,6 +1401,7 @@
|
||||
"event_name_info": "Name des Ereignistyps",
|
||||
"event_date_info": "Das Datum der Veranstaltung",
|
||||
"event_time_info": "Die Startzeit des Termins",
|
||||
"event_type_not_found": "Ereignistyp nicht gefunden",
|
||||
"location_variable": "Ort",
|
||||
"location_info": "Der Ort des Events",
|
||||
"additional_notes_variable": "Zusätzliche Notizen",
|
||||
@@ -1394,6 +1444,7 @@
|
||||
"slot_length": "Zeitfensterlänge",
|
||||
"booking_appearance": "Darstellung der Buchungsseite",
|
||||
"appearance_team_description": "Einstellungen für die Buchungsdarstellung Ihres Teams verwalten",
|
||||
"appearance_org_description": "Einstellungen für die Buchungsdarstellung Ihrer Organisation verwalten",
|
||||
"only_owner_change": "Nur der Besitzer dieses Teams kann Änderungen an den Buchungen des Teams vornehmen ",
|
||||
"team_disable_cal_branding_description": "Entfernt alle {{appName}}-bezogenen Markenzeichen, z. B. „Powered by {{appName}}“",
|
||||
"invited_by_team": "{{teamName}} hat Sie eingeladen, dem Team als {{role}} beizutreten",
|
||||
@@ -1447,6 +1498,7 @@
|
||||
"password_updated": "Passwort aktualisiert!",
|
||||
"pending_payment": "Zahlung ausstehend",
|
||||
"pending_invites": "Offene Einladungen",
|
||||
"pending_organization_invites": "Ausstehende Organisationseinladungen",
|
||||
"not_on_cal": "Nicht auf {{appName}}",
|
||||
"no_calendar_installed": "Kein Kalender installiert",
|
||||
"no_calendar_installed_description": "Sie haben noch keinen Ihrer Kalender verbunden",
|
||||
@@ -1458,6 +1510,8 @@
|
||||
"report_app": "App melden",
|
||||
"limit_booking_frequency": "Buchungsfrequenz begrenzen",
|
||||
"limit_booking_frequency_description": "Begrenzen, wie oft dieses Ereignis gebucht werden kann",
|
||||
"limit_booking_only_first_slot": "Buchun auf den ersten Platz beschränken",
|
||||
"limit_booking_only_first_slot_description": "Nur die Buchung des ersten Platzes am Tag erlauben",
|
||||
"limit_total_booking_duration": "Gesamtbuchungsdauer beschränken",
|
||||
"limit_total_booking_duration_description": "Begrenzt die Gesamtanzahl, die dieser Termin gebucht werden kann",
|
||||
"add_limit": "Begrenzung hinzufügen",
|
||||
@@ -1525,10 +1579,14 @@
|
||||
"your_org_disbanded_successfully": "Ihre Organization wurde erfolgreich aufgelöst",
|
||||
"error_creating_team": "Fehler beim Erstellen des Teams",
|
||||
"you": "Sie",
|
||||
"or_continue_with": "Oder fortfahren mit",
|
||||
"resend_email": "E-Mail erneut senden",
|
||||
"member_already_invited": "Mitglied wurde bereits eingeladen",
|
||||
"already_in_use_error": "Benutzername wird bereits verwendet",
|
||||
"enter_email_or_username": "E-Mail oder Benutzername eingeben",
|
||||
"enter_email": "E-Mail-Adresse eingeben",
|
||||
"enter_emails": "E-Mail-Adressen eingeben",
|
||||
"too_many_invites": "Sie sind darauf beschränkt, maximal {{nbUsers}} Benutzer gleichzeitig einzuladen.",
|
||||
"team_name_taken": "Dieser Name ist bereits vergeben",
|
||||
"must_enter_team_name": "Team-Name muss eingegeben werden",
|
||||
"team_url_required": "Team-URL muss eingegeben werden",
|
||||
@@ -1608,6 +1666,7 @@
|
||||
"individual": "Person",
|
||||
"all_bookings_filter_label": "Alle Buchungen",
|
||||
"all_users_filter_label": "Alle Benutzer",
|
||||
"all_event_types_filter_label": "Alle Ereignistypen",
|
||||
"your_bookings_filter_label": "Ihre Buchungen",
|
||||
"meeting_url_variable": "Meeting-URL",
|
||||
"meeting_url_info": "Die URL der Meeting-Konferenz",
|
||||
@@ -1704,9 +1763,11 @@
|
||||
"organizer_timezone": "Zeitzone des Veranstalters",
|
||||
"email_user_cta": "Einladung anzeigen",
|
||||
"email_no_user_invite_heading_team": "Sie wurden eingeladen, einem {{appName}}-Team beizutreten",
|
||||
"email_no_user_invite_heading_subteam": "Sie wurden eingeladen, einem Team der Organisation {{parentTeamName}} beizutreten",
|
||||
"email_no_user_invite_heading_org": "Sie wurden eingeladen, einer {{appName}}-Organisation 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_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_user_invite_subheading_subteam": "{{invitedBy}} hat Sie eingeladen, dem Team „{{teamName}}“ in seiner bzw. ihrer Organisation {{parentTeamName}} bei {{appName}} beizutreten. {{appName}} ist der Terminplaner, der es Ihnen und Ihrem Team ermöglicht, Meetings ohne Hin und Her zu planen.",
|
||||
"email_user_invite_subheading_org": "{{invitedBy}} hat Sie eingeladen, ihrer Organisation „{{teamName}}“ in {{appName}} beizutreten. {{appName}} ist der Terminplaner, der es Ihnen und Ihrer Organisation ermöglicht, Meetings ohne ständiges 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 Ihrer {{entity}} genießen.",
|
||||
"email_no_user_step_one": "Wählen Sie Ihren Benutzernamen",
|
||||
@@ -1839,7 +1900,9 @@
|
||||
"review_event_type": "Ereignistyp überprüfen",
|
||||
"looking_for_more_analytics": "Suchen Sie nach weiteren Analysedaten?",
|
||||
"looking_for_more_insights": "Möchten Sie mehr Insights?",
|
||||
"filters": "Filter",
|
||||
"add_filter": "Filter hinzufügen",
|
||||
"remove_filters": "Alle Filter löschen",
|
||||
"select_user": "Benutzer auswählen",
|
||||
"select_event_type": "Ereignistyp auswählen",
|
||||
"select_date_range": "Datumsbereich auswählen",
|
||||
@@ -1930,6 +1993,7 @@
|
||||
"connect_google_workspace": "Google Workspace verbinden",
|
||||
"google_workspace_admin_tooltip": "Sie müssen ein Workspace-Admin sein, um diese Funktion nutzen zu können",
|
||||
"first_event_type_webhook_description": "Erstellen Sie Ihren ersten Webhook für diesen Termintypen",
|
||||
"create_instant_meeting_webhook_description": "Erstellen Sie Ihren ersten Webhook mit „Sofortiges Meeting erstellt“ als Auslöser für diesen Ereignistyp",
|
||||
"install_app_on": "App installieren auf",
|
||||
"create_for": "Erstellen für",
|
||||
"currency": "Währung",
|
||||
@@ -1994,6 +2058,8 @@
|
||||
"add_times_to_your_email": "Zeiten zur E-Mail hinzufügen",
|
||||
"select_time": "Zeit auswählen",
|
||||
"select_date": "Datum auswählen",
|
||||
"connecting_you_to_someone": "Wir verbinden Sie mit jemandem.",
|
||||
"please_do_not_close_this_tab": "Diesen Tab bitte nicht schließen",
|
||||
"see_all_available_times": "Alle verfügbaren Zeiten ansehen",
|
||||
"org_team_names_example_1": "z.B. Marketing-Team",
|
||||
"org_team_names_example_2": "z.B. Vertriebsteam",
|
||||
@@ -2011,8 +2077,12 @@
|
||||
"description_requires_booker_email_verification": "Um sicherzustellen, dass die E-Mail des Buchenden vor der Terminplanung verifiziert ist",
|
||||
"requires_confirmation_mandatory": "Textnachrichten können nur an Teilnehmer gesendet werden, wenn der Ereignistyp bestätigt werden muss.",
|
||||
"organizations": "Organisationen",
|
||||
"upload_cal_video_logo": "Cal Video Logo hochladen",
|
||||
"update_cal_video_logo": "Cal Video Logo aktualisieren",
|
||||
"cal_video_logo_upload_instruction": "Um sicherzustellen, dass Ihr Logo vor dem dunklen Hintergrund von Cal Video gut sichtbar ist, laden Sie bitte ein helles Bild im PNG- oder SVG-Format hoch, um die Transparenz zu erhalten.",
|
||||
"org_admin_other_teams": "Weitere Teams",
|
||||
"org_admin_other_teams_description": "Hier können Sie Teams innerhalb Ihrer Organisation sehen, zu denen Sie nicht gehören. Sie können sich bei Bedarf selbst hinzufügen.",
|
||||
"not_part_of_org": "Sie gehören keiner Organisation an",
|
||||
"no_other_teams_found": "Keine weiteren Teams gefunden",
|
||||
"no_other_teams_found_description": "Es gibt keine weiteren Teams in dieser Organisation.",
|
||||
"attendee_first_name_variable": "Vorname des Teilnehmers",
|
||||
@@ -2048,7 +2118,9 @@
|
||||
"org_error_processing": "Beim Verarbeiten dieser Organisation ist ein Fehler aufgetreten",
|
||||
"orgs_page_description": "Eine Liste aller Organisationen. Wenn Sie eine Organisation akzeptieren, können sich alle Benutzer mit dieser E-Mail-Domain OHNE E-Mail-Überprüfung registrieren.",
|
||||
"unverified": "Nicht verifiziert",
|
||||
"verified": "Verifiziert",
|
||||
"dns_missing": "DNS fehlt",
|
||||
"dns_configured": "DNS konfiguriert",
|
||||
"mark_dns_configured": "Als DNS konfiguriert kennzeichnen",
|
||||
"value": "Wert",
|
||||
"your_organization_updated_sucessfully": "Ihre Organisation wurde erfolgreich aktualisiert",
|
||||
@@ -2058,8 +2130,11 @@
|
||||
"oAuth": "OAuth",
|
||||
"recently_added": "Kürzlich hinzugefügt",
|
||||
"connect_all_calendars": "Verbinden Sie alle Ihre Kalender",
|
||||
"connect_all_calendars_description": "{{appName}} liest die Verfügbarkeit aus allen Ihren vorhandenen Kalendern.",
|
||||
"workflow_automation": "Workflow-Automatisierung",
|
||||
"workflow_automation_description": "Personalisieren Sie Ihre Planungserfahrung mit Workflows",
|
||||
"scheduling_for_your_team": "Workflow-Automatisierung",
|
||||
"scheduling_for_your_team_description": "Planen Sie Ihr Team mit kollektiver Terminplanung und Round-Robin-Terminplanung",
|
||||
"no_members_found": "Keine Mitglieder gefunden",
|
||||
"event_setup_length_error": "Ereignis-Einrichtung: Die Dauer muss mindestens 1 Minute betragen.",
|
||||
"availability_schedules": "Verfügbarkeitspläne",
|
||||
@@ -2092,10 +2167,100 @@
|
||||
"overlay_my_calendar": "Meinen Kalender überlagern",
|
||||
"overlay_my_calendar_toc": "Durch das Verbinden mit Ihrem Kalender akzeptieren Sie unsere Datenschutzerklärung und Nutzungsbedingungen. Sie können den Zugriff jederzeit widerrufen.",
|
||||
"view_overlay_calendar_events": "Sehen Sie sich Ihre Kalendertermine an, um Buchungskonflikte zu vermeiden.",
|
||||
"join_event_location": "{{eventLocationType}} beitreten",
|
||||
"troubleshooting": "Problembehandlung",
|
||||
"calendars_were_checking_for_conflicts": "Kalender, die wir auf Konflikten überprüfen",
|
||||
"availabilty_schedules": "Verfügbarkeitspläne",
|
||||
"manage_calendars": "Kalender verwalten",
|
||||
"manage_availability_schedules": "Verfügbarkeitspläne verwalten",
|
||||
"lock_timezone_toggle_on_booking_page": "Zeitzone auf der Buchungsseite sperren",
|
||||
"description_lock_timezone_toggle_on_booking_page": "Um die Zeitzone auf der Buchungsseite zu sperren, nützlich für Termine in Person.",
|
||||
"event_setup_multiple_payment_apps_error": "Sie können nur eine Zahlungs-App pro Ereignistyp aktiv haben.",
|
||||
"number_in_international_format": "Bitte geben Sie die Nummer im internationalen Format ein.",
|
||||
"install_calendar": "Kalender installieren",
|
||||
"branded_subdomain": "Marken-Subdomain",
|
||||
"branded_subdomain_description": "Holen Sie sich Ihre eigene Marken-Subdomain, wie z.B. acme.cal.com",
|
||||
"org_insights": "Organisationsweite Insights",
|
||||
"org_insights_description": "Verstehen Sie, wie Ihre gesamte Organisation Zeit verbringt",
|
||||
"extensive_whitelabeling": "Dedizierte Onboarding- und Engineeringsupport",
|
||||
"extensive_whitelabeling_description": "Passen Sie Ihre Planungserfahrung mit Ihrem eigenen Logo, eigenen Farben und mehr an",
|
||||
"unlimited_teams": "Unbegrenzte Teams",
|
||||
"unlimited_teams_description": "Fügen Sie Ihrer Organisation so viele Unterteams hinzu, wie notwendig",
|
||||
"unified_billing": "Einheitliche Abrechnung",
|
||||
"unified_billing_description": "Fügen Sie eine einzige Kreditkarte hinzu, um alle Abos Ihres Teams zu bezahlen",
|
||||
"advanced_managed_events": "Erweiterte verwaltete Ereignistypen",
|
||||
"advanced_managed_events_description": "Fügen Sie eine einzige Kreditkarte hinzu, um alle Abonnements Ihres Teams zu bezahlen",
|
||||
"enterprise_description": "Rüsten Sie auf Enterprise auf, um Ihre Organisation zu erstellen",
|
||||
"create_your_org": "Erstellen Sie Ihre Organisation",
|
||||
"create_your_org_description": "Rüsten Sie auf Enterprise auf und erhalten Sie eine Subdomain, einheitliche Abrechnung, Insights, umfangreiches Whitelabeling und mehr",
|
||||
"other_payment_app_enabled": "Sie können nur eine Zahlungs-App pro Ereignistyp aktivieren",
|
||||
"admin_delete_organization_description": "<ul><li>Teams, die Mitglieder dieser Organisation sind, werden ebenfalls zusammen mit ihren Ereignistypen gelöscht</li><li>Benutzer, die Teil der Organisation waren, werden nicht gelöscht und ihre Ereignistypen bleiben ebenfalls intakt.</li><li>Benutzernamen würden geändert werden, um zu ermöglichen, dass sie außerhalb der Organisation existieren können</li></ul>",
|
||||
"admin_delete_organization_title": "{{organizationName}} löschen?",
|
||||
"published": "Veröffentlicht",
|
||||
"unpublished": "Unveröffentlicht",
|
||||
"publish": "Veröffentlichen",
|
||||
"org_publish_error": "Organisation konnte nicht veröffentlicht werden",
|
||||
"troubleshooter_tooltip": "Öffnen Sie die Problembehandlung und finden Sie heraus, was mit Ihrem Zeitplan nicht stimmt",
|
||||
"need_help": "Brauchst du Hilfe?",
|
||||
"troubleshooter": "Problembehandlung",
|
||||
"please_install_a_calendar": "Bitte installieren Sie einen Kalender",
|
||||
"instant_tab_title": "Sofortige Buchung",
|
||||
"instant_event_tab_description": "Personen sofort buchen lassen",
|
||||
"uprade_to_create_instant_bookings": "Rüsten Sie auf Enterprise auf und lassen Sie Ihre Gäste an einem Sofortanruf teilnehmen, an dem die Teilnehmer direkt teilnehmen können. Dies ist nur für Team-Ereignistypen verfügbar",
|
||||
"dont_want_to_wait": "Sie möchten nicht warten?",
|
||||
"meeting_started": "Meeting gestartet",
|
||||
"pay_and_book": "Zum Buchen bezahlen",
|
||||
"booking_not_found_error": "Buchung konnte nicht gefunden werden",
|
||||
"booking_seats_full_error": "Die Buchungsplätze sind voll",
|
||||
"missing_payment_credential_error": "Fehlende Zahlungsinformationen",
|
||||
"missing_payment_app_id_error": "Fehlende Zahlungsapp-ID",
|
||||
"not_enough_available_seats_error": "Die Buchung hat nicht genügend Plätze verfügbar",
|
||||
"user_redirect_title": "{{username}} ist derzeit kurzzeitig abwesend.",
|
||||
"user_redirect_description": "In der Zwischenzeit wird {{profile.username}} für alle neuen geplanten Meetings im Namen von {{username}} verantwortlich sein.",
|
||||
"out_of_office": "Abwesend",
|
||||
"out_of_office_description": "Konfigurieren Sie Aktionen in Ihrem Profil während Sie abwesend sind.",
|
||||
"send_request": "Anfrage senden",
|
||||
"start_date_and_end_date_required": "Startdatum und Enddatum sind erforderlich",
|
||||
"start_date_must_be_before_end_date": "Das Startdatum muss vor dem Enddatum liegen",
|
||||
"start_date_must_be_in_the_future": "Das Startdatum muss in der Zukunft liegen",
|
||||
"user_not_found": "Benutzer nicht gefunden",
|
||||
"out_of_office_entry_already_exists": "Abwesenheitseintrag existiert bereits",
|
||||
"out_of_office_id_required": "Abwesenheitseintrags-ID erforderlich",
|
||||
"booking_redirect_infinite_not_allowed": "Es besteht bereits eine Buchungsumleitung von diesem Benutzer zu Ihnen.",
|
||||
"success_entry_created": "Neuer Eintrag wurde erfolgreich erstellt",
|
||||
"booking_redirect_email_subject": "Buchungsumleitungsbenachrichtigung",
|
||||
"booking_redirect_email_title": "Buchungsumleitungsbenachrichtigung",
|
||||
"booking_redirect_email_description": "Sie haben eine Umleitung der Buchung von {{toName}} erhalten, so dass deren Profillinks für das Zeitfenster an Sie umgeleitet werden: ",
|
||||
"success_accept_booking_redirect": "Sie haben diese Buchungsumleitungsanfrage akzeptiert.",
|
||||
"success_reject_booking_redirect": "Sie haben diese Buchungsumleitungsanfrage abgelehnt.",
|
||||
"copy_link_booking_redirect_request": "Link zum Teilen der Anfrage kopieren",
|
||||
"booking_redirect_request_title": "Buchungsumleitungsanfrage",
|
||||
"select_team_member": "Teammitglied auswählen",
|
||||
"going_away_title": "Verreisen Sie? Markieren Sie einfach Ihren Profillink für eine gewisse Zeit als nicht verfügbar.",
|
||||
"redirect_team_enabled": "Leiten Sie Ihr Profil an ein anderes Teammitglied um",
|
||||
"redirect_team_disabled": "Leiten Sie Ihr Profil an ein anderes Teammitglied um (Teamplan erforderlich)",
|
||||
"out_of_office_unavailable_list": "Abwesenheits-Unverfügbarkeitsliste",
|
||||
"success_deleted_entry_out_of_office": "Eintrag erfolgreich gelöscht",
|
||||
"temporarily_out_of_office": "Temporär abwesend?",
|
||||
"add_a_redirect": "Umleitung hinzufügen",
|
||||
"create_entry": "Eintrag erstellen",
|
||||
"time_range": "Zeitspanne",
|
||||
"automatically_add_all_team_members": "Alle Teammitglieder hinzufügen, einschließlich zukünftiger Mitglieder",
|
||||
"redirect_to": "Umleiten zu",
|
||||
"having_trouble_finding_time": "Haben Sie Schwierigkeiten, ein Zeitfenster zu finden?",
|
||||
"show_more": "Mehr anzeigen",
|
||||
"assignment_description": "Planen Sie Meetings ein, wenn jeder verfügbar ist, oder rotieren Sie durch die Mitglieder Ihres Team",
|
||||
"lowest": "Niedrigste",
|
||||
"low": "Niedrig",
|
||||
"medium": "Mittel",
|
||||
"high": "Hoch",
|
||||
"Highest": "Höchste",
|
||||
"send_booker_to": "Bucher senden an",
|
||||
"set_priority": "Priorität festlegen",
|
||||
"priority_for_user": "Priorität von {{userName}}",
|
||||
"change_priority": "Priorität ändern",
|
||||
"field_identifiers_as_variables": "Verwenden Sie Feldbezeichner als Variablen für Ihre benutzerdefinierte Ereignisumleitung",
|
||||
"field_identifiers_as_variables_with_example": "Verwenden Sie Feldbezeichner als Variablen für Ihre benutzerdefinierte Ereignisumleitung (z. B. {{variable}})",
|
||||
"account_already_linked": "Konto ist bereits verknüpft",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -808,6 +808,7 @@
|
||||
"label": "Label",
|
||||
"placeholder": "Placeholder",
|
||||
"display_add_to_calendar_organizer":"Display 'Add to calendar' email as the organizer",
|
||||
"reconnect_calendar_to_use": "Please note that you may need to disconnect and then reconnect your 'Add to Calendar' account to use this feature.",
|
||||
"type": "Type",
|
||||
"edit": "Edit",
|
||||
"add_input": "Add an Input",
|
||||
|
||||
@@ -13,10 +13,17 @@
|
||||
"reset_password_subject": "{{appName}} : Instructions de réinitialisation de mot de passe",
|
||||
"verify_email_subject": "{{appName}} : Vérifiez votre compte",
|
||||
"check_your_email": "Vérifiez votre e-mail",
|
||||
"old_email_address": "Ancienne adresse e-mail",
|
||||
"new_email_address": "Nouvelle adresse e-mail",
|
||||
"verify_email_page_body": "Nous avons envoyé un e-mail à {{email}}. Il est important de vérifier votre adresse e-mail pour garantir la meilleure délivrabilité des e-mails et des calendriers de {{appName}}.",
|
||||
"verify_email_banner_body": "Vérifiez votre adresse e-mail pour garantir la meilleure délivrabilité des e-mails et des calendriers",
|
||||
"verify_email_email_header": "Vérifiez votre adresse e-mail",
|
||||
"verify_email_email_button": "Vérifier l'adresse e-mail",
|
||||
"verify_email_change_description": "Vous avez récemment demandé à changer l'adresse e-mail que vous utilisez pour vous connecter à votre compte {{appName}}. Veuillez cliquer sur le bouton ci-dessous pour confirmer votre nouvelle adresse e-mail.",
|
||||
"verify_email_change_success_toast": "Mise à jour de votre adresse e-mail. Nouvelle : {{email}}",
|
||||
"verify_email_change_failure_toast": "Échec de la mise à jour de l'adresse e-mail.",
|
||||
"change_of_email": "Faites vérifier votre nouvelle adresse e-mail {{appName}}",
|
||||
"change_of_email_toast": "Nous avons envoyé un lien de vérification à {{email}}. Nous mettrons à jour votre adresse e-mail une fois que vous aurez cliqué sur ce lien.",
|
||||
"copy_somewhere_safe": "Sauvegardez cette clé API dans un endroit sûr. Vous ne pourrez plus la revoir.",
|
||||
"verify_email_email_body": "Veuillez vérifier votre adresse e-mail en cliquant sur le bouton ci-dessous.",
|
||||
"verify_email_by_code_email_body": "Veuillez vérifier votre adresse e-mail en utilisant le code ci-dessous.",
|
||||
@@ -56,6 +63,17 @@
|
||||
"a_refund_failed": "Un remboursement a échoué",
|
||||
"awaiting_payment_subject": "En attente de paiement : {{title}} le {{date}}",
|
||||
"meeting_awaiting_payment": "Votre rendez-vous est en attente de paiement",
|
||||
"dark_theme_contrast_error": "La couleur du thème sombre ne passe pas la vérification du contraste. Nous vous recommandons de modifier cette couleur afin que vos boutons soient plus visibles.",
|
||||
"light_theme_contrast_error": "La couleur du thème clair ne passe pas la vérification du contraste. Nous vous recommandons de modifier cette couleur afin que vos boutons soient plus visibles.",
|
||||
"payment_not_created_error": "Le paiement n'a pas pu être créé",
|
||||
"couldnt_charge_card_error": "Impossible de débiter la carte pour le Paiement",
|
||||
"no_available_users_found_error": "Aucun utilisateur disponible n'a été trouvé. Pourriez-vous essayer un autre créneau horaire ?",
|
||||
"request_body_end_time_internal_error": "Erreur interne. Le corps de la demande ne contient pas l'heure de fin",
|
||||
"create_calendar_event_error": "Impossible de créer un événement de Calendrier dans le calendrier de l'Organisateur",
|
||||
"update_calendar_event_error": "Impossible de mettre à jour l'événement de Calendrier.",
|
||||
"delete_calendar_event_error": "Impossible de supprimer l'événement de Calendrier.",
|
||||
"already_signed_up_for_this_booking_error": "Vous êtes déjà inscrit(e) à cette réservation.",
|
||||
"hosts_unavailable_for_booking": "Certains hôtes sont indisponibles pour la réservation.",
|
||||
"help": "Aide",
|
||||
"price": "Prix",
|
||||
"paid": "Payé",
|
||||
@@ -101,6 +119,7 @@
|
||||
"requested_to_reschedule_subject_attendee": "Replanification requise : veuillez réserver un nouveau créneau pour {{eventType}} avec {{name}}",
|
||||
"hi_user_name": "Bonjour {{name}}",
|
||||
"ics_event_title": "{{eventType}} avec {{name}}",
|
||||
"please_book_a_time_sometime_later": "Oups, nous n'avons pas pu vous connecter cette fois-ci. Veuillez plutôt planifier un appel ultérieur",
|
||||
"new_event_subject": "Nouvel événement : {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"join_by_entrypoint": "Rejoindre par {{entryPoint}}",
|
||||
"notes": "Notes",
|
||||
@@ -117,15 +136,20 @@
|
||||
"meeting_id": "ID du rendez-vous",
|
||||
"meeting_password": "Mot de passe du rendez-vous",
|
||||
"meeting_url": "Lien du rendez-vous",
|
||||
"meeting_url_not_found": "L'URL du rendez-vous est introuvable",
|
||||
"token_not_found": "Jeton introuvable",
|
||||
"some_other_host_already_accepted_the_meeting": "D'autres hôtes ont déjà accepté le rendez-vous. Voulez-vous quand même participer ? <1>Aller quand même dans Rendez-vous</1>",
|
||||
"meeting_request_rejected": "Votre demande de rendez-vous a été refusée",
|
||||
"rejected_event_type_with_organizer": "Refusé : {{eventType}} avec {{organizer}} le {{date}}",
|
||||
"hi": "Bonjour",
|
||||
"join_team": "Rejoindre l'équipe",
|
||||
"manage_this_team": "Gérer cette équipe",
|
||||
"team_info": "Informations sur l'équipe",
|
||||
"join_meeting": "Rejoindre le rendez-vous",
|
||||
"request_another_invitation_email": "Si vous préférez ne pas utiliser {{toEmail}} comme votre adresse e-mail {{appName}} ou si vous avez déjà un compte {{appName}}, veuillez demander une autre invitation à cette adresse.",
|
||||
"you_have_been_invited": "Vous avez été invité à rejoindre l'équipe {{teamName}}",
|
||||
"user_invited_you": "{{user}} vous a invité à rejoindre l'{{entity}} {{team}} sur {{appName}}",
|
||||
"user_invited_you_to_subteam": "{{user}} vous a invité(e) à rejoindre l'équipe {{team}} de l'organisation {{parentTeamName}} sur {{appName}}",
|
||||
"hidden_team_member_title": "Vous êtes masqué dans cette équipe",
|
||||
"hidden_team_member_message": "Votre place n'est pas payée, passez au plan Pro ou faites savoir au propriétaire de l'équipe qu'il peut payer votre place.",
|
||||
"hidden_team_owner_message": "Vous avez besoin d'un compte Pro pour utiliser les équipes, vous êtes masqué jusqu'à ce que vous mettiez à niveau.",
|
||||
@@ -285,6 +309,7 @@
|
||||
"welcome_to_calcom": "Bienvenue sur {{appName}}",
|
||||
"welcome_instructions": "Dites-nous comment vous appeler et indiquez-nous dans quel fuseau horaire vous vous trouvez. Vous pourrez modifier cela plus tard. ",
|
||||
"connect_caldav": "Se connecter au serveur CalDAV (Bêta)",
|
||||
"connect_ics_feed": "Connexion à un flux ICS",
|
||||
"connect": "Connecter",
|
||||
"try_for_free": "Essayez gratuitement",
|
||||
"create_booking_link_with_calcom": "Créez votre propre lien de réservation avec {{appName}}",
|
||||
@@ -423,12 +448,14 @@
|
||||
"browse_api_documentation": "Parcourez la documentation de l'API",
|
||||
"leverage_our_api": "Profitez de notre API pour tout personnaliser et contrôler.",
|
||||
"create_webhook": "Créer un webhook",
|
||||
"instant_meeting": "Rendez-vous instantané créé",
|
||||
"booking_cancelled": "Réservation annulée",
|
||||
"booking_rescheduled": "Réservation replanifiée",
|
||||
"recording_ready": "Lien de téléchargement de l'enregistrement prêt",
|
||||
"booking_created": "Réservation créée",
|
||||
"booking_rejected": "Réservation rejetée",
|
||||
"booking_requested": "Réservation demandée",
|
||||
"booking_payment_initiated": "Paiement de la réservation initiée",
|
||||
"meeting_ended": "Rendez-vous terminé",
|
||||
"form_submitted": "Formulaire envoyé",
|
||||
"booking_paid": "Réservation payée",
|
||||
@@ -536,6 +563,7 @@
|
||||
"enter_number_between_range": "Veuillez entrer un nombre entre 1 et {{maxOccurences}}",
|
||||
"email_address": "Adresse e-mail",
|
||||
"enter_valid_email": "Veuillez saisir une adresse e-mail valide",
|
||||
"please_schedule_future_call": " Veuillez programmer un appel ultérieur si nous ne sommes pas disponibles dans {{seconds}} secondes",
|
||||
"location": "Lieu",
|
||||
"address": "Adresse",
|
||||
"enter_address": "Entrer une adresse",
|
||||
@@ -595,6 +623,7 @@
|
||||
"new_member": "Nouveau membre",
|
||||
"invite": "Inviter",
|
||||
"add_team_members": "Ajouter des membres d'équipe",
|
||||
"add_org_members": "Ajouter des membres",
|
||||
"add_team_members_description": "Invitez d'autres personnes à rejoindre votre équipe.",
|
||||
"add_team_member": "Ajouter un membre d'équipe",
|
||||
"invite_new_member": "Inviter un nouveau membre d'équipe",
|
||||
@@ -609,6 +638,7 @@
|
||||
"hide_book_a_team_member_description": "Masquez le bouton Réserver un membre d'équipe de vos pages publiques.",
|
||||
"danger_zone": "Zone de danger",
|
||||
"account_deletion_cannot_be_undone": "Attention, la suppression de compte est irréversible.",
|
||||
"team_deletion_cannot_be_undone": "Attention. La suppression de l'équipe est irréversible",
|
||||
"back": "Retour",
|
||||
"cancel": "Annuler",
|
||||
"cancel_all_remaining": "Annuler tous les événements restants",
|
||||
@@ -672,6 +702,7 @@
|
||||
"add_members": "Ajouter des membres...",
|
||||
"no_assigned_members": "Aucun membre assigné",
|
||||
"assigned_to": "Assigné à",
|
||||
"you_must_be_logged_in_to": "Vous devez être connecté(e) à {{url}}",
|
||||
"start_assigning_members_above": "Commencez à assigner des membres ci-dessus",
|
||||
"locked_fields_admin_description": "Les membres ne pourront pas modifier ceci.",
|
||||
"locked_fields_member_description": "Cette option a été verrouillée par l'administrateur de l'équipe.",
|
||||
@@ -776,6 +807,7 @@
|
||||
"additional_input_description": "Exiger que le planificateur saisisse des champs supplémentaires avant la confirmation de la réservation.",
|
||||
"label": "Libellé",
|
||||
"placeholder": "Texte de substitution",
|
||||
"display_add_to_calendar_organizer": "Afficher l'e-mail « Ajouter au calendrier » en tant qu'organisateur",
|
||||
"type": "Type",
|
||||
"edit": "Modifier",
|
||||
"add_input": "Ajouter un champ",
|
||||
@@ -784,6 +816,9 @@
|
||||
"requires_confirmation_description": "La réservation doit être confirmée manuellement avant d'être poussée vers les intégrations et qu'un e-mail de confirmation soit envoyé.",
|
||||
"recurring_event": "Événement récurrent",
|
||||
"recurring_event_description": "Les personnes peuvent s'inscrire à des événements récurrents.",
|
||||
"cannot_be_used_with_paid_event_types": "À ne pas utiliser avec les types d'événement payants",
|
||||
"warning_payment_instant_meeting_event": "Les Rendez-vous instantanés ne sont pas encore pris en charge par les événements récurrents et les applications de paiement",
|
||||
"warning_instant_meeting_experimental": "Expérimental : Les événements de Rendez-vous instantané sont actuellement expérimentaux.",
|
||||
"starting": "Début",
|
||||
"disable_guests": "Désactiver les invités",
|
||||
"disable_guests_description": "Désactivez l'ajout d'invités supplémentaires lors de la réservation.",
|
||||
@@ -792,6 +827,7 @@
|
||||
"private_link_label": "Lien privé",
|
||||
"private_link_hint": "Votre lien privé sera régénéré après chaque utilisation",
|
||||
"copy_private_link": "Copier le lien privé",
|
||||
"copy_private_link_to_event": "Copier le lien privé de l'événement",
|
||||
"private_link_description": "Générez un lien privé à partager sans révéler votre nom d'utilisateur {{appName}}.",
|
||||
"invitees_can_schedule": "Les invités peuvent réserver",
|
||||
"date_range": "Plage de dates",
|
||||
@@ -866,6 +902,7 @@
|
||||
"toggle_calendars_conflict": "Activez/désactivez les calendriers pour lesquels vous souhaitez vérifier les conflits afin d'éviter les doubles réservations.",
|
||||
"connect_additional_calendar": "Connecter un calendrier supplémentaire",
|
||||
"calendar_updated_successfully": "Calendrier mis à jour avec succès",
|
||||
"check_here": "Vérifier ici",
|
||||
"conferencing": "Visioconférence",
|
||||
"calendar": "Calendrier",
|
||||
"payments": "Paiements",
|
||||
@@ -1038,6 +1075,8 @@
|
||||
"user_impersonation_description": "Autorisez notre équipe d'assistance à se connecter temporairement à votre place pour nous aider à résoudre les problèmes que vous pourriez nous signaler.",
|
||||
"team_impersonation_description": "Autorisez les propriétaires/administrateurs de votre équipe à se connecter temporairement à votre place.",
|
||||
"cal_signup_description": "Gratuit pour les particuliers. Plans Équipes pour les fonctionnalités collaboratives.",
|
||||
"make_org_private": "Rendre l'organisation privée",
|
||||
"make_org_private_description": "Les membres de votre organisation ne pourront pas voir les autres membres de l'organisation lorsque cette option est activée.",
|
||||
"make_team_private": "Rendre l'équipe privée",
|
||||
"make_team_private_description": "Les membres de votre équipe ne pourront pas voir les autres membres de l'équipe si cette option est activée.",
|
||||
"you_cannot_see_team_members": "Vous ne pouvez pas voir tous les membres d'une équipe privée.",
|
||||
@@ -1222,6 +1261,7 @@
|
||||
"organizer_name_variable": "Nom de l'organisateur",
|
||||
"app_upgrade_description": "Pour pouvoir utiliser cette fonctionnalité, vous devez passer à un compte Pro.",
|
||||
"invalid_number": "Numéro de téléphone invalide",
|
||||
"invalid_url_error_message": "URL non valide pour {{label}}. Exemple d'URL : {{sampleUrl}}",
|
||||
"navigate": "Naviguer",
|
||||
"open": "Ouvrir",
|
||||
"close": "Fermer",
|
||||
@@ -1260,6 +1300,7 @@
|
||||
"default_calendar_selected": "Calendrier par défaut",
|
||||
"hide_from_profile": "Masquer du profil",
|
||||
"event_setup_tab_title": "Configuration",
|
||||
"availability_not_found_in_schedule_error": "Aucune disponibilité trouvée dans le planning",
|
||||
"event_limit_tab_title": "Limites",
|
||||
"event_limit_tab_description": "Fréquence de réservation",
|
||||
"event_advanced_tab_description": "Paramètres du calendrier, etc.",
|
||||
@@ -1280,6 +1321,7 @@
|
||||
"do_this": "Effectuer ceci",
|
||||
"turn_off": "Désactiver",
|
||||
"turn_on": "Activer",
|
||||
"cancelled_bookings_cannot_be_rescheduled": "Les réservations annulées ne peuvent pas être reprogrammées",
|
||||
"settings_updated_successfully": "Paramètres mis à jour avec succès",
|
||||
"error_updating_settings": "Erreur lors de la mise à jour des paramètres",
|
||||
"personal_cal_url": "Mon lien {{appName}} personnel",
|
||||
@@ -1296,12 +1338,14 @@
|
||||
"select_calendars": "Sélectionnez les calendriers dont vous souhaitez vérifier les conflits afin d'éviter les doubles réservations.",
|
||||
"check_for_conflicts": "Vérifier les conflits",
|
||||
"view_recordings": "Voir les enregistrements",
|
||||
"check_for_recordings": "Chercher des enregistrements",
|
||||
"adding_events_to": "Ajout d'événements",
|
||||
"follow_system_preferences": "Suivre les préférences système",
|
||||
"custom_brand_colors": "Couleurs de marque personnalisées",
|
||||
"customize_your_brand_colors": "Personnalisez la couleur de votre page de réservation à l'image de votre marque.",
|
||||
"pro": "Pro",
|
||||
"removes_cal_branding": "Supprimez toutes les références à la marque {{appName}}, par exemple « Propulsé par {{appName}} ».",
|
||||
"instant_meeting_with_title": "Rendez-vous instantané avec {{name}}",
|
||||
"profile_picture": "Photo de profil",
|
||||
"upload": "Télécharger",
|
||||
"add_profile_photo": "Ajouter une photo de profil",
|
||||
@@ -1357,6 +1401,7 @@
|
||||
"event_name_info": "Nom du type d'événement",
|
||||
"event_date_info": "Date de l'événement",
|
||||
"event_time_info": "Heure de début de l'événement",
|
||||
"event_type_not_found": "Type d'événement introuvable",
|
||||
"location_variable": "Lieu",
|
||||
"location_info": "Lieu de l'événement",
|
||||
"additional_notes_variable": "Notes supplémentaires",
|
||||
@@ -1399,6 +1444,7 @@
|
||||
"slot_length": "Durée du créneau",
|
||||
"booking_appearance": "Apparence",
|
||||
"appearance_team_description": "Gérez les paramètres d'apparence de vos réservations d'équipe.",
|
||||
"appearance_org_description": "Gérez les paramètres d'apparence de vos réservations d'organisation",
|
||||
"only_owner_change": "Seul le propriétaire de cette équipe peut apporter des modifications à la réservation de l'équipe ",
|
||||
"team_disable_cal_branding_description": "Supprimez toutes les références à la marque {{appName}}, par exemple « Propulsé par {{appName}} ».",
|
||||
"invited_by_team": "{{teamName}} vous a invité à rejoindre son équipe en tant que {{role}}",
|
||||
@@ -1452,6 +1498,7 @@
|
||||
"password_updated": "Mot de passe mis à jour !",
|
||||
"pending_payment": "Paiement en attente",
|
||||
"pending_invites": "Invitations en attente",
|
||||
"pending_organization_invites": "Invitations de l'organisation en attente",
|
||||
"not_on_cal": "Pas sur {{appName}}",
|
||||
"no_calendar_installed": "Aucun calendrier installé",
|
||||
"no_calendar_installed_description": "Vous n'avez pas encore connecté vos calendriers.",
|
||||
@@ -1463,6 +1510,8 @@
|
||||
"report_app": "Signaler l'application",
|
||||
"limit_booking_frequency": "Limiter la fréquence de réservation",
|
||||
"limit_booking_frequency_description": "Limitez le nombre de réservations possibles pour cet événement.",
|
||||
"limit_booking_only_first_slot": "Limiter la réservation à un seul créneau",
|
||||
"limit_booking_only_first_slot_description": "N'autoriser la réservation que du premier créneau de chaque jour",
|
||||
"limit_total_booking_duration": "Limiter la durée totale de réservation",
|
||||
"limit_total_booking_duration_description": "Limitez la durée totale pendant laquelle cet événement peut être réservé.",
|
||||
"add_limit": "Ajouter une limite",
|
||||
@@ -1535,6 +1584,9 @@
|
||||
"member_already_invited": "Le membre a déjà été invité",
|
||||
"already_in_use_error": "Nom d'utilisateur déjà utilisé",
|
||||
"enter_email_or_username": "Saisissez une adresse e-mail ou un nom d'utilisateur",
|
||||
"enter_email": "Saisissez une adresse e-mail",
|
||||
"enter_emails": "Saisissez des adresses e-mail",
|
||||
"too_many_invites": "Vous ne pouvez inviter qu'un maximum de {{nbUsers}} utilisateurs à la fois.",
|
||||
"team_name_taken": "Ce nom est déjà pris",
|
||||
"must_enter_team_name": "Vous devez saisir un nom d'équipe",
|
||||
"team_url_required": "Vous devez saisir un lien d'équipe",
|
||||
@@ -1614,6 +1666,7 @@
|
||||
"individual": "Particulier",
|
||||
"all_bookings_filter_label": "Toutes les réservations",
|
||||
"all_users_filter_label": "Tous les utilisateurs",
|
||||
"all_event_types_filter_label": "Tous les types d'événement",
|
||||
"your_bookings_filter_label": "Vos réservations",
|
||||
"meeting_url_variable": "Lien du rendez-vous",
|
||||
"meeting_url_info": "Lien de la visioconférence du rendez-vous",
|
||||
@@ -1628,6 +1681,7 @@
|
||||
"date_overrides_mark_all_day_unavailable_other": "Marquer comme indisponible aux dates sélectionnées",
|
||||
"date_overrides_add_btn": "Ajouter la superposition",
|
||||
"date_overrides_update_btn": "Mettre à jour la superposition",
|
||||
"date_successfully_added": "Ajout du remplacement de date réussi",
|
||||
"event_type_duplicate_copy_text": "{{slug}}-copie",
|
||||
"set_as_default": "Définir par défaut",
|
||||
"hide_eventtype_details": "Masquer les détails du type d'événement",
|
||||
@@ -1654,6 +1708,7 @@
|
||||
"minimum_round_robin_hosts_count": "Nombre d'hôtes requis pour participer",
|
||||
"hosts": "Hôtes",
|
||||
"upgrade_to_enable_feature": "Vous devez créer une équipe pour activer cette fonctionnalité. Cliquez pour créer une équipe.",
|
||||
"orgs_upgrade_to_enable_feature": "Vous devez passer sur notre formule entreprise pour activer cette fonctionnalité.",
|
||||
"new_attendee": "Nouveau participant",
|
||||
"awaiting_approval": "En attente de validation",
|
||||
"requires_google_calendar": "Cette application nécessite une connexion à Google Agenda",
|
||||
@@ -1708,9 +1763,11 @@
|
||||
"organizer_timezone": "Fuseau horaire de l'organisateur",
|
||||
"email_user_cta": "Voir l'invitation",
|
||||
"email_no_user_invite_heading_team": "Vous avez été invité à rejoindre une équipe {{appName}}",
|
||||
"email_no_user_invite_heading_subteam": "Vous avez été invité(e) à rejoindre une équipe de l'organisation {{parentTeamName}}",
|
||||
"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_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_subteam": "{{invitedBy}} vous a invité(e) à rejoindre son équipe {{teamName}} dans son organisation {{parentTeamName}} 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",
|
||||
@@ -1843,6 +1900,7 @@
|
||||
"review_event_type": "Examiner le type d'événement",
|
||||
"looking_for_more_analytics": "Vous cherchez plus d'analyses ?",
|
||||
"looking_for_more_insights": "Vous cherchez plus de statistiques ?",
|
||||
"filters": "Filtres",
|
||||
"add_filter": "Ajouter un filtre",
|
||||
"remove_filters": "Effacer les filtres",
|
||||
"select_user": "Sélectionner un utilisateur",
|
||||
@@ -1935,6 +1993,7 @@
|
||||
"connect_google_workspace": "Connecter Google Workspace",
|
||||
"google_workspace_admin_tooltip": "Vous devez être un administrateur d'espace de travail pour utiliser cette fonctionnalité",
|
||||
"first_event_type_webhook_description": "Créez votre premier webhook pour ce type d'événement",
|
||||
"create_instant_meeting_webhook_description": "Créez votre premier webhook avec « Rendez-vous instantané créé » comme déclencheur pour ce type d'événement",
|
||||
"install_app_on": "Installer l’application sur",
|
||||
"create_for": "Créer pour",
|
||||
"currency": "Devise",
|
||||
@@ -1999,6 +2058,8 @@
|
||||
"add_times_to_your_email": "Sélectionnez quelques créneaux disponibles et intégrez-les dans votre e-mail.",
|
||||
"select_time": "Sélectionner un créneau",
|
||||
"select_date": "Sélectionner une date",
|
||||
"connecting_you_to_someone": "Nous vous mettons en relation avec quelqu'un.",
|
||||
"please_do_not_close_this_tab": "Veuillez ne pas fermer cet onglet",
|
||||
"see_all_available_times": "Voir tous les créneaux disponibles",
|
||||
"org_team_names_example_1": "p. ex. Équipe marketing",
|
||||
"org_team_names_example_2": "p. ex. Équipe de vente",
|
||||
@@ -2016,8 +2077,12 @@
|
||||
"description_requires_booker_email_verification": "Pour assurer la vérification par e-mail du participant avant la planification des événements.",
|
||||
"requires_confirmation_mandatory": "Les messages ne peuvent être envoyés aux participants que lorsque le type d'événement nécessite une confirmation.",
|
||||
"organizations": "Organisations",
|
||||
"upload_cal_video_logo": "Téléverser un logo vidéo Cal",
|
||||
"update_cal_video_logo": "Mettre à jour un logo vidéo Cal",
|
||||
"cal_video_logo_upload_instruction": "Pour assurer que votre logo reste visible sur le fond sombre de la vidéo Cal, veuillez téléverser une image de couleur claire au format PNG ou SVG afin de maintenir la transparence.",
|
||||
"org_admin_other_teams": "Autres équipes",
|
||||
"org_admin_other_teams_description": "Ici, vous pouvez voir les équipes au sein de votre organisation dont vous ne faites pas partie. Vous pouvez vous y ajouter si nécessaire.",
|
||||
"not_part_of_org": "Vous ne faites partie d'aucune organisation",
|
||||
"no_other_teams_found": "Aucune autre équipe trouvée",
|
||||
"no_other_teams_found_description": "Il n'y a pas d'autres équipes dans cette organisation.",
|
||||
"attendee_first_name_variable": "Prénom du participant",
|
||||
@@ -2053,52 +2118,149 @@
|
||||
"org_error_processing": "Une erreur s'est produite lors du traitement de cette organisation",
|
||||
"orgs_page_description": "Une liste de toutes les organisations. Accepter une organisation permettra à tous les utilisateurs avec ce domaine de messagerie de s'inscrire SANS vérification d'e-mail.",
|
||||
"unverified": "Non vérifié",
|
||||
"verified": "Vérifié",
|
||||
"dns_missing": "DNS manquant",
|
||||
"dns_configured": "DNS configuré",
|
||||
"mark_dns_configured": "Marquer comme DNS configuré",
|
||||
"value": "Valeur",
|
||||
"your_organization_updated_sucessfully": "Votre organisation a été mise à jour avec succès",
|
||||
"team_no_event_types": "Cette équipe n'a aucun type d'événement",
|
||||
"seat_options_doesnt_multiple_durations": "L'option par place ne prend pas en charge les durées multiples",
|
||||
"include_calendar_event": "Inclure l'événement du calendrier",
|
||||
"oAuth": "OAuth",
|
||||
"recently_added": "Ajoutées récemment",
|
||||
"connect_all_calendars": "Connectez vos calendriers",
|
||||
"connect_all_calendars_description": "{{appName}} relève la disponibilité de tous vos calendriers existants.",
|
||||
"workflow_automation": "Automatisation de workflows",
|
||||
"workflow_automation_description": "Personnalisez votre expérience de planification avec les workflows",
|
||||
"scheduling_for_your_team": "Automatisation de workflows",
|
||||
"scheduling_for_your_team_description": "Planifiez pour votre équipe grâce à la planification collective et par permutation circulaire",
|
||||
"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é",
|
||||
"unauthorized": "Non autorisé",
|
||||
"access_cal_account": "{{clientName}} aimerait accéder à votre compte {{appName}}",
|
||||
"select_account_team": "Sélectionner un compte ou une équipe",
|
||||
"allow_client_to": "Cela permettra au {{clientName}} de",
|
||||
"associate_with_cal_account": "Vous associer à vos informations personnelles de {{clientName}}",
|
||||
"see_personal_info": "Voir vos informations personnelles, y compris celles que vous avez rendues publiques",
|
||||
"see_primary_email_address": "Voir votre adresse e-mail principale",
|
||||
"connect_installed_apps": "Vous connecter à vos applications installées",
|
||||
"access_event_type": "Lire, modifier, supprimer vos types d'événements",
|
||||
"access_availability": "Lire, modifier, supprimer vos disponibilités",
|
||||
"access_bookings": "Lire, modifier, supprimer vos réservations",
|
||||
"allow_client_to_do": "Autoriser {{clientName}} à faire cela ?",
|
||||
"oauth_access_information": "En cliquant sur autoriser, vous autorisez cette application à utiliser vos informations conformément à ses conditions de service et à sa politique de confidentialité. Vous pouvez supprimer l'accès dans l'App Store de {{appName}}.",
|
||||
"allow": "Autoriser",
|
||||
"view_only_edit_availability_not_onboarded": "Cet utilisateur n'a pas terminé son intégration. Vous ne pourrez pas définir sa disponibilité tant qu'il n'aura pas terminé son intégration.",
|
||||
"view_only_edit_availability": "Vous consultez les disponibilités de cet utilisateur. Vous ne pouvez modifier que vos propres disponibilités.",
|
||||
"you_can_override_calendar_in_advanced_tab": "Vous pouvez modifier ceci pour chaque événement dans les paramètres avancés de chaque type d'événement.",
|
||||
"edit_users_availability": "Modifier la disponibilité de l'utilisateur : {{username}}",
|
||||
"resend_invitation": "Renvoyer l'invitation",
|
||||
"invitation_resent": "L'invitation a été renvoyée.",
|
||||
"saml_sso": "SAML",
|
||||
"add_client": "Ajouter un client",
|
||||
"copy_client_secret_info": "Après avoir copié le secret, vous ne pourrez plus le voir",
|
||||
"add_new_client": "Ajouter un nouveau client",
|
||||
"this_app_is_not_setup_already": "Cette application n'a pas encore été configurée",
|
||||
"as_csv": "au format CSV",
|
||||
"overlay_my_calendar": "Superposer mon calendrier",
|
||||
"overlay_my_calendar_toc": "En vous connectant à votre calendrier, vous acceptez notre politique de confidentialité et nos conditions d'utilisation. Vous pouvez révoquer cet accès à tout moment.",
|
||||
"view_overlay_calendar_events": "Consultez les événements de votre calendrier afin d'éviter les réservations incompatibles.",
|
||||
"join_event_location": "Rejoignez {{eventLocationType}}",
|
||||
"troubleshooting": "Dépannage",
|
||||
"calendars_were_checking_for_conflicts": "Calendriers dont nous vérifions les conflits",
|
||||
"availabilty_schedules": "Horaires de disponibilité",
|
||||
"manage_calendars": "Gérer les calendriers",
|
||||
"manage_availability_schedules": "Gérer les horaires de disponibilité",
|
||||
"lock_timezone_toggle_on_booking_page": "Verrouiller le fuseau horaire sur la page de réservation",
|
||||
"description_lock_timezone_toggle_on_booking_page": "Pour verrouiller le fuseau horaire sur la page de réservation, utile pour les événements en personne.",
|
||||
"event_setup_multiple_payment_apps_error": "Vous ne pouvez activer qu'une seule application de paiement par type d'événement.",
|
||||
"number_in_international_format": "Veuillez entrer le numéro au format international.",
|
||||
"install_calendar": "Installer Calendrier",
|
||||
"branded_subdomain": "Sous-domaine de marque",
|
||||
"branded_subdomain_description": "Obtenez votre propre sous-domaine de marque, tel que acme.cal.com",
|
||||
"org_insights": "Statistiques à l'échelle de l'organisation",
|
||||
"org_insights_description": "Comprenez comment l'ensemble de votre organisation passe son temps",
|
||||
"extensive_whitelabeling": "Marque blanche étendue",
|
||||
"extensive_whitelabeling_description": "Personnalisez votre expérience de planification avec votre propre logo, vos couleurs, etc.",
|
||||
"unlimited_teams": "Équipes illimitées",
|
||||
"unlimited_teams_description": "Ajoutez autant de sous-équipes que nécessaire à votre organisation",
|
||||
"unified_billing": "Facturation unifiée",
|
||||
"unified_billing_description": "Ajoutez une seule carte de crédit pour payer tous les abonnements de votre équipe",
|
||||
"advanced_managed_events": "Types d'événements gérés avancés",
|
||||
"advanced_managed_events_description": "Ajoutez une seule carte de crédit pour payer tous les abonnements de votre équipe",
|
||||
"enterprise_description": "Passez en Entreprise pour créer votre organisation",
|
||||
"create_your_org": "Créer votre organisation",
|
||||
"create_your_org_description": "Passez en Entreprise et recevez un sous-domaine, une facturation unifiée, des statistiques, un marquage blanc extensif et plus encore",
|
||||
"other_payment_app_enabled": "Vous ne pouvez activer qu'une seule application de paiement par type d'événement",
|
||||
"admin_delete_organization_description": "<ul><li>Les équipes qui sont membres de cette organisation seront également supprimées, ainsi que leurs types d'événements</li><li>Les utilisateurs qui faisaient partie de l'organisation ne seront pas supprimés et leurs types d'événements resteront intacts.</li><li>Les noms d'utilisateur seront modifiés pour leur permettre d'exister en dehors de l'organisation</li></ul>",
|
||||
"admin_delete_organization_title": "Supprimer {{organizationName}} ?",
|
||||
"published": "Publiée",
|
||||
"unpublished": "Non publiée",
|
||||
"publish": "Publier",
|
||||
"org_publish_error": "L'organisation n'a pas pu être publiée",
|
||||
"troubleshooter_tooltip": "Ouvrez l'outil de dépannage et déterminez ce qui ne va pas avec votre planning",
|
||||
"need_help": "Besoin d'aide ?",
|
||||
"troubleshooter": "Dépannage",
|
||||
"please_install_a_calendar": "Veuillez installer un publier",
|
||||
"instant_tab_title": "Réservation instantanée",
|
||||
"instant_event_tab_description": "Autoriser les gens à réserver immédiatement",
|
||||
"uprade_to_create_instant_bookings": "Passez en Entreprise et autorisez les invités à rejoindre un appel instantané auquel les participants peuvent accéder directement. Ceci n'est valable que pour les types d'événements d'équipe",
|
||||
"dont_want_to_wait": "Vous ne voulez pas attendre ?",
|
||||
"meeting_started": "Le rendez-vous a commencé",
|
||||
"pay_and_book": "Payer pour réserver",
|
||||
"booking_not_found_error": "Impossible de trouver la réservation",
|
||||
"booking_seats_full_error": "Les places de réservation sont complètes",
|
||||
"missing_payment_credential_error": "Identifiants de paiement manquants",
|
||||
"missing_payment_app_id_error": "Id de l'application de paiement manquant",
|
||||
"not_enough_available_seats_error": "La réservation n'a pas assez de places disponibles",
|
||||
"user_redirect_title": "{{username}} est actuellement en absence de courte durée.",
|
||||
"user_redirect_description": "Pendant ce temps, {{profile.username}} sera responsable de tous les rendez-vous programmés de la part de {{username}}.",
|
||||
"out_of_office": "Pas au bureau",
|
||||
"out_of_office_description": "Configurez des actions dans votre profil pendant votre absence.",
|
||||
"send_request": "Envoyer une demande",
|
||||
"start_date_and_end_date_required": "Les dates de début et de fin sont requises",
|
||||
"start_date_must_be_before_end_date": "La date de début doit être antérieure à la date de fin",
|
||||
"start_date_must_be_in_the_future": "La date de début doit être dans le futur",
|
||||
"user_not_found": "Utilisateur introuvable",
|
||||
"out_of_office_entry_already_exists": "L'entrée Pas au bureau existe déjà",
|
||||
"out_of_office_id_required": "L'id de l'entrée Pas au bureau est requis",
|
||||
"booking_redirect_infinite_not_allowed": "Une redirection de réservation de cet utilisateur vers vous existe déjà.",
|
||||
"success_entry_created": "Création d'une nouvelle entrée réussie",
|
||||
"booking_redirect_email_subject": "Notification de redirection de réservation",
|
||||
"booking_redirect_email_title": "Notification de redirection de réservation",
|
||||
"booking_redirect_email_description": "Vous avez reçu une redirection de réservation de {{toName}} pour que les liens vers son profil soient redirigés vers vous pendant l'intervalle de temps : ",
|
||||
"success_accept_booking_redirect": "Vous avez accepté cette demande de redirection de réservation.",
|
||||
"success_reject_booking_redirect": "Vous avez refusé cette demande de redirection de réservation.",
|
||||
"copy_link_booking_redirect_request": "Copier le lien pour partager la demande",
|
||||
"booking_redirect_request_title": "Demande de redirection de réservation",
|
||||
"select_team_member": "Sélectionner un membre de l'équipe",
|
||||
"going_away_title": "Vous partez ? Marquez simplement le lien de votre profil comme étant indisponible pendant un certain temps.",
|
||||
"redirect_team_enabled": "Redirigez votre profil vers un autre membre de l'équipe",
|
||||
"redirect_team_disabled": "Redirigez votre profil vers un autre membre de l'équipe (formule Équipe requise)",
|
||||
"out_of_office_unavailable_list": "Liste des indisponibilités Pas au bureau",
|
||||
"success_deleted_entry_out_of_office": "Suppression de l'entrée réussie",
|
||||
"temporarily_out_of_office": "Temporairement Pas au bureau ?",
|
||||
"add_a_redirect": "Ajouter une redirection",
|
||||
"create_entry": "Créer une entrée",
|
||||
"time_range": "Plage temporelle",
|
||||
"automatically_add_all_team_members": "Ajouter tous les membres de l'équipe, y compris les futurs membres",
|
||||
"redirect_to": "Redirection vers",
|
||||
"having_trouble_finding_time": "Vous avez du mal à trouver une heure ?",
|
||||
"show_more": "En voir plus",
|
||||
"assignment_description": "Planifiez des rendez-vous lorsque tout le monde est disponible ou faites tourner les membres de votre équipe",
|
||||
"lowest": "le plus bas",
|
||||
"low": "bas",
|
||||
"medium": "moyen",
|
||||
"high": "élevé",
|
||||
"Highest": "le plus élevé",
|
||||
"send_booker_to": "Envoyer un utilisateur vers",
|
||||
"set_priority": "Définir la priorité",
|
||||
"priority_for_user": "Priorité pour {{userName}}",
|
||||
"change_priority": "changer la priorité",
|
||||
"field_identifiers_as_variables": "Utiliser les identifiants de champs comme variables pour la redirection de votre événement personnalisé",
|
||||
"field_identifiers_as_variables_with_example": "Utiliser les identifiants de champs comme variables pour la redirection de votre événement personnalisé (par ex. : {{variable}})",
|
||||
"account_already_linked": "Le compte est déjà associé",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -807,6 +807,8 @@
|
||||
"additional_input_description": "דרוש מהמתזמן להזין נתונים נוספים לפני אישור התזמון",
|
||||
"label": "תווית",
|
||||
"placeholder": "מציין מיקום",
|
||||
"display_add_to_calendar_organizer": "הצגת דוא״ל ‚הוספה ללוח השנה’ כגוף מארגן",
|
||||
"reconnect_calendar_to_use": "נא לשים לב שכנראה צריך להתנתק ואז להתחבר ללוח השנה שלך מחדש כדי להשתמש ביכולת הזאת.",
|
||||
"type": "סוג",
|
||||
"edit": "עריכה",
|
||||
"add_input": "הוספת קלט",
|
||||
|
||||
@@ -13,10 +13,17 @@
|
||||
"reset_password_subject": "{{appName}}: istruzioni per reimpostare la password",
|
||||
"verify_email_subject": "{{appName}}: verifica il tuo account",
|
||||
"check_your_email": "Controlla la tua e-mail",
|
||||
"old_email_address": "Vecchio indirizzo email",
|
||||
"new_email_address": "Nuovo indirizzo email",
|
||||
"verify_email_page_body": "Abbiamo inviato un'e-mail a {{email}}. È importante verificare il tuo indirizzo e-mail per garantire la consegna delle e-mail e delle notifiche del calendario {{appName}}.",
|
||||
"verify_email_banner_body": "Verifica il tuo indirizzo e-mail per garantire la consegna delle e-mail e delle notifiche del calendario",
|
||||
"verify_email_email_header": "Verifica il tuo indirizzo e-mail",
|
||||
"verify_email_email_button": "Verifica l'e-mail",
|
||||
"verify_email_change_description": "Hai recentemente chiesto di cambiare l'indirizzo email che utilizzi per accedere al tuo account {{appName}}. Fai clic sul pulsante qui sotto pe confermare il tuo nuovo indirizzo email.",
|
||||
"verify_email_change_success_toast": "Indirizzo email aggiornato: {{email}}",
|
||||
"verify_email_change_failure_toast": "Aggiornamento email non riuscito.",
|
||||
"change_of_email": "Verifica il tuo nuovo indirizzo email per {{appName}}",
|
||||
"change_of_email_toast": "Abbiamo inviato un link di verifica a {{email}}. Aggiorneremo il tuo indirizzo email quando farai clic sul link.",
|
||||
"copy_somewhere_safe": "Salva questa chiave API in un posto sicuro. Non potrai visualizzarla in seguito.",
|
||||
"verify_email_email_body": "Verifica il tuo indirizzo e-mail facendo clic sul pulsante qui sotto.",
|
||||
"verify_email_by_code_email_body": "Verifica il tuo indirizzo e-mail utilizzando il codice sottostante.",
|
||||
@@ -56,6 +63,17 @@
|
||||
"a_refund_failed": "Un rimborso non è riuscito",
|
||||
"awaiting_payment_subject": "In attesa del pagamento: {{title}} il {{date}}",
|
||||
"meeting_awaiting_payment": "La tua riunione è in attesa di pagamento",
|
||||
"dark_theme_contrast_error": "Il colore del tema scuro non supera il controllo del contrasto. Ti consigliamo di cambiare questo colore in modo che i pulsanti siano più visibili.",
|
||||
"light_theme_contrast_error": "Il colore del tema chiaro non supera il controllo del contrasto. Ti consigliamo di cambiare questo colore in modo che i pulsanti siano più visibili.",
|
||||
"payment_not_created_error": "Non è stato possibile creare il pagamento",
|
||||
"couldnt_charge_card_error": "Non è stato possibile addebitare la carta per il pagamento",
|
||||
"no_available_users_found_error": "Nessun utente disponibile. Puoi provare un altro intervallo temporale?",
|
||||
"request_body_end_time_internal_error": "Errore interno. La richiesta non contiene l'ora finale",
|
||||
"create_calendar_event_error": "Impossibile creare l'evento nel calendario dell'organizzatore",
|
||||
"update_calendar_event_error": "Impossibile aggiornare l'evento nel calendario.",
|
||||
"delete_calendar_event_error": "Impossibile eliminare l'evento dal calendario.",
|
||||
"already_signed_up_for_this_booking_error": "Ti sei già registrato per questa prenotazione.",
|
||||
"hosts_unavailable_for_booking": "Alcuni degli organizzatori non sono disponibili per la prenotazione.",
|
||||
"help": "Guida",
|
||||
"price": "Prezzo",
|
||||
"paid": "Pagato",
|
||||
@@ -101,6 +119,7 @@
|
||||
"requested_to_reschedule_subject_attendee": "Azione richiesta per la riprogrammazione: prenota un nuovo orario per {{eventType}} con {{name}}",
|
||||
"hi_user_name": "Ciao {{name}}",
|
||||
"ics_event_title": "{{eventType}} con {{name}}",
|
||||
"please_book_a_time_sometime_later": "Oops, non siamo riusciti a collegarti. Pianifica una chiamata futura",
|
||||
"new_event_subject": "Richiesta di nuovo evento: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"join_by_entrypoint": "Entra da {{entryPoint}}",
|
||||
"notes": "Osservazioni",
|
||||
@@ -117,15 +136,20 @@
|
||||
"meeting_id": "Meeting ID",
|
||||
"meeting_password": "Password del meeting",
|
||||
"meeting_url": "URL del Meeting",
|
||||
"meeting_url_not_found": "URL della riunione non trovato",
|
||||
"token_not_found": "Token non trovato",
|
||||
"some_other_host_already_accepted_the_meeting": "Qualche altro host ha già accettato la riunione. Vuoi comunque partecipare? <1>Prosegui con la riunione</1>",
|
||||
"meeting_request_rejected": "La tua richiesta di riunione è stata rifiutata",
|
||||
"rejected_event_type_with_organizer": "Rifiutato: {{eventType}} con {{organizer}} il {{date}}",
|
||||
"hi": "Ciao",
|
||||
"join_team": "Unisciti al team",
|
||||
"manage_this_team": "Gestisci questo team",
|
||||
"team_info": "Informazioni sul team",
|
||||
"join_meeting": "Partecipa alla riunione",
|
||||
"request_another_invitation_email": "Se preferisci non usare {{toEmail}} come email {{appName}} o già hai un account {{appName}}, per favore richiedi un altro invito a quella email.",
|
||||
"you_have_been_invited": "Sei stato invitato ad unirti al team {{teamName}}",
|
||||
"user_invited_you": "{{user}} ti ha invitato a unirti a {{entity}} {{team}} su {{appName}}",
|
||||
"user_invited_you_to_subteam": "{{user}} ti ha invitato a entrare nel team {{team}} dell'organizzazione {{parentTeamName}} su {{appName}}",
|
||||
"hidden_team_member_title": "Sei nascosto in questo team",
|
||||
"hidden_team_member_message": "Il tuo posto non è pagato. Passa a Pro oppure informa il proprietario del team che può pagare il tuo posto.",
|
||||
"hidden_team_owner_message": "Per utilizzare i team ti occorre un account Pro. Sarai nascosto finché non cambierai piano.",
|
||||
@@ -229,6 +253,7 @@
|
||||
"reset_your_password": "Imposta la nuova password seguendo le istruzioni che ti sono state inviate via e-mail.",
|
||||
"email_change": "Accedi di nuovo con il nuovo indirizzo e-mail e la nuova password.",
|
||||
"create_your_account": "Crea il tuo account",
|
||||
"create_your_calcom_account": "Crea il tuo accout Cal.com",
|
||||
"sign_up": "Registrati",
|
||||
"youve_been_logged_out": "Hai fatto il log out",
|
||||
"hope_to_see_you_soon": "Speriamo di rivederti presto!",
|
||||
@@ -266,6 +291,9 @@
|
||||
"nearly_there_instructions": "Infine: qualche informazione su di te e una foto sono utili per ottenere prenotazioni e per far capire alle persone con chi stanno prenotando.",
|
||||
"set_availability_instructions": "Definisci intervalli di tempo di quando sei disponibile su una base ricorrente. Puoi crearne di più in seguito e assegnarli a calendari diversi.",
|
||||
"set_availability": "Imposta la tua disponibilità",
|
||||
"set_availbility_description": "Programma gli orari in cui vuoi essere prenotato.",
|
||||
"share_a_link_or_embed": "Condividi o pubblica un link",
|
||||
"share_a_link_or_embed_description": "Condividi il tuo link {{appName}} o inseriscilo nel tuo sito.",
|
||||
"availability_settings": "Impostazioni di disponibilità",
|
||||
"continue_without_calendar": "Continua senza calendario",
|
||||
"continue_with": "Continua con {{appName}}",
|
||||
@@ -281,6 +309,7 @@
|
||||
"welcome_to_calcom": "Benvenuto in {{appName}}",
|
||||
"welcome_instructions": "Dimmi come chiamarti e facci sapere in che timezone sei. Potrai cambiarlo dopo.",
|
||||
"connect_caldav": "Connetti al server CalDav",
|
||||
"connect_ics_feed": "Collega a un feed ICS",
|
||||
"connect": "Connetti",
|
||||
"try_for_free": "Provalo gratuitamente",
|
||||
"create_booking_link_with_calcom": "Crea il tuo link di prenotazione con {{appName}}",
|
||||
@@ -419,6 +448,7 @@
|
||||
"browse_api_documentation": "Sfoglia la nostra documentazione API",
|
||||
"leverage_our_api": "Sfrutta la nostra API per il pieno controllo e la personalizzabilità.",
|
||||
"create_webhook": "Crea Webhook",
|
||||
"instant_meeting": "Riunione immediata creata",
|
||||
"booking_cancelled": "Prenotazione cancellata",
|
||||
"booking_rescheduled": "Prenotazione Riprogrammata",
|
||||
"recording_ready": "Il link per scaricare la registrazione è pronto",
|
||||
@@ -533,11 +563,13 @@
|
||||
"enter_number_between_range": "Insrisci un numero compreso tra 1 e {{maxOccurences}}",
|
||||
"email_address": "Indirizzo email",
|
||||
"enter_valid_email": "Immettere un indirizzo e-mail valido",
|
||||
"please_schedule_future_call": " Pianifica una chiamata futura se non siamo disponibili entro {{seconds}} secondi",
|
||||
"location": "Luogo",
|
||||
"address": "Indirizzo",
|
||||
"enter_address": "Immettere l'indirizzo",
|
||||
"in_person_attendee_address": "Di persona (indirizzo del partecipante)",
|
||||
"yes": "Sì",
|
||||
"no": "No",
|
||||
"additional_notes": "Note aggiuntive",
|
||||
"booking_fail": "Impossibile prenotare la riunione.",
|
||||
"reschedule_fail": "Impossibile riprogrammare la riunione.",
|
||||
@@ -591,6 +623,7 @@
|
||||
"new_member": "Nuovo Membro",
|
||||
"invite": "Invita",
|
||||
"add_team_members": "Aggiungi membri del team",
|
||||
"add_org_members": "Aggiungi membri",
|
||||
"add_team_members_description": "Invita altri utenti a unirsi al tuo team",
|
||||
"add_team_member": "Aggiungi un membro del team",
|
||||
"invite_new_member": "Invita un nuovo membro",
|
||||
@@ -605,6 +638,7 @@
|
||||
"hide_book_a_team_member_description": "Nasconde il pulsante Prenota un membro del team nelle pagine pubbliche.",
|
||||
"danger_zone": "Zona pericolosa",
|
||||
"account_deletion_cannot_be_undone": "Attenzione. Non è possibile annullare l'eliminazione dell'account.",
|
||||
"team_deletion_cannot_be_undone": "Attenzione. Non è possibile annullare l'eliminazione del team",
|
||||
"back": "Indietro",
|
||||
"cancel": "Annulla",
|
||||
"cancel_all_remaining": "Annulla tutti i rimanenti",
|
||||
@@ -655,6 +689,7 @@
|
||||
"default_duration": "Durata predefinita",
|
||||
"default_duration_no_options": "Scegliere prima delle durate disponibili",
|
||||
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
|
||||
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
|
||||
"minutes": "Minuti",
|
||||
"round_robin": "Round Robin",
|
||||
"round_robin_description": "Ciclo di riunioni tra più membri del team.",
|
||||
@@ -667,6 +702,7 @@
|
||||
"add_members": "Aggiungi membri...",
|
||||
"no_assigned_members": "Nessun membro assegnato",
|
||||
"assigned_to": "Assegnato a",
|
||||
"you_must_be_logged_in_to": "Devi effettuare l'accesso a {{url}}",
|
||||
"start_assigning_members_above": "Inizia ad assegnare membri in alto",
|
||||
"locked_fields_admin_description": "I membri non potranno fare modifiche",
|
||||
"locked_fields_member_description": "Questa opzione è stata bloccata dall'amministratore del team",
|
||||
@@ -771,6 +807,7 @@
|
||||
"additional_input_description": "Chiedi al creatore del programma di inserire dei campi di input aggiuntivi prima che la prenotazione sia confermata",
|
||||
"label": "Etichetta",
|
||||
"placeholder": "Segnaposto",
|
||||
"display_add_to_calendar_organizer": "Visualizza l'email \"Aggiungi al calendario\" come organizzatore",
|
||||
"type": "Tipo",
|
||||
"edit": "Modifica",
|
||||
"add_input": "Aggiungi un input",
|
||||
@@ -779,6 +816,9 @@
|
||||
"requires_confirmation_description": "La prenotazione deve essere confermata manualmente prima di essere inviata alle integrazioni e che venga inviata una e-mail di conferma.",
|
||||
"recurring_event": "Evento ricorrente",
|
||||
"recurring_event_description": "Le persone possono iscriversi a eventi ricorrenti",
|
||||
"cannot_be_used_with_paid_event_types": "Non può essere utilizzato con i tipi di evento a pagamento",
|
||||
"warning_payment_instant_meeting_event": "Le riunioni immediate non sono ancora disponibili con gli eventi ricorrenti e le app di pagamento",
|
||||
"warning_instant_meeting_experimental": "Sperimentale: gli eventi Riunione immediata al momento sono in fase di sperimentazione.",
|
||||
"starting": "Inizio",
|
||||
"disable_guests": "Disabilita Ospiti",
|
||||
"disable_guests_description": "Disabilita l'aggiunta di ulteriori ospiti durante la prenotazione.",
|
||||
@@ -787,6 +827,7 @@
|
||||
"private_link_label": "Link privato",
|
||||
"private_link_hint": "Il tuo link privato verrà rigenerato dopo ogni utilizzo",
|
||||
"copy_private_link": "Copia link privato",
|
||||
"copy_private_link_to_event": "Copia link privato nell'evento",
|
||||
"private_link_description": "Genera un URL privato da condividere senza mostrare il tuo nome utente {{appName}}",
|
||||
"invitees_can_schedule": "Gli inviti possono programmare",
|
||||
"date_range": "Range di date",
|
||||
@@ -846,6 +887,7 @@
|
||||
"next_step": "Salta passo",
|
||||
"prev_step": "Passo precedente",
|
||||
"install": "Installa",
|
||||
"start_paid_trial": "Inizia la prova gratuita",
|
||||
"installed": "Installato",
|
||||
"active_install_one": "{{count}} installazione attiva",
|
||||
"active_install_other": "{{count}} installazioni attive",
|
||||
@@ -860,6 +902,7 @@
|
||||
"toggle_calendars_conflict": "Attiva/disattiva i calendari in cui desideri controllare i conflitti per evitare doppie prenotazioni.",
|
||||
"connect_additional_calendar": "Collega calendario aggiuntivo",
|
||||
"calendar_updated_successfully": "Calendario aggiornato correttamente",
|
||||
"check_here": "Controlla qui",
|
||||
"conferencing": "Conferenze",
|
||||
"calendar": "Calendario",
|
||||
"payments": "Pagamenti",
|
||||
@@ -1031,6 +1074,9 @@
|
||||
"user_impersonation_heading": "Accesso con altra identità",
|
||||
"user_impersonation_description": "Consente al nostro team di supporto di effettuare l'accesso temporaneo a tuo nome per risolvere rapidamente gli eventuali problemi da te segnalati.",
|
||||
"team_impersonation_description": "Consente ai proprietari/amministratori del tuo team di accedere temporaneamente con il tuo nome.",
|
||||
"cal_signup_description": "Gratuito per le singole pesone. Piani collettivi per le funzionalità di collaborazione.",
|
||||
"make_org_private": "Rendi l'organizzazione privata",
|
||||
"make_org_private_description": "Quando questa opzione è attivata, i membri della tua organizzazione non possono vedere gli altri membri.",
|
||||
"make_team_private": "Rendi il team privato",
|
||||
"make_team_private_description": "Quando questa opzione è attivata, i membri del tuo team non potranno vedere gli altri membri del team.",
|
||||
"you_cannot_see_team_members": "Non è possibile vedere tutti i membri di un team privato.",
|
||||
@@ -1090,6 +1136,7 @@
|
||||
"developer_documentation": "Documentazione sviluppatore",
|
||||
"get_in_touch": "Contattaci",
|
||||
"contact_support": "Contatta il supporto",
|
||||
"premium_support": "Assistenza Premium",
|
||||
"community_support": "Supporto della community",
|
||||
"feedback": "Feedback",
|
||||
"submitted_feedback": "Grazie per il tuo feedback!",
|
||||
@@ -1253,6 +1300,7 @@
|
||||
"default_calendar_selected": "Calendario predefinito",
|
||||
"hide_from_profile": "Nascondi nel profilo",
|
||||
"event_setup_tab_title": "Impostazione evento",
|
||||
"availability_not_found_in_schedule_error": "Nessuna disponibilità trovata nel programma",
|
||||
"event_limit_tab_title": "Limiti",
|
||||
"event_limit_tab_description": "Con quale frequenza è possibile fare una prenotazione",
|
||||
"event_advanced_tab_description": "Impostazioni calendario e altro...",
|
||||
@@ -1273,6 +1321,7 @@
|
||||
"do_this": "Esegui questa operazione",
|
||||
"turn_off": "Disattiva",
|
||||
"turn_on": "Attiva",
|
||||
"cancelled_bookings_cannot_be_rescheduled": "Le prenotazioni annullate non possono essere riprogrammate",
|
||||
"settings_updated_successfully": "Impostazioni aggiornate correttamente",
|
||||
"error_updating_settings": "Errore durante l'aggiornamento delle impostazioni",
|
||||
"personal_cal_url": "Il mio URL personale di {{appName}}",
|
||||
@@ -1296,6 +1345,7 @@
|
||||
"customize_your_brand_colors": "Personalizza la tua pagina di prenotazione con i colori del tuo marchio.",
|
||||
"pro": "Pro",
|
||||
"removes_cal_branding": "Rimuove tutte le scritte con il marchio {{appName}}, ad es., 'Powered by {{appName}}.'",
|
||||
"instant_meeting_with_title": "Riunione immediata con {{name}}",
|
||||
"profile_picture": "Immagine del profilo",
|
||||
"upload": "Carica",
|
||||
"add_profile_photo": "Aggiungi foto del profilo",
|
||||
@@ -1351,6 +1401,7 @@
|
||||
"event_name_info": "Nome del tipo di evento",
|
||||
"event_date_info": "Data dell'evento",
|
||||
"event_time_info": "Ora di inizio dell'evento",
|
||||
"event_type_not_found": "EventType non trovato",
|
||||
"location_variable": "Luogo",
|
||||
"location_info": "Luogo dell'evento",
|
||||
"additional_notes_variable": "Note aggiuntive",
|
||||
@@ -1393,6 +1444,7 @@
|
||||
"slot_length": "Durata della fascia oraria",
|
||||
"booking_appearance": "Aspetto prenotazione",
|
||||
"appearance_team_description": "Gestisci le impostazioni di aspetto delle prenotazioni del tuo team",
|
||||
"appearance_org_description": "Imposta l'aspetto delle prenotazioni per la tua organizzazione",
|
||||
"only_owner_change": "Solo il proprietario del team può apportare modifiche alla prenotazione del team ",
|
||||
"team_disable_cal_branding_description": "Rimuove tutte le scritte con il marchio {{appName}}, ad es., 'Powered by {{appName}}'",
|
||||
"invited_by_team": "{{teamName}} ti ha invitato a unirti al team come {{role}}",
|
||||
@@ -1446,6 +1498,7 @@
|
||||
"password_updated": "Password aggiornata!",
|
||||
"pending_payment": "Pagamento in sospeso",
|
||||
"pending_invites": "Inviti in sospeso",
|
||||
"pending_organization_invites": "Inviti in sospeso dell'organizzazione",
|
||||
"not_on_cal": "Non presente su {{appName}}",
|
||||
"no_calendar_installed": "Nessun calendario installato",
|
||||
"no_calendar_installed_description": "Non hai ancora collegato alcun calendario",
|
||||
@@ -1457,6 +1510,8 @@
|
||||
"report_app": "Segnala app",
|
||||
"limit_booking_frequency": "Limita frequenza di prenotazione",
|
||||
"limit_booking_frequency_description": "Indica quante volte è possibile prenotare questo evento",
|
||||
"limit_booking_only_first_slot": "Limita prenotazione solo prima fascia",
|
||||
"limit_booking_only_first_slot_description": "Consenti la prenotazione solo nella prima fascia oraria di ogni giorno",
|
||||
"limit_total_booking_duration": "Limita durata totale prenotazione",
|
||||
"limit_total_booking_duration_description": "Indicare la quantità di tempo totale disponibile per prenotare questo evento",
|
||||
"add_limit": "Aggiungi limite",
|
||||
@@ -1524,10 +1579,14 @@
|
||||
"your_org_disbanded_successfully": "La tua organizzazione è stata sciolta",
|
||||
"error_creating_team": "Errore durante la creazione del team",
|
||||
"you": "Tu",
|
||||
"or_continue_with": "Oppure continua con",
|
||||
"resend_email": "Invia di nuovo l'e-mail",
|
||||
"member_already_invited": "Membro già invitato",
|
||||
"already_in_use_error": "Nome utente già in uso",
|
||||
"enter_email_or_username": "Immettere un indirizzo e-mail o un nome utente",
|
||||
"enter_email": "Inserisci un'email",
|
||||
"enter_emails": "Inserisci email",
|
||||
"too_many_invites": "Puoi invitare un massimo di {{nbUsers}} utenti alla volta.",
|
||||
"team_name_taken": "Nome già utilizzato",
|
||||
"must_enter_team_name": "Necessario immettere un nome per il team",
|
||||
"team_url_required": "Immettere un URL per il team",
|
||||
@@ -1607,6 +1666,7 @@
|
||||
"individual": "Persona",
|
||||
"all_bookings_filter_label": "Tutte le prenotazioni",
|
||||
"all_users_filter_label": "Tutti gli utenti",
|
||||
"all_event_types_filter_label": "Tutti i tipi di evento",
|
||||
"your_bookings_filter_label": "Le tue prenotazioni",
|
||||
"meeting_url_variable": "URL della riunione",
|
||||
"meeting_url_info": "URL dell'evento, dell'incontro o della conferenza",
|
||||
@@ -1703,9 +1763,11 @@
|
||||
"organizer_timezone": "Fuso orario organizzatore",
|
||||
"email_user_cta": "Visualizza invito",
|
||||
"email_no_user_invite_heading_team": "Hai ricevuto un invito a entrare a far parte del team di {{appName}}",
|
||||
"email_no_user_invite_heading_subteam": "Sei stato invitato a partecipare a un team dell'organizzazione {{parentTeamName}}",
|
||||
"email_no_user_invite_heading_org": "Hai ricevuto un invito a entrare a far parte dell'organizzazione {{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_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_user_invite_subheading_subteam": "{{invitedBy}} ti ha invitato a entrare nel team {{teamName}} della sua organizzazione {{parentTeamName}} su {{appName}}. {{appName}} è lo strumento di programmazione degli eventi che consente a te e al tuo team di programmare riunioni senza lo scambio di infinite email.",
|
||||
"email_user_invite_subheading_org": "{{invitedBy}} ti ha invitato a unirti alla sua organizzazione `{{teamName}}` su {{appName}}. {{appName}} è uno strumento di pianificazione di eventi che permette a te e alla tua organizzazione 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",
|
||||
@@ -1838,7 +1900,9 @@
|
||||
"review_event_type": "Rivedi tipo di evento",
|
||||
"looking_for_more_analytics": "Hai bisogno di più dati analitici?",
|
||||
"looking_for_more_insights": "Ti servono più Insights?",
|
||||
"filters": "Filtri",
|
||||
"add_filter": "Aggiungi filtro",
|
||||
"remove_filters": "Cancella tutti i filtri",
|
||||
"select_user": "Seleziona utente",
|
||||
"select_event_type": "Seleziona tipo di evento",
|
||||
"select_date_range": "Seleziona intervallo di date",
|
||||
@@ -1929,6 +1993,7 @@
|
||||
"connect_google_workspace": "Collega Google Workspace",
|
||||
"google_workspace_admin_tooltip": "Per usare questa funzione è necessario essere amministratori di Workspace",
|
||||
"first_event_type_webhook_description": "Crea il primo webhook per questo tipo di evento",
|
||||
"create_instant_meeting_webhook_description": "Crea il tuo primo webhook con \"Riunione immediata creata\" come attivatore per questo tipo di evento",
|
||||
"install_app_on": "Installa l'app su",
|
||||
"create_for": "Crea per",
|
||||
"currency": "Valuta",
|
||||
@@ -1993,6 +2058,8 @@
|
||||
"add_times_to_your_email": "Seleziona alcuni orari disponibili e incorporali nella tua e-mail",
|
||||
"select_time": "Seleziona l'ora",
|
||||
"select_date": "Seleziona la data",
|
||||
"connecting_you_to_someone": "Ti stiamo collegando con qualcuno.",
|
||||
"please_do_not_close_this_tab": "Non chiudere questa scheda",
|
||||
"see_all_available_times": "Vedi tutti gli orari disponibili",
|
||||
"org_team_names_example_1": "ad es., team di marketing",
|
||||
"org_team_names_example_2": "ad es., team vendite",
|
||||
@@ -2010,8 +2077,12 @@
|
||||
"description_requires_booker_email_verification": "Per garantire la verifica dell'indirizzo e-mail della persona che prenota prima di panificare eventi",
|
||||
"requires_confirmation_mandatory": "I messaggi di testo possono solo essere inviati ai partecipanti quando il tipo di evento richiede una conferma.",
|
||||
"organizations": "Organizzazioni",
|
||||
"upload_cal_video_logo": "Carica il logo Cal Video",
|
||||
"update_cal_video_logo": "Aggiorna il logo Cal Video",
|
||||
"cal_video_logo_upload_instruction": "Per fare in modo che il tuo logo sia visibile sullo sfondo scuro del video Cal, carica un'immagine di colore chiaro in formato PNG o SVG per mantenere la trasparenza.",
|
||||
"org_admin_other_teams": "Atri team",
|
||||
"org_admin_other_teams_description": "Qui puoi vedere i team di cui non fai parte all'interno della tua organizzazione. Puoi entrare a far parte di tali team se necessario.",
|
||||
"not_part_of_org": "Non fai parte di alcuna organizzazione",
|
||||
"no_other_teams_found": "Nessun altro team trovato",
|
||||
"no_other_teams_found_description": "Non ci sono altri team in questa organizzazione.",
|
||||
"attendee_first_name_variable": "Nome del partecipante",
|
||||
@@ -2047,7 +2118,9 @@
|
||||
"org_error_processing": "Si è verificato un errore durante l'elaborazione di questa organizzazione",
|
||||
"orgs_page_description": "Un elenco di tutte le organizzazioni. Accettando un'organizzazione, tutti gli utenti con il rispettivo dominio e-mail potranno registrarsi SENZA verificare l'indirizzo e-mail.",
|
||||
"unverified": "Non verificato",
|
||||
"verified": "Verificato",
|
||||
"dns_missing": "DNS mancante",
|
||||
"dns_configured": "DNS Configurato",
|
||||
"mark_dns_configured": "Contrassegna DNS come configurato",
|
||||
"value": "Valore",
|
||||
"your_organization_updated_sucessfully": "L'organizzazione è stata aggiornata correttamente",
|
||||
@@ -2057,8 +2130,11 @@
|
||||
"oAuth": "OAuth",
|
||||
"recently_added": "Aggiunti di recente",
|
||||
"connect_all_calendars": "Collega tutti i tuoi calendari",
|
||||
"connect_all_calendars_description": "{{appName}} legge la disponibilità da tutti i tuoi calendari esistenti.",
|
||||
"workflow_automation": "Automazione dei flussi di lavoro",
|
||||
"workflow_automation_description": "Personalizza la tua esperienza di programmazione con i flussi di lavoro",
|
||||
"scheduling_for_your_team": "Automazione dei flussi di lavoro",
|
||||
"scheduling_for_your_team_description": "Usa la programmazione collettiva e round robin per il tuo team",
|
||||
"no_members_found": "Nessun membro trovato",
|
||||
"event_setup_length_error": "Impostazione evento: la durata deve essere di almeno 1 minuto.",
|
||||
"availability_schedules": "Calendario disponibilità",
|
||||
@@ -2091,10 +2167,100 @@
|
||||
"overlay_my_calendar": "Sovrapponi il mio calendario",
|
||||
"overlay_my_calendar_toc": "Collegando il tuo calendario, accetti la nostra informativa sulla privacy e i termini di servizio. Puoi revocare l'accesso in qualsiasi momento.",
|
||||
"view_overlay_calendar_events": "Visualizza gli eventi del tuo calendario per prevenire prenotazioni in conflitto.",
|
||||
"join_event_location": "Partecipa a {{eventLocationType}}",
|
||||
"troubleshooting": "Risoluzione problemi",
|
||||
"calendars_were_checking_for_conflicts": "Calendari in cui stiamo cercando possibili conflitti",
|
||||
"availabilty_schedules": "Programmi di disponibilità",
|
||||
"manage_calendars": "Gestisci calendari",
|
||||
"manage_availability_schedules": "Gestisci programmi di disponibilità",
|
||||
"lock_timezone_toggle_on_booking_page": "Blocca fuso orario nella pagina di prenotazione",
|
||||
"description_lock_timezone_toggle_on_booking_page": "Per bloccare il fuso orario nella pagina di prenotazione, utile per gli eventi di persona.",
|
||||
"event_setup_multiple_payment_apps_error": "Puoi avere solo un'app di pagamento abilitata per ciascun tipo di evento.",
|
||||
"number_in_international_format": "Inserisci il numero in formato internazionale.",
|
||||
"install_calendar": "Installa Calendario",
|
||||
"branded_subdomain": "Sottodominio del marchio",
|
||||
"branded_subdomain_description": "Crea un sottodominio per il tuo marchio, come acme.cal.com",
|
||||
"org_insights": "Approfondimenti a livello di organizzazione",
|
||||
"org_insights_description": "Comprendi come la tua organizzazione utilizza il tempo",
|
||||
"extensive_whitelabeling": "Assistenza per l'onboarding e supporto tecnico dedicati",
|
||||
"extensive_whitelabeling_description": "Personalizza la tua esperienza di programmazione con il tuo logo, colori e altro ancora",
|
||||
"unlimited_teams": "Numero illimitato di team",
|
||||
"unlimited_teams_description": "Aggiungi alla tua organizzazione tutti i sotto-team necessari",
|
||||
"unified_billing": "Fatturazione unificata",
|
||||
"unified_billing_description": "Indica una singola carta di credito per pagare tutti gli abbonamenti del tuo team",
|
||||
"advanced_managed_events": "Tipi di evento gestiti avanzati",
|
||||
"advanced_managed_events_description": "Indica una singola carta di credito per pagare tutti gli abbonamenti del tuo team",
|
||||
"enterprise_description": "Passa a Enterprise per creare la tua organizzazione",
|
||||
"create_your_org": "Crea la tua organizzazione",
|
||||
"create_your_org_description": "Passa a Enterprise per avere sottodominio, fatturazione unificata, approfondimenti, personalizzazione completa e altro ancora",
|
||||
"other_payment_app_enabled": "Puoi attivare solo un'app di pagamento per ciascun tipo di evento",
|
||||
"admin_delete_organization_description": "<ul><li>Verranno eliminati anche i team che sono membri di questa organizzazione, insieme ai rispettivi tipi di evento</li><li>Gli utenti che facevano parte dell'organizzazione non verranno eliminati e anche i loro tipi di evento rimarranno inalterati.</li><li>I nomi utente saranno modificati in modo che possano esistere al di fuori dell'organizzazione</li></ul>",
|
||||
"admin_delete_organization_title": "Eliminare {{organizationName}}?",
|
||||
"published": "Pubblicato",
|
||||
"unpublished": "Pubblicazione annullata",
|
||||
"publish": "Pubblica",
|
||||
"org_publish_error": "Non è stato possibile pubblicare l'organizzazione",
|
||||
"troubleshooter_tooltip": "Apri lo strumento di soluzione dei problemi per individuare il problema del tuo programma",
|
||||
"need_help": "Hai bisogno di aiuto?",
|
||||
"troubleshooter": "Risoluzione problemi",
|
||||
"please_install_a_calendar": "Installa un calendario",
|
||||
"instant_tab_title": "Prenotazione immediata",
|
||||
"instant_event_tab_description": "Consenti alle persone di prenotare immediatamente",
|
||||
"uprade_to_create_instant_bookings": "Passa a Enterprise e permetti agli ospiti di partecipare a una chiamata immediata a cui i partecipanti possono accedere direttamente. Ciò è riservato agli eventi per team",
|
||||
"dont_want_to_wait": "Non vuoi aspettare?",
|
||||
"meeting_started": "Riunione iniziata",
|
||||
"pay_and_book": "Pagare per prenotare",
|
||||
"booking_not_found_error": "Prenotazione non trovata",
|
||||
"booking_seats_full_error": "I posti sono esauriti",
|
||||
"missing_payment_credential_error": "Credenziali di pagamento mancanti",
|
||||
"missing_payment_app_id_error": "ID dell'app di pagamento mancante",
|
||||
"not_enough_available_seats_error": "La prenotazione non ha abbastanza posti disponibili",
|
||||
"user_redirect_title": "{{username}} è attualmente assente per un breve periodo.",
|
||||
"user_redirect_description": "Nel frattempo, {{profile.username}} sarà responsabile di tutte le nuove riunioni programmate per conto di {{username}}.",
|
||||
"out_of_office": "Assente",
|
||||
"out_of_office_description": "Configura le azioni nel tuo profilo mentre sei assente.",
|
||||
"send_request": "Invia richiesta",
|
||||
"start_date_and_end_date_required": "Data iniziale e data finale sono obbligatorie",
|
||||
"start_date_must_be_before_end_date": "La data iniziale deve precedere quella finale",
|
||||
"start_date_must_be_in_the_future": "La data iniziale deve essere nel futuro",
|
||||
"user_not_found": "Utente non trovato",
|
||||
"out_of_office_entry_already_exists": "La voce per l'assenza esiste già",
|
||||
"out_of_office_id_required": "L'ID della voce per l'assenza è obbligatorio",
|
||||
"booking_redirect_infinite_not_allowed": "Esiste già un reindirizzamento della prenotazione dall'utente a te.",
|
||||
"success_entry_created": "Nuova voce creata correttamente",
|
||||
"booking_redirect_email_subject": "Notifica di reindirizzamento prenotazione",
|
||||
"booking_redirect_email_title": "Notifica di reindirizzamento prenotazione",
|
||||
"booking_redirect_email_description": "Hai ricevuto un reindirizzamento delle prenotazioni da {{toName}}. I link del suo profilo rimanderanno ai tuoi, nel periodo: ",
|
||||
"success_accept_booking_redirect": "Hai accettato questa richiesta di reindirizzamento delle prenotazioni.",
|
||||
"success_reject_booking_redirect": "Hai rifiutato questa richiesta di reindirizzamento delle prenotazioni.",
|
||||
"copy_link_booking_redirect_request": "Copia link per condividere la richiesta",
|
||||
"booking_redirect_request_title": "Richiesta di reindirizzamento delle prenotazioni",
|
||||
"select_team_member": "Seleziona un membro del team",
|
||||
"going_away_title": "Devi assentarti? Segnala che il link al tuo profilo non è disponibile per un periodo.",
|
||||
"redirect_team_enabled": "Rendirizza il tuo profilo a un altro membro del team",
|
||||
"redirect_team_disabled": "Reindirizza il tuo profilo a un altro membro del team (è necessario il piano Team)",
|
||||
"out_of_office_unavailable_list": "Elenco di indisponibilità per assenza",
|
||||
"success_deleted_entry_out_of_office": "Voce eliminata",
|
||||
"temporarily_out_of_office": "Temporaneamente assente?",
|
||||
"add_a_redirect": "Aggiungi un reindirizzamento",
|
||||
"create_entry": "Crea una voce",
|
||||
"time_range": "Intervallo temporale",
|
||||
"automatically_add_all_team_members": "Aggiungi tutti i membri del team, compresi quelli futuri",
|
||||
"redirect_to": "Reindirizza a",
|
||||
"having_trouble_finding_time": "Non riesci a trovare un orario?",
|
||||
"show_more": "Mostra altro",
|
||||
"assignment_description": "Pianifica le riunioni quando tutti sono disponibili, oppure ruota tra i membri del tuo team",
|
||||
"lowest": "più basso",
|
||||
"low": "basso",
|
||||
"medium": "medio",
|
||||
"high": "alto",
|
||||
"Highest": "più alto",
|
||||
"send_booker_to": "Rimanda la persona che prenota a",
|
||||
"set_priority": "Imposta priorità",
|
||||
"priority_for_user": "Priorità per {{userName}}",
|
||||
"change_priority": "cambia priorità",
|
||||
"field_identifiers_as_variables": "Usa gli identificatori di campo come variabili per il reindirizzamento dell'evento personalizzato",
|
||||
"field_identifiers_as_variables_with_example": "Usa gli identificatori di campo come variabili per il reindirizzamento dell'evento personalizzato (es: {{variable}})",
|
||||
"account_already_linked": "L'account è già collegato",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Aggiungi le tue nuove stringhe qui sopra ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -13,10 +13,17 @@
|
||||
"reset_password_subject": "{{appName}}: Instruções para redefinir sua senha",
|
||||
"verify_email_subject": "{{appName}}: Verificar sua conta",
|
||||
"check_your_email": "Verifique seu e-mail",
|
||||
"old_email_address": "E-mail antigo",
|
||||
"new_email_address": "Novo e-mail",
|
||||
"verify_email_page_body": "Enviamos um e-mail para {{email}}. É importante verificar seu endereço de e-mail para garantir o melhor envio de e-mail e calendário do {{appName}}.",
|
||||
"verify_email_banner_body": "Verifique seu endereço de e-mail para garantir o fornecimento do melhor e-mail e calendário",
|
||||
"verify_email_email_header": "Verifique seu endereço de e-mail",
|
||||
"verify_email_email_button": "Verificar e-mail",
|
||||
"verify_email_change_description": "Você solicitou recentemente a alteração do endereço de e-mail que você usa para entrar em sua conta {{appName}}. Por favor, clique no botão abaixo para confirmar o seu novo endereço de e-mail.",
|
||||
"verify_email_change_success_toast": "Seu e-mail foi atualizado para {{email}}",
|
||||
"verify_email_change_failure_toast": "Não foi possível atualizar o e-mail.",
|
||||
"change_of_email": "Verifique seu novo endereço de e-mail da sua conta {{appName}}",
|
||||
"change_of_email_toast": "Enviamos um link de verificação para {{email}}. Atualizaremos seu endereço de e-mail quando você clicar neste link.",
|
||||
"copy_somewhere_safe": "Salve esta chave de API em um lugar seguro. Você não será capaz de vê-la novamente.",
|
||||
"verify_email_email_body": "Por favor, verifique seu endereço de e-mail clicando no botão abaixo.",
|
||||
"verify_email_by_code_email_body": "Por favor, verifique seu endereço de e-mail usando o código abaixo.",
|
||||
@@ -56,6 +63,17 @@
|
||||
"a_refund_failed": "Um reembolso falhou",
|
||||
"awaiting_payment_subject": "Aguardando pagamento: {{title}} em {{date}}",
|
||||
"meeting_awaiting_payment": "Sua reunião está aguardando o pagamento",
|
||||
"dark_theme_contrast_error": "A cor do tema escuro não foi aprovada pela verificação de contraste. Recomendamos que você altere esta cor para que seus botões fiquem mais visíveis.",
|
||||
"light_theme_contrast_error": "A cor do tema claro não foi aprovada pela verificação de contraste. Recomendamos que você altere esta cor para que seus botões fiquem mais visíveis.",
|
||||
"payment_not_created_error": "O pagamento não pôde ser criado",
|
||||
"couldnt_charge_card_error": "Não foi possível cobrar o cartão para o pagamento",
|
||||
"no_available_users_found_error": "Nenhum usuário disponível foi encontrado. Você poderia tentar outra faixa horária?",
|
||||
"request_body_end_time_internal_error": "Erro interno. O corpo da solicitação não contém a hora final",
|
||||
"create_calendar_event_error": "Não foi possível criar um evento no calendário do organizador",
|
||||
"update_calendar_event_error": "Não foi possível atualizar o evento.",
|
||||
"delete_calendar_event_error": "Não foi possível deletar o evento.",
|
||||
"already_signed_up_for_this_booking_error": "Você já está inscrito para esta reserva.",
|
||||
"hosts_unavailable_for_booking": "Alguns dos anfitriões não estão disponíveis para reserva.",
|
||||
"help": "Ajuda",
|
||||
"price": "Preço",
|
||||
"paid": "Pago",
|
||||
@@ -101,6 +119,7 @@
|
||||
"requested_to_reschedule_subject_attendee": "Ação de reagendamento necessária: reserve outro horário para {{eventType}} com {{name}}",
|
||||
"hi_user_name": "Olá {{name}}",
|
||||
"ics_event_title": "{{eventType}} com {{name}}",
|
||||
"please_book_a_time_sometime_later": "Ops, não foi possível conectá-lo desta vez. Por favor, agende uma chamada mais tarde",
|
||||
"new_event_subject": "Novo evento: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"join_by_entrypoint": "Junte-se através de {{entryPoint}}",
|
||||
"notes": "Notas",
|
||||
@@ -117,15 +136,20 @@
|
||||
"meeting_id": "ID da reunião",
|
||||
"meeting_password": "Senha da reunião",
|
||||
"meeting_url": "URL da reunião",
|
||||
"meeting_url_not_found": "URL da reunião não encontrada",
|
||||
"token_not_found": "Token não encontrado",
|
||||
"some_other_host_already_accepted_the_meeting": "Algum outro anfitrião já aceitou a reunião. Ainda deseja entrar? <1>Entrar na reunião</1>",
|
||||
"meeting_request_rejected": "A sua solicitação de reunião foi rejeitada",
|
||||
"rejected_event_type_with_organizer": "Rejeitado: {{eventType}} com {{organizer}} em {{date}}",
|
||||
"hi": "Olá",
|
||||
"join_team": "Entrar no time",
|
||||
"manage_this_team": "Gerenciar esta equipe",
|
||||
"team_info": "Informações da equipe",
|
||||
"join_meeting": "Entrar na reunião",
|
||||
"request_another_invitation_email": "Se não quiser utilizar {{toEmail}} como email do {{appName}}, ou já tem uma conta em {{appName}}, por favor peça outro convite para esse email.",
|
||||
"you_have_been_invited": "Você recebeu um convite para se juntar ao time {{teamName}}",
|
||||
"user_invited_you": "{{user}} convidou para ingressar na equipe {{team}} de {{entity}} em {{appName}}",
|
||||
"user_invited_you_to_subteam": "{{user}} convidou você para se juntar à equipe {{team}} da organização {{parentTeamName}} no {{appName}}",
|
||||
"hidden_team_member_title": "Você está oculto nesta equipe",
|
||||
"hidden_team_member_message": "Seu assento não é pago, atualize para a versão Pro ou avise o proprietário da equipe de que é possível pagar pelo seu assento.",
|
||||
"hidden_team_owner_message": "É preciso ter uma conta Pro para usar equipes, você permanecerá oculto até a atualização.",
|
||||
@@ -229,6 +253,7 @@
|
||||
"reset_your_password": "Defina sua nova senha com as instruções enviadas para seu endereço de e-mail.",
|
||||
"email_change": "Volte a entrar com seu novo endereço de e-mail e senha.",
|
||||
"create_your_account": "Criar sua conta",
|
||||
"create_your_calcom_account": "Crie sua conta Cal.com",
|
||||
"sign_up": "Cadastro",
|
||||
"youve_been_logged_out": "A sessão foi encerrada",
|
||||
"hope_to_see_you_soon": "Esperamos te ver novamente em breve!",
|
||||
@@ -266,6 +291,9 @@
|
||||
"nearly_there_instructions": "Por último, uma breve descrição de você mesmo e uma foto ajudam a conseguir agendamentos e a mostrar às pessoas com quem elas estão agendando.",
|
||||
"set_availability_instructions": "Definir períodos em que você está disponível de forma recorrente. Mais tarde você poderá criar outros períodos e atribuí-los a diferentes calendários.",
|
||||
"set_availability": "Definir disponibilidade",
|
||||
"set_availbility_description": "Defina os horários que você quer que estejam disponíveis para reservas.",
|
||||
"share_a_link_or_embed": "Compartilhe um link ou incorpore",
|
||||
"share_a_link_or_embed_description": "Compartilhe o link da sua conta {{appName}} ou incorpore no seu site.",
|
||||
"availability_settings": "Configurações de disponibilidade",
|
||||
"continue_without_calendar": "Continuar sem calendário",
|
||||
"continue_with": "Continuar com {{appName}}",
|
||||
@@ -281,6 +309,7 @@
|
||||
"welcome_to_calcom": "Bem vindo ao {{appName}}",
|
||||
"welcome_instructions": "Diga como devemos te chamar e qual o seu fuso horário. Você poderá editar isto mais tarde.",
|
||||
"connect_caldav": "Conectar a um servidor CalDav",
|
||||
"connect_ics_feed": "Conectar a um feed ICS",
|
||||
"connect": "Conectar",
|
||||
"try_for_free": "Experimente gratuitamente",
|
||||
"create_booking_link_with_calcom": "Crie o seu próprio link para reservas com {{appName}}",
|
||||
@@ -419,6 +448,7 @@
|
||||
"browse_api_documentation": "Consulte a nossa documentação da API",
|
||||
"leverage_our_api": "Utilize nossa API para controle e personalização completa.",
|
||||
"create_webhook": "Criar Webhook",
|
||||
"instant_meeting": "Reunião instantânea criada",
|
||||
"booking_cancelled": "Reserva cancelada",
|
||||
"booking_rescheduled": "Reserva Reagendada",
|
||||
"recording_ready": "Link para download de gravação pronto",
|
||||
@@ -533,6 +563,7 @@
|
||||
"enter_number_between_range": "Por favor, insira um número entre 1 e {{maxOccurences}}",
|
||||
"email_address": "Endereço de e-mail",
|
||||
"enter_valid_email": "Insira um e-mail válido",
|
||||
"please_schedule_future_call": " Por favor, agende uma chamada futura se não estivermos disponíveis em {{seconds}} segundos",
|
||||
"location": "Local",
|
||||
"address": "Local",
|
||||
"enter_address": "Insira o endereço",
|
||||
@@ -592,6 +623,7 @@
|
||||
"new_member": "Novo Membro",
|
||||
"invite": "Convidar",
|
||||
"add_team_members": "Adicionar membros da equipe",
|
||||
"add_org_members": "Adicionar membros",
|
||||
"add_team_members_description": "Convidar outros para se juntar à sua equipe",
|
||||
"add_team_member": "Adicionar membro da equipe",
|
||||
"invite_new_member": "Convidar um novo membro",
|
||||
@@ -606,6 +638,7 @@
|
||||
"hide_book_a_team_member_description": "Oculte o botão de agendar um membro da equipe em suas páginas públicas.",
|
||||
"danger_zone": "Zona de perigo",
|
||||
"account_deletion_cannot_be_undone": "Cuidado. A exclusão da conta não pode ser desfeita.",
|
||||
"team_deletion_cannot_be_undone": "Cuidado. A exclusão de uma equipe não pode ser desfeita",
|
||||
"back": "Voltar",
|
||||
"cancel": "Cancelar",
|
||||
"cancel_all_remaining": "Cancelar todos os restantes",
|
||||
@@ -668,6 +701,7 @@
|
||||
"add_members": "Adicionar membros...",
|
||||
"no_assigned_members": "Nenhum membro atribuído",
|
||||
"assigned_to": "Atribuído a",
|
||||
"you_must_be_logged_in_to": "Você deve estar logado em {{url}}",
|
||||
"start_assigning_members_above": "Começar a atribuir os membros acima",
|
||||
"locked_fields_admin_description": "Os membros não poderão editar isto",
|
||||
"locked_fields_member_description": "Esta opção foi bloqueada pelo administrador da equipe",
|
||||
@@ -772,6 +806,7 @@
|
||||
"additional_input_description": "Solicitar que o agendador insira entradas adicionais antes que o pedido seja confirmado",
|
||||
"label": "Etiqueta",
|
||||
"placeholder": "Preenchimento",
|
||||
"display_add_to_calendar_organizer": "Exibir o e-mail 'Adicionar ao calendário' como organizador",
|
||||
"type": "Tipo",
|
||||
"edit": "Editar",
|
||||
"add_input": "Adicionar um campo",
|
||||
|
||||
@@ -13,10 +13,17 @@
|
||||
"reset_password_subject": "{{appName}}: Instruções de redefinição da senha",
|
||||
"verify_email_subject": "{{appName}}: Verifique a sua conta",
|
||||
"check_your_email": "Verifique o seu e-mail",
|
||||
"old_email_address": "E-mail antigo",
|
||||
"new_email_address": "Novo 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",
|
||||
"verify_email_email_button": "Confirmar o seu e-mail",
|
||||
"verify_email_change_description": "Solicitou recentemente a alteração do endereço de e-mail que utiliza para entrar na sua conta {{appName}}. Clique no botão abaixo para confirmar o seu novo endereço de e-mail.",
|
||||
"verify_email_change_success_toast": "Atualizou o seu e-mail para {{email}}",
|
||||
"verify_email_change_failure_toast": "Falha ao atualizar o e-mail.",
|
||||
"change_of_email": "Confirme o seu novo endereço de e-mail para {{appName}}",
|
||||
"change_of_email_toast": "Enviámos uma ligação de confirmação para {{email}}. Atualizaremos o seu endereço de e-mail após clicar nessa ligação.",
|
||||
"copy_somewhere_safe": "Guarde esta chave da API num local seguro. Não poderá consultar a mesma novamente.",
|
||||
"verify_email_email_body": "Confirme o seu endereço de e-mail clicando no botão abaixo.",
|
||||
"verify_email_by_code_email_body": "Confirme o seu endereço de e-mail utilizando o código abaixo.",
|
||||
@@ -142,6 +149,7 @@
|
||||
"request_another_invitation_email": "Se não quiser utilizar {{toEmail}} como email do {{appName}}, ou já tem uma conta em {{appName}}, por favor peça outro convite para esse email.",
|
||||
"you_have_been_invited": "Recebeu um convite para juntar-se à equipa {{teamName}}",
|
||||
"user_invited_you": "{{user}} enviou um convite para se juntar a {{team}} de {{entity}} em {{appName}}",
|
||||
"user_invited_you_to_subteam": "{{user}} enviou um convite para se juntar à equipa {{team}} da organização {{parentTeamName}} em {{appName}}",
|
||||
"hidden_team_member_title": "Está oculto nesta equipa",
|
||||
"hidden_team_member_message": "O seu lugar não está pago, faça upgrade para PRO ou informe ao proprietário da equipa que pode pagar pelo seu lugar.",
|
||||
"hidden_team_owner_message": "Precisa de uma conta Pro para usar equipas, ficará oculto até que actualize.",
|
||||
@@ -245,6 +253,7 @@
|
||||
"reset_your_password": "Defina a sua nova palavra-passe seguindo as instruções enviadas para o seu endereço de e-mail.",
|
||||
"email_change": "Inicie sessão novamente com o seu novo endereço de e-mail e palavra-passe.",
|
||||
"create_your_account": "Crie a sua conta",
|
||||
"create_your_calcom_account": "Crie a sua conta Cal.com",
|
||||
"sign_up": "Registar",
|
||||
"youve_been_logged_out": "A sessão foi encerrada",
|
||||
"hope_to_see_you_soon": "Esperamos ver-nos em breve de novo!",
|
||||
@@ -282,6 +291,9 @@
|
||||
"nearly_there_instructions": "Por último, uma breve descrição sobre si e uma fotografia ajudam a obter reservas e a mostrar às pessoas com quem estão a reservar.",
|
||||
"set_availability_instructions": "Definir períodos em que está disponível numa base recorrente. Mais tarde poderá criar mais destes, e atribuí-los a diferentes calendários.",
|
||||
"set_availability": "Definir disponibilidade",
|
||||
"set_availbility_description": "Defina agendamentos para os horários que pretende que sejam reservados.",
|
||||
"share_a_link_or_embed": "Partilhe uma ligação ou incorpore",
|
||||
"share_a_link_or_embed_description": "Partilhe a sua ligação {{appName}} ou incorpore no seu site.",
|
||||
"availability_settings": "Configurações de disponibilidade",
|
||||
"continue_without_calendar": "Continuar sem calendário",
|
||||
"continue_with": "Continuar com {{appName}}",
|
||||
@@ -297,6 +309,7 @@
|
||||
"welcome_to_calcom": "Boas-vindas ao {{appName}}",
|
||||
"welcome_instructions": "Diga-nos como lhe devemos chamar e qual o seu fuso horário. Poderá editar isto mais tarde.",
|
||||
"connect_caldav": "Ligar a servidor CalDav",
|
||||
"connect_ics_feed": "Ligue-se a uma Feed ICS",
|
||||
"connect": "Ligar",
|
||||
"try_for_free": "Experimente gratuitamente",
|
||||
"create_booking_link_with_calcom": "Crie a sua própria ligação de reservas com {{appName}}",
|
||||
@@ -435,6 +448,7 @@
|
||||
"browse_api_documentation": "Consulte a nossa documentação da API",
|
||||
"leverage_our_api": "Tire partido da nossa API para controlo e personalização total.",
|
||||
"create_webhook": "Criar Webhook",
|
||||
"instant_meeting": "Reunião instantânea criada",
|
||||
"booking_cancelled": "Reserva cancelada",
|
||||
"booking_rescheduled": "Reserva Reagendada",
|
||||
"recording_ready": "Ligação de transferência da gravação pronta",
|
||||
@@ -549,6 +563,7 @@
|
||||
"enter_number_between_range": "Introduza um número entre 1 e {{maxOccurences}}",
|
||||
"email_address": "Endereço de email",
|
||||
"enter_valid_email": "Insira um e-mail válido",
|
||||
"please_schedule_future_call": " Se não estivermos disponíveis em {{seconds}} segundos, por favor, agende uma chamada para mais tarde",
|
||||
"location": "Localização",
|
||||
"address": "Local",
|
||||
"enter_address": "Especifique o endereço",
|
||||
@@ -608,6 +623,7 @@
|
||||
"new_member": "Novo Membro",
|
||||
"invite": "Convidar",
|
||||
"add_team_members": "Adicionar membros da equipa",
|
||||
"add_org_members": "Adicionar membros",
|
||||
"add_team_members_description": "Convidar outros a juntarem-se à sua equipa",
|
||||
"add_team_member": "Adicionar membro da equipa",
|
||||
"invite_new_member": "Convidar um Novo Membro",
|
||||
@@ -622,6 +638,7 @@
|
||||
"hide_book_a_team_member_description": "Ocultar o botão Reservar um Membro da equipa das suas páginas públicas.",
|
||||
"danger_zone": "Zona de perigo",
|
||||
"account_deletion_cannot_be_undone": "Cuidado. A eliminação da conta não pode ser anulada.",
|
||||
"team_deletion_cannot_be_undone": "Cuidado. A eliminação de uma equipa não pode ser anulada",
|
||||
"back": "Anterior",
|
||||
"cancel": "Cancelar",
|
||||
"cancel_all_remaining": "Cancelar todos os restantes",
|
||||
@@ -672,6 +689,7 @@
|
||||
"default_duration": "Duração predefinida",
|
||||
"default_duration_no_options": "Por favor, selecione primeiro as durações disponíveis",
|
||||
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
|
||||
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
|
||||
"minutes": "Minutos",
|
||||
"round_robin": "Round Robin",
|
||||
"round_robin_description": "Reuniões de ciclo entre vários membros da equipa.",
|
||||
@@ -684,6 +702,7 @@
|
||||
"add_members": "Adicionar membros...",
|
||||
"no_assigned_members": "Sem membros atribuídos",
|
||||
"assigned_to": "Atribuído a",
|
||||
"you_must_be_logged_in_to": "Deve ter sessão iniciada para {{url}}",
|
||||
"start_assigning_members_above": "Comece a atribuir membros acima",
|
||||
"locked_fields_admin_description": "Os membros não poderão editar isto",
|
||||
"locked_fields_member_description": "Esta opção foi bloqueada pelo administrador da equipa",
|
||||
@@ -788,6 +807,7 @@
|
||||
"additional_input_description": "Requerer que o responsável pelo agendamento especifique entradas complementares antes do pedido ser confirmado",
|
||||
"label": "Etiqueta",
|
||||
"placeholder": "Preenchimento",
|
||||
"display_add_to_calendar_organizer": "Mostrar o e-mail 'Adicionar ao calendário' como organizador",
|
||||
"type": "Tipo",
|
||||
"edit": "Editar",
|
||||
"add_input": "Adicionar um Campo",
|
||||
@@ -796,6 +816,9 @@
|
||||
"requires_confirmation_description": "A reserva precisa ser confirmada manualmente antes de ser enviada para as integrações e ser enviada uma mensagem de confirmação.",
|
||||
"recurring_event": "Evento recorrente",
|
||||
"recurring_event_description": "As pessoas podem subscrever para eventos recorrentes",
|
||||
"cannot_be_used_with_paid_event_types": "Não pode ser utilizado com tipos de eventos pagos",
|
||||
"warning_payment_instant_meeting_event": "As Reuniões Instantâneas ainda não são suportadas com eventos recorrentes e aplicações de pagamento",
|
||||
"warning_instant_meeting_experimental": "Experimental: atualmente os Eventos de Reuniões Instantâneas são experimentais.",
|
||||
"starting": "A iniciar",
|
||||
"disable_guests": "Desativar Convidados",
|
||||
"disable_guests_description": "Desativar a adição de convidados adicionais durante a reserva.",
|
||||
@@ -804,6 +827,7 @@
|
||||
"private_link_label": "Ligação privada",
|
||||
"private_link_hint": "A sua ligação será renovada após cada utilização",
|
||||
"copy_private_link": "Copiar ligação privada",
|
||||
"copy_private_link_to_event": "Copiar ligação privada para o evento",
|
||||
"private_link_description": "Crie um URL privado para partilhar sem expor o seu nome de utilizador do {{appName}}",
|
||||
"invitees_can_schedule": "Os convidados podem agendar",
|
||||
"date_range": "Intervalo de Datas",
|
||||
@@ -863,6 +887,7 @@
|
||||
"next_step": "Ignorar passo",
|
||||
"prev_step": "Passo anterior",
|
||||
"install": "Instalar",
|
||||
"start_paid_trial": "Iniciar avaliação gratuita",
|
||||
"installed": "Instalado",
|
||||
"active_install_one": "{{count}} instalação activa",
|
||||
"active_install_other": "{{count}} instalações activas",
|
||||
@@ -877,6 +902,7 @@
|
||||
"toggle_calendars_conflict": "Escolha os calendários que deseja verificar para evitar conflitos e duplicação de reservas.",
|
||||
"connect_additional_calendar": "Ligar um calendário adicional",
|
||||
"calendar_updated_successfully": "Calendário atualizado com sucesso",
|
||||
"check_here": "Verificar aqui",
|
||||
"conferencing": "Conferência",
|
||||
"calendar": "Calendário",
|
||||
"payments": "Pagamentos",
|
||||
@@ -1048,6 +1074,9 @@
|
||||
"user_impersonation_heading": "Representação de utilizador",
|
||||
"user_impersonation_description": "Permite à nossa equipa de suporte iniciar sessão temporariamente em seu nome para rapidamente resolver quaisquer erros que nos reporte.",
|
||||
"team_impersonation_description": "Permite que os administradores da sua equipa iniciem a sessão temporariamente em seu nome.",
|
||||
"cal_signup_description": "Gratuito para pessoas singulares. Planos de equipa para funcionalidades colaborativas.",
|
||||
"make_org_private": "Tornar a organização privada",
|
||||
"make_org_private_description": "Os membros da sua organização não poderão ver outros membros da organização quando esta opção estiver ativada.",
|
||||
"make_team_private": "Tornar a equipa privada",
|
||||
"make_team_private_description": "Os membros da sua equipa não poderão ver outros membros da equipa quando esta opção estiver ativada.",
|
||||
"you_cannot_see_team_members": "Não pode ver todos os membros da equipa de uma equipa privada.",
|
||||
@@ -1107,6 +1136,7 @@
|
||||
"developer_documentation": "Documentação para programadores",
|
||||
"get_in_touch": "Entre em contacto",
|
||||
"contact_support": "Contacte o suporte",
|
||||
"premium_support": "Suporte Premium",
|
||||
"community_support": "Apoio da Comunidade",
|
||||
"feedback": "Feedback",
|
||||
"submitted_feedback": "Obrigado pelo seu feedback!",
|
||||
@@ -1270,6 +1300,7 @@
|
||||
"default_calendar_selected": "Calendário predefinido",
|
||||
"hide_from_profile": "Ocultar do perfil",
|
||||
"event_setup_tab_title": "Configuração do evento",
|
||||
"availability_not_found_in_schedule_error": "Não foi encontrada nenhuma disponibilidade na agenda",
|
||||
"event_limit_tab_title": "Limites",
|
||||
"event_limit_tab_description": "Com que frequência pode ser reservado",
|
||||
"event_advanced_tab_description": "Definições do calendário e muito mais...",
|
||||
@@ -1290,6 +1321,7 @@
|
||||
"do_this": "Fazer isto",
|
||||
"turn_off": "Desligar",
|
||||
"turn_on": "Ativar",
|
||||
"cancelled_bookings_cannot_be_rescheduled": "As reservas canceladas não podem ser reagendadas",
|
||||
"settings_updated_successfully": "Definições atualizadas com sucesso",
|
||||
"error_updating_settings": "Erro ao atualizar as definições",
|
||||
"personal_cal_url": "O meu endereço pessoal do {{appName}}",
|
||||
@@ -1313,6 +1345,7 @@
|
||||
"customize_your_brand_colors": "Personalize a cor da sua marca na sua página de reservas.",
|
||||
"pro": "Pro",
|
||||
"removes_cal_branding": "Remove qualquer identificação de marca relacionada o {{appName}}, por exemplo, 'Com tecnologia {{appName}}.'",
|
||||
"instant_meeting_with_title": "Reunião instantânea com {{name}}",
|
||||
"profile_picture": "Imagem do perfil",
|
||||
"upload": "Carregar",
|
||||
"add_profile_photo": "Adicionar imagem de perfil",
|
||||
@@ -1368,6 +1401,7 @@
|
||||
"event_name_info": "O nome do tipo de evento",
|
||||
"event_date_info": "A data do evento",
|
||||
"event_time_info": "A hora de início do evento",
|
||||
"event_type_not_found": "Tipo de evento não encontrado",
|
||||
"location_variable": "Localização",
|
||||
"location_info": "O local do evento",
|
||||
"additional_notes_variable": "Notas Adicionais",
|
||||
@@ -1410,6 +1444,7 @@
|
||||
"slot_length": "Comprimento do espaço",
|
||||
"booking_appearance": "Aparência da reserva",
|
||||
"appearance_team_description": "Faça a gestão das definições de aparência da reserva da sua equipa",
|
||||
"appearance_org_description": "Faça a gestão das definições de aparência da reserva da sua organização",
|
||||
"only_owner_change": "Só o proprietário desta equipa pode fazer alterações na reserva da equipa ",
|
||||
"team_disable_cal_branding_description": "Remove qualquer identificação de marca relacionada o {{appName}}, por exemplo, 'Com tecnologia {{appName}}'",
|
||||
"invited_by_team": "{{teamName}} enviou um convite para se juntar à sua equipa como {{role}}",
|
||||
@@ -1463,6 +1498,7 @@
|
||||
"password_updated": "Palavra-passe atualizada!",
|
||||
"pending_payment": "Pagamento pendente",
|
||||
"pending_invites": "Convites pendentes",
|
||||
"pending_organization_invites": "Convites de organização pendentes",
|
||||
"not_on_cal": "Não existe em {{appName}}",
|
||||
"no_calendar_installed": "Nenhum calendário instalado",
|
||||
"no_calendar_installed_description": "Ainda não associou nenhum dos seus calendários",
|
||||
@@ -1474,6 +1510,8 @@
|
||||
"report_app": "Reportar aplicação",
|
||||
"limit_booking_frequency": "Limitar a frequência das reservas",
|
||||
"limit_booking_frequency_description": "Limitar o número de vezes que este evento pode ser reservado",
|
||||
"limit_booking_only_first_slot": "Limitar a reserva apenas à primeira vaga",
|
||||
"limit_booking_only_first_slot_description": "Permitir que apenas a primeira vaga de todos os dias seja reservada",
|
||||
"limit_total_booking_duration": "Limitar a duração total da reserva",
|
||||
"limit_total_booking_duration_description": "Limitar o tempo total que pode ser reservado para este evento",
|
||||
"add_limit": "Adicionar limite",
|
||||
@@ -1541,10 +1579,14 @@
|
||||
"your_org_disbanded_successfully": "A sua organização foi dissolvida com sucesso",
|
||||
"error_creating_team": "Erro ao criar a equipa",
|
||||
"you": "Você",
|
||||
"or_continue_with": "Ou continuar com",
|
||||
"resend_email": "Reenviar e-mail",
|
||||
"member_already_invited": "O membro já foi convidado",
|
||||
"already_in_use_error": "O nome de utilizador já está a ser utilizado",
|
||||
"enter_email_or_username": "Especifique um e-mail ou nome de utilizador",
|
||||
"enter_email": "Inserir um e-mail",
|
||||
"enter_emails": "Inserir e-mails",
|
||||
"too_many_invites": "Está limitado a convidar um máximo de {{nbUsers}} utilizadores de cada vez.",
|
||||
"team_name_taken": "Este nome já está a ser utilizado",
|
||||
"must_enter_team_name": "Deve especificar um nome para a equipa",
|
||||
"team_url_required": "Deve especificar um endereço para a equipa",
|
||||
@@ -1624,6 +1666,7 @@
|
||||
"individual": "Individual",
|
||||
"all_bookings_filter_label": "Todas as reservas",
|
||||
"all_users_filter_label": "Todos os utilizadores",
|
||||
"all_event_types_filter_label": "Todos os tipos de evento",
|
||||
"your_bookings_filter_label": "As suas reservas",
|
||||
"meeting_url_variable": "URL da reunião",
|
||||
"meeting_url_info": "URL da reunião de conferência",
|
||||
@@ -1720,9 +1763,11 @@
|
||||
"organizer_timezone": "Fuso horário do organizador",
|
||||
"email_user_cta": "Ver convite",
|
||||
"email_no_user_invite_heading_team": "Recebeu um convite para fazer parte de uma equipa {{appName}}",
|
||||
"email_no_user_invite_heading_subteam": "Recebeu um convite para fazer parte de uma equipa da organização {{parentTeamName}}",
|
||||
"email_no_user_invite_heading_org": "Recebeu um convite para fazer parte de uma organização {{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_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_user_invite_subheading_subteam": "Recebeu um convite de {{invitedBy}} para fazer parte da equipa {{teamName}} da organização {{parentTeamName}} 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_user_invite_subheading_org": "Recebeu um convite de {{invitedBy}} para fazer parte de {{teamName}} da sua organização em {{appName}}. {{appName}} é a solução de agendamento de eventos que lhe permite a si e à sua organização agendar reuniões sem o pingue-pongue de e-mails.",
|
||||
"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",
|
||||
@@ -1855,7 +1900,9 @@
|
||||
"review_event_type": "Rever Tipo de Evento",
|
||||
"looking_for_more_analytics": "À procura de mais análises?",
|
||||
"looking_for_more_insights": "À procura de mais Insights?",
|
||||
"filters": "Filtros",
|
||||
"add_filter": "Adicionar filtro",
|
||||
"remove_filters": "Limpar todos os filtros",
|
||||
"select_user": "Selecionar utilizador",
|
||||
"select_event_type": "Selecionar tipo de evento",
|
||||
"select_date_range": "Selecionar intervalo de datas",
|
||||
@@ -1946,6 +1993,7 @@
|
||||
"connect_google_workspace": "Associar o Google Workspace",
|
||||
"google_workspace_admin_tooltip": "Tem de ser um Administrador do Workspace para utilizar esta funcionalidade",
|
||||
"first_event_type_webhook_description": "Crie o seu primeiro webhook para este tipo de evento",
|
||||
"create_instant_meeting_webhook_description": "Crie o seu primeiro webhook com 'Reunião instantânea criada' como acionador para este tipo de evento",
|
||||
"install_app_on": "Instalar aplicação em",
|
||||
"create_for": "Criar para",
|
||||
"currency": "Moeda",
|
||||
@@ -2010,6 +2058,8 @@
|
||||
"add_times_to_your_email": "Selecione alguns horários disponíveis e incorpore-os no seu E-mail",
|
||||
"select_time": "Selecionar horário",
|
||||
"select_date": "Selecionar data",
|
||||
"connecting_you_to_someone": "Estamos a conectá-lo a alguém.",
|
||||
"please_do_not_close_this_tab": "Não feche este separador",
|
||||
"see_all_available_times": "Ver todos os horários disponíveis",
|
||||
"org_team_names_example_1": "Por exemplo, Equipa de Marketing",
|
||||
"org_team_names_example_2": "Por exemplo, Equipa de Vendas",
|
||||
@@ -2027,8 +2077,12 @@
|
||||
"description_requires_booker_email_verification": "Para garantir a confirmação do e-mail do responsável pela reserva antes de agendar eventos",
|
||||
"requires_confirmation_mandatory": "As mensagens de texto só podem ser enviadas aos participantes quando o tipo de evento requerer confirmação.",
|
||||
"organizations": "Organizações",
|
||||
"upload_cal_video_logo": "Carregar logótipo de Cal Video",
|
||||
"update_cal_video_logo": "Atualizar logótipo de Cal Video",
|
||||
"cal_video_logo_upload_instruction": "Para garantir que o seu logótipo seja visível contra o fundo escuro do Cal Video, carregue uma imagem de cores claras em formato PNG ou SVG, para manter a transparência.",
|
||||
"org_admin_other_teams": "Outras equipas",
|
||||
"org_admin_other_teams_description": "Aqui pode ver equipas dentro da sua organização e das quais não faz parte. Pode adicionar-se às mesmas, se necessário.",
|
||||
"not_part_of_org": "Você não faz parte de nenhuma organização",
|
||||
"no_other_teams_found": "Não foram encontradas outras equipas",
|
||||
"no_other_teams_found_description": "Não há outras equipas nesta organização.",
|
||||
"attendee_first_name_variable": "Primeiro nome do participante",
|
||||
@@ -2064,7 +2118,9 @@
|
||||
"org_error_processing": "Ocorreu um erro ao processar esta organização",
|
||||
"orgs_page_description": "Uma lista de todas as organizações. Aceitar uma organização permitirá que todos os utilizadores com esse domínio de e-mail se registem SEM a verificação de e-mail.",
|
||||
"unverified": "Não verificada",
|
||||
"verified": "Confirmado",
|
||||
"dns_missing": "DNS em falta",
|
||||
"dns_configured": "DNS configurado",
|
||||
"mark_dns_configured": "Marcar como o DNS configurado",
|
||||
"value": "Valor",
|
||||
"your_organization_updated_sucessfully": "A sua organização foi atualizada com sucesso",
|
||||
@@ -2074,8 +2130,11 @@
|
||||
"oAuth": "OAuth",
|
||||
"recently_added": "Adicionados recentemente",
|
||||
"connect_all_calendars": "Associe todos os seus calendários",
|
||||
"connect_all_calendars_description": "{{appName}} lê a disponibilidade de todos os seus calendários existentes.",
|
||||
"workflow_automation": "Automação de fluxo de trabalho",
|
||||
"workflow_automation_description": "Personalize a sua experiência de agendamento com fluxos de trabalho",
|
||||
"scheduling_for_your_team": "Automação de fluxo de trabalho",
|
||||
"scheduling_for_your_team_description": "Faça o agendamento para a sua equipa, com agendamento coletivo e distribuição equilibrada",
|
||||
"no_members_found": "Nenhum membro encontrado",
|
||||
"event_setup_length_error": "Configuração do Evento: a duração deve ser de, pelo menos, 1 minuto.",
|
||||
"availability_schedules": "Horários de Disponibilidade",
|
||||
@@ -2108,10 +2167,100 @@
|
||||
"overlay_my_calendar": "Sobrepor o meu calendário",
|
||||
"overlay_my_calendar_toc": "Ao ligar-se ao seu calendário, você aceita a nossa política de privacidade e termos de utilização. Pode revogar o acesso a qualquer momento.",
|
||||
"view_overlay_calendar_events": "Consulte os eventos do seu calendário para evitar agendamentos conflituantes.",
|
||||
"join_event_location": "Junte-se a {{eventLocationType}}",
|
||||
"troubleshooting": "Resolução de problemas",
|
||||
"calendars_were_checking_for_conflicts": "Calendários que estamos a verificar por conflitos",
|
||||
"availabilty_schedules": "Horários de disponibilidade",
|
||||
"manage_calendars": "Gerir calendários",
|
||||
"manage_availability_schedules": "Gerir horários de disponibilidade",
|
||||
"lock_timezone_toggle_on_booking_page": "Bloquear fuso horário na página de reserva",
|
||||
"description_lock_timezone_toggle_on_booking_page": "Para bloquear o fuso horário na página de reservas. Útil para eventos presenciais.",
|
||||
"event_setup_multiple_payment_apps_error": "Só pode ter uma aplicação de pagamentos ativada por tipo de evento.",
|
||||
"number_in_international_format": "Insira o número no formato internacional.",
|
||||
"install_calendar": "Instalar Calendário",
|
||||
"branded_subdomain": "Subdomínio de marca",
|
||||
"branded_subdomain_description": "Obtenha o seu próprio subdomínio de marca. Por exemplo, acme.cal.com",
|
||||
"org_insights": "Insights de toda a organização",
|
||||
"org_insights_description": "Compreenda como toda a sua organização está a utilizar o tempo",
|
||||
"extensive_whitelabeling": "Apoio dedicado à integração e engenharia",
|
||||
"extensive_whitelabeling_description": "Personalize a sua experiência de agendamento com o seu próprio logótipo, cores e muito mais",
|
||||
"unlimited_teams": "Equipas ilimitadas",
|
||||
"unlimited_teams_description": "Adicione à sua organização tantas subequipas quanto for necessário",
|
||||
"unified_billing": "Faturação unificada",
|
||||
"unified_billing_description": "Adicione um único cartão de crédito para pagar todas as subscrições da sua equipa",
|
||||
"advanced_managed_events": "Tipos de evento geridos avançados",
|
||||
"advanced_managed_events_description": "Adicione um único cartão de crédito para pagar todas as subscrições da sua equipa",
|
||||
"enterprise_description": "Atualize para Enterprise para criar a sua Organização",
|
||||
"create_your_org": "Crie a sua Organização",
|
||||
"create_your_org_description": "Atualize para Enterprise e receba um subdomínio, faturação unificada, Insights, personalização abrangente da imagem corporativa e muito mais",
|
||||
"other_payment_app_enabled": "Só pode ativar uma aplicação de pagamento por tipo de evento",
|
||||
"admin_delete_organization_description": "<ul><li>As equipas que são membros desta organização também serão eliminadas, em conjunto com os respetivos tipos de evento</li><li>Utilizadores que faziam parte da organização não serão eliminados e os seus tipos de evento também permanecerão intactos.</li><li>Os nomes de utilizador serão alterados para permitir que possam existir fora da organização</li></ul>",
|
||||
"admin_delete_organization_title": "Eliminar {{organizationName}}?",
|
||||
"published": "Publicada",
|
||||
"unpublished": "Não publicada",
|
||||
"publish": "Publicar",
|
||||
"org_publish_error": "Não foi possível publicar a organização",
|
||||
"troubleshooter_tooltip": "Abra o solucionador de problemas e descubra o que está errado com o seu agendamento",
|
||||
"need_help": "Precisa de ajuda?",
|
||||
"troubleshooter": "Solucionador de problemas",
|
||||
"please_install_a_calendar": "Por favor, instale um calendário",
|
||||
"instant_tab_title": "Reserva imediata",
|
||||
"instant_event_tab_description": "Permita que as pessoas reservem imediatamente",
|
||||
"uprade_to_create_instant_bookings": "Atualize para Enterprise e permita que os convidados se juntem a uma chamada instantânea em que os participantes podem entrar diretamente. Isto é apenas para tipos de evento de equipa",
|
||||
"dont_want_to_wait": "Não quer esperar?",
|
||||
"meeting_started": "Reunião iniciada",
|
||||
"pay_and_book": "Pagar para reservar",
|
||||
"booking_not_found_error": "Não foi possível encontrar a reserva",
|
||||
"booking_seats_full_error": "Os lugares para reserva estão cheios",
|
||||
"missing_payment_credential_error": "Credenciais de pagamento em falta",
|
||||
"missing_payment_app_id_error": "ID da aplicação de pagamento em falta",
|
||||
"not_enough_available_seats_error": "A reserva não tem lugares disponíveis suficientes",
|
||||
"user_redirect_title": "{{username}} está ausente por um breve período de tempo.",
|
||||
"user_redirect_description": "Entretanto, {{profile.username}} ficará responsável por todas as novas reuniões agendadas em nome de {{username}}.",
|
||||
"out_of_office": "Fora do escritório",
|
||||
"out_of_office_description": "Configure as ações no seu perfil enquanto estiver ausente.",
|
||||
"send_request": "Enviar pedido",
|
||||
"start_date_and_end_date_required": "Data de início e data de fim são necessárias",
|
||||
"start_date_must_be_before_end_date": "Data de início deve ser anterior à data de fim",
|
||||
"start_date_must_be_in_the_future": "Data de início deve ser no futuro",
|
||||
"user_not_found": "Utilizador não encontrado",
|
||||
"out_of_office_entry_already_exists": "Já existe uma entrada de fora do escritório",
|
||||
"out_of_office_id_required": "É necessária a ID da entrada de fora do escritório",
|
||||
"booking_redirect_infinite_not_allowed": "Já existe um redirecionamento de reservas desse utilizador para si.",
|
||||
"success_entry_created": "Criou uma nova entrada com sucesso",
|
||||
"booking_redirect_email_subject": "Notificação de redirecionamento de reserva",
|
||||
"booking_redirect_email_title": "Notificação de redirecionamento de reserva",
|
||||
"booking_redirect_email_description": "Recebeu um redirecionamento de reservas de {{toName}}, pelo que as ligações do respetivo perfil serão redirecionados para o seu perfil durante o intervalo de tempo: ",
|
||||
"success_accept_booking_redirect": "Aceitou este pedido de redirecionamento de reservas.",
|
||||
"success_reject_booking_redirect": "Rejeitou este pedido de redirecionamento de reservas.",
|
||||
"copy_link_booking_redirect_request": "Copiar ligação para partilhar o pedido",
|
||||
"booking_redirect_request_title": "Pedido de redirecionamento de reservas",
|
||||
"select_team_member": "Selecionar o membro da equipa",
|
||||
"going_away_title": "Vai ausentar-se? Simplesmente marque a ligação do seu perfil como indisponível por um determinado período de tempo.",
|
||||
"redirect_team_enabled": "Redirecionar o seu perfil para outro membro da equipa",
|
||||
"redirect_team_disabled": "Redirecionar o seu perfil para outro membro da equipa (é necessário um plano Team)",
|
||||
"out_of_office_unavailable_list": "Lista de indisponibilidades de fora do escritório",
|
||||
"success_deleted_entry_out_of_office": "Entrada eliminada com sucesso",
|
||||
"temporarily_out_of_office": "Temporariamente fora do escritório?",
|
||||
"add_a_redirect": "Adicionar um redirecionamento",
|
||||
"create_entry": "Criar entrada",
|
||||
"time_range": "Intervalo de tempo",
|
||||
"automatically_add_all_team_members": "Adicionar todos os membros da equipa, incluindo membros futuros",
|
||||
"redirect_to": "Redirecionar para",
|
||||
"having_trouble_finding_time": "Com dificuldades em encontrar um horário?",
|
||||
"show_more": "Mostrar mais",
|
||||
"assignment_description": "Agende reuniões quando todos estiverem disponíveis ou rode entre os membros da sua equipa",
|
||||
"lowest": "a mais baixa",
|
||||
"low": "baixa",
|
||||
"medium": "média",
|
||||
"high": "alta",
|
||||
"Highest": "a mais alta",
|
||||
"send_booker_to": "Enviar responsável pela reserva para",
|
||||
"set_priority": "Definir prioridade",
|
||||
"priority_for_user": "Prioridade para {{userName}}",
|
||||
"change_priority": "alterar prioridade",
|
||||
"field_identifiers_as_variables": "Utilize identificadores de campo como variáveis para o seu redirecionamento de evento personalizado",
|
||||
"field_identifiers_as_variables_with_example": "Utilize identificadores de campo como variáveis para o seu redirecionamento de evento personalizado (ex.: {{variable}})",
|
||||
"account_already_linked": "A conta já está vinculada",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(result.newUserIds).toEqual(undefined);
|
||||
expect(result.oldUserIds).toEqual(undefined);
|
||||
@@ -62,6 +63,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(result.newUserIds).toEqual(undefined);
|
||||
expect(result.oldUserIds).toEqual(undefined);
|
||||
@@ -88,6 +90,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(result.newUserIds).toEqual(undefined);
|
||||
expect(result.oldUserIds).toEqual(undefined);
|
||||
@@ -125,6 +128,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
@@ -173,13 +177,14 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {
|
||||
bookingLimits: undefined,
|
||||
},
|
||||
});
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...evType,
|
||||
hidden: false,
|
||||
bookingLimits: undefined,
|
||||
durationLimits: undefined,
|
||||
recurringEvent: undefined,
|
||||
hashedLink: { create: { link: expect.any(String) } },
|
||||
},
|
||||
where: {
|
||||
@@ -207,6 +212,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(result.newUserIds).toEqual([]);
|
||||
expect(result.oldUserIds).toEqual([]);
|
||||
@@ -232,6 +238,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
// Have been called
|
||||
expect(result.newUserIds).toEqual([5]);
|
||||
@@ -270,6 +277,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
@@ -318,14 +326,15 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {
|
||||
length: 30,
|
||||
},
|
||||
});
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...evType,
|
||||
bookingLimits: undefined,
|
||||
durationLimits: undefined,
|
||||
recurringEvent: undefined,
|
||||
scheduleId: undefined,
|
||||
hidden: false,
|
||||
length: 30,
|
||||
hashedLink: undefined,
|
||||
},
|
||||
where: {
|
||||
userId_parentId: {
|
||||
@@ -379,6 +388,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
@@ -404,13 +414,8 @@ describe("handleChildrenEventTypes", () => {
|
||||
});
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...evType,
|
||||
bookingLimits: undefined,
|
||||
durationLimits: undefined,
|
||||
recurringEvent: undefined,
|
||||
hidden: false,
|
||||
hashedLink: undefined,
|
||||
workflows: undefined,
|
||||
scheduleId: undefined,
|
||||
},
|
||||
where: {
|
||||
userId_parentId: {
|
||||
|
||||
@@ -34,6 +34,7 @@ import ics_feedcalendar_config_json from "./ics-feedcalendar/config.json";
|
||||
import intercom_config_json from "./intercom/config.json";
|
||||
import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata";
|
||||
import { metadata as larkcalendar__metadata_ts } from "./larkcalendar/_metadata";
|
||||
import linear_config_json from "./linear/config.json";
|
||||
import make_config_json from "./make/config.json";
|
||||
import matomo_config_json from "./matomo/config.json";
|
||||
import metapixel_config_json from "./metapixel/config.json";
|
||||
@@ -116,6 +117,7 @@ export const appStoreMetadata = {
|
||||
intercom: intercom_config_json,
|
||||
jitsivideo: jitsivideo__metadata_ts,
|
||||
larkcalendar: larkcalendar__metadata_ts,
|
||||
linear: linear_config_json,
|
||||
make: make_config_json,
|
||||
matomo: matomo_config_json,
|
||||
metapixel: metapixel_config_json,
|
||||
|
||||
@@ -34,6 +34,7 @@ export const apiHandlers = {
|
||||
intercom: import("./intercom/api"),
|
||||
jitsivideo: import("./jitsivideo/api"),
|
||||
larkcalendar: import("./larkcalendar/api"),
|
||||
linear: import("./linear/api"),
|
||||
make: import("./make/api"),
|
||||
matomo: import("./matomo/api"),
|
||||
metapixel: import("./metapixel/api"),
|
||||
|
||||
@@ -11,6 +11,7 @@ import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
|
||||
export const scopes = [
|
||||
"https://www.googleapis.com/auth/calendar.readonly",
|
||||
"https://www.googleapis.com/auth/calendar.events",
|
||||
"https://www.googleapis.com/auth/userinfo.profile",
|
||||
];
|
||||
|
||||
let client_id = "";
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { Auth } from "googleapis";
|
||||
import { google } from "googleapis";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { WEBAPP_URL_FOR_OAUTH, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
@@ -91,6 +93,10 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
throw new HttpError({ message: "Internal Error", statusCode: 500 });
|
||||
}
|
||||
|
||||
// Update the user's profile photo with google profile photo if it's null
|
||||
// Since we don't want to block the user from using the app if this fails, we don't await this
|
||||
updateProfilePhoto(oAuth2Client, req.session.user.id);
|
||||
|
||||
const credential = await prisma.credential.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
@@ -156,6 +162,23 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
);
|
||||
}
|
||||
|
||||
async function updateProfilePhoto(oAuth2Client: Auth.OAuth2Client, userId: number) {
|
||||
try {
|
||||
const oauth2 = google.oauth2({ version: "v2", auth: oAuth2Client });
|
||||
const userDetails = await oauth2.userinfo.get();
|
||||
if (userDetails.data?.picture) {
|
||||
await prisma.user.update({
|
||||
where: { id: userId, avatarUrl: null, avatar: null },
|
||||
data: {
|
||||
avatarUrl: userDetails.data.picture,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error updating avatarUrl from google calendar connect", error);
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
items:
|
||||
- 1.jpg
|
||||
---
|
||||
|
||||
{DESCRIPTION}
|
||||
|
||||
### Overview
|
||||
The Linear integration with Google calendar allows you to connect your calendar and automate your status. If your teammates need to know whether you’re out of office, now they will.
|
||||
|
||||
### How it works
|
||||
Once connected, your Linear status will detect out of office events in Google calendar and update your status when those events are upcoming or active.
|
||||
|
||||
### To Configure
|
||||
Install Google Calendar from your Linear settings under My Account > Linked accounts.
|
||||
|
||||
Connect your account and sign into your Google account to allow access.
|
||||
|
||||
Success! Your calendar is connected and will update your Linear status with out of office notices.
|
||||
@@ -0,0 +1,20 @@
|
||||
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
|
||||
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
|
||||
|
||||
import appConfig from "../config.json";
|
||||
|
||||
const handler: AppDeclarativeHandler = {
|
||||
appType: appConfig.type,
|
||||
variant: appConfig.variant,
|
||||
slug: appConfig.slug,
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
redirect: {
|
||||
newTab: true,
|
||||
url: "https://linear.app/settings/integrations/google-calendar",
|
||||
},
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as add } from "./add";
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||
"name": "Linear",
|
||||
"slug": "linear",
|
||||
"type": "linear_other",
|
||||
"logo": "icon.svg",
|
||||
"url": "https://linear.app",
|
||||
"variant": "other",
|
||||
"categories": ["other"],
|
||||
"publisher": "Linear",
|
||||
"email": "support@linear.app",
|
||||
"description": "Linear is a better way to build products. Connect your calendar to automate your status",
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"dependencies": ["google-calendar"],
|
||||
"__template": "link-as-an-app"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * as api from "./api";
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"name": "@calcom/linear",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
},
|
||||
"description": "Connect your calendar to automate your status"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,50 @@
|
||||
<svg width="2484" height="2484" viewBox="0 0 2484 2484" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_981_40)">
|
||||
<path d="M0 0H2484V2484H0V0Z" fill="url(#paint0_linear_981_40)"/>
|
||||
<g filter="url(#filter0_dd_981_40)">
|
||||
<path d="M636.219 1385.11C633.456 1373.33 647.489 1365.91 656.048 1374.47L1109.53 1827.95C1118.09 1836.51 1110.67 1850.54 1098.89 1847.78C870.04 1794.1 689.904 1613.96 636.219 1385.11Z" fill="url(#paint1_linear_981_40)"/>
|
||||
<path d="M621.024 1203.36C620.804 1206.88 622.128 1210.32 624.619 1212.81L1271.19 1859.38C1273.68 1861.87 1277.12 1863.2 1280.64 1862.98C1310.06 1861.14 1338.93 1857.26 1367.11 1851.48C1376.6 1849.53 1379.9 1837.86 1373.05 1831.01L652.994 1110.95C646.138 1104.1 634.473 1107.4 632.522 1116.89C626.737 1145.07 622.856 1173.94 621.024 1203.36Z" fill="url(#paint2_linear_981_40)"/>
|
||||
<path d="M673.3 989.942C671.233 994.585 672.286 1000.01 675.879 1003.6L1480.4 1808.12C1483.99 1811.71 1489.42 1812.77 1494.06 1810.7C1516.24 1800.82 1537.74 1789.68 1558.46 1777.36C1565.32 1773.29 1566.38 1763.87 1560.74 1758.23L725.772 923.262C720.132 917.622 710.71 918.68 706.635 925.537C694.322 946.258 683.18 967.758 673.3 989.942Z" fill="url(#paint3_linear_981_40)"/>
|
||||
<path d="M778.222 845.479C773.625 840.882 773.339 833.508 777.671 828.661C891.501 701.225 1057.08 621 1241.4 621C1584.7 621 1863 899.298 1863 1242.6C1863 1426.92 1782.77 1592.5 1655.34 1706.33C1650.49 1710.66 1643.12 1710.37 1638.52 1705.78L778.222 845.479Z" fill="url(#paint4_linear_981_40)"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd_981_40" x="585.357" y="590.926" width="1313.29" height="1313.29" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="5.5693"/>
|
||||
<feGaussianBlur stdDeviation="17.8218"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.118924 0 0 0 0 0.158031 0 0 0 0 0.570833 0 0 0 0.7 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_981_40"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.94926"/>
|
||||
<feGaussianBlur stdDeviation="7.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.17 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_981_40" result="effect2_dropShadow_981_40"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_981_40" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_981_40" x1="1242" y1="0" x2="1242" y2="2484" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5C6BF1"/>
|
||||
<stop offset="1" stop-color="#283188"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_981_40" x1="665.358" y1="736.329" x2="1312.97" y2="1685.57" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0.65"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_981_40" x1="665.358" y1="736.329" x2="1312.97" y2="1685.57" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0.65"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_981_40" x1="665.358" y1="736.329" x2="1312.97" y2="1685.57" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0.65"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_981_40" x1="665.358" y1="736.329" x2="1312.97" y2="1685.57" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0.65"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_981_40">
|
||||
<rect width="2484" height="2484" rx="200" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -1,6 +1,6 @@
|
||||
import { Meta, Canvas, ArgsTable } from "@storybook/blocks";
|
||||
import { Title } from "@calcom/storybook/components";
|
||||
import { Booker } from "./Booker";
|
||||
import { BookerWebWrapper as Booker } from "./wrappers/BookerWebWrapper";
|
||||
import * as BookerStories from "./Booker.stories";
|
||||
|
||||
<Meta of={BookerStories} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { Booker } from "./Booker";
|
||||
import { BookerWebWrapper as Booker } from "./wrappers/BookerWebWrapper";
|
||||
|
||||
const meta: Meta<typeof Booker> = {
|
||||
component: Booker,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { BookerProps } from "@calcom/features/bookings/Booker";
|
||||
import { Booker as BookerComponent } from "@calcom/features/bookings/Booker";
|
||||
|
||||
import type { AtomsGlobalConfigProps } from "../types";
|
||||
|
||||
type BookerAtomProps = BookerProps & AtomsGlobalConfigProps;
|
||||
|
||||
/**
|
||||
* @TODO Before we can turn this into a reusable atom
|
||||
* * Use the webAppUrl coming from AtomsGlobalConfigProps to make url dynamic
|
||||
* * Find a solution for translations
|
||||
*/
|
||||
export const Booker = (props: BookerAtomProps) => <BookerComponent {...props} />;
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Export file is only used for building the dist version of this Atom. */
|
||||
// import "../globals.css";
|
||||
|
||||
export { Booker } from "./Booker";
|
||||
export { BookerWebWrapper as Booker } from "./wrappers/BookerWebWrapper";
|
||||
export * from "../types";
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { Booker } from "./Booker";
|
||||
export { BookerWebWrapper as Booker } from "./wrappers/BookerWebWrapper";
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useMemo, useCallback } from "react";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import type { BookerProps } from "@calcom/features/bookings/Booker";
|
||||
import { Booker as BookerComponent } from "@calcom/features/bookings/Booker";
|
||||
import { useBookerLayout } from "@calcom/features/bookings/Booker/components/hooks/useBookerLayout";
|
||||
import { useBookingForm } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm";
|
||||
import { useBookings } from "@calcom/features/bookings/Booker/components/hooks/useBookings";
|
||||
import { useCalendars } from "@calcom/features/bookings/Booker/components/hooks/useCalendars";
|
||||
import { useSlots } from "@calcom/features/bookings/Booker/components/hooks/useSlots";
|
||||
import { useVerifyCode } from "@calcom/features/bookings/Booker/components/hooks/useVerifyCode";
|
||||
import { useVerifyEmail } from "@calcom/features/bookings/Booker/components/hooks/useVerifyEmail";
|
||||
import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
|
||||
import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import type { AtomsGlobalConfigProps } from "../../types";
|
||||
|
||||
type BookerWebWrapperAtomProps = BookerProps & AtomsGlobalConfigProps;
|
||||
|
||||
export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const event = useEvent();
|
||||
const bookerLayout = useBookerLayout(event.data);
|
||||
|
||||
const selectedDate = searchParams?.get("date");
|
||||
const isRedirect = searchParams?.get("redirected") === "true" || false;
|
||||
const fromUserNameRedirected = searchParams?.get("username") || "";
|
||||
const rescheduleUid =
|
||||
typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("rescheduleUid") : null;
|
||||
const bookingUid =
|
||||
typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("bookingUid") : null;
|
||||
const date = dayjs(selectedDate).format("YYYY-MM-DD");
|
||||
|
||||
useInitializeBookerStore({
|
||||
...props,
|
||||
eventId: event?.data?.id,
|
||||
rescheduleUid,
|
||||
bookingUid: bookingUid,
|
||||
layout: bookerLayout.defaultLayout,
|
||||
org: props.entity.orgSlug,
|
||||
});
|
||||
|
||||
const [bookerState, _] = useBookerStore((state) => [state.state, state.setState], shallow);
|
||||
const [dayCount] = useBookerStore((state) => [state.dayCount, state.setDayCount], shallow);
|
||||
const { data: session } = useSession();
|
||||
const routerQuery = useRouterQuery();
|
||||
const hasSession = !!session;
|
||||
const firstNameQueryParam = searchParams?.get("firstName");
|
||||
const lastNameQueryParam = searchParams?.get("lastName");
|
||||
const metadata = Object.keys(routerQuery)
|
||||
.filter((key) => key.startsWith("metadata"))
|
||||
.reduce(
|
||||
(metadata, key) => ({
|
||||
...metadata,
|
||||
[key.substring("metadata[".length, key.length - 1)]: searchParams?.get(key),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
const prefillFormParams = useMemo(() => {
|
||||
return {
|
||||
name:
|
||||
searchParams?.get("name") ||
|
||||
(firstNameQueryParam ? `${firstNameQueryParam} ${lastNameQueryParam}` : null),
|
||||
guests: searchParams?.getAll("guests") || searchParams?.getAll("guest"),
|
||||
};
|
||||
}, [searchParams, firstNameQueryParam, lastNameQueryParam]);
|
||||
|
||||
const bookerForm = useBookingForm({
|
||||
event,
|
||||
sessionEmail: session?.user.email,
|
||||
sessionUsername: session?.user.username,
|
||||
sessionName: session?.user.name,
|
||||
hasSession,
|
||||
extraOptions: routerQuery,
|
||||
prefillFormParams,
|
||||
});
|
||||
const bookings = useBookings({
|
||||
event,
|
||||
hashedLink: props.hashedLink,
|
||||
bookingForm: bookerForm.bookingForm,
|
||||
metadata: metadata ?? {},
|
||||
});
|
||||
const calendars = useCalendars({ hasSession });
|
||||
const verifyEmail = useVerifyEmail({
|
||||
email: bookerForm.formEmail,
|
||||
name: bookerForm.formName,
|
||||
requiresBookerEmailVerification: event?.data?.requiresBookerEmailVerification,
|
||||
onVerifyEmail: bookerForm.beforeVerifyEmail,
|
||||
});
|
||||
const slots = useSlots(event);
|
||||
|
||||
const prefetchNextMonth =
|
||||
(bookerLayout.layout === BookerLayouts.WEEK_VIEW &&
|
||||
!!bookerLayout.extraDays &&
|
||||
dayjs(date).month() !== dayjs(date).add(bookerLayout.extraDays, "day").month()) ||
|
||||
(bookerLayout.layout === BookerLayouts.COLUMN_VIEW &&
|
||||
dayjs(date).month() !== dayjs(date).add(bookerLayout.columnViewExtraDays.current, "day").month());
|
||||
|
||||
const monthCount =
|
||||
((bookerLayout.layout !== BookerLayouts.WEEK_VIEW && bookerState === "selecting_time") ||
|
||||
bookerLayout.layout === BookerLayouts.COLUMN_VIEW) &&
|
||||
dayjs(date).add(1, "month").month() !==
|
||||
dayjs(date).add(bookerLayout.columnViewExtraDays.current, "day").month()
|
||||
? 2
|
||||
: undefined;
|
||||
/**
|
||||
* Prioritize dateSchedule load
|
||||
* Component will render but use data already fetched from here, and no duplicate requests will be made
|
||||
* */
|
||||
const schedule = useScheduleForEvent({
|
||||
prefetchNextMonth,
|
||||
username: props.username,
|
||||
monthCount,
|
||||
dayCount,
|
||||
eventSlug: props.eventSlug,
|
||||
month: props.month,
|
||||
duration: props.duration,
|
||||
selectedDate,
|
||||
});
|
||||
|
||||
const verifyCode = useVerifyCode({
|
||||
onSuccess: () => {
|
||||
verifyEmail.setVerifiedEmail(bookerForm.formEmail);
|
||||
verifyEmail.setEmailVerificationModalVisible(false);
|
||||
bookings.handleBookEvent();
|
||||
},
|
||||
});
|
||||
|
||||
// Toggle query param for overlay calendar
|
||||
const onOverlaySwitchStateChange = useCallback(
|
||||
(state: boolean) => {
|
||||
const current = new URLSearchParams(Array.from(searchParams?.entries() ?? []));
|
||||
if (state) {
|
||||
current.set("overlayCalendar", "true");
|
||||
localStorage.setItem("overlayCalendarSwitchDefault", "true");
|
||||
} else {
|
||||
current.delete("overlayCalendar");
|
||||
localStorage.removeItem("overlayCalendarSwitchDefault");
|
||||
}
|
||||
// cast to string
|
||||
const value = current.toString();
|
||||
const query = value ? `?${value}` : "";
|
||||
router.push(`${pathname}${query}`);
|
||||
},
|
||||
[searchParams, pathname, router]
|
||||
);
|
||||
|
||||
return (
|
||||
<BookerComponent
|
||||
{...props}
|
||||
onGoBackInstantMeeting={() => {
|
||||
if (pathname) window.location.href = pathname;
|
||||
}}
|
||||
onConnectNowInstantMeeting={() => {
|
||||
const newPath = `${pathname}?isInstantMeeting=true`;
|
||||
router.push(newPath);
|
||||
}}
|
||||
onOverlayClickNoCalendar={() => {
|
||||
router.push("/apps/categories/calendar");
|
||||
}}
|
||||
onClickOverlayContinue={() => {
|
||||
const currentUrl = new URL(window.location.href);
|
||||
currentUrl.pathname = "/login/";
|
||||
currentUrl.searchParams.set("callbackUrl", window.location.pathname);
|
||||
currentUrl.searchParams.set("overlayCalendar", "true");
|
||||
router.push(currentUrl.toString());
|
||||
}}
|
||||
onOverlaySwitchStateChange={onOverlaySwitchStateChange}
|
||||
sessionUsername={session?.user.username}
|
||||
isRedirect={isRedirect}
|
||||
fromUserNameRedirected={fromUserNameRedirected}
|
||||
rescheduleUid={rescheduleUid}
|
||||
bookingUid={bookingUid}
|
||||
hasSession={hasSession}
|
||||
extraOptions={routerQuery}
|
||||
bookings={bookings}
|
||||
calendars={calendars}
|
||||
slots={slots}
|
||||
verifyEmail={verifyEmail}
|
||||
bookerForm={bookerForm}
|
||||
event={event}
|
||||
bookerLayout={bookerLayout}
|
||||
schedule={schedule}
|
||||
verifyCode={verifyCode}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +1,2 @@
|
||||
export { Booker } from "./booker/Booker";
|
||||
export { Booker } from "./booker";
|
||||
export { CalProvider } from "./cal-provider/index";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** @see https://gist.github.com/zomars/4c366a0118a5b7fb391529ab1f27527a */
|
||||
const RawHtml = ({ html = "" }) => (
|
||||
<script dangerouslySetInnerHTML={{ __html: `</script>${html}<script>` }} />
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { AuthOptions, Session } from "next-auth";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
|
||||
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
@@ -94,7 +94,10 @@ export async function getServerSession(options: {
|
||||
emailVerified: user.emailVerified,
|
||||
email_verified: user.emailVerified !== null,
|
||||
role: user.role,
|
||||
image: `${WEBAPP_URL}/${user.username}/avatar.png`,
|
||||
image: getUserAvatarUrl({
|
||||
...user,
|
||||
profile: user.profile,
|
||||
}),
|
||||
belongsToActiveTeam: token.belongsToActiveTeam,
|
||||
org: token.org,
|
||||
locale: user.locale ?? undefined,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Membership, Team, UserPermissionRole } from "@prisma/client";
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||
import type { AuthOptions, Session } from "next-auth";
|
||||
import type { JWT } from "next-auth/jwt";
|
||||
import { encode } from "next-auth/jwt";
|
||||
@@ -806,18 +807,32 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||
existingUserWithEmail.identityProvider === IdentityProvider.CAL &&
|
||||
(idP === IdentityProvider.GOOGLE || idP === IdentityProvider.SAML)
|
||||
) {
|
||||
await prisma.user.update({
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { email: existingUserWithEmail.email },
|
||||
// also update email to the IdP email
|
||||
data: {
|
||||
password: {
|
||||
delete: true,
|
||||
},
|
||||
email: user.email,
|
||||
identityProvider: idP,
|
||||
identityProviderId: account.providerAccountId,
|
||||
},
|
||||
});
|
||||
|
||||
// safely delete password from UserPassword table if it exists
|
||||
try {
|
||||
await prisma.userPassword.delete({
|
||||
where: { userId: updatedUser.id },
|
||||
});
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof PrismaClientKnownRequestError &&
|
||||
(err.code === "P2025" || err.code === "P2016")
|
||||
) {
|
||||
log.warn("UserPassword not found for user", safeStringify(existingUserWithEmail));
|
||||
} else {
|
||||
log.warn("Could not delete UserPassword for user", safeStringify(existingUserWithEmail));
|
||||
}
|
||||
}
|
||||
|
||||
if (existingUserWithEmail.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUserWithEmail.email);
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { LazyMotion, m, AnimatePresence } from "framer-motion";
|
||||
import { useSession } from "next-auth/react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect, useRef, useMemo } from "react";
|
||||
import StickyBox from "react-sticky-box";
|
||||
import { shallow } from "zustand/shallow";
|
||||
@@ -29,16 +26,9 @@ import { OverlayCalendar } from "./components/OverlayCalendar/OverlayCalendar";
|
||||
import { RedirectToInstantMeetingModal } from "./components/RedirectToInstantMeetingModal";
|
||||
import { BookerSection } from "./components/Section";
|
||||
import { Away, NotFound } from "./components/Unavailable";
|
||||
import { useBookerLayout } from "./components/hooks/useBookerLayout";
|
||||
import { useBookingForm } from "./components/hooks/useBookingForm";
|
||||
import { useBookings } from "./components/hooks/useBookings";
|
||||
import { useCalendars } from "./components/hooks/useCalendars";
|
||||
import { useSlots } from "./components/hooks/useSlots";
|
||||
import { useVerifyEmail } from "./components/hooks/useVerifyEmail";
|
||||
import { fadeInLeft, getBookerSizeClassNames, useBookerResizeAnimation } from "./config";
|
||||
import { useBookerStore, useInitializeBookerStore } from "./store";
|
||||
import type { BookerProps } from "./types";
|
||||
import { useEvent, useScheduleForEvent } from "./utils/event";
|
||||
import { useBookerStore } from "./store";
|
||||
import type { BookerProps, WrappedBookerProps } from "./types";
|
||||
import { useBrandColors } from "./utils/use-brand-colors";
|
||||
|
||||
const loadFramerFeatures = () => import("./framer-features").then((res) => res.default);
|
||||
@@ -53,28 +43,32 @@ const DatePicker = dynamic(() => import("./components/DatePicker").then((mod) =>
|
||||
const BookerComponent = ({
|
||||
username,
|
||||
eventSlug,
|
||||
month,
|
||||
bookingData,
|
||||
hideBranding = false,
|
||||
isTeamEvent,
|
||||
entity,
|
||||
durationConfig,
|
||||
duration,
|
||||
hashedLink,
|
||||
isInstantMeeting = false,
|
||||
}: BookerProps) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
onGoBackInstantMeeting,
|
||||
onConnectNowInstantMeeting,
|
||||
onOverlayClickNoCalendar,
|
||||
onClickOverlayContinue,
|
||||
onOverlaySwitchStateChange,
|
||||
sessionUsername,
|
||||
isRedirect,
|
||||
fromUserNameRedirected,
|
||||
rescheduleUid,
|
||||
hasSession,
|
||||
extraOptions,
|
||||
bookings,
|
||||
verifyEmail,
|
||||
slots,
|
||||
calendars,
|
||||
bookerForm,
|
||||
event,
|
||||
bookerLayout,
|
||||
schedule,
|
||||
verifyCode,
|
||||
}: BookerProps & WrappedBookerProps) => {
|
||||
const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow);
|
||||
const selectedDate = useBookerStore((state) => state.selectedDate);
|
||||
const [seatedEventData, setSeatedEventData] = useBookerStore(
|
||||
(state) => [state.seatedEventData, state.setSeatedEventData],
|
||||
shallow
|
||||
);
|
||||
const event = useEvent();
|
||||
const session = useSession();
|
||||
const { selectedTimeslot, setSelectedTimeslot } = useSlots(event);
|
||||
|
||||
const {
|
||||
shouldShowFormInDialog,
|
||||
hasDarkBackground,
|
||||
@@ -82,47 +76,19 @@ const BookerComponent = ({
|
||||
columnViewExtraDays,
|
||||
isMobile,
|
||||
layout,
|
||||
defaultLayout,
|
||||
hideEventTypeDetails,
|
||||
isEmbed,
|
||||
bookerLayouts,
|
||||
} = useBookerLayout(event.data);
|
||||
} = bookerLayout;
|
||||
|
||||
const [seatedEventData, setSeatedEventData] = useBookerStore(
|
||||
(state) => [state.seatedEventData, state.setSeatedEventData],
|
||||
shallow
|
||||
);
|
||||
const { selectedTimeslot, setSelectedTimeslot } = slots;
|
||||
|
||||
const [dayCount, setDayCount] = useBookerStore((state) => [state.dayCount, state.setDayCount], shallow);
|
||||
|
||||
const date = dayjs(selectedDate).format("YYYY-MM-DD");
|
||||
|
||||
const prefetchNextMonth =
|
||||
(layout === BookerLayouts.WEEK_VIEW &&
|
||||
!!extraDays &&
|
||||
dayjs(date).month() !== dayjs(date).add(extraDays, "day").month()) ||
|
||||
(layout === BookerLayouts.COLUMN_VIEW &&
|
||||
dayjs(date).month() !== dayjs(date).add(columnViewExtraDays.current, "day").month());
|
||||
|
||||
const monthCount =
|
||||
((layout !== BookerLayouts.WEEK_VIEW && bookerState === "selecting_time") ||
|
||||
layout === BookerLayouts.COLUMN_VIEW) &&
|
||||
dayjs(date).add(1, "month").month() !== dayjs(date).add(columnViewExtraDays.current, "day").month()
|
||||
? 2
|
||||
: undefined;
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
/**
|
||||
* Prioritize dateSchedule load
|
||||
* Component will render but use data already fetched from here, and no duplicate requests will be made
|
||||
* */
|
||||
const schedule = useScheduleForEvent({
|
||||
prefetchNextMonth,
|
||||
username,
|
||||
monthCount,
|
||||
dayCount,
|
||||
eventSlug,
|
||||
month,
|
||||
duration,
|
||||
selectedDate: searchParams?.get("date"),
|
||||
});
|
||||
|
||||
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots).filter(
|
||||
(slot) => dayjs(selectedDate).diff(slot, "day") <= 0
|
||||
);
|
||||
@@ -146,67 +112,24 @@ const BookerComponent = ({
|
||||
|
||||
const timeslotsRef = useRef<HTMLDivElement>(null);
|
||||
const StickyOnDesktop = isMobile ? "div" : StickyBox;
|
||||
const rescheduleUid =
|
||||
typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("rescheduleUid") : null;
|
||||
const bookingUid =
|
||||
typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("bookingUid") : null;
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
const isRedirect = searchParams?.get("redirected") === "true" || false;
|
||||
const fromUserNameRedirected = searchParams?.get("username") || "";
|
||||
|
||||
useBrandColors({
|
||||
brandColor: event.data?.profile.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
|
||||
darkBrandColor: event.data?.profile.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR,
|
||||
theme: event.data?.profile.theme,
|
||||
});
|
||||
|
||||
useInitializeBookerStore({
|
||||
username,
|
||||
eventSlug,
|
||||
month,
|
||||
eventId: event?.data?.id,
|
||||
rescheduleUid,
|
||||
bookingUid,
|
||||
bookingData,
|
||||
layout: defaultLayout,
|
||||
isTeamEvent,
|
||||
org: entity.orgSlug,
|
||||
durationConfig,
|
||||
isInstantMeeting,
|
||||
});
|
||||
|
||||
const {
|
||||
bookerFormErrorRef,
|
||||
key,
|
||||
formEmail,
|
||||
formName,
|
||||
bookingForm,
|
||||
beforeVerifyEmail,
|
||||
errors: formErrors,
|
||||
} = useBookingForm({
|
||||
event,
|
||||
});
|
||||
|
||||
const { handleBookEvent, hasInstantMeetingTokenExpired, errors, loadingStates, expiryTime } = useBookings({
|
||||
event,
|
||||
hashedLink,
|
||||
bookingForm,
|
||||
});
|
||||
const { bookerFormErrorRef, key, formEmail, bookingForm, errors: formErrors } = bookerForm;
|
||||
|
||||
const { handleBookEvent, hasInstantMeetingTokenExpired, errors, loadingStates, expiryTime } = bookings;
|
||||
const {
|
||||
isEmailVerificationModalVisible,
|
||||
setEmailVerificationModalVisible,
|
||||
handleVerifyEmail,
|
||||
setVerifiedEmail,
|
||||
renderConfirmNotVerifyEmailButtonCond,
|
||||
} = useVerifyEmail({
|
||||
email: formEmail,
|
||||
name: formName,
|
||||
requiresBookerEmailVerification: event?.data?.requiresBookerEmailVerification,
|
||||
onVerifyEmail: beforeVerifyEmail,
|
||||
});
|
||||
} = verifyEmail;
|
||||
|
||||
const {
|
||||
overlayBusyDates,
|
||||
@@ -214,7 +137,7 @@ const BookerComponent = ({
|
||||
connectedCalendars,
|
||||
loadingConnectedCalendar,
|
||||
onToggleCalendar,
|
||||
} = useCalendars();
|
||||
} = calendars;
|
||||
|
||||
useEffect(() => {
|
||||
if (event.isPending) return setBookerState("loading");
|
||||
@@ -223,76 +146,81 @@ const BookerComponent = ({
|
||||
return setBookerState("booking");
|
||||
}, [event, selectedDate, selectedTimeslot, setBookerState]);
|
||||
|
||||
const EventBooker = useMemo(
|
||||
() =>
|
||||
bookerState === "booking" ? (
|
||||
<BookEventForm
|
||||
key={key}
|
||||
onCancel={() => {
|
||||
setSelectedTimeslot(null);
|
||||
if (seatedEventData.bookingUid) {
|
||||
setSeatedEventData({ ...seatedEventData, bookingUid: undefined, attendees: undefined });
|
||||
}
|
||||
}}
|
||||
onSubmit={renderConfirmNotVerifyEmailButtonCond ? handleBookEvent : handleVerifyEmail}
|
||||
errorRef={bookerFormErrorRef}
|
||||
errors={{ ...formErrors, ...errors }}
|
||||
loadingStates={loadingStates}
|
||||
renderConfirmNotVerifyEmailButtonCond={renderConfirmNotVerifyEmailButtonCond}
|
||||
bookingForm={bookingForm}
|
||||
eventQuery={event}
|
||||
rescheduleUid={rescheduleUid}>
|
||||
<>
|
||||
<VerifyCodeDialog
|
||||
isOpenDialog={isEmailVerificationModalVisible}
|
||||
setIsOpenDialog={setEmailVerificationModalVisible}
|
||||
email={formEmail}
|
||||
onSuccess={() => {
|
||||
setVerifiedEmail(formEmail);
|
||||
setEmailVerificationModalVisible(false);
|
||||
handleBookEvent();
|
||||
}}
|
||||
isUserSessionRequiredToVerify={false}
|
||||
/>
|
||||
<RedirectToInstantMeetingModal
|
||||
expiryTime={expiryTime}
|
||||
hasInstantMeetingTokenExpired={hasInstantMeetingTokenExpired}
|
||||
bookingId={parseInt(getQueryParam("bookingId") || "0")}
|
||||
onGoBack={() => {
|
||||
// Prevent null on app directory
|
||||
if (pathname) window.location.href = pathname;
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</BookEventForm>
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
[
|
||||
bookerFormErrorRef,
|
||||
bookerState,
|
||||
bookingForm,
|
||||
errors,
|
||||
event,
|
||||
formEmail,
|
||||
formErrors,
|
||||
handleBookEvent,
|
||||
handleVerifyEmail,
|
||||
hasInstantMeetingTokenExpired,
|
||||
isEmailVerificationModalVisible,
|
||||
key,
|
||||
loadingStates,
|
||||
pathname,
|
||||
renderConfirmNotVerifyEmailButtonCond,
|
||||
rescheduleUid,
|
||||
seatedEventData,
|
||||
setEmailVerificationModalVisible,
|
||||
setSeatedEventData,
|
||||
setSelectedTimeslot,
|
||||
setVerifiedEmail,
|
||||
expiryTime,
|
||||
]
|
||||
);
|
||||
const EventBooker = useMemo(() => {
|
||||
return bookerState === "booking" ? (
|
||||
<BookEventForm
|
||||
key={key}
|
||||
onCancel={() => {
|
||||
setSelectedTimeslot(null);
|
||||
if (seatedEventData.bookingUid) {
|
||||
setSeatedEventData({ ...seatedEventData, bookingUid: undefined, attendees: undefined });
|
||||
}
|
||||
}}
|
||||
onSubmit={renderConfirmNotVerifyEmailButtonCond ? handleBookEvent : handleVerifyEmail}
|
||||
errorRef={bookerFormErrorRef}
|
||||
errors={{ ...formErrors, ...errors }}
|
||||
loadingStates={loadingStates}
|
||||
renderConfirmNotVerifyEmailButtonCond={renderConfirmNotVerifyEmailButtonCond}
|
||||
bookingForm={bookingForm}
|
||||
eventQuery={event}
|
||||
extraOptions={extraOptions}
|
||||
rescheduleUid={rescheduleUid}>
|
||||
<>
|
||||
<VerifyCodeDialog
|
||||
isOpenDialog={isEmailVerificationModalVisible}
|
||||
setIsOpenDialog={setEmailVerificationModalVisible}
|
||||
email={formEmail}
|
||||
isUserSessionRequiredToVerify={false}
|
||||
verifyCodeWithSessionNotRequired={verifyCode.verifyCodeWithSessionNotRequired}
|
||||
verifyCodeWithSessionRequired={verifyCode.verifyCodeWithSessionRequired}
|
||||
error={verifyCode.error}
|
||||
resetErrors={verifyCode.resetErrors}
|
||||
isPending={verifyCode.isPending}
|
||||
setIsPending={verifyCode.setIsPending}
|
||||
/>
|
||||
<RedirectToInstantMeetingModal
|
||||
expiryTime={expiryTime}
|
||||
hasInstantMeetingTokenExpired={hasInstantMeetingTokenExpired}
|
||||
bookingId={parseInt(getQueryParam("bookingId") || "0")}
|
||||
onGoBack={() => {
|
||||
onGoBackInstantMeeting();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</BookEventForm>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}, [
|
||||
bookerFormErrorRef,
|
||||
bookerState,
|
||||
bookingForm,
|
||||
errors,
|
||||
event,
|
||||
expiryTime,
|
||||
extraOptions,
|
||||
formEmail,
|
||||
formErrors,
|
||||
handleBookEvent,
|
||||
handleVerifyEmail,
|
||||
hasInstantMeetingTokenExpired,
|
||||
isEmailVerificationModalVisible,
|
||||
key,
|
||||
loadingStates,
|
||||
onGoBackInstantMeeting,
|
||||
renderConfirmNotVerifyEmailButtonCond,
|
||||
rescheduleUid,
|
||||
seatedEventData,
|
||||
setEmailVerificationModalVisible,
|
||||
setSeatedEventData,
|
||||
setSelectedTimeslot,
|
||||
verifyCode.error,
|
||||
verifyCode.isPending,
|
||||
verifyCode.resetErrors,
|
||||
verifyCode.setIsPending,
|
||||
verifyCode.verifyCodeWithSessionNotRequired,
|
||||
verifyCode.verifyCodeWithSessionRequired,
|
||||
]);
|
||||
|
||||
if (entity.isUnpublished) {
|
||||
return <UnpublishedEntity {...entity} />;
|
||||
@@ -317,8 +245,7 @@ const BookerComponent = ({
|
||||
<InstantBooking
|
||||
event={event.data}
|
||||
onConnectNow={() => {
|
||||
const newPath = `${pathname}?isInstantMeeting=true`;
|
||||
router.push(newPath);
|
||||
onConnectNowInstantMeeting();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -372,7 +299,7 @@ const BookerComponent = ({
|
||||
"bg-default dark:bg-muted sticky top-0 z-10"
|
||||
)}>
|
||||
<Header
|
||||
isMyLink={Boolean(username === session?.data?.user.username)}
|
||||
isMyLink={Boolean(username === sessionUsername)}
|
||||
eventSlug={eventSlug}
|
||||
enabledLayouts={bookerLayouts.enabledLayouts}
|
||||
extraDays={layout === BookerLayouts.COLUMN_VIEW ? columnViewExtraDays.current : extraDays}
|
||||
@@ -389,8 +316,11 @@ const BookerComponent = ({
|
||||
loadingConnectedCalendar={loadingConnectedCalendar}
|
||||
overlayBusyDates={overlayBusyDates}
|
||||
onToggleCalendar={onToggleCalendar}
|
||||
hasSession={hasSession}
|
||||
handleClickContinue={onClickOverlayContinue}
|
||||
handleSwitchStateChange={onOverlaySwitchStateChange}
|
||||
handleClickNoCalendar={() => {
|
||||
router.push("/apps/categories/calendar");
|
||||
onOverlayClickNoCalendar();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
@@ -444,7 +374,12 @@ const BookerComponent = ({
|
||||
visible={layout === BookerLayouts.WEEK_VIEW}
|
||||
className="border-subtle sticky top-0 ml-[-1px] h-full md:border-l"
|
||||
{...fadeInLeft}>
|
||||
<LargeCalendar extraDays={extraDays} schedule={schedule.data} isLoading={schedule.isPending} />
|
||||
<LargeCalendar
|
||||
extraDays={extraDays}
|
||||
schedule={schedule.data}
|
||||
isLoading={schedule.isPending}
|
||||
event={event}
|
||||
/>
|
||||
</BookerSection>
|
||||
|
||||
<BookerSection
|
||||
@@ -505,7 +440,7 @@ const BookerComponent = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const Booker = (props: BookerProps) => {
|
||||
export const Booker = (props: BookerProps & WrappedBookerProps) => {
|
||||
if (props.isAway) return <Away />;
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,13 +7,12 @@ import type { FieldError } from "react-hook-form";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { Alert, Button, EmptyScreen, Form } from "@calcom/ui";
|
||||
import { Calendar } from "@calcom/ui/components/icon";
|
||||
|
||||
import { useBookerStore } from "../../store";
|
||||
import type { useEventReturnType } from "../../utils/event";
|
||||
import type { useBookingFormReturnType } from "../hooks/useBookingForm";
|
||||
import type { UseBookingFormReturnType } from "../hooks/useBookingForm";
|
||||
import type { IUseBookingErrors, IUseBookingLoadingStates } from "../hooks/useBookings";
|
||||
import { BookingFields } from "./BookingFields";
|
||||
import { FormSkeleton } from "./Skeleton";
|
||||
@@ -22,11 +21,12 @@ type BookEventFormProps = {
|
||||
onCancel?: () => void;
|
||||
onSubmit: () => void;
|
||||
errorRef: React.RefObject<HTMLDivElement>;
|
||||
errors: useBookingFormReturnType["errors"] & IUseBookingErrors;
|
||||
errors: UseBookingFormReturnType["errors"] & IUseBookingErrors;
|
||||
loadingStates: IUseBookingLoadingStates;
|
||||
children?: React.ReactNode;
|
||||
bookingForm: useBookingFormReturnType["bookingForm"];
|
||||
bookingForm: UseBookingFormReturnType["bookingForm"];
|
||||
renderConfirmNotVerifyEmailButtonCond: boolean;
|
||||
extraOptions: Record<string, string | string[]>;
|
||||
};
|
||||
|
||||
export const BookEventForm = ({
|
||||
@@ -40,12 +40,12 @@ export const BookEventForm = ({
|
||||
renderConfirmNotVerifyEmailButtonCond,
|
||||
bookingForm,
|
||||
children,
|
||||
extraOptions,
|
||||
}: Omit<BookEventFormProps, "event"> & {
|
||||
eventQuery: useEventReturnType;
|
||||
rescheduleUid: string | null;
|
||||
}) => {
|
||||
const eventType = eventQuery.data;
|
||||
const routerQuery = useRouterQuery();
|
||||
const setFormValues = useBookerStore((state) => state.setFormValues);
|
||||
const bookingData = useBookerStore((state) => state.bookingData);
|
||||
const timeslot = useBookerStore((state) => state.selectedTimeslot);
|
||||
@@ -76,7 +76,7 @@ export const BookEventForm = ({
|
||||
);
|
||||
|
||||
if (!eventType) {
|
||||
console.warn("No event type found for event", routerQuery);
|
||||
console.warn("No event type found for event", extraOptions);
|
||||
return <Alert severity="warning" message={t("error_booking_event")} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { useEventReturnType } from "@calcom/features/bookings/Booker/utils/event";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { type RouterOutputs } from "@calcom/trpc";
|
||||
import { Button, UserAvatarGroupWithOrg } from "@calcom/ui";
|
||||
|
||||
interface IInstantBookingProps {
|
||||
onConnectNow: () => void;
|
||||
event: NonNullable<RouterOutputs["viewer"]["public"]["event"]>;
|
||||
event: NonNullable<useEventReturnType["data"]>;
|
||||
}
|
||||
|
||||
export const InstantBooking = ({ onConnectNow, event }: IInstantBookingProps) => {
|
||||
|
||||
@@ -7,8 +7,7 @@ import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/week
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
|
||||
import { useBookerStore } from "../store";
|
||||
import type { useScheduleForEventReturnType } from "../utils/event";
|
||||
import { useEvent } from "../utils/event";
|
||||
import type { useEventReturnType, useScheduleForEventReturnType } from "../utils/event";
|
||||
import { getQueryParam } from "../utils/query-param";
|
||||
import { useOverlayCalendarStore } from "./OverlayCalendar/store";
|
||||
|
||||
@@ -16,10 +15,12 @@ export const LargeCalendar = ({
|
||||
extraDays,
|
||||
schedule,
|
||||
isLoading,
|
||||
event,
|
||||
}: {
|
||||
extraDays: number;
|
||||
schedule?: useScheduleForEventReturnType["data"];
|
||||
isLoading: boolean;
|
||||
event: useEventReturnType;
|
||||
}) => {
|
||||
const selectedDate = useBookerStore((state) => state.selectedDate);
|
||||
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
|
||||
@@ -28,7 +29,6 @@ export const LargeCalendar = ({
|
||||
const displayOverlay =
|
||||
getQueryParam("overlayCalendar") === "true" || localStorage.getItem("overlayCalendarSwitchDefault");
|
||||
|
||||
const event = useEvent();
|
||||
const eventDuration = selectedEventDuration || event?.data?.length || 30;
|
||||
|
||||
const availableSlots = useMemo(() => {
|
||||
|
||||
@@ -11,7 +11,12 @@ type OverlayCalendarProps = Pick<
|
||||
| "onToggleCalendar"
|
||||
| "loadingConnectedCalendar"
|
||||
| "isOverlayCalendarEnabled"
|
||||
> & { handleClickNoCalendar: () => void };
|
||||
> & {
|
||||
handleClickNoCalendar: () => void;
|
||||
hasSession: boolean;
|
||||
handleClickContinue: () => void;
|
||||
handleSwitchStateChange: (state: boolean) => void;
|
||||
};
|
||||
|
||||
export const OverlayCalendar = ({
|
||||
connectedCalendars,
|
||||
@@ -20,6 +25,9 @@ export const OverlayCalendar = ({
|
||||
isOverlayCalendarEnabled,
|
||||
loadingConnectedCalendar,
|
||||
handleClickNoCalendar,
|
||||
handleSwitchStateChange,
|
||||
handleClickContinue,
|
||||
hasSession,
|
||||
}: OverlayCalendarProps) => {
|
||||
const {
|
||||
handleCloseContinueModal,
|
||||
@@ -31,8 +39,16 @@ export const OverlayCalendar = ({
|
||||
} = useOverlayCalendar({ connectedCalendars, overlayBusyDates, onToggleCalendar });
|
||||
return (
|
||||
<>
|
||||
<OverlayCalendarSwitch enabled={isOverlayCalendarEnabled} />
|
||||
<OverlayCalendarContinueModal open={isOpenOverlayContinueModal} onClose={handleCloseContinueModal} />
|
||||
<OverlayCalendarSwitch
|
||||
enabled={isOverlayCalendarEnabled}
|
||||
hasSession={hasSession}
|
||||
onStateChange={handleSwitchStateChange}
|
||||
/>
|
||||
<OverlayCalendarContinueModal
|
||||
open={isOpenOverlayContinueModal}
|
||||
onClose={handleCloseContinueModal}
|
||||
onContinue={handleClickContinue}
|
||||
/>
|
||||
<OverlayCalendarSettingsModal
|
||||
connectedCalendars={connectedCalendars}
|
||||
open={isOpenOverlaySettingsModal}
|
||||
|
||||
+2
-8
@@ -1,5 +1,4 @@
|
||||
import { CalendarSearch } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -8,10 +7,10 @@ import { Button, Dialog, DialogContent, DialogFooter } from "@calcom/ui";
|
||||
interface IOverlayCalendarContinueModalProps {
|
||||
open?: boolean;
|
||||
onClose?: (state: boolean) => void;
|
||||
onContinue: () => void;
|
||||
}
|
||||
|
||||
export function OverlayCalendarContinueModal(props: IOverlayCalendarContinueModalProps) {
|
||||
const router = useRouter();
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<>
|
||||
@@ -24,12 +23,7 @@ export function OverlayCalendarContinueModal(props: IOverlayCalendarContinueModa
|
||||
<Button
|
||||
data-testid="overlay-calendar-continue-button"
|
||||
onClick={() => {
|
||||
const currentUrl = new URL(window.location.href);
|
||||
currentUrl.pathname = "/login/";
|
||||
currentUrl.searchParams.set("callbackUrl", window.location.pathname);
|
||||
currentUrl.searchParams.set("overlayCalendar", "true");
|
||||
|
||||
router.push(currentUrl.toString());
|
||||
props.onContinue();
|
||||
}}
|
||||
className="gap w-full items-center justify-center font-semibold"
|
||||
StartIcon={CalendarSearch}>
|
||||
|
||||
+10
-34
@@ -1,10 +1,7 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useSearchParams, useRouter, usePathname } from "next/navigation";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
import { Button, Switch } from "@calcom/ui";
|
||||
import { Settings } from "@calcom/ui/components/icon";
|
||||
|
||||
@@ -13,50 +10,29 @@ import { useOverlayCalendarStore } from "./store";
|
||||
|
||||
interface OverlayCalendarSwitchProps {
|
||||
enabled?: boolean;
|
||||
hasSession: boolean;
|
||||
onStateChange: (state: boolean) => void;
|
||||
}
|
||||
|
||||
export function OverlayCalendarSwitch({ enabled }: OverlayCalendarSwitchProps) {
|
||||
export function OverlayCalendarSwitch({ enabled, hasSession, onStateChange }: OverlayCalendarSwitchProps) {
|
||||
const { t } = useLocale();
|
||||
const setContinueWithProvider = useOverlayCalendarStore((state) => state.setContinueWithProviderModal);
|
||||
const setCalendarSettingsOverlay = useOverlayCalendarStore(
|
||||
(state) => state.setCalendarSettingsOverlayModal
|
||||
);
|
||||
const layout = useBookerStore((state) => state.layout);
|
||||
const searchParams = useSearchParams();
|
||||
const { data: session } = useSession();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const switchEnabled = enabled;
|
||||
|
||||
// Toggle query param for overlay calendar
|
||||
const toggleOverlayCalendarQueryParam = useCallback(
|
||||
(state: boolean) => {
|
||||
const current = new URLSearchParams(Array.from(searchParams?.entries() ?? []));
|
||||
if (state) {
|
||||
current.set("overlayCalendar", "true");
|
||||
localStorage.setItem("overlayCalendarSwitchDefault", "true");
|
||||
} else {
|
||||
current.delete("overlayCalendar");
|
||||
localStorage.removeItem("overlayCalendarSwitchDefault");
|
||||
}
|
||||
// cast to string
|
||||
const value = current.toString();
|
||||
const query = value ? `?${value}` : "";
|
||||
router.push(`${pathname}${query}`);
|
||||
},
|
||||
[searchParams, pathname, router]
|
||||
);
|
||||
|
||||
/**
|
||||
* If a user is not logged in and the overlay calendar query param is true,
|
||||
* show the continue modal so they can login / create an account
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!session && switchEnabled) {
|
||||
toggleOverlayCalendarQueryParam(false);
|
||||
if (!hasSession && switchEnabled) {
|
||||
onStateChange(false);
|
||||
setContinueWithProvider(true);
|
||||
}
|
||||
}, [session, switchEnabled, setContinueWithProvider, toggleOverlayCalendarQueryParam]);
|
||||
}, [hasSession, switchEnabled, setContinueWithProvider, onStateChange]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -70,10 +46,10 @@ export function OverlayCalendarSwitch({ enabled }: OverlayCalendarSwitchProps) {
|
||||
checked={switchEnabled}
|
||||
id="overlayCalendar"
|
||||
onCheckedChange={(state) => {
|
||||
if (!session) {
|
||||
if (!hasSession) {
|
||||
setContinueWithProvider(state);
|
||||
} else {
|
||||
toggleOverlayCalendarQueryParam(state);
|
||||
onStateChange(state);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -83,7 +59,7 @@ export function OverlayCalendarSwitch({ enabled }: OverlayCalendarSwitchProps) {
|
||||
{t("overlay_my_calendar")}
|
||||
</label>
|
||||
</div>
|
||||
{session && (
|
||||
{hasSession && (
|
||||
<Button
|
||||
size="base"
|
||||
data-testid="overlay-calendar-settings-button"
|
||||
|
||||
@@ -13,6 +13,8 @@ import type { useEventReturnType } from "../../utils/event";
|
||||
import { validateLayout } from "../../utils/layout";
|
||||
import { getQueryParam } from "../../utils/query-param";
|
||||
|
||||
export type UseBookerLayoutType = ReturnType<typeof useBookerLayout>;
|
||||
|
||||
export const useBookerLayout = (event: useEventReturnType["data"]) => {
|
||||
const [_layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow);
|
||||
const isEmbed = useIsEmbed();
|
||||
|
||||
@@ -13,11 +13,28 @@ import { useInitialFormValues } from "./useInitialFormValues";
|
||||
|
||||
export interface IUseBookingForm {
|
||||
event: useEventReturnType;
|
||||
sessionEmail?: string | null;
|
||||
sessionName?: string | null;
|
||||
sessionUsername?: string | null;
|
||||
hasSession: boolean;
|
||||
extraOptions: Record<string, string | string[]>;
|
||||
prefillFormParams: {
|
||||
guests: string[];
|
||||
name: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export type useBookingFormReturnType = ReturnType<typeof useBookingForm>;
|
||||
export type UseBookingFormReturnType = ReturnType<typeof useBookingForm>;
|
||||
|
||||
export const useBookingForm = ({ event }: IUseBookingForm) => {
|
||||
export const useBookingForm = ({
|
||||
event,
|
||||
sessionEmail,
|
||||
sessionName,
|
||||
sessionUsername,
|
||||
hasSession,
|
||||
extraOptions,
|
||||
prefillFormParams,
|
||||
}: IUseBookingForm) => {
|
||||
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
|
||||
const bookingData = useBookerStore((state) => state.bookingData);
|
||||
const { t } = useLocale();
|
||||
@@ -48,6 +65,12 @@ export const useBookingForm = ({ event }: IUseBookingForm) => {
|
||||
eventType: event.data,
|
||||
rescheduleUid,
|
||||
isRescheduling,
|
||||
email: sessionEmail,
|
||||
name: sessionName,
|
||||
username: sessionUsername,
|
||||
hasSession,
|
||||
extraOptions,
|
||||
prefillFormParams,
|
||||
});
|
||||
|
||||
const bookingForm = useForm<BookingFormValues>({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRef, useState, useEffect } from "react";
|
||||
|
||||
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
||||
@@ -18,17 +18,17 @@ import {
|
||||
import { getFullName } from "@calcom/features/form-builder/utils";
|
||||
import { useBookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { showToast } from "@calcom/ui";
|
||||
|
||||
import type { useBookingFormReturnType } from "./useBookingForm";
|
||||
import type { UseBookingFormReturnType } from "./useBookingForm";
|
||||
|
||||
export interface IUseBookings {
|
||||
event: useEventReturnType;
|
||||
hashedLink?: string | null;
|
||||
bookingForm: useBookingFormReturnType["bookingForm"];
|
||||
bookingForm: UseBookingFormReturnType["bookingForm"];
|
||||
metadata: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface IUseBookingLoadingStates {
|
||||
@@ -41,8 +41,9 @@ export interface IUseBookingErrors {
|
||||
hasDataErrors: boolean;
|
||||
dataErrors: unknown;
|
||||
}
|
||||
export type UseBookingsReturnType = ReturnType<typeof useBookings>;
|
||||
|
||||
export const useBookings = ({ event, hashedLink, bookingForm }: IUseBookings) => {
|
||||
export const useBookings = ({ event, hashedLink, bookingForm, metadata }: IUseBookings) => {
|
||||
const router = useRouter();
|
||||
const eventSlug = useBookerStore((state) => state.eventSlug);
|
||||
const setFormValues = useBookerStore((state) => state.setFormValues);
|
||||
@@ -59,8 +60,6 @@ export const useBookings = ({ event, hashedLink, bookingForm }: IUseBookings) =>
|
||||
const duration = useBookerStore((state) => state.selectedDuration);
|
||||
const { timezone } = useTimePreferences();
|
||||
const username = useBookerStore((state) => state.username);
|
||||
const routerQuery = useRouterQuery();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const isRescheduling = !!rescheduleUid && !!bookingData;
|
||||
|
||||
@@ -220,15 +219,7 @@ export const useBookings = ({ event, hashedLink, bookingForm }: IUseBookings) =>
|
||||
rescheduleUid: rescheduleUid || undefined,
|
||||
bookingUid: (bookingData && bookingData.uid) || seatedEventData?.bookingUid || undefined,
|
||||
username: username || "",
|
||||
metadata: Object.keys(routerQuery)
|
||||
.filter((key) => key.startsWith("metadata"))
|
||||
.reduce(
|
||||
(metadata, key) => ({
|
||||
...metadata,
|
||||
[key.substring("metadata[".length, key.length - 1)]: searchParams?.get(key),
|
||||
}),
|
||||
{}
|
||||
),
|
||||
metadata: metadata,
|
||||
hashedLink,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { shallow } from "zustand/shallow";
|
||||
@@ -12,7 +11,10 @@ import { useOverlayCalendarStore } from "../OverlayCalendar/store";
|
||||
import { useLocalSet } from "./useLocalSet";
|
||||
|
||||
export type UseCalendarsReturnType = ReturnType<typeof useCalendars>;
|
||||
export const useCalendars = () => {
|
||||
type UseCalendarsProps = {
|
||||
hasSession: boolean;
|
||||
};
|
||||
export const useCalendars = ({ hasSession }: UseCalendarsProps) => {
|
||||
const searchParams = useSearchParams();
|
||||
const selectedDate = useBookerStore((state) => state.selectedDate);
|
||||
const { timezone } = useTimePreferences();
|
||||
@@ -24,7 +26,6 @@ export const useCalendars = () => {
|
||||
externalId: string;
|
||||
}>("toggledConnectedCalendars", []);
|
||||
const utils = trpc.useContext();
|
||||
const { data: session } = useSession();
|
||||
|
||||
const [calendarSettingsOverlay] = useOverlayCalendarStore(
|
||||
(state) => [state.calendarSettingsOverlayModal, state.setCalendarSettingsOverlayModal],
|
||||
@@ -42,7 +43,7 @@ export const useCalendars = () => {
|
||||
})),
|
||||
},
|
||||
{
|
||||
enabled: !!session && set.size > 0 && switchEnabled,
|
||||
enabled: hasSession && set.size > 0 && switchEnabled,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { z } from "zod";
|
||||
|
||||
@@ -7,25 +5,38 @@ import { useBookerStore } from "@calcom/features/bookings/Booker/store";
|
||||
import type { useEvent } from "@calcom/features/bookings/Booker/utils/event";
|
||||
import type getBookingResponsesSchema from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
|
||||
export type useInitialFormValuesReturnType = ReturnType<typeof useInitialFormValues>;
|
||||
|
||||
type UseInitialFormValuesProps = {
|
||||
eventType: ReturnType<typeof useEvent>["data"];
|
||||
rescheduleUid: string | null;
|
||||
isRescheduling: boolean;
|
||||
email?: string | null;
|
||||
name?: string | null;
|
||||
username?: string | null;
|
||||
hasSession: boolean;
|
||||
extraOptions: Record<string, string | string[]>;
|
||||
prefillFormParams: {
|
||||
guests: string[];
|
||||
name: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export function useInitialFormValues({
|
||||
eventType,
|
||||
rescheduleUid,
|
||||
isRescheduling,
|
||||
}: {
|
||||
eventType: ReturnType<typeof useEvent>["data"];
|
||||
rescheduleUid: string | null;
|
||||
isRescheduling: boolean;
|
||||
}) {
|
||||
email,
|
||||
name,
|
||||
username,
|
||||
hasSession,
|
||||
extraOptions,
|
||||
prefillFormParams,
|
||||
}: UseInitialFormValuesProps) {
|
||||
const [initialValues, setDefaultValues] = useState<Record<string, unknown>>({});
|
||||
const bookingData = useBookerStore((state) => state.bookingData);
|
||||
const formValues = useBookerStore((state) => state.formValues);
|
||||
const searchParams = useSearchParams();
|
||||
const routerQuery = useRouterQuery();
|
||||
const session = useSession();
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
if (Object.keys(formValues).length) {
|
||||
@@ -41,19 +52,12 @@ export function useInitialFormValues({
|
||||
view: rescheduleUid ? "reschedule" : "booking",
|
||||
});
|
||||
|
||||
// Routing Forms don't support Split full name(because no Form Builder in there), so user needs to create two fields in there themselves. If they name these fields, `firstName` and `lastName`, we can prefill the Booking Form with them
|
||||
// Once we support formBuilder in Routing Forms, we should be able to forward JSON form of name field value to Booking Form and prefill it there without having these two query params separately.
|
||||
const firstNameQueryParam = searchParams?.get("firstName");
|
||||
const lastNameQueryParam = searchParams?.get("lastName");
|
||||
|
||||
const parsedQuery = await querySchema.parseAsync({
|
||||
...routerQuery,
|
||||
name:
|
||||
searchParams?.get("name") ||
|
||||
(firstNameQueryParam ? `${firstNameQueryParam} ${lastNameQueryParam}` : null),
|
||||
...extraOptions,
|
||||
name: prefillFormParams.name,
|
||||
// `guest` because we need to support legacy URL with `guest` query param support
|
||||
// `guests` because the `name` of the corresponding bookingField is `guests`
|
||||
guests: searchParams?.getAll("guests") || searchParams?.getAll("guest"),
|
||||
guests: prefillFormParams.guests,
|
||||
});
|
||||
|
||||
const defaultUserValues = {
|
||||
@@ -62,13 +66,13 @@ export function useInitialFormValues({
|
||||
? bookingData?.attendees[0].email
|
||||
: !!parsedQuery["email"]
|
||||
? parsedQuery["email"]
|
||||
: session.data?.user?.email ?? "",
|
||||
: email ?? "",
|
||||
name:
|
||||
rescheduleUid && bookingData && bookingData.attendees.length > 0
|
||||
? bookingData?.attendees[0].name
|
||||
: !!parsedQuery["name"]
|
||||
? parsedQuery["name"]
|
||||
: session.data?.user?.name ?? session.data?.user?.username ?? "",
|
||||
: name ?? username ?? "",
|
||||
};
|
||||
|
||||
if (!isRescheduling) {
|
||||
@@ -115,7 +119,7 @@ export function useInitialFormValues({
|
||||
};
|
||||
setDefaultValues(defaults);
|
||||
})();
|
||||
// do not add routerQuery as a dependency, it will cause infinite loop
|
||||
// do not add extraOptions as a dependency, it will cause infinite loop
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
eventType?.bookingFields,
|
||||
@@ -123,14 +127,14 @@ export function useInitialFormValues({
|
||||
isRescheduling,
|
||||
bookingData,
|
||||
rescheduleUid,
|
||||
searchParams,
|
||||
session.data?.user?.email,
|
||||
session.data?.user?.name,
|
||||
session.data?.user?.username,
|
||||
email,
|
||||
name,
|
||||
username,
|
||||
prefillFormParams,
|
||||
]);
|
||||
|
||||
// When initialValues is available(after doing async schema parsing) or session is available(so that we can prefill logged-in user email and name), we need to reset the form with the initialValues
|
||||
const key = `${Object.keys(initialValues).length}_${session ? 1 : 0}`;
|
||||
const key = `${Object.keys(initialValues).length}_${hasSession ? 1 : 0}`;
|
||||
|
||||
return { initialValues, key };
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import { useOverlayCalendarStore } from "../OverlayCalendar/store";
|
||||
import type { UseCalendarsReturnType } from "./useCalendars";
|
||||
import { useLocalSet } from "./useLocalSet";
|
||||
|
||||
export type UseOverlayCalendarReturnType = ReturnType<typeof useOverlayCalendar>;
|
||||
|
||||
export const useOverlayCalendar = ({
|
||||
connectedCalendars,
|
||||
overlayBusyDates,
|
||||
|
||||
@@ -8,6 +8,8 @@ import type { useEventReturnType } from "@calcom/features/bookings/Booker/utils/
|
||||
import { MINUTES_TO_BOOK } from "@calcom/lib/constants";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
export type UseSlotsReturnType = ReturnType<typeof useSlots>;
|
||||
|
||||
export const useSlots = (event: useEventReturnType) => {
|
||||
const selectedDuration = useBookerStore((state) => state.selectedDuration);
|
||||
const [selectedTimeslot, setSelectedTimeslot] = useBookerStore(
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
export type UseVerifyCodeReturnType = ReturnType<typeof useVerifyCode>;
|
||||
|
||||
type UseVerifyCodeProps = {
|
||||
onSuccess: (isVerified: boolean) => void;
|
||||
};
|
||||
|
||||
export const useVerifyCode = ({ onSuccess }: UseVerifyCodeProps) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [value, setValue] = useState("");
|
||||
const [hasVerified, setHasVerified] = useState(false);
|
||||
|
||||
const verifyCodeMutationUserSessionRequired = trpc.viewer.organizations.verifyCode.useMutation({
|
||||
onSuccess: (data) => {
|
||||
setIsPending(false);
|
||||
onSuccess(data);
|
||||
},
|
||||
onError: (err) => {
|
||||
setIsPending(false);
|
||||
setHasVerified(false);
|
||||
if (err.message === "invalid_code") {
|
||||
setError(t("code_provided_invalid"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const verifyCodeMutationUserSessionNotRequired = trpc.viewer.auth.verifyCodeUnAuthenticated.useMutation({
|
||||
onSuccess: (data) => {
|
||||
setIsPending(false);
|
||||
onSuccess(data);
|
||||
},
|
||||
onError: (err) => {
|
||||
setIsPending(false);
|
||||
setHasVerified(false);
|
||||
if (err.message === "invalid_code") {
|
||||
setError(t("code_provided_invalid"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const verifyCodeWithSessionRequired = (code: string, email: string) => {
|
||||
verifyCodeMutationUserSessionRequired.mutate({
|
||||
code,
|
||||
email,
|
||||
});
|
||||
};
|
||||
|
||||
const verifyCodeWithSessionNotRequired = (code: string, email: string) => {
|
||||
verifyCodeMutationUserSessionNotRequired.mutate({
|
||||
code,
|
||||
email,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
verifyCodeWithSessionRequired,
|
||||
verifyCodeWithSessionNotRequired,
|
||||
isPending,
|
||||
setIsPending,
|
||||
error,
|
||||
value,
|
||||
hasVerified,
|
||||
setValue,
|
||||
setHasVerified,
|
||||
resetErrors: () => setError(""),
|
||||
};
|
||||
};
|
||||
@@ -11,7 +11,7 @@ export interface IUseVerifyEmailProps {
|
||||
name?: string | { firstName: string; lastname?: string };
|
||||
requiresBookerEmailVerification?: boolean;
|
||||
}
|
||||
|
||||
export type UseVerifyEmailReturnType = ReturnType<typeof useVerifyEmail>;
|
||||
export const useVerifyEmail = ({
|
||||
email,
|
||||
name,
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
import type { UseBookerLayoutType } from "@calcom/features/bookings/Booker/components/hooks/useBookerLayout";
|
||||
import type { UseBookingFormReturnType } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm";
|
||||
import type { UseBookingsReturnType } from "@calcom/features/bookings/Booker/components/hooks/useBookings";
|
||||
import type { UseCalendarsReturnType } from "@calcom/features/bookings/Booker/components/hooks/useCalendars";
|
||||
import type { UseSlotsReturnType } from "@calcom/features/bookings/Booker/components/hooks/useSlots";
|
||||
import type { UseVerifyCodeReturnType } from "@calcom/features/bookings/Booker/components/hooks/useVerifyCode";
|
||||
import type { UseVerifyEmailReturnType } from "@calcom/features/bookings/Booker/components/hooks/useVerifyEmail";
|
||||
import type {
|
||||
useEventReturnType,
|
||||
useScheduleForEventReturnType,
|
||||
} from "@calcom/features/bookings/Booker/utils/event";
|
||||
import type { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import type { GetBookingType } from "../lib/get-booking";
|
||||
@@ -75,6 +86,30 @@ export interface BookerProps {
|
||||
isInstantMeeting?: boolean;
|
||||
}
|
||||
|
||||
export type WrappedBookerProps = {
|
||||
sessionUsername?: string | null;
|
||||
rescheduleUid: string | null;
|
||||
bookingUid: string | null;
|
||||
isRedirect: boolean;
|
||||
fromUserNameRedirected: string;
|
||||
hasSession: boolean;
|
||||
onGoBackInstantMeeting: () => void;
|
||||
onConnectNowInstantMeeting: () => void;
|
||||
onOverlayClickNoCalendar: () => void;
|
||||
onClickOverlayContinue: () => void;
|
||||
onOverlaySwitchStateChange: (state: boolean) => void;
|
||||
extraOptions: Record<string, string | string[]>;
|
||||
bookings: UseBookingsReturnType;
|
||||
slots: UseSlotsReturnType;
|
||||
calendars: UseCalendarsReturnType;
|
||||
verifyEmail: UseVerifyEmailReturnType;
|
||||
bookerForm: UseBookingFormReturnType;
|
||||
event: useEventReturnType;
|
||||
schedule: useScheduleForEventReturnType;
|
||||
bookerLayout: UseBookerLayoutType;
|
||||
verifyCode: UseVerifyCodeReturnType;
|
||||
};
|
||||
|
||||
export type BookerState = "loading" | "selecting_date" | "selecting_time" | "booking";
|
||||
export type BookerLayout = BookerLayouts | "mobile";
|
||||
export type BookerAreas = "calendar" | "timeslots" | "main" | "meta" | "header";
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useState, useEffect, useCallback } from "react";
|
||||
import useDigitInput from "react-digit-input";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
@@ -20,19 +19,26 @@ export const VerifyCodeDialog = ({
|
||||
isOpenDialog,
|
||||
setIsOpenDialog,
|
||||
email,
|
||||
onSuccess,
|
||||
isUserSessionRequiredToVerify = true,
|
||||
verifyCodeWithSessionNotRequired,
|
||||
verifyCodeWithSessionRequired,
|
||||
resetErrors,
|
||||
setIsPending,
|
||||
isPending,
|
||||
error,
|
||||
}: {
|
||||
isOpenDialog: boolean;
|
||||
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
|
||||
email: string;
|
||||
onSuccess: (isVerified: boolean) => void;
|
||||
isUserSessionRequiredToVerify?: boolean;
|
||||
verifyCodeWithSessionNotRequired: (code: string, email: string) => void;
|
||||
verifyCodeWithSessionRequired: (code: string, email: string) => void;
|
||||
resetErrors: () => void;
|
||||
isPending: boolean;
|
||||
setIsPending: (status: boolean) => void;
|
||||
error: string;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
// Not using the mutation isPending flag because after verifying we submit the underlying org creation form
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [value, setValue] = useState("");
|
||||
const [hasVerified, setHasVerified] = useState(false);
|
||||
|
||||
@@ -42,60 +48,28 @@ export const VerifyCodeDialog = ({
|
||||
value,
|
||||
onChange: useCallback((value: string) => {
|
||||
// whenever there's a change in the input, we reset the error value.
|
||||
setError("");
|
||||
resetErrors();
|
||||
setValue(value);
|
||||
}, []),
|
||||
});
|
||||
|
||||
const verifyCodeMutationUserSessionRequired = trpc.viewer.organizations.verifyCode.useMutation({
|
||||
onSuccess: (data) => {
|
||||
setIsPending(false);
|
||||
onSuccess(data);
|
||||
},
|
||||
onError: (err) => {
|
||||
setIsPending(false);
|
||||
setHasVerified(false);
|
||||
if (err.message === "invalid_code") {
|
||||
setError(t("code_provided_invalid"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const verifyCodeMutationUserSessionNotRequired = trpc.viewer.auth.verifyCodeUnAuthenticated.useMutation({
|
||||
onSuccess: (data) => {
|
||||
setIsPending(false);
|
||||
onSuccess(data);
|
||||
},
|
||||
onError: (err) => {
|
||||
setIsPending(false);
|
||||
setHasVerified(false);
|
||||
if (err.message === "invalid_code") {
|
||||
setError(t("code_provided_invalid"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const verifyCode = useCallback(() => {
|
||||
setError("");
|
||||
resetErrors();
|
||||
setIsPending(true);
|
||||
if (isUserSessionRequiredToVerify) {
|
||||
verifyCodeMutationUserSessionRequired.mutate({
|
||||
code: value,
|
||||
email,
|
||||
});
|
||||
verifyCodeWithSessionRequired(value, email);
|
||||
} else {
|
||||
verifyCodeMutationUserSessionNotRequired.mutate({
|
||||
code: value,
|
||||
email,
|
||||
});
|
||||
verifyCodeWithSessionNotRequired(value, email);
|
||||
}
|
||||
setHasVerified(true);
|
||||
}, [
|
||||
email,
|
||||
resetErrors,
|
||||
setIsPending,
|
||||
isUserSessionRequiredToVerify,
|
||||
verifyCodeWithSessionRequired,
|
||||
value,
|
||||
verifyCodeMutationUserSessionNotRequired,
|
||||
verifyCodeMutationUserSessionRequired,
|
||||
email,
|
||||
verifyCodeWithSessionNotRequired,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -116,7 +90,7 @@ export const VerifyCodeDialog = ({
|
||||
open={isOpenDialog}
|
||||
onOpenChange={(open) => {
|
||||
setValue("");
|
||||
setError("");
|
||||
resetErrors();
|
||||
setIsOpenDialog(open);
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime";
|
||||
|
||||
type BufferedBusyTimes = BufferedBusyTime[];
|
||||
const log = logger.getSubLogger({ prefix: ["[api] book:user"] });
|
||||
|
||||
// if true, there are conflicts.
|
||||
export function checkForConflicts(busyTimes: BufferedBusyTimes, time: dayjs.ConfigType, length: number) {
|
||||
// Early return
|
||||
if (!Array.isArray(busyTimes) || busyTimes.length < 1) {
|
||||
return false; // guaranteed no conflicts when there is no busy times.
|
||||
}
|
||||
|
||||
for (const busyTime of busyTimes) {
|
||||
const startTime = dayjs(busyTime.start);
|
||||
const endTime = dayjs(busyTime.end);
|
||||
// Check if time is between start and end times
|
||||
if (dayjs(time).isBetween(startTime, endTime, null, "[)")) {
|
||||
log.error(
|
||||
`NAUF: start between a busy time slot ${safeStringify({
|
||||
...busyTime,
|
||||
time: dayjs(time).format(),
|
||||
})}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
// Check if slot end time is between start and end time
|
||||
if (dayjs(time).add(length, "minutes").isBetween(startTime, endTime)) {
|
||||
log.error(
|
||||
`NAUF: Ends between a busy time slot ${safeStringify({
|
||||
...busyTime,
|
||||
time: dayjs(time).add(length, "minutes").format(),
|
||||
})}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
// Check if startTime is between slot
|
||||
if (startTime.isBetween(dayjs(time), dayjs(time).add(length, "minutes"))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
|
||||
import type { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import type { userSelect } from "@calcom/prisma";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
type User = Prisma.UserGetPayload<typeof userSelect>;
|
||||
|
||||
/**
|
||||
* Gets credentials from the user, team, and org if applicable
|
||||
*
|
||||
*/
|
||||
export const getAllCredentials = async (
|
||||
user: User & { credentials: CredentialPayload[] },
|
||||
eventType: Awaited<ReturnType<typeof getEventTypesFromDB>>
|
||||
) => {
|
||||
const allCredentials = user.credentials;
|
||||
|
||||
// If it's a team event type query for team credentials
|
||||
if (eventType.team?.id) {
|
||||
const teamCredentialsQuery = await prisma.credential.findMany({
|
||||
where: {
|
||||
teamId: eventType.team.id,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
allCredentials.push(...teamCredentialsQuery);
|
||||
}
|
||||
|
||||
// If it's a managed event type, query for the parent team's credentials
|
||||
if (eventType.parentId) {
|
||||
const teamCredentialsQuery = await prisma.team.findFirst({
|
||||
where: {
|
||||
eventTypes: {
|
||||
some: {
|
||||
id: eventType.parentId,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (teamCredentialsQuery?.credentials) {
|
||||
allCredentials.push(...teamCredentialsQuery?.credentials);
|
||||
}
|
||||
}
|
||||
|
||||
const { profile } = await UserRepository.enrichUserWithItsProfile({
|
||||
user: user,
|
||||
});
|
||||
|
||||
// If the user is a part of an organization, query for the organization's credentials
|
||||
if (profile?.organizationId) {
|
||||
const org = await prisma.team.findUnique({
|
||||
where: {
|
||||
id: profile.organizationId,
|
||||
},
|
||||
select: {
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (org?.credentials) {
|
||||
allCredentials.push(...org.credentials);
|
||||
}
|
||||
}
|
||||
|
||||
return allCredentials;
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import prisma from "@calcom/prisma";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
/**
|
||||
* Refreshes a Credential with fresh data from the database.
|
||||
*
|
||||
* @param credential
|
||||
*/
|
||||
export async function refreshCredential(credential: CredentialPayload): Promise<CredentialPayload> {
|
||||
const newCredential = await prisma.credential.findUnique({
|
||||
where: {
|
||||
id: credential.id,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
|
||||
if (!newCredential) {
|
||||
return credential;
|
||||
} else {
|
||||
return newCredential;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import async from "async";
|
||||
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
import { refreshCredential } from "./refreshCredential";
|
||||
|
||||
/**
|
||||
* Refreshes the given set of credentials.
|
||||
*
|
||||
* @param credentials
|
||||
*/
|
||||
export async function refreshCredentials(
|
||||
credentials: Array<CredentialPayload>
|
||||
): Promise<Array<CredentialPayload>> {
|
||||
return await async.mapLimit(credentials, 5, refreshCredential);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { App, DestinationCalendar, EventTypeCustomInput } from "@prisma/client";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import async from "async";
|
||||
import type { IncomingMessage } from "http";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
@@ -85,7 +84,6 @@ import {
|
||||
EventTypeMetaDataSchema,
|
||||
userMetadata as userMetadataSchema,
|
||||
} from "@calcom/prisma/zod-utils";
|
||||
import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime";
|
||||
import type {
|
||||
AdditionalInformation,
|
||||
AppsStatus,
|
||||
@@ -97,6 +95,9 @@ import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
|
||||
|
||||
import type { EventTypeInfo } from "../../webhooks/lib/sendPayload";
|
||||
import { checkForConflicts } from "./conflictChecker/checkForConflicts";
|
||||
import { getAllCredentials } from "./getAllCredentialsForUsersOnEvent/getAllCredentials";
|
||||
import { refreshCredentials } from "./getAllCredentialsForUsersOnEvent/refreshCredentials";
|
||||
import getBookingDataSchema from "./getBookingDataSchema";
|
||||
import handleSeats from "./handleSeats/handleSeats";
|
||||
import type { BookingSeat } from "./handleSeats/types";
|
||||
@@ -105,7 +106,6 @@ const translator = short();
|
||||
const log = logger.getSubLogger({ prefix: ["[api] book:user"] });
|
||||
|
||||
type User = Prisma.UserGetPayload<typeof userSelect>;
|
||||
type BufferedBusyTimes = BufferedBusyTime[];
|
||||
type BookingType = Prisma.PromiseReturnType<typeof getOriginalRescheduledBooking>;
|
||||
export type Booking = Prisma.PromiseReturnType<typeof createBooking>;
|
||||
export type NewBookingEventType =
|
||||
@@ -153,142 +153,6 @@ export interface IEventTypePaymentCredentialType {
|
||||
key: Prisma.JsonValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes a Credential with fresh data from the database.
|
||||
*
|
||||
* @param credential
|
||||
*/
|
||||
async function refreshCredential(credential: CredentialPayload): Promise<CredentialPayload> {
|
||||
const newCredential = await prisma.credential.findUnique({
|
||||
where: {
|
||||
id: credential.id,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
|
||||
if (!newCredential) {
|
||||
return credential;
|
||||
} else {
|
||||
return newCredential;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the given set of credentials.
|
||||
*
|
||||
* @param credentials
|
||||
*/
|
||||
export async function refreshCredentials(
|
||||
credentials: Array<CredentialPayload>
|
||||
): Promise<Array<CredentialPayload>> {
|
||||
return await async.mapLimit(credentials, 5, refreshCredential);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets credentials from the user, team, and org if applicable
|
||||
*
|
||||
*/
|
||||
const getAllCredentials = async (
|
||||
user: User & { credentials: CredentialPayload[] },
|
||||
eventType: Awaited<ReturnType<typeof getEventTypesFromDB>>
|
||||
) => {
|
||||
const allCredentials = user.credentials;
|
||||
|
||||
// If it's a team event type query for team credentials
|
||||
if (eventType.team?.id) {
|
||||
const teamCredentialsQuery = await prisma.credential.findMany({
|
||||
where: {
|
||||
teamId: eventType.team.id,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
allCredentials.push(...teamCredentialsQuery);
|
||||
}
|
||||
|
||||
// If it's a managed event type, query for the parent team's credentials
|
||||
if (eventType.parentId) {
|
||||
const teamCredentialsQuery = await prisma.team.findFirst({
|
||||
where: {
|
||||
eventTypes: {
|
||||
some: {
|
||||
id: eventType.parentId,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (teamCredentialsQuery?.credentials) {
|
||||
allCredentials.push(...teamCredentialsQuery?.credentials);
|
||||
}
|
||||
}
|
||||
|
||||
const { profile } = await UserRepository.enrichUserWithItsProfile({
|
||||
user: user,
|
||||
});
|
||||
|
||||
// If the user is a part of an organization, query for the organization's credentials
|
||||
if (profile?.organizationId) {
|
||||
const org = await prisma.team.findUnique({
|
||||
where: {
|
||||
id: profile.organizationId,
|
||||
},
|
||||
select: {
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (org?.credentials) {
|
||||
allCredentials.push(...org.credentials);
|
||||
}
|
||||
}
|
||||
|
||||
return allCredentials;
|
||||
};
|
||||
|
||||
// if true, there are conflicts.
|
||||
function checkForConflicts(busyTimes: BufferedBusyTimes, time: dayjs.ConfigType, length: number) {
|
||||
// Early return
|
||||
if (!Array.isArray(busyTimes) || busyTimes.length < 1) {
|
||||
return false; // guaranteed no conflicts when there is no busy times.
|
||||
}
|
||||
|
||||
for (const busyTime of busyTimes) {
|
||||
const startTime = dayjs(busyTime.start);
|
||||
const endTime = dayjs(busyTime.end);
|
||||
// Check if time is between start and end times
|
||||
if (dayjs(time).isBetween(startTime, endTime, null, "[)")) {
|
||||
log.error(
|
||||
`NAUF: start between a busy time slot ${safeStringify({
|
||||
...busyTime,
|
||||
time: dayjs(time).format(),
|
||||
})}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
// Check if slot end time is between start and end time
|
||||
if (dayjs(time).add(length, "minutes").isBetween(startTime, endTime)) {
|
||||
log.error(
|
||||
`NAUF: Ends between a busy time slot ${safeStringify({
|
||||
...busyTime,
|
||||
time: dayjs(time).add(length, "minutes").format(),
|
||||
})}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
// Check if startTime is between slot
|
||||
if (startTime.isBetween(dayjs(time), dayjs(time).add(length, "minutes"))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export const getEventTypesFromDB = async (eventTypeId: number) => {
|
||||
const eventType = await prisma.eventType.findUniqueOrThrow({
|
||||
where: {
|
||||
@@ -451,6 +315,16 @@ export async function ensureAvailableUsers(
|
||||
loggerWithEventDetails: Logger<unknown>
|
||||
) {
|
||||
const availableUsers: IsFixedAwareUser[] = [];
|
||||
const getStartDateTimeUtc = (startDateTimeInput: string, timeZone?: string) => {
|
||||
return timeZone === "Etc/GMT"
|
||||
? dayjs.utc(startDateTimeInput)
|
||||
: dayjs(startDateTimeInput).tz(timeZone).utc();
|
||||
};
|
||||
|
||||
const startDateTimeUtc = getStartDateTimeUtc(input.dateFrom, input.timeZone);
|
||||
const endDateTimeUtc =
|
||||
input.timeZone === "Etc/GMT" ? dayjs.utc(input.dateTo) : dayjs(input.dateTo).tz(input.timeZone).utc();
|
||||
|
||||
const duration = dayjs(input.dateTo).diff(input.dateFrom, "minute");
|
||||
const originalBookingDuration = input.originalRescheduledBooking
|
||||
? dayjs(input.originalRescheduledBooking.endTime).diff(
|
||||
@@ -467,23 +341,24 @@ export async function ensureAvailableUsers(
|
||||
busyTimesFromLimitsBookingsAllUsers = await getBusyTimesForLimitChecks({
|
||||
userIds: eventType.users.map((u) => u.id),
|
||||
eventTypeId: eventType.id,
|
||||
startDate: input.dateFrom,
|
||||
endDate: input.dateTo,
|
||||
startDate: startDateTimeUtc.format(),
|
||||
endDate: endDateTimeUtc.format(),
|
||||
rescheduleUid: input.originalRescheduledBooking?.uid ?? null,
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
});
|
||||
}
|
||||
|
||||
/** Let's start checking for availability */
|
||||
for (const user of eventType.users) {
|
||||
const { dateRanges, busy: bufferedBusyTimes } = await getUserAvailability(
|
||||
{
|
||||
...input,
|
||||
userId: user.id,
|
||||
eventTypeId: eventType.id,
|
||||
duration: originalBookingDuration,
|
||||
returnDateOverrides: false,
|
||||
...input,
|
||||
dateFrom: startDateTimeUtc.format(),
|
||||
dateTo: endDateTimeUtc.format(),
|
||||
},
|
||||
{
|
||||
user,
|
||||
@@ -499,7 +374,14 @@ export async function ensureAvailableUsers(
|
||||
);
|
||||
|
||||
if (!dateRanges.length) {
|
||||
// user does not have availability at this time, skip user.
|
||||
loggerWithEventDetails.error(
|
||||
`User does not have availability at this time.`,
|
||||
safeStringify({
|
||||
startDateTimeUtc,
|
||||
endDateTimeUtc,
|
||||
timeZone: input.timeZone,
|
||||
})
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -510,9 +392,8 @@ export async function ensureAvailableUsers(
|
||||
//check if event time is within the date range
|
||||
for (const dateRange of dateRanges) {
|
||||
if (
|
||||
(dayjs.utc(input.dateFrom).isAfter(dateRange.start) ||
|
||||
dayjs.utc(input.dateFrom).isSame(dateRange.start)) &&
|
||||
(dayjs.utc(input.dateTo).isBefore(dateRange.end) || dayjs.utc(input.dateTo).isSame(dateRange.end))
|
||||
(startDateTimeUtc.isAfter(dateRange.start) || startDateTimeUtc.isSame(dateRange.start)) &&
|
||||
(endDateTimeUtc.isBefore(dateRange.end) || endDateTimeUtc.isSame(dateRange.end))
|
||||
) {
|
||||
dateRangeForBooking = true;
|
||||
break;
|
||||
@@ -520,15 +401,21 @@ export async function ensureAvailableUsers(
|
||||
}
|
||||
|
||||
if (!dateRangeForBooking) {
|
||||
loggerWithEventDetails.error(
|
||||
`No date range for booking.`,
|
||||
safeStringify({
|
||||
startDateTimeUtc,
|
||||
endDateTimeUtc,
|
||||
timeZone: input.timeZone,
|
||||
})
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
foundConflict = checkForConflicts(bufferedBusyTimes, input.dateFrom, duration);
|
||||
} catch {
|
||||
log.debug({
|
||||
message: "Unable set isAvailableToBeBooked. Using true. ",
|
||||
});
|
||||
foundConflict = checkForConflicts(bufferedBusyTimes, startDateTimeUtc, duration);
|
||||
} catch (error) {
|
||||
loggerWithEventDetails.error("Unable set isAvailableToBeBooked. Using true. ", error);
|
||||
}
|
||||
// no conflicts found, add to available users.
|
||||
if (!foundConflict) {
|
||||
@@ -536,7 +423,14 @@ export async function ensureAvailableUsers(
|
||||
}
|
||||
}
|
||||
if (!availableUsers.length) {
|
||||
loggerWithEventDetails.error(`No available users found.`);
|
||||
loggerWithEventDetails.error(
|
||||
`No available users found.`,
|
||||
safeStringify({
|
||||
startDateTimeUtc,
|
||||
endDateTimeUtc,
|
||||
timeZone: input.timeZone,
|
||||
})
|
||||
);
|
||||
throw new Error(ErrorCode.NoAvailableUsersFound);
|
||||
}
|
||||
return availableUsers;
|
||||
@@ -1063,7 +957,7 @@ async function handler(
|
||||
!!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType);
|
||||
|
||||
const paymentAppData = getPaymentAppData(eventType);
|
||||
loggerWithEventDetails.debug(
|
||||
loggerWithEventDetails.info(
|
||||
`Booking eventType ${eventTypeId} started`,
|
||||
safeStringify({
|
||||
reqBody: {
|
||||
@@ -1074,6 +968,7 @@ async function handler(
|
||||
endTime: reqBody.end,
|
||||
rescheduleUid: reqBody.rescheduleUid,
|
||||
location: location,
|
||||
timeZone: reqBody.timeZone,
|
||||
},
|
||||
isTeamEventType,
|
||||
eventType: getPiiFreeEventType(eventType),
|
||||
@@ -1647,7 +1542,7 @@ async function handler(
|
||||
};
|
||||
|
||||
// For seats, if the booking already exists then we want to add the new attendee to the existing booking
|
||||
if (eventType.seatsPerTimeSlot && (reqBody.bookingUid || rescheduleUid)) {
|
||||
if (eventType.seatsPerTimeSlot) {
|
||||
const newBooking = await handleSeats({
|
||||
rescheduleUid,
|
||||
reqBookingUid: reqBody.bookingUid,
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { describe, expect } from "vitest";
|
||||
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||
import {
|
||||
createBookingScenario,
|
||||
getGoogleCalendarCredential,
|
||||
TestData,
|
||||
getDate,
|
||||
getOrganizer,
|
||||
getBooker,
|
||||
getScenarioData,
|
||||
mockSuccessfulVideoMeetingCreation,
|
||||
mockCalendarToHaveNoBusySlots,
|
||||
BookingLocations,
|
||||
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest";
|
||||
import {
|
||||
expectSuccessfulBookingCreationEmails,
|
||||
expectBookingToBeInDatabase,
|
||||
expectSuccessfulCalendarEventCreationInCalendar,
|
||||
expectICalUIDAsString,
|
||||
} from "@calcom/web/test/utils/bookingScenario/expects";
|
||||
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
|
||||
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
|
||||
|
||||
export const Timezones = {
|
||||
"-05:00": "America/New_York",
|
||||
"00:00": "Europe/London",
|
||||
};
|
||||
|
||||
export type CustomNextApiRequest = NextApiRequest & Request;
|
||||
|
||||
export type CustomNextApiResponse = NextApiResponse & Response;
|
||||
// Local test runs sometime gets too slow
|
||||
const timeout = process.env.CI ? 5000 : 20000;
|
||||
|
||||
describe("handleNewBooking", () => {
|
||||
setupAndTeardown();
|
||||
|
||||
describe("Booking for slot only available by date override:", () => {
|
||||
test(
|
||||
`should be able to create a booking for the exact slot overridden`,
|
||||
async ({ emails }) => {
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const newYorkTimeZone = Timezones["-05:00"];
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
// Using .endOf("day") here to ensure our date doesn't change when we set the time zone
|
||||
const startDateTimeOrganizerTz = dayjs(plus1DateString)
|
||||
.endOf("day")
|
||||
.tz(newYorkTimeZone)
|
||||
.hour(11)
|
||||
.minute(0)
|
||||
.second(0);
|
||||
|
||||
const endDateTimeOrganizerTz = dayjs(plus1DateString)
|
||||
.endOf("day")
|
||||
.tz(newYorkTimeZone)
|
||||
.hour(12)
|
||||
.minute(0)
|
||||
.second(0);
|
||||
|
||||
const overrideSchedule = {
|
||||
name: "11:00AM to 12PM in New York",
|
||||
availability: [
|
||||
{
|
||||
days: [],
|
||||
startTime: dayjs("1970-01-01").utc().hour(11).toDate(), // These times are stored with Z offset
|
||||
endTime: dayjs("1970-01-01").utc().hour(12).toDate(), // These times are stored with Z offset
|
||||
date: plus1DateString,
|
||||
},
|
||||
],
|
||||
timeZone: newYorkTimeZone,
|
||||
};
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [overrideSchedule],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
|
||||
await createBookingScenario(
|
||||
getScenarioData({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 60,
|
||||
length: 60,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
})
|
||||
);
|
||||
|
||||
mockSuccessfulVideoMeetingCreation({
|
||||
metadataLookupKey: "dailyvideo",
|
||||
videoMeetingData: {
|
||||
id: "MOCK_ID",
|
||||
password: "MOCK_PASS",
|
||||
url: `http://mock-dailyvideo.example.com/meeting-1`,
|
||||
},
|
||||
});
|
||||
|
||||
// Mock a Scenario where iCalUID isn't returned by Google Calendar in which case booking UID is used as the ics UID
|
||||
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
id: "GOOGLE_CALENDAR_EVENT_ID",
|
||||
uid: "MOCK_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: BookingLocations.CalVideo },
|
||||
},
|
||||
start: startDateTimeOrganizerTz.format(),
|
||||
end: endDateTimeOrganizerTz.format(),
|
||||
timeZone: Timezones["-05:00"],
|
||||
},
|
||||
});
|
||||
|
||||
const { req } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData,
|
||||
});
|
||||
|
||||
const createdBooking = await handleNewBooking(req);
|
||||
expect(createdBooking.responses).toContain({
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
});
|
||||
|
||||
expect(createdBooking).toContain({
|
||||
location: BookingLocations.CalVideo,
|
||||
});
|
||||
|
||||
await expectBookingToBeInDatabase({
|
||||
description: "",
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
uid: createdBooking.uid!,
|
||||
eventTypeId: mockBookingData.eventTypeId,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
references: [
|
||||
{
|
||||
type: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
},
|
||||
{
|
||||
type: appStoreMetadata.googlecalendar.type,
|
||||
uid: "GOOGLE_CALENDAR_EVENT_ID",
|
||||
meetingId: "GOOGLE_CALENDAR_EVENT_ID",
|
||||
meetingPassword: "MOCK_PASSWORD",
|
||||
meetingUrl: "https://UNUSED_URL",
|
||||
},
|
||||
],
|
||||
iCalUID: createdBooking.iCalUID,
|
||||
});
|
||||
|
||||
expectSuccessfulCalendarEventCreationInCalendar(calendarMock, {
|
||||
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
// We won't be sending evt.destinationCalendar in this case.
|
||||
// Google Calendar in this case fallbacks to the "primary" calendar - https://github.com/calcom/cal.com/blob/7d5dad7fea78ff24dddbe44f1da5d7e08e1ff568/packages/app-store/googlecalendar/lib/CalendarService.ts#L217
|
||||
// Not sure if it's the correct behaviour. Right now, it isn't possible to have an organizer with connected calendar but no destination calendar - As soon as the Google Calendar app is installed, a destination calendar is created.
|
||||
calendarId: null,
|
||||
});
|
||||
|
||||
const iCalUID = expectICalUIDAsString(createdBooking.iCalUID);
|
||||
|
||||
expectSuccessfulBookingCreationEmails({
|
||||
booking: {
|
||||
uid: createdBooking.uid!,
|
||||
},
|
||||
booker,
|
||||
organizer,
|
||||
emails,
|
||||
iCalUID,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
test(
|
||||
`should be able to book the last slot before midnight`,
|
||||
async ({ emails }) => {
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const newYorkTimeZone = Timezones["-05:00"];
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
// Using .endOf("day") here to ensure our date doesn't change when we set the time zone
|
||||
const startDateTimeOrganizerTz = dayjs(plus1DateString)
|
||||
.endOf("day")
|
||||
.tz(newYorkTimeZone)
|
||||
.hour(23)
|
||||
.minute(0)
|
||||
.second(0);
|
||||
|
||||
const endDateTimeOrganizerTz = dayjs(plus1DateString)
|
||||
.endOf("day")
|
||||
.tz(newYorkTimeZone)
|
||||
.startOf("day")
|
||||
.add(1, "day");
|
||||
|
||||
const overrideSchedule = {
|
||||
name: "11:00PM to 11:59PM in New York",
|
||||
availability: [
|
||||
{
|
||||
days: [],
|
||||
startTime: dayjs("1970-01-01").utc().hour(23).toDate(), // These times are stored with Z offset
|
||||
endTime: dayjs("1970-01-01").utc().hour(23).minute(59).toDate(), // These times are stored with Z offset
|
||||
date: plus1DateString,
|
||||
},
|
||||
],
|
||||
timeZone: newYorkTimeZone,
|
||||
};
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [overrideSchedule],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
|
||||
await createBookingScenario(
|
||||
getScenarioData({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 60,
|
||||
length: 60,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
})
|
||||
);
|
||||
|
||||
mockSuccessfulVideoMeetingCreation({
|
||||
metadataLookupKey: "dailyvideo",
|
||||
videoMeetingData: {
|
||||
id: "MOCK_ID",
|
||||
password: "MOCK_PASS",
|
||||
url: `http://mock-dailyvideo.example.com/meeting-1`,
|
||||
},
|
||||
});
|
||||
|
||||
// Mock a Scenario where iCalUID isn't returned by Google Calendar in which case booking UID is used as the ics UID
|
||||
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: {
|
||||
id: "GOOGLE_CALENDAR_EVENT_ID",
|
||||
uid: "MOCK_ID",
|
||||
},
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: BookingLocations.CalVideo },
|
||||
},
|
||||
start: startDateTimeOrganizerTz.format(),
|
||||
end: endDateTimeOrganizerTz.format(),
|
||||
timeZone: Timezones["-05:00"],
|
||||
},
|
||||
});
|
||||
|
||||
const { req } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData,
|
||||
});
|
||||
|
||||
const createdBooking = await handleNewBooking(req);
|
||||
expect(createdBooking.responses).toContain({
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
});
|
||||
|
||||
expect(createdBooking).toContain({
|
||||
location: BookingLocations.CalVideo,
|
||||
});
|
||||
|
||||
await expectBookingToBeInDatabase({
|
||||
description: "",
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
uid: createdBooking.uid!,
|
||||
eventTypeId: mockBookingData.eventTypeId,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
references: [
|
||||
{
|
||||
type: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
},
|
||||
{
|
||||
type: appStoreMetadata.googlecalendar.type,
|
||||
uid: "GOOGLE_CALENDAR_EVENT_ID",
|
||||
meetingId: "GOOGLE_CALENDAR_EVENT_ID",
|
||||
meetingPassword: "MOCK_PASSWORD",
|
||||
meetingUrl: "https://UNUSED_URL",
|
||||
},
|
||||
],
|
||||
iCalUID: createdBooking.iCalUID,
|
||||
});
|
||||
|
||||
expectSuccessfulCalendarEventCreationInCalendar(calendarMock, {
|
||||
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
// We won't be sending evt.destinationCalendar in this case.
|
||||
// Google Calendar in this case fallbacks to the "primary" calendar - https://github.com/calcom/cal.com/blob/7d5dad7fea78ff24dddbe44f1da5d7e08e1ff568/packages/app-store/googlecalendar/lib/CalendarService.ts#L217
|
||||
// Not sure if it's the correct behaviour. Right now, it isn't possible to have an organizer with connected calendar but no destination calendar - As soon as the Google Calendar app is installed, a destination calendar is created.
|
||||
calendarId: null,
|
||||
});
|
||||
|
||||
const iCalUID = expectICalUIDAsString(createdBooking.iCalUID);
|
||||
|
||||
expectSuccessfulBookingCreationEmails({
|
||||
booking: {
|
||||
uid: createdBooking.uid!,
|
||||
},
|
||||
booker,
|
||||
organizer,
|
||||
emails,
|
||||
iCalUID,
|
||||
});
|
||||
},
|
||||
timeout
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { uuid } from "short-uuid";
|
||||
|
||||
import EventManager from "@calcom/core/EventManager";
|
||||
import { sendScheduledSeatsEmails } from "@calcom/emails";
|
||||
import { refreshCredentials } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/refreshCredentials";
|
||||
import {
|
||||
allowDisablingAttendeeConfirmationEmails,
|
||||
allowDisablingHostConfirmationEmails,
|
||||
@@ -15,7 +16,7 @@ import prisma from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
import type { IEventTypePaymentCredentialType } from "../../handleNewBooking";
|
||||
import { refreshCredentials, findBookingQuery } from "../../handleNewBooking";
|
||||
import { findBookingQuery } from "../../handleNewBooking";
|
||||
import type { SeatedBooking, NewSeatedBookingObject, HandleSeatsResultBooking } from "../types";
|
||||
|
||||
const createNewSeat = async (
|
||||
@@ -27,7 +28,6 @@ const createNewSeat = async (
|
||||
attendeeLanguage,
|
||||
invitee,
|
||||
eventType,
|
||||
reqBookingUid,
|
||||
additionalNotes,
|
||||
noEmail,
|
||||
paymentAppData,
|
||||
@@ -67,7 +67,7 @@ const createNewSeat = async (
|
||||
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
uid: reqBookingUid,
|
||||
uid: seatedBooking.uid,
|
||||
},
|
||||
include: {
|
||||
attendees: true,
|
||||
|
||||
@@ -41,9 +41,10 @@ const handleSeats = async (newSeatedBookingObject: NewSeatedBookingObject) => {
|
||||
{
|
||||
uid: rescheduleUid || reqBookingUid,
|
||||
},
|
||||
|
||||
{
|
||||
eventTypeId: eventType.id,
|
||||
startTime: evt.startTime,
|
||||
startTime: new Date(evt.startTime),
|
||||
},
|
||||
],
|
||||
status: BookingStatus.ACCEPTED,
|
||||
@@ -63,10 +64,15 @@ const handleSeats = async (newSeatedBookingObject: NewSeatedBookingObject) => {
|
||||
},
|
||||
});
|
||||
|
||||
if (!seatedBooking) {
|
||||
if (!seatedBooking && rescheduleUid) {
|
||||
throw new HttpError({ statusCode: 404, message: ErrorCode.BookingNotFound });
|
||||
}
|
||||
|
||||
// We might be trying to create a new booking
|
||||
if (!seatedBooking) {
|
||||
return;
|
||||
}
|
||||
|
||||
// See if attendee is already signed up for timeslot
|
||||
if (
|
||||
seatedBooking.attendees.find((attendee) => attendee.email === invitee[0].email) &&
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import EventManager from "@calcom/core/EventManager";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { refreshCredentials } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/refreshCredentials";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { Person } from "@calcom/types/Calendar";
|
||||
|
||||
import { refreshCredentials } from "../../handleNewBooking";
|
||||
import type { createLoggerWithEventDetails } from "../../handleNewBooking";
|
||||
import type {
|
||||
HandleSeatsResultBooking,
|
||||
|
||||
@@ -90,7 +90,7 @@ describe("handleSeats", () => {
|
||||
|
||||
await handleNewBooking(req);
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("handleSeats is called when a new attendee is added", async () => {
|
||||
@@ -194,7 +194,6 @@ describe("handleSeats", () => {
|
||||
expect(handleSeatsCall).toEqual(
|
||||
expect.objectContaining({
|
||||
bookerEmail: booker.email,
|
||||
reqBookingUid: bookingUid,
|
||||
reqBodyUser: reqBookingUser,
|
||||
tAttendees: expect.any(Function),
|
||||
additionalNotes: expect.anything(),
|
||||
@@ -338,7 +337,6 @@ describe("handleSeats", () => {
|
||||
expect.objectContaining({
|
||||
rescheduleUid: bookingUid,
|
||||
bookerEmail: booker.email,
|
||||
reqBookingUid: bookingUid,
|
||||
reqBodyUser: reqBookingUser,
|
||||
tAttendees: expect.any(Function),
|
||||
additionalNotes: expect.anything(),
|
||||
@@ -511,6 +509,135 @@ describe("handleSeats", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// Testing in case of a wave of people book a time slot at the same time
|
||||
test("Attendee should be added to existing seated event when bookingUid is not present", async () => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
|
||||
const booker = getBooker({
|
||||
email: "seat2@example.com",
|
||||
name: "Seat 2",
|
||||
});
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
});
|
||||
|
||||
const bookingId = 1;
|
||||
const bookingUid = "abc123";
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const bookingStartTime = `${plus1DateString}T04:00:00Z`;
|
||||
const bookingEndTime = `${plus1DateString}T04:30:00Z`;
|
||||
await createBookingScenario(
|
||||
getScenarioData({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slug: "seated-event",
|
||||
slotInterval: 30,
|
||||
length: 30,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
seatsPerTimeSlot: 3,
|
||||
seatsShowAttendees: false,
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
id: bookingId,
|
||||
uid: bookingUid,
|
||||
eventTypeId: 1,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: bookingStartTime,
|
||||
endTime: bookingEndTime,
|
||||
metadata: {
|
||||
videoCallUrl: "https://existing-daily-video-call-url.example.com",
|
||||
},
|
||||
references: [
|
||||
{
|
||||
type: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com",
|
||||
credentialId: null,
|
||||
},
|
||||
],
|
||||
attendees: [
|
||||
getMockBookingAttendee({
|
||||
id: 1,
|
||||
name: "Seat 1",
|
||||
email: "seat1@test.com",
|
||||
locale: "en",
|
||||
|
||||
timeZone: "America/Toronto",
|
||||
bookingSeat: {
|
||||
referenceUid: "booking-seat-1",
|
||||
data: {},
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
})
|
||||
);
|
||||
|
||||
mockSuccessfulVideoMeetingCreation({
|
||||
metadataLookupKey: "dailyvideo",
|
||||
videoMeetingData: {
|
||||
id: "MOCK_ID",
|
||||
password: "MOCK_PASS",
|
||||
url: `http://mock-dailyvideo.example.com/meeting-1`,
|
||||
},
|
||||
});
|
||||
|
||||
const reqBookingUser = "seatedAttendee";
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: BookingLocations.CalVideo },
|
||||
},
|
||||
user: reqBookingUser,
|
||||
},
|
||||
});
|
||||
|
||||
const { req } = createMockNextJsRequest({
|
||||
method: "POST",
|
||||
body: mockBookingData,
|
||||
});
|
||||
|
||||
await handleNewBooking(req);
|
||||
|
||||
const newAttendee = await prismaMock.attendee.findFirst({
|
||||
where: {
|
||||
email: booker.email,
|
||||
bookingId: bookingId,
|
||||
},
|
||||
include: {
|
||||
bookingSeat: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Check for the existence of the new attendee w/ booking seat
|
||||
expect(newAttendee?.bookingSeat).toEqual(
|
||||
expect.objectContaining({
|
||||
referenceUid: expect.any(String),
|
||||
data: expect.any(Object),
|
||||
bookingId: bookingId,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("If attendee is already a part of the booking then throw an error", async () => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ interface handleChildrenEventTypesProps {
|
||||
}[]
|
||||
| undefined;
|
||||
prisma: PrismaClient | DeepMockProxy<PrismaClient>;
|
||||
updatedValues: Prisma.EventTypeUpdateInput;
|
||||
}
|
||||
|
||||
const sendAllSlugReplacementEmails = async (
|
||||
@@ -95,6 +96,7 @@ export default async function handleChildrenEventTypes({
|
||||
children,
|
||||
prisma,
|
||||
profileId,
|
||||
updatedValues,
|
||||
}: handleChildrenEventTypesProps) {
|
||||
// Check we are dealing with a managed event type
|
||||
if (updatedEventType?.schedulingType !== SchedulingType.MANAGED)
|
||||
@@ -114,8 +116,12 @@ export default async function handleChildrenEventTypes({
|
||||
message: "Missing event type",
|
||||
};
|
||||
|
||||
// Define what values are expected to be changed from a managed event type
|
||||
const allManagedEventTypePropsZod = _EventTypeModel.pick(allManagedEventTypeProps);
|
||||
// bookingFields is expected to be filled by the _EventTypeModel but is null at create event
|
||||
const _ManagedEventTypeModel = _EventTypeModel.extend({
|
||||
bookingFields: _EventTypeModel.shape.bookingFields.nullish(),
|
||||
});
|
||||
|
||||
const allManagedEventTypePropsZod = _ManagedEventTypeModel.pick(allManagedEventTypeProps); //FIXME
|
||||
const managedEventTypeValues = allManagedEventTypePropsZod
|
||||
.omit(unlockedManagedEventTypeProps)
|
||||
.parse(eventType);
|
||||
@@ -130,14 +136,12 @@ export default async function handleChildrenEventTypes({
|
||||
const unlockedEventTypeValues = allManagedEventTypePropsZod
|
||||
.pick(unlockedManagedEventTypeProps)
|
||||
.parse(eventType);
|
||||
|
||||
// Calculate if there are new/existent/deleted children users for which the event type needs to be created/updated/deleted
|
||||
const previousUserIds = oldEventType.children?.flatMap((ch) => ch.userId ?? []);
|
||||
const currentUserIds = children?.map((ch) => ch.owner.id);
|
||||
const deletedUserIds = previousUserIds?.filter((id) => !currentUserIds?.includes(id));
|
||||
const newUserIds = currentUserIds?.filter((id) => !previousUserIds?.includes(id));
|
||||
const oldUserIds = currentUserIds?.filter((id) => previousUserIds?.includes(id));
|
||||
|
||||
// Calculate if there are new workflows for which assigned members will get too
|
||||
const currentWorkflowIds = eventType.workflows?.map((wf) => wf.workflowId);
|
||||
|
||||
@@ -227,18 +231,9 @@ export default async function handleChildrenEventTypes({
|
||||
},
|
||||
},
|
||||
data: {
|
||||
...managedEventTypeValues,
|
||||
profileId: profileId ?? null,
|
||||
hidden: children?.find((ch) => ch.owner.id === userId)?.hidden ?? false,
|
||||
bookingLimits:
|
||||
(managedEventTypeValues.bookingLimits as unknown as Prisma.InputJsonObject) ?? undefined,
|
||||
onlyShowFirstAvailableSlot: managedEventTypeValues.onlyShowFirstAvailableSlot ?? false,
|
||||
recurringEvent:
|
||||
(managedEventTypeValues.recurringEvent as unknown as Prisma.InputJsonValue) ?? undefined,
|
||||
metadata: (managedEventTypeValues.metadata as Prisma.InputJsonValue) ?? undefined,
|
||||
bookingFields: (managedEventTypeValues.bookingFields as Prisma.InputJsonValue) ?? undefined,
|
||||
durationLimits: (managedEventTypeValues.durationLimits as Prisma.InputJsonValue) ?? undefined,
|
||||
...updatedValues,
|
||||
hashedLink: hashedLinkQuery(userId),
|
||||
hidden: children?.find((ch) => ch.owner.id === userId)?.hidden ?? false,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { useVerifyCode } from "@calcom/features/bookings/Booker/components/hooks/useVerifyCode";
|
||||
import { VerifyCodeDialog } from "@calcom/features/bookings/components/VerifyCodeDialog";
|
||||
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -74,6 +75,18 @@ export const CreateANewOrganizationForm = ({ slug }: { slug?: string }) => {
|
||||
},
|
||||
});
|
||||
|
||||
const verifyCode = useVerifyCode({
|
||||
onSuccess: (isVerified) => {
|
||||
if (isVerified) {
|
||||
createOrganizationMutation.mutate({
|
||||
...newOrganizationFormMethods.getValues(),
|
||||
language: i18n.language,
|
||||
check: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
@@ -203,15 +216,12 @@ export const CreateANewOrganizationForm = ({ slug }: { slug?: string }) => {
|
||||
isOpenDialog={showVerifyCode}
|
||||
setIsOpenDialog={setShowVerifyCode}
|
||||
email={watchAdminEmail}
|
||||
onSuccess={(isVerified) => {
|
||||
if (isVerified) {
|
||||
createOrganizationMutation.mutate({
|
||||
...newOrganizationFormMethods.getValues(),
|
||||
language: i18n.language,
|
||||
check: false,
|
||||
});
|
||||
}
|
||||
}}
|
||||
verifyCodeWithSessionNotRequired={verifyCode.verifyCodeWithSessionNotRequired}
|
||||
verifyCodeWithSessionRequired={verifyCode.verifyCodeWithSessionRequired}
|
||||
error={verifyCode.error}
|
||||
resetErrors={verifyCode.resetErrors}
|
||||
isPending={verifyCode.isPending}
|
||||
setIsPending={verifyCode.setIsPending}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -325,7 +325,7 @@ const TeamProfileForm = ({ team }: TeamProfileFormProps) => {
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="logo"
|
||||
render={({ field: { value } }) => {
|
||||
render={({ field: { value, onChange } }) => {
|
||||
const showRemoveLogoButton = !!value;
|
||||
|
||||
return (
|
||||
@@ -340,9 +340,7 @@ const TeamProfileForm = ({ team }: TeamProfileFormProps) => {
|
||||
target="avatar"
|
||||
id="avatar-upload"
|
||||
buttonMsg={t("upload_logo")}
|
||||
handleAvatarChange={(newLogo) => {
|
||||
form.setValue("logo", newLogo, { shouldDirty: true });
|
||||
}}
|
||||
handleAvatarChange={onChange}
|
||||
triggerButtonColor={showRemoveLogoButton ? "secondary" : "primary"}
|
||||
imageSrc={value ?? undefined}
|
||||
/>
|
||||
@@ -350,7 +348,7 @@ const TeamProfileForm = ({ team }: TeamProfileFormProps) => {
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
form.setValue("logo", null, { shouldDirty: true });
|
||||
onChange(null);
|
||||
}}>
|
||||
{t("remove")}
|
||||
</Button>
|
||||
|
||||
@@ -8,19 +8,20 @@ import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/
|
||||
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { isRecurringEvent, parseRecurringEvent } from "@calcom/lib";
|
||||
import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { BookerLayoutSettings } from "@calcom/prisma/zod-utils";
|
||||
import {
|
||||
bookerLayoutOptions,
|
||||
EventTypeMetaDataSchema,
|
||||
customInputSchema,
|
||||
userMetadata as userMetadataSchema,
|
||||
bookerLayouts as bookerLayoutsSchema,
|
||||
BookerLayouts,
|
||||
EventTypeMetaDataSchema,
|
||||
bookerLayoutOptions,
|
||||
bookerLayouts as bookerLayoutsSchema,
|
||||
customInputSchema,
|
||||
teamMetadataSchema,
|
||||
userMetadata as userMetadataSchema,
|
||||
} from "@calcom/prisma/zod-utils";
|
||||
import type { UserProfile } from "@calcom/types/UserProfile";
|
||||
|
||||
@@ -163,7 +164,10 @@ export const getPublicEvent = async (
|
||||
username: users[0].username,
|
||||
name: users[0].name,
|
||||
weekStart: users[0].weekStart,
|
||||
image: `/${users[0].username}/avatar.png`,
|
||||
image: getUserAvatarUrl({
|
||||
...users[0],
|
||||
profile: users[0].profile,
|
||||
}),
|
||||
brandColor: users[0].brandColor,
|
||||
darkBrandColor: users[0].darkBrandColor,
|
||||
theme: null,
|
||||
|
||||
@@ -17,9 +17,10 @@ import { filterQuerySchema } from "../lib/getTeamsFiltersFromQuery";
|
||||
export type IEventTypesFilters = RouterOutputs["viewer"]["eventTypes"]["listWithTeam"];
|
||||
export type IEventTypeFilter = IEventTypesFilters[0];
|
||||
|
||||
export const TEAMS_FILTER_KEY = "TEAMS_FILTER_KEY";
|
||||
function useFilterQuery() {
|
||||
// passthrough allows additional params to not be removed
|
||||
return useTypedQuery(filterQuerySchema.passthrough());
|
||||
return useTypedQuery(filterQuerySchema.passthrough(), TEAMS_FILTER_KEY);
|
||||
}
|
||||
|
||||
export const TeamsFilter = ({
|
||||
|
||||
@@ -436,7 +436,7 @@ function FieldEditDialog({
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
fieldForm.setValue("type", value);
|
||||
fieldForm.setValue("type", value, { shouldDirty: true });
|
||||
}}
|
||||
value={fieldTypesConfigMap[fieldForm.getValues("type")]}
|
||||
options={fieldTypes.filter((f) => !f.systemOnly)}
|
||||
@@ -451,7 +451,9 @@ function FieldEditDialog({
|
||||
{...fieldForm.register("name")}
|
||||
containerClassName="mt-6"
|
||||
onChange={(e) => {
|
||||
fieldForm.setValue("name", getFieldIdentifier(e.target.value || ""));
|
||||
fieldForm.setValue("name", getFieldIdentifier(e.target.value || ""), {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
disabled={
|
||||
fieldForm.getValues("editable") === "system" ||
|
||||
|
||||
@@ -752,8 +752,10 @@ const NavigationItem: React.FC<{
|
||||
/>
|
||||
)}
|
||||
{isLocaleReady ? (
|
||||
<span className="hidden w-full justify-between lg:flex" data-testid={`${item.name}-test`}>
|
||||
<div className="flex">{t(item.name)}</div>
|
||||
<span
|
||||
className="hidden w-full justify-between truncate text-ellipsis lg:flex"
|
||||
data-testid={`${item.name}-test`}>
|
||||
{t(item.name)}
|
||||
{item.badge && item.badge}
|
||||
</span>
|
||||
) : (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
import { LOGO, WEBAPP_URL } from "./constants";
|
||||
import { CAL_URL, LOGO, WEBAPP_URL } from "./constants";
|
||||
|
||||
// Ensures tw prop is typed.
|
||||
declare module "react" {
|
||||
@@ -62,7 +62,7 @@ export const constructMeetingImage = (
|
||||
`?type=meeting`,
|
||||
`&title=${encodeURIComponent(title)}`,
|
||||
`&meetingProfileName=${encodeURIComponent(profile.name)}`,
|
||||
profile.image && `&meetingImage=${encodeURIComponent(WEBAPP_URL + profile.image)}`,
|
||||
profile.image && `&meetingImage=${encodeURIComponent(profile.image)}`,
|
||||
`${users.map((user) => `&names=${encodeURIComponent(user.name)}`).join("")}`,
|
||||
`${users.map((user) => `&usernames=${encodeURIComponent(user.username)}`).join("")}`,
|
||||
// Joining a multiline string for readability.
|
||||
@@ -125,7 +125,7 @@ export const Meeting = ({ title, users = [], profile }: MeetingImageProps) => {
|
||||
const avatars = attendees
|
||||
.map((user) => {
|
||||
if ("image" in user && user?.image) return user.image;
|
||||
if ("username" in user && user?.username) return `${WEBAPP_URL}/${user.username}/avatar.png`;
|
||||
if ("username" in user && user?.username) return `${CAL_URL}/${user.username}/avatar.png`;
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
@@ -32,7 +32,11 @@ export const SENDER_NAME = process.env.NEXT_PUBLIC_SENDGRID_SENDER_NAME || "Cal.
|
||||
// This is the URL from which all Cal Links and their assets are served.
|
||||
// Use website URL to make links shorter(cal.com and not app.cal.com)
|
||||
// As website isn't setup for preview environments, use the webapp url instead
|
||||
export const CAL_URL = new URL(WEBAPP_URL).hostname.endsWith(".vercel.app") ? WEBAPP_URL : WEBSITE_URL;
|
||||
// If it's a .vercel.app domain, keep it.
|
||||
// Else use the website url if defined and finally fallback to the webapp url
|
||||
export const CAL_URL = new URL(WEBAPP_URL).hostname.endsWith(".vercel.app")
|
||||
? WEBAPP_URL
|
||||
: process.env.NEXT_PUBLIC_WEBSITE_URL || WEBAPP_URL;
|
||||
|
||||
export const IS_CALCOM =
|
||||
WEBAPP_URL &&
|
||||
@@ -73,15 +77,15 @@ export const JOIN_DISCORD = "https://go.cal.com/discord";
|
||||
export const POWERED_BY_URL = `${WEBSITE_URL}/?utm_source=embed&utm_medium=powered-by-button`;
|
||||
export const DOCS_URL = "https://cal.com/docs";
|
||||
export const DEVELOPER_DOCS = "https://developer.cal.com";
|
||||
export const SEO_IMG_DEFAULT = `${WEBSITE_URL}/og-image.png`;
|
||||
export const SEO_IMG_DEFAULT = `${CAL_URL}/og-image.png`;
|
||||
// The Dynamic OG Image is passed through Next's Image API to further optimize it.
|
||||
// This results in a 80% smaller image 🤯. It is however important that for the query
|
||||
// parameters you pass to the /api/social/og/image endpoint, you wrap them in encodeURIComponent
|
||||
// as well, otherwise the URL won't be valid.
|
||||
export const SEO_IMG_OGIMG = `${WEBAPP_URL}/_next/image?w=1200&q=100&url=${encodeURIComponent(
|
||||
export const SEO_IMG_OGIMG = `${CAL_URL}/_next/image?w=1200&q=100&url=${encodeURIComponent(
|
||||
"/api/social/og/image"
|
||||
)}`;
|
||||
export const SEO_IMG_OGIMG_VIDEO = `${WEBSITE_URL}/video-og-image.png`;
|
||||
export const SEO_IMG_OGIMG_VIDEO = `${CAL_URL}/video-og-image.png`;
|
||||
export const IS_STRIPE_ENABLED = !!(
|
||||
process.env.STRIPE_CLIENT_ID &&
|
||||
process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY &&
|
||||
|
||||
@@ -77,11 +77,19 @@ export function processDateOverride({
|
||||
.second(0)
|
||||
.tz(timeZone, true);
|
||||
|
||||
const endDate = itemDateStartOfDay
|
||||
.add(item.endTime.getUTCHours(), "hours")
|
||||
.add(item.endTime.getUTCMinutes(), "minutes")
|
||||
.second(0)
|
||||
.tz(timeZone, true);
|
||||
let endDate = itemDateStartOfDay;
|
||||
const endTimeHours = item.endTime.getUTCHours();
|
||||
const endTimeMinutes = item.endTime.getUTCMinutes();
|
||||
|
||||
if (endTimeHours === 23 && endTimeMinutes === 59) {
|
||||
endDate = endDate.add(1, "day").tz(timeZone, true);
|
||||
} else {
|
||||
endDate = itemDateStartOfDay
|
||||
.add(endTimeHours, "hours")
|
||||
.add(endTimeMinutes, "minutes")
|
||||
.second(0)
|
||||
.tz(timeZone, true);
|
||||
}
|
||||
|
||||
return {
|
||||
start: startDate,
|
||||
@@ -116,7 +124,19 @@ export function buildDateRanges({
|
||||
availability.reduce((processed: DateRange[], item) => {
|
||||
if ("date" in item && !!item.date) {
|
||||
const itemDateAsUtc = dayjs.utc(item.date);
|
||||
if (itemDateAsUtc.isBetween(dateFrom.startOf("day"), dateTo.endOf("day"), null, "[]")) {
|
||||
// TODO: Remove the .subtract(1, "day") and .add(1, "day") part and
|
||||
// refactor this to actually work with correct dates.
|
||||
// As of 2024-02-20, there are mismatches between local and UTC dates for overrides
|
||||
// and the dateFrom and dateTo fields, resulting in this if not returning true, which
|
||||
// results in "no available users found" errors.
|
||||
if (
|
||||
itemDateAsUtc.isBetween(
|
||||
dateFrom.subtract(1, "day").startOf("day"),
|
||||
dateTo.add(1, "day").endOf("day"),
|
||||
null,
|
||||
"[]"
|
||||
)
|
||||
) {
|
||||
processed.push(processDateOverride({ item, itemDateAsUtc, timeZone }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { AVATAR_FALLBACK } from "@calcom/lib/constants";
|
||||
import type { User, Team } from "@calcom/prisma/client";
|
||||
import { AVATAR_FALLBACK, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import type { Team, User } from "@calcom/prisma/client";
|
||||
import type { UserProfile } from "@calcom/types/UserProfile";
|
||||
|
||||
/**
|
||||
* Gives an organization aware avatar url for a user
|
||||
* It ensures that the wrong avatar isn't fetched by ensuring that organizationId is always passed
|
||||
* It should always return a fully formed url
|
||||
*/
|
||||
export const getUserAvatarUrl = (
|
||||
user:
|
||||
@@ -16,11 +16,11 @@ export const getUserAvatarUrl = (
|
||||
| undefined
|
||||
) => {
|
||||
if (user?.avatarUrl) {
|
||||
return user.avatarUrl;
|
||||
return CAL_URL + user.avatarUrl;
|
||||
}
|
||||
if (!user?.username) return AVATAR_FALLBACK;
|
||||
if (!user?.username) return CAL_URL + AVATAR_FALLBACK;
|
||||
// avatar.png automatically redirects to fallback avatar if user doesn't have one
|
||||
return `${WEBAPP_URL}/${user.profile?.username}/avatar.png${
|
||||
return `${CAL_URL}/${user.profile?.username}/avatar.png${
|
||||
user.profile?.organizationId ? `?orgId=${user.profile.organizationId}` : ""
|
||||
}`;
|
||||
};
|
||||
@@ -29,7 +29,7 @@ export function getTeamAvatarUrl(
|
||||
team: Pick<Team, "slug"> & {
|
||||
organizationId?: number | null;
|
||||
logoUrl?: string | null;
|
||||
requestedSlug: string | null;
|
||||
requestedSlug?: string | null;
|
||||
}
|
||||
) {
|
||||
if (team.logoUrl) {
|
||||
@@ -42,7 +42,7 @@ export function getTeamAvatarUrl(
|
||||
export const getOrgAvatarUrl = (
|
||||
org: Pick<Team, "slug"> & {
|
||||
logoUrl?: string | null;
|
||||
requestedSlug: string | null;
|
||||
requestedSlug?: string | null;
|
||||
}
|
||||
) => {
|
||||
if (org.logoUrl) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { usePathname, useRouter } from "next/navigation";
|
||||
import { useCallback, useMemo, useEffect } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import { localStorage } from "../webstorage";
|
||||
import { useRouterQuery } from "./useRouterQuery";
|
||||
|
||||
type OptionalKeys<T> = {
|
||||
@@ -32,7 +33,7 @@ export const queryStringArray = z
|
||||
.preprocess((a) => z.string().parse(a).split(","), z.string().array())
|
||||
.or(z.string().array());
|
||||
|
||||
export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
|
||||
export function useTypedQuery<T extends z.AnyZodObject>(schema: T, localStorageKey: string | null = null) {
|
||||
type Output = z.infer<typeof schema>;
|
||||
type FullOutput = Required<Output>;
|
||||
type OutputKeys = Required<keyof FullOutput>;
|
||||
@@ -43,7 +44,7 @@ export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
|
||||
const unparsedQuery = useRouterQuery();
|
||||
const pathname = usePathname();
|
||||
const parsedQuerySchema = schema.safeParse(unparsedQuery);
|
||||
|
||||
const useLocalStorage = !!localStorageKey;
|
||||
let parsedQuery: Output = useMemo(() => {
|
||||
return {} as Output;
|
||||
}, []);
|
||||
@@ -73,6 +74,39 @@ export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[parsedQuery, router]
|
||||
);
|
||||
//If the value for {localStorageKey} exist in localStorage set it to queryParams
|
||||
useEffect(() => {
|
||||
if (!useLocalStorage) return;
|
||||
const storedValueString = localStorage.getItem(localStorageKey);
|
||||
if (!storedValueString || storedValueString == "{}") return;
|
||||
const storedValue: Output = JSON.parse(storedValueString);
|
||||
if (storedValue) {
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(storedValue)) {
|
||||
searchParams.set(String(key), String(value));
|
||||
}
|
||||
router.replace(`${pathname}?${searchParams.toString()}`);
|
||||
}
|
||||
}, []);
|
||||
|
||||
function setLocalStorage<J extends OutputKeys>(key: J, value: Output[J]) {
|
||||
if (!useLocalStorage) return;
|
||||
const storedValue: Output = JSON.parse(localStorage.getItem(localStorageKey) || "{}");
|
||||
const newValue = { ...storedValue, [key]: value };
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(newValue));
|
||||
}
|
||||
function removeByKeyFromLocalStorage(key: OutputOptionalKeys) {
|
||||
if (!useLocalStorage) return;
|
||||
const storedValue: Output = JSON.parse(localStorage.getItem(localStorageKey) || "{}");
|
||||
const newValue = storedValue;
|
||||
delete newValue[key];
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(newValue));
|
||||
}
|
||||
//Remove {localStorageKey} from the localStorage
|
||||
function removeAllValuesFromLocalStorage() {
|
||||
if (!useLocalStorage) return;
|
||||
localStorage.removeItem(localStorageKey);
|
||||
}
|
||||
|
||||
// Delete a key from the query
|
||||
function removeByKey(key: OutputOptionalKeys) {
|
||||
@@ -88,9 +122,13 @@ export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
|
||||
if (existingValue.includes(value)) return; // prevent adding the same value to the array
|
||||
// @ts-expect-error this is too much for TS it seems
|
||||
setQuery(key, [...existingValue, value]);
|
||||
// @ts-expect-error this is too much for TS it seems
|
||||
setLocalStorage(key, [...existingValue, value]);
|
||||
} else {
|
||||
// @ts-expect-error this is too much for TS it seems
|
||||
setQuery(key, [value]);
|
||||
// @ts-expect-error this is too much for TS it seems
|
||||
setLocalStorage(key, [value]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,9 +139,12 @@ export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
|
||||
// @ts-expect-error this is too much for TS it seems
|
||||
const newValue = existingValue.filter((item) => item !== value);
|
||||
setQuery(key, newValue);
|
||||
setLocalStorage(key, newValue);
|
||||
} else {
|
||||
// @ts-expect-error this is too much for TS it seems
|
||||
removeByKey(key);
|
||||
//@ts-expect-error this is too much for TS it seems
|
||||
removeByKeyFromLocalStorage(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +152,7 @@ export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
|
||||
function removeAllQueryParams() {
|
||||
if (pathname !== null) {
|
||||
router.replace(pathname);
|
||||
removeAllValuesFromLocalStorage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ export async function getTeamWithMembers(args: {
|
||||
name: true,
|
||||
slug: true,
|
||||
...(!!includeTeamLogo ? { logo: true } : {}),
|
||||
logoUrl: true,
|
||||
bio: true,
|
||||
hideBranding: true,
|
||||
hideBookATeamMember: true,
|
||||
@@ -232,6 +233,7 @@ export async function getTeamWithMembers(args: {
|
||||
|
||||
return {
|
||||
...teamWithoutInviteTokens,
|
||||
...(teamWithoutInviteTokens.logoUrl ? { logo: teamWithoutInviteTokens.logoUrl } : {}),
|
||||
/** To prevent breaking we only return non-email attached token here, if we have one */
|
||||
inviteToken: inviteTokens.find(
|
||||
(token) =>
|
||||
|
||||
@@ -88,7 +88,7 @@ export class EventTypeRepository {
|
||||
}
|
||||
|
||||
static async findAllByUpId(
|
||||
{ upId }: { upId: string },
|
||||
{ upId, userId }: { upId: string; userId: number },
|
||||
{
|
||||
orderBy,
|
||||
where = {},
|
||||
@@ -99,12 +99,6 @@ export class EventTypeRepository {
|
||||
const profileId = lookupTarget.type === LookupTarget.User ? null : lookupTarget.id;
|
||||
const select = {
|
||||
...eventTypeSelect,
|
||||
// TODO: As required by getByViewHandler - Make it configurable
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
hashedLink: true,
|
||||
users: { select: userSelect },
|
||||
children: {
|
||||
@@ -156,6 +150,13 @@ export class EventTypeRepository {
|
||||
{
|
||||
profileId,
|
||||
},
|
||||
// Fetch children event-types by userId because profileId is wrong
|
||||
{
|
||||
userId,
|
||||
parentId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
...where,
|
||||
},
|
||||
@@ -165,7 +166,18 @@ export class EventTypeRepository {
|
||||
} else {
|
||||
return await prisma.eventType.findMany({
|
||||
where: {
|
||||
profileId,
|
||||
OR: [
|
||||
{
|
||||
profileId,
|
||||
},
|
||||
// Fetch children event-types by userId because profileId is wrong
|
||||
{
|
||||
userId: userId,
|
||||
parentId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
...where,
|
||||
},
|
||||
select,
|
||||
|
||||
@@ -103,11 +103,6 @@ export class MembershipRepository {
|
||||
eventTypes: {
|
||||
select: {
|
||||
...eventTypeSelect,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
hashedLink: true,
|
||||
users: { select: userSelect },
|
||||
children: {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
export const uploadLogo = async ({
|
||||
teamId,
|
||||
logo: data,
|
||||
}: {
|
||||
teamId: number;
|
||||
logo: string;
|
||||
}): Promise<string> => {
|
||||
const objectKey = uuidv4();
|
||||
|
||||
await prisma.avatar.upsert({
|
||||
where: {
|
||||
teamId_userId: {
|
||||
teamId,
|
||||
userId: 0,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
teamId,
|
||||
data,
|
||||
objectKey,
|
||||
},
|
||||
update: {
|
||||
data,
|
||||
objectKey,
|
||||
},
|
||||
});
|
||||
|
||||
return `/api/avatar/${objectKey}.png`;
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
import prismaMock from "../../../tests/libs/__mocks__/prismaMock";
|
||||
|
||||
import { describe, expect, it, beforeEach } from "vitest";
|
||||
|
||||
import { usernameCheckForSignup } from "./username";
|
||||
|
||||
describe("usernameCheckForSignup ", async () => {
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
prismaMock.user.findUnique.mockImplementation(() => {
|
||||
return null;
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
prismaMock.user.findMany.mockImplementation(() => {
|
||||
return [];
|
||||
});
|
||||
});
|
||||
|
||||
it("should return available true for an email that doesn't exist", async () => {
|
||||
const res = await usernameCheckForSignup({ username: "johnny", email: "johnny@example.com" });
|
||||
expect(res).toEqual({
|
||||
available: true,
|
||||
premium: false,
|
||||
suggestedUsername: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return available false for an email that exists and a different username is provided", async () => {
|
||||
mockUserInDB({
|
||||
id: 1,
|
||||
email: "john@example.com",
|
||||
username: "john",
|
||||
});
|
||||
const res = await usernameCheckForSignup({ username: "johnny", email: "john@example.com" });
|
||||
expect(res).toEqual({
|
||||
available: false,
|
||||
premium: false,
|
||||
suggestedUsername: "johnny001",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return available true for an email that exists but the user is signing up for an organization", async () => {
|
||||
const userId = 1;
|
||||
mockUserInDB({
|
||||
id: userId,
|
||||
email: "john@example.com",
|
||||
username: "john",
|
||||
});
|
||||
mockMembership({ userId });
|
||||
const res = await usernameCheckForSignup({ username: "john", email: "john@example.com" });
|
||||
expect(res).toEqual({
|
||||
available: true,
|
||||
// An organization can't have premium username
|
||||
premium: false,
|
||||
suggestedUsername: "",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mockUserInDB({ id, email, username }: { id: number; email: string; username: string }) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
prismaMock.user.findUnique.mockImplementation((arg) => {
|
||||
if (arg.where.email === email) {
|
||||
return {
|
||||
id,
|
||||
email,
|
||||
username,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
function mockMembership({ userId }: { userId: number }) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
prismaMock.membership.findFirst.mockImplementation((arg) => {
|
||||
const isOrganizationWhereClause =
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
arg?.where?.team?.metadata?.path[0] === "isOrganization" && arg?.where?.team?.metadata?.equals === true;
|
||||
if (arg?.where?.userId === userId && isOrganizationWhereClause) {
|
||||
return {
|
||||
userId,
|
||||
teamId: 1,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -178,7 +178,7 @@ const usernameCheckForSignup = async ({
|
||||
|
||||
const username = slugify(usernameRaw);
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
@@ -207,7 +207,9 @@ const usernameCheckForSignup = async ({
|
||||
// The only way to differentiate b/w 'a new email that was invited to an Org' and 'a user that was created using regular signup' is to check if the user is a member of an org.
|
||||
// If username is in global namespace
|
||||
if (!userIsAMemberOfAnOrg) {
|
||||
response.available = false;
|
||||
const isClaimingAlreadySetUsername = user.username === username;
|
||||
const isClaimingUnsetUsername = !user.username;
|
||||
response.available = isClaimingUnsetUsername || isClaimingAlreadySetUsername;
|
||||
// There are no premium users outside an organization only
|
||||
response.premium = await isPremiumUserName(username);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user