Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b06e46342c | ||
|
|
4cf482f860 | ||
|
|
f2478ea7d3 | ||
|
|
0e110022fb | ||
|
|
ee80577cc3 | ||
|
|
8402dfa3a2 | ||
|
|
4585199b5e | ||
|
|
ae2446f42f | ||
|
|
2f6d109a70 | ||
|
|
7ff884ca7d | ||
|
|
9230b2d2ca | ||
|
|
1a645f3110 | ||
|
|
4eb11e76ea | ||
|
|
e3a5ce5f6e | ||
|
|
12e171823b | ||
|
|
3c723d6f5b | ||
|
|
1482d9a9af | ||
|
|
0efe25feb4 | ||
|
|
602192e026 | ||
|
|
aeb54e6401 | ||
|
|
b8c1d623f3 | ||
|
|
30e4d769e3 | ||
|
|
3babfa7cf3 | ||
|
|
e45ee8fab3 | ||
|
|
3c96e122b4 | ||
|
|
f21a915814 | ||
|
|
3467e2272f |
@@ -76,6 +76,26 @@ function PageWrapper(props: AppProps) {
|
||||
id="page-status"
|
||||
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
|
||||
/>
|
||||
{process.env.NEXT_PUBLIC_GTM_ID ? (
|
||||
<>
|
||||
<Script
|
||||
id="gtm-init-script"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(function (w, d, s, l, i) {
|
||||
w[l] = w[l] || []; w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
|
||||
var f = d.getElementsByTagName(s)[0], j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : '';
|
||||
j.async = true; j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);
|
||||
})(window, document, 'script', 'dataLayer', '${process.env.NEXT_PUBLIC_GTM_ID}');`,
|
||||
}}
|
||||
/>
|
||||
<noscript
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `<iframe src="https://www.googletagmanager.com/ns.html?id=${process.env.NEXT_PUBLIC_GTM_ID}" height="0" width="0" style="display:none;visibility:hidden"></iframe>`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<style jsx global>{`
|
||||
:root {
|
||||
--font-inter: ${interFont.style.fontFamily};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import { getEventLocationTypeFromApp, type EventLocationType } from "@calcom/app-store/locations";
|
||||
import type { CredentialOwner } from "@calcom/app-store/types";
|
||||
import { AppSetDefaultLinkDialog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
|
||||
import { BulkEditDefaultConferencingModal } from "@calcom/features/eventtypes/components/BulkEditDefaultConferencingModal";
|
||||
import { BulkEditDefaultForEventsModal } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { AppCategories } from "@calcom/prisma/enums";
|
||||
import { trpc, type RouterOutputs } from "@calcom/trpc";
|
||||
@@ -154,6 +154,12 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL
|
||||
});
|
||||
|
||||
const { t } = useLocale();
|
||||
const updateLocationsMutation = trpc.viewer.eventTypes.bulkUpdateToDefaultLocation.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
||||
setBulkUpdateModal(false);
|
||||
},
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<List className={listClassName}>
|
||||
@@ -173,7 +179,12 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL
|
||||
)}
|
||||
|
||||
{bulkUpdateModal && (
|
||||
<BulkEditDefaultConferencingModal open={bulkUpdateModal} setOpen={setBulkUpdateModal} />
|
||||
<BulkEditDefaultForEventsModal
|
||||
bulkUpdateFunction={updateLocationsMutation.mutate}
|
||||
open={bulkUpdateModal}
|
||||
setOpen={setBulkUpdateModal}
|
||||
isPending={updateLocationsMutation.isPending}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -162,7 +162,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
let bookedActions: ActionType[] = [
|
||||
{
|
||||
id: "cancel",
|
||||
label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel"),
|
||||
label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel_event"),
|
||||
/* When cancelling we need to let the UI and the API know if the intention is to
|
||||
cancel all remaining bookings or just that booking instance. */
|
||||
href: `/booking/${booking.uid}?cancel=true${
|
||||
@@ -297,6 +297,8 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
},
|
||||
];
|
||||
|
||||
const showPendingPayment = paymentAppData.enabled && booking.payment.length && !booking.paid;
|
||||
|
||||
return (
|
||||
<>
|
||||
<RescheduleDialog
|
||||
@@ -463,7 +465,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
{booking.eventType.team.name}
|
||||
</Badge>
|
||||
)}
|
||||
{!!booking?.eventType?.price && !booking.paid && (
|
||||
{showPendingPayment && (
|
||||
<Badge className="ltr:mr-2 rtl:ml-2 sm:hidden" variant="orange">
|
||||
{t("pending_payment")}
|
||||
</Badge>
|
||||
@@ -490,8 +492,8 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
{title}
|
||||
<span> </span>
|
||||
|
||||
{paymentAppData.enabled && !booking.paid && booking.payment.length && (
|
||||
<Badge className="me-2 ms-2 hidden sm:inline-flex" variant="orange">
|
||||
{showPendingPayment && (
|
||||
<Badge className="hidden sm:inline-flex" variant="orange">
|
||||
{t("pending_payment")}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import type { z } from "zod";
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
allowDisablingAttendeeConfirmationEmails,
|
||||
allowDisablingHostConfirmationEmails,
|
||||
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
||||
import type { EditableSchema } from "@calcom/features/form-builder/schema";
|
||||
import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
|
||||
@@ -55,7 +56,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
formMethods.getValues("useEventTypeDestinationCalendarEmail")
|
||||
);
|
||||
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
||||
|
||||
const bookingFields: Prisma.JsonObject = {};
|
||||
|
||||
const workflows = eventType.workflows.map((workflowOnEventType) => workflowOnEventType.workflow);
|
||||
@@ -86,6 +86,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
|
||||
useEffect(() => {
|
||||
!hashedUrl && setHashedUrl(generateHashedLink(formMethods.getValues("users")[0]?.id ?? team?.id));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [formMethods.getValues("users"), hashedUrl, team?.id]);
|
||||
|
||||
const toggleGuests = (enabled: boolean) => {
|
||||
@@ -108,11 +109,11 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
);
|
||||
};
|
||||
|
||||
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||
formMethods.getValues(),
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
const { isChildrenManagedEventType, isManagedEventType, shouldLockDisableProps } = useLockedFieldsManager({
|
||||
eventType,
|
||||
translate: t,
|
||||
formMethods,
|
||||
});
|
||||
const eventNamePlaceholder = getEventName({
|
||||
...eventNameObject,
|
||||
eventName: formMethods.watch("eventName"),
|
||||
@@ -120,10 +121,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
|
||||
const successRedirectUrlLocked = shouldLockDisableProps("successRedirectUrl");
|
||||
const seatsLocked = shouldLockDisableProps("seatsPerTimeSlotEnabled");
|
||||
const requiresBookerEmailVerificationProps = shouldLockDisableProps("requiresBookerEmailVerification");
|
||||
const hideCalendarNotesLocked = shouldLockDisableProps("hideCalendarNotes");
|
||||
const lockTimeZoneToggleOnBookingPageLocked = shouldLockDisableProps("lockTimeZoneToggleOnBookingPage");
|
||||
|
||||
const closeEventNameTip = () => setShowEventNameTip(false);
|
||||
const displayDestinationCalendarSelector =
|
||||
!!connectedCalendarsQuery.data?.connectedCalendars.length && !team;
|
||||
!!connectedCalendarsQuery.data?.connectedCalendars.length && (!team || isChildrenManagedEventType);
|
||||
|
||||
const verifiedSecondaryEmails = [
|
||||
{
|
||||
label: user?.email || "",
|
||||
@@ -181,6 +186,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
<Button
|
||||
color="minimal"
|
||||
size="sm"
|
||||
{...(shouldLockDisableProps("eventName").disabled ? { disabled: true } : {})}
|
||||
aria-label="edit custom name"
|
||||
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
|
||||
onClick={() => setShowEventNameTip((old) => !old)}>
|
||||
@@ -233,9 +239,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BookerLayoutSelector fallbackToUserSettings isDark={selectedThemeIsDark} isOuterBorder={true} />
|
||||
|
||||
<div className="border-subtle space-y-6 rounded-lg border p-6">
|
||||
<FormBuilder
|
||||
title={t("booking_questions_title")}
|
||||
@@ -250,7 +254,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<RequiresConfirmationController
|
||||
eventType={eventType}
|
||||
seatsEnabled={seatsEnabled}
|
||||
@@ -258,7 +261,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
requiresConfirmation={requiresConfirmation}
|
||||
onRequiresConfirmation={setRequiresConfirmation}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="requiresBookerEmailVerification"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
@@ -268,14 +270,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
|
||||
title={t("requires_booker_email_verification")}
|
||||
data-testid="requires-booker-email-verification"
|
||||
{...shouldLockDisableProps("requiresBookerEmailVerification")}
|
||||
{...requiresBookerEmailVerificationProps}
|
||||
description={t("description_requires_booker_email_verification")}
|
||||
checked={value}
|
||||
onCheckedChange={(e) => onChange(e)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="hideCalendarNotes"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
@@ -283,16 +284,15 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
labelClassName="text-sm"
|
||||
toggleSwitchAtTheEnd={true}
|
||||
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
|
||||
title={t("disable_notes")}
|
||||
data-testid="disable-notes"
|
||||
{...shouldLockDisableProps("hideCalendarNotes")}
|
||||
title={t("disable_notes")}
|
||||
{...hideCalendarNotesLocked}
|
||||
description={t("disable_notes_description")}
|
||||
checked={value}
|
||||
onCheckedChange={(e) => onChange(e)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="successRedirectUrl"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
@@ -339,7 +339,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
<SettingsToggle
|
||||
labelClassName="text-sm"
|
||||
toggleSwitchAtTheEnd={true}
|
||||
@@ -359,52 +358,53 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
<Info className="ml-1.5 h-4 w-4 cursor-pointer" />
|
||||
</a>
|
||||
}
|
||||
{...shouldLockDisableProps("hashedLinkCheck")}
|
||||
{...shouldLockDisableProps("hashedLink")}
|
||||
description={t("private_link_description", { appName: APP_NAME })}
|
||||
checked={hashedLinkVisible}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("hashedLink", e ? hashedUrl : undefined, { shouldDirty: true });
|
||||
setHashedLinkVisible(e);
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
{!IS_VISUAL_REGRESSION_TESTING && (
|
||||
<TextField
|
||||
disabled
|
||||
name="hashedLink"
|
||||
label={t("private_link_label")}
|
||||
data-testid="generated-hash-url"
|
||||
labelSrOnly
|
||||
type="text"
|
||||
hint={t("private_link_hint")}
|
||||
defaultValue={placeholderHashedLink}
|
||||
addOnSuffix={
|
||||
<Tooltip
|
||||
content={
|
||||
formMethods.getValues("hashedLink") ? t("copy_to_clipboard") : t("enabled_after_update")
|
||||
}>
|
||||
<Button
|
||||
color="minimal"
|
||||
size="sm"
|
||||
type="button"
|
||||
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
|
||||
aria-label="copy link"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(placeholderHashedLink);
|
||||
if (formMethods.getValues("hashedLink")) {
|
||||
showToast(t("private_link_copied"), "success");
|
||||
} else {
|
||||
showToast(t("enabled_after_update_description"), "warning");
|
||||
}
|
||||
}}>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!isManagedEventType && (
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
{!IS_VISUAL_REGRESSION_TESTING && (
|
||||
<TextField
|
||||
disabled
|
||||
name="hashedLink"
|
||||
label={t("private_link_label")}
|
||||
data-testid="generated-hash-url"
|
||||
labelSrOnly
|
||||
type="text"
|
||||
hint={t("private_link_hint")}
|
||||
defaultValue={placeholderHashedLink}
|
||||
addOnSuffix={
|
||||
<Tooltip
|
||||
content={
|
||||
formMethods.getValues("hashedLink") ? t("copy_to_clipboard") : t("enabled_after_update")
|
||||
}>
|
||||
<Button
|
||||
color="minimal"
|
||||
size="sm"
|
||||
type="button"
|
||||
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
|
||||
aria-label="copy link"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(placeholderHashedLink);
|
||||
if (formMethods.getValues("hashedLink")) {
|
||||
showToast(t("private_link_copied"), "success");
|
||||
} else {
|
||||
showToast(t("enabled_after_update_description"), "warning");
|
||||
}
|
||||
}}>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SettingsToggle>
|
||||
|
||||
<Controller
|
||||
name="seatsPerTimeSlotEnabled"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
@@ -504,7 +504,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
toggleSwitchAtTheEnd={true}
|
||||
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
|
||||
title={t("lock_timezone_toggle_on_booking_page")}
|
||||
{...shouldLockDisableProps("lockTimeZoneToggleOnBookingPage")}
|
||||
{...lockTimeZoneToggleOnBookingPageLocked}
|
||||
description={t("description_lock_timezone_toggle_on_booking_page")}
|
||||
checked={value}
|
||||
onCheckedChange={(e) => onChange(e)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
||||
@@ -8,6 +8,7 @@ import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCard
|
||||
import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
||||
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, EmptyScreen, Alert } from "@calcom/ui";
|
||||
@@ -74,11 +75,13 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
};
|
||||
};
|
||||
|
||||
const { shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||
const { shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager({
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
translate: t,
|
||||
formMethods,
|
||||
});
|
||||
const appsDisableProps = shouldLockDisableProps("apps", { simple: true });
|
||||
const lockedText = appsDisableProps.isLocked ? "locked" : "unlocked";
|
||||
|
||||
const appsWithTeamCredentials = eventTypeApps?.items.filter((app) => app.teams.length) || [];
|
||||
const cardsForAppsWithTeams = appsWithTeamCredentials.map((app) => {
|
||||
@@ -97,7 +100,6 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
app={app}
|
||||
eventType={eventType}
|
||||
eventTypeFormMetadata={eventTypeFormMetadata}
|
||||
{...shouldLockDisableProps("apps")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -121,7 +123,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
}}
|
||||
eventType={eventType}
|
||||
eventTypeFormMetadata={eventTypeFormMetadata}
|
||||
{...shouldLockDisableProps("apps")}
|
||||
disabled={shouldLockDisableProps("apps").disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -133,12 +135,28 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
<>
|
||||
<div>
|
||||
<div className="before:border-0">
|
||||
{isManagedEventType && (
|
||||
{(isManagedEventType || isChildrenManagedEventType) && (
|
||||
<Alert
|
||||
severity="neutral"
|
||||
severity={appsDisableProps.isLocked ? "neutral" : "green"}
|
||||
className="mb-2"
|
||||
title={t("locked_for_members")}
|
||||
message={t("locked_apps_description")}
|
||||
title={
|
||||
<Trans i18nKey={`${lockedText}_${isManagedEventType ? "for_members" : "by_team_admins"}`}>
|
||||
{lockedText[0].toUpperCase()}
|
||||
{lockedText.slice(1)} {isManagedEventType ? "for members" : "by team admins"}
|
||||
</Trans>
|
||||
}
|
||||
actions={<div className="flex h-full items-center">{appsDisableProps.LockedIcon}</div>}
|
||||
message={
|
||||
<Trans
|
||||
i18nKey={`apps_${lockedText}_${
|
||||
isManagedEventType ? "for_members" : "by_team_admins"
|
||||
}_description`}>
|
||||
{isManagedEventType ? "Members" : "You"}{" "}
|
||||
{appsDisableProps.isLocked
|
||||
? "will be able to see the active apps but will not be able to edit any app settings"
|
||||
: "will be able to see the active apps and will be able to edit any app settings"}
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!isPending && !installedApps?.length ? (
|
||||
@@ -147,9 +165,9 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
headline={t("empty_installed_apps_headline")}
|
||||
description={t("empty_installed_apps_description")}
|
||||
buttonRaw={
|
||||
isChildrenManagedEventType && !isManagedEventType ? (
|
||||
appsDisableProps.disabled ? (
|
||||
<Button StartIcon={Lock} color="secondary" disabled>
|
||||
{t("locked_by_admin")}
|
||||
{t("locked_by_team_admin")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button target="_blank" color="secondary" href="/apps">
|
||||
@@ -174,13 +192,12 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
app={app}
|
||||
eventType={eventType}
|
||||
eventTypeFormMetadata={eventTypeFormMetadata}
|
||||
{...shouldLockDisableProps("apps")}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{!shouldLockDisableProps("apps").disabled && (
|
||||
{!appsDisableProps.disabled && (
|
||||
<div className="bg-muted mt-6 rounded-md p-8">
|
||||
{!isPending && notInstalledApps?.length ? (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetup } from "pages/event-types/[type]";
|
||||
import { useState, memo, useEffect } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import type { OptionProps, SingleValueProps } from "react-select";
|
||||
@@ -6,6 +6,7 @@ import { components } from "react-select";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { AvailabilityOption, FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { weekdayNames } from "@calcom/lib/weekday";
|
||||
@@ -17,13 +18,6 @@ import { ExternalLink, Globe } from "@calcom/ui/components/icon";
|
||||
|
||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||
|
||||
export type AvailabilityOption = {
|
||||
label: string;
|
||||
value: number;
|
||||
isDefault: boolean;
|
||||
isManaged?: boolean;
|
||||
};
|
||||
|
||||
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
||||
const { label, isDefault, isManaged = false } = props.data;
|
||||
const { t } = useLocale();
|
||||
@@ -160,12 +154,10 @@ EventTypeScheduleDetails.displayName = "EventTypeScheduleDetails";
|
||||
|
||||
const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
const { t } = useLocale();
|
||||
const { shouldLockIndicator, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
const { watch, setValue, getValues } = useFormContext<FormValues>();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const { shouldLockIndicator, shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } =
|
||||
useLockedFieldsManager({ eventType, translate: t, formMethods });
|
||||
const { watch, setValue, getValues } = formMethods;
|
||||
const watchSchedule = watch("schedule");
|
||||
const [options, setOptions] = useState<AvailabilityOption[]>([]);
|
||||
|
||||
@@ -246,7 +238,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
<div className="border-subtle rounded-t-md border p-6">
|
||||
<label htmlFor="availability" className="text-default mb-2 block text-sm font-medium leading-none">
|
||||
{t("availability")}
|
||||
{shouldLockIndicator("availability")}
|
||||
{(isManagedEventType || isChildrenManagedEventType) && shouldLockIndicator("availability")}
|
||||
</label>
|
||||
{isPending && <SelectSkeletonLoader />}
|
||||
{!isPending && (
|
||||
@@ -257,6 +249,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
<Select
|
||||
placeholder={t("select")}
|
||||
options={options}
|
||||
isDisabled={shouldLockDisableProps("availability").disabled}
|
||||
isSearchable={false}
|
||||
onChange={(selected) => {
|
||||
field.onChange(selected?.value || null);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||
import type { FormValues } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import type { Key } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import type { UseFormRegisterReturn } from "react-hook-form";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import type { SingleValue } from "react-select";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import type { DurationType } from "@calcom/lib/convertToNewDurationType";
|
||||
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
||||
@@ -108,7 +110,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
||||
);
|
||||
});
|
||||
|
||||
export const EventLimitsTab = () => {
|
||||
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
||||
const { t, i18n } = useLocale();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
|
||||
@@ -129,6 +131,18 @@ export const EventLimitsTab = () => {
|
||||
|
||||
const watchPeriodType = formMethods.watch("periodType");
|
||||
|
||||
const { shouldLockIndicator, shouldLockDisableProps } = useLockedFieldsManager({
|
||||
eventType,
|
||||
translate: t,
|
||||
formMethods,
|
||||
});
|
||||
|
||||
const bookingLimitsLocked = shouldLockDisableProps("bookingLimits");
|
||||
const durationLimitsLocked = shouldLockDisableProps("durationLimits");
|
||||
const onlyFirstAvailableSlotLocked = shouldLockDisableProps("onlyShowFirstAvailableSlot");
|
||||
const periodTypeLocked = shouldLockDisableProps("periodType");
|
||||
const offsetStartLockedProps = shouldLockDisableProps("offsetStart");
|
||||
|
||||
const optionsPeriod = [
|
||||
{ value: 0, label: t("business_days") },
|
||||
{ value: 1, label: t("calendar_days") },
|
||||
@@ -147,7 +161,10 @@ export const EventLimitsTab = () => {
|
||||
<div className="border-subtle space-y-6 rounded-lg border p-6">
|
||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-x-4 lg:space-y-0">
|
||||
<div className="w-full">
|
||||
<Label htmlFor="beforeEventBuffer">{t("before_event")}</Label>
|
||||
<Label htmlFor="beforeBufferTime">
|
||||
{t("before_event")}
|
||||
{shouldLockIndicator("beforeBufferTime")}
|
||||
</Label>
|
||||
<Controller
|
||||
name="beforeEventBuffer"
|
||||
render={({ field: { onChange, value } }) => {
|
||||
@@ -167,6 +184,7 @@ export const EventLimitsTab = () => {
|
||||
onChange={(val) => {
|
||||
if (val) onChange(val.value);
|
||||
}}
|
||||
isDisabled={shouldLockDisableProps("beforeBufferTime").disabled}
|
||||
defaultValue={
|
||||
beforeBufferOptions.find((option) => option.value === value) || beforeBufferOptions[0]
|
||||
}
|
||||
@@ -177,7 +195,10 @@ export const EventLimitsTab = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Label htmlFor="afterEventBuffer">{t("after_event")}</Label>
|
||||
<Label htmlFor="afterBufferTime">
|
||||
{t("after_event")}
|
||||
{shouldLockIndicator("afterBufferTime")}
|
||||
</Label>
|
||||
<Controller
|
||||
name="afterEventBuffer"
|
||||
render={({ field: { onChange, value } }) => {
|
||||
@@ -197,6 +218,7 @@ export const EventLimitsTab = () => {
|
||||
onChange={(val) => {
|
||||
if (val) onChange(val.value);
|
||||
}}
|
||||
isDisabled={shouldLockDisableProps("afterBufferTime").disabled}
|
||||
defaultValue={
|
||||
afterBufferOptions.find((option) => option.value === value) || afterBufferOptions[0]
|
||||
}
|
||||
@@ -209,11 +231,20 @@ export const EventLimitsTab = () => {
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-x-4 lg:space-y-0">
|
||||
<div className="w-full">
|
||||
<Label htmlFor="minimumBookingNotice">{t("minimum_booking_notice")}</Label>
|
||||
<MinimumBookingNoticeInput {...formMethods.register("minimumBookingNotice")} />
|
||||
<Label htmlFor="minimumBookingNotice">
|
||||
{t("minimum_booking_notice")}
|
||||
{shouldLockIndicator("minimumBookingNotice")}
|
||||
</Label>
|
||||
<MinimumBookingNoticeInput
|
||||
disabled={shouldLockDisableProps("minimumBookingNotice").disabled}
|
||||
{...formMethods.register("minimumBookingNotice")}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Label htmlFor="slotInterval">{t("slot_interval")}</Label>
|
||||
<Label htmlFor="slotInterval">
|
||||
{t("slot_interval")}
|
||||
{shouldLockIndicator("slotInterval")}
|
||||
</Label>
|
||||
<Controller
|
||||
name="slotInterval"
|
||||
render={() => {
|
||||
@@ -230,6 +261,7 @@ export const EventLimitsTab = () => {
|
||||
return (
|
||||
<Select
|
||||
isSearchable={false}
|
||||
isDisabled={shouldLockDisableProps("slotInterval").disabled}
|
||||
onChange={(val) => {
|
||||
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null, {
|
||||
shouldDirty: true,
|
||||
@@ -257,6 +289,7 @@ export const EventLimitsTab = () => {
|
||||
toggleSwitchAtTheEnd={true}
|
||||
labelClassName="text-sm"
|
||||
title={t("limit_booking_frequency")}
|
||||
{...bookingLimitsLocked}
|
||||
description={t("limit_booking_frequency_description")}
|
||||
checked={isChecked}
|
||||
onCheckedChange={(active) => {
|
||||
@@ -278,7 +311,12 @@ export const EventLimitsTab = () => {
|
||||
)}
|
||||
childrenClassName="lg:ml-0">
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
<IntervalLimitsManager propertyName="bookingLimits" defaultLimit={1} step={1} />
|
||||
<IntervalLimitsManager
|
||||
disabled={bookingLimitsLocked.disabled}
|
||||
propertyName="bookingLimits"
|
||||
defaultLimit={1}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
</SettingsToggle>
|
||||
);
|
||||
@@ -295,6 +333,7 @@ export const EventLimitsTab = () => {
|
||||
title={t("limit_booking_only_first_slot")}
|
||||
description={t("limit_booking_only_first_slot_description")}
|
||||
checked={isChecked}
|
||||
{...onlyFirstAvailableSlotLocked}
|
||||
onCheckedChange={(active) => {
|
||||
onChange(active ?? false);
|
||||
}}
|
||||
@@ -321,6 +360,7 @@ export const EventLimitsTab = () => {
|
||||
childrenClassName="lg:ml-0"
|
||||
title={t("limit_total_booking_duration")}
|
||||
description={t("limit_total_booking_duration_description")}
|
||||
{...durationLimitsLocked}
|
||||
checked={isChecked}
|
||||
onCheckedChange={(active) => {
|
||||
if (active) {
|
||||
@@ -335,6 +375,7 @@ export const EventLimitsTab = () => {
|
||||
<IntervalLimitsManager
|
||||
propertyName="durationLimits"
|
||||
defaultLimit={60}
|
||||
disabled={durationLimitsLocked.disabled}
|
||||
step={15}
|
||||
textFieldSuffix={t("minutes")}
|
||||
/>
|
||||
@@ -359,6 +400,7 @@ export const EventLimitsTab = () => {
|
||||
childrenClassName="lg:ml-0"
|
||||
title={t("limit_future_bookings")}
|
||||
description={t("limit_future_bookings_description")}
|
||||
{...periodTypeLocked}
|
||||
checked={isChecked}
|
||||
onCheckedChange={(bool) => {
|
||||
if (bool && !formMethods.getValues("periodDays")) {
|
||||
@@ -372,7 +414,9 @@ export const EventLimitsTab = () => {
|
||||
onValueChange={(val) =>
|
||||
formMethods.setValue("periodType", val as PeriodType, { shouldDirty: true })
|
||||
}>
|
||||
{PERIOD_TYPES.map((period) => {
|
||||
{PERIOD_TYPES.filter((opt) =>
|
||||
periodTypeLocked.disabled ? watchPeriodType === opt.type : true
|
||||
).map((period) => {
|
||||
if (period.type === "UNLIMITED") return null;
|
||||
return (
|
||||
<div
|
||||
@@ -381,12 +425,14 @@ export const EventLimitsTab = () => {
|
||||
watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30"
|
||||
)}
|
||||
key={period.type}>
|
||||
<RadioGroup.Item
|
||||
id={period.type}
|
||||
value={period.type}
|
||||
className="min-w-4 bg-default border-default flex h-4 w-4 cursor-pointer items-center rounded-full border focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
||||
<RadioGroup.Indicator className="after:bg-inverted relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full" />
|
||||
</RadioGroup.Item>
|
||||
{!periodTypeLocked.disabled && (
|
||||
<RadioGroup.Item
|
||||
id={period.type}
|
||||
value={period.type}
|
||||
className="min-w-4 bg-default border-default flex h-4 w-4 cursor-pointer items-center rounded-full border focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
||||
<RadioGroup.Indicator className="after:bg-inverted relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full" />
|
||||
</RadioGroup.Item>
|
||||
)}
|
||||
|
||||
{period.prefix ? <span>{period.prefix} </span> : null}
|
||||
{period.type === "ROLLING" && (
|
||||
@@ -396,18 +442,20 @@ export const EventLimitsTab = () => {
|
||||
type="number"
|
||||
className="border-default my-0 block w-16 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
||||
placeholder="30"
|
||||
disabled={periodTypeLocked.disabled}
|
||||
{...formMethods.register("periodDays", { valueAsNumber: true })}
|
||||
/>
|
||||
<Select
|
||||
options={optionsPeriod}
|
||||
isSearchable={false}
|
||||
onChange={(opt) =>
|
||||
isDisabled={periodTypeLocked.disabled}
|
||||
onChange={(opt) => {
|
||||
formMethods.setValue(
|
||||
"periodCountCalendarDays",
|
||||
opt?.value === 1 ? true : false,
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
);
|
||||
}}
|
||||
name="periodCoundCalendarDays"
|
||||
value={optionsPeriod.find((opt) => {
|
||||
opt.value ===
|
||||
@@ -429,6 +477,7 @@ export const EventLimitsTab = () => {
|
||||
<DateRangePicker
|
||||
startDate={formMethods.getValues("periodDates").startDate}
|
||||
endDate={formMethods.getValues("periodDates").endDate}
|
||||
disabled={periodTypeLocked.disabled}
|
||||
onDatesChange={({ startDate, endDate }) => {
|
||||
onChange({
|
||||
startDate,
|
||||
@@ -460,6 +509,7 @@ export const EventLimitsTab = () => {
|
||||
childrenClassName="lg:ml-0"
|
||||
title={t("offset_toggle")}
|
||||
description={t("offset_toggle_description")}
|
||||
{...offsetStartLockedProps}
|
||||
checked={offsetToggle}
|
||||
onCheckedChange={(active) => {
|
||||
setOffsetToggle(active);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { ErrorMessage } from "@hookform/error-message";
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useFormContext, useFieldArray } from "react-hook-form";
|
||||
import type { MultiValue } from "react-select";
|
||||
@@ -11,6 +11,7 @@ import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import { getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
@@ -142,12 +143,7 @@ export const EventSetupTab = (
|
||||
);
|
||||
|
||||
const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } =
|
||||
useLockedFieldsManager(
|
||||
formMethods.getValues(),
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
|
||||
useLockedFieldsManager({ eventType, translate: t, formMethods });
|
||||
const Locations = () => {
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
@@ -183,7 +179,6 @@ export const EventSetupTab = (
|
||||
index: number;
|
||||
}) => {
|
||||
const { eventLocationType, index, ...remainingProps } = props;
|
||||
|
||||
if (eventLocationType?.organizerInputType === "text") {
|
||||
const { defaultValue, ...rest } = remainingProps;
|
||||
|
||||
@@ -200,6 +195,7 @@ export const EventSetupTab = (
|
||||
required
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
{...(shouldLockDisableProps("locations").disabled ? { disabled: true } : {})}
|
||||
className="my-0"
|
||||
{...rest}
|
||||
/>
|
||||
@@ -218,6 +214,7 @@ export const EventSetupTab = (
|
||||
return (
|
||||
<PhoneInput
|
||||
required
|
||||
disabled={shouldLockDisableProps("locations").disabled}
|
||||
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
|
||||
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
|
||||
value={value}
|
||||
@@ -233,7 +230,10 @@ export const EventSetupTab = (
|
||||
};
|
||||
|
||||
const [showEmptyLocationSelect, setShowEmptyLocationSelect] = useState(false);
|
||||
const [selectedNewOption, setSelectedNewOption] = useState<SingleValueLocationOption | null>(null);
|
||||
const defaultInitialLocation = defaultValue || null;
|
||||
const [selectedNewOption, setSelectedNewOption] = useState<SingleValueLocationOption | null>(
|
||||
defaultInitialLocation
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -288,16 +288,18 @@ export const EventSetupTab = (
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
data-testid={`delete-locations.${index}.type`}
|
||||
className="min-h-9 block h-9 px-2"
|
||||
type="button"
|
||||
onClick={() => remove(index)}
|
||||
aria-label={t("remove")}>
|
||||
<div className="h-4 w-4">
|
||||
<X className="border-l-1 hover:text-emphasis text-subtle h-4 w-4" />
|
||||
</div>
|
||||
</button>
|
||||
{!(shouldLockDisableProps("locations").disabled && isChildrenManagedEventType) && (
|
||||
<button
|
||||
data-testid={`delete-locations.${index}.type`}
|
||||
className="min-h-9 block h-9 px-2"
|
||||
type="button"
|
||||
onClick={() => remove(index)}
|
||||
aria-label={t("remove")}>
|
||||
<div className="h-4 w-4">
|
||||
<X className="border-l-1 hover:text-emphasis text-subtle h-4 w-4" />
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{eventLocationType?.organizerInputType && (
|
||||
@@ -329,6 +331,7 @@ export const EventSetupTab = (
|
||||
<CheckboxField
|
||||
name={`locations[${index}].displayLocationPublicly`}
|
||||
data-testid="display-location"
|
||||
disabled={shouldLockDisableProps("locations").disabled}
|
||||
defaultChecked={defaultLocation?.displayLocationPublicly}
|
||||
description={t("display_location_label")}
|
||||
onChange={(e) => {
|
||||
@@ -417,7 +420,8 @@ export const EventSetupTab = (
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{validLocations.length > 0 && !isManagedEventType && !isChildrenManagedEventType && (
|
||||
{validLocations.length > 0 && !shouldLockDisableProps("locations").disabled && (
|
||||
// && !isChildrenManagedEventType : Add this to hide add-location button only when location is disabled by Admin
|
||||
<li>
|
||||
<Button
|
||||
data-testid="add-location"
|
||||
@@ -444,7 +448,8 @@ export const EventSetupTab = (
|
||||
|
||||
const lengthLockedProps = shouldLockDisableProps("length");
|
||||
const descriptionLockedProps = shouldLockDisableProps("description");
|
||||
|
||||
const urlLockedProps = shouldLockDisableProps("slug");
|
||||
const titleLockedProps = shouldLockDisableProps("title");
|
||||
const urlPrefix = orgBranding
|
||||
? orgBranding?.fullDomain.replace(/^(https?:|)\/\//, "")
|
||||
: `${WEBSITE_URL?.replace(/^(https?:|)\/\//, "")}`;
|
||||
@@ -456,20 +461,22 @@ export const EventSetupTab = (
|
||||
<TextField
|
||||
required
|
||||
label={t("title")}
|
||||
{...shouldLockDisableProps("title")}
|
||||
{...(isManagedEventType || isChildrenManagedEventType ? titleLockedProps : {})}
|
||||
defaultValue={eventType.title}
|
||||
{...formMethods.register("title")}
|
||||
/>
|
||||
<div>
|
||||
<Label>
|
||||
<Label htmlFor="editor">
|
||||
{t("description")}
|
||||
{shouldLockIndicator("description")}
|
||||
{(isManagedEventType || isChildrenManagedEventType) && shouldLockIndicator("description")}
|
||||
</Label>
|
||||
<DescriptionEditor isEditable={!descriptionLockedProps.disabled} />
|
||||
</div>
|
||||
<TextField
|
||||
required
|
||||
label={t("URL")}
|
||||
{...shouldLockDisableProps("slug")}
|
||||
{...(isManagedEventType || isChildrenManagedEventType ? urlLockedProps : {})}
|
||||
defaultValue={eventType.slug}
|
||||
addOnLeading={
|
||||
<>
|
||||
{urlPrefix}/
|
||||
@@ -498,6 +505,7 @@ export const EventSetupTab = (
|
||||
defaultValue={selectedMultipleDuration}
|
||||
name="metadata.multipleDuration"
|
||||
isSearchable={false}
|
||||
isDisabled={lengthLockedProps.disabled}
|
||||
className="h-auto !min-h-[36px] text-sm"
|
||||
options={multipleDurationOptions}
|
||||
value={selectedMultipleDuration}
|
||||
@@ -551,7 +559,8 @@ export const EventSetupTab = (
|
||||
<TextField
|
||||
required
|
||||
type="number"
|
||||
{...lengthLockedProps}
|
||||
data-testid="duration"
|
||||
{...(isManagedEventType || isChildrenManagedEventType ? lengthLockedProps : {})}
|
||||
label={t("duration")}
|
||||
defaultValue={formMethods.getValues("length") ?? 15}
|
||||
{...formMethods.register("length")}
|
||||
@@ -583,15 +592,20 @@ export const EventSetupTab = (
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-subtle rounded-lg border p-6">
|
||||
<div>
|
||||
<Skeleton as={Label} loadingClassName="w-16">
|
||||
<Skeleton as={Label} loadingClassName="w-16" htmlFor="locations">
|
||||
{t("location")}
|
||||
{/*improve shouldLockIndicator function to also accept eventType and then conditionally render
|
||||
based on Managed Event type or not.*/}
|
||||
{shouldLockIndicator("locations")}
|
||||
</Skeleton>
|
||||
|
||||
<Controller name="locations" render={() => <Locations />} />
|
||||
<Controller
|
||||
name="locations"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.locations || []}
|
||||
render={() => <Locations />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps, FormValues, Host } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetupProps, Host } from "pages/event-types/[type]";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { ComponentProps, Dispatch, SetStateAction } from "react";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
@@ -9,6 +9,7 @@ import type { Options } from "react-select";
|
||||
import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||
import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { Label, Select, SettingsToggle } from "@calcom/ui";
|
||||
|
||||
@@ -2,12 +2,13 @@ import { Webhook as TbWebhook } from "lucide-react";
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { Trans } from "next-i18next";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import { useMemo, useState, Suspense } from "react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/embed/EventTypeEmbed";
|
||||
import type { FormValues, AvailabilityOption } from "@calcom/features/eventtypes/lib/types";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -52,8 +53,6 @@ import {
|
||||
Loader,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
eventType: EventTypeSetupProps["eventType"];
|
||||
@@ -214,11 +213,11 @@ function EventTypeSingleLayout({
|
||||
formMethods.getValues("schedulingType") === SchedulingType.MANAGED ||
|
||||
isUserOrganizationAdmin;
|
||||
|
||||
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||
formMethods.getValues(),
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager({
|
||||
eventType,
|
||||
translate: t,
|
||||
formMethods,
|
||||
});
|
||||
|
||||
const length = formMethods.watch("length");
|
||||
const multipleDuration = formMethods.watch("metadata")?.multipleDuration;
|
||||
|
||||
@@ -4,8 +4,10 @@ import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import { useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
||||
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
||||
@@ -20,6 +22,7 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
||||
const { t } = useLocale();
|
||||
|
||||
const utils = trpc.useContext();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
|
||||
const { data: webhooks } = trpc.viewer.webhook.list.useQuery({ eventTypeId: eventType.id });
|
||||
|
||||
@@ -96,11 +99,11 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
||||
);
|
||||
};
|
||||
|
||||
const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager(
|
||||
const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager({
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
translate: t,
|
||||
formMethods,
|
||||
});
|
||||
const webhookLockedStatus = shouldLockDisableProps("webhooks");
|
||||
|
||||
return (
|
||||
@@ -163,7 +166,7 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
||||
buttonRaw={
|
||||
isChildrenManagedEventType && !isManagedEventType ? (
|
||||
<Button StartIcon={Lock} color="secondary" disabled>
|
||||
{t("locked_by_admin")}
|
||||
{t("locked_by_team_admin")}
|
||||
</Button>
|
||||
) : (
|
||||
<NewWebhookButton />
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { Webhook } from "@prisma/client";
|
||||
import { useSession } from "next-auth/react";
|
||||
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetup } from "pages/event-types/[type]";
|
||||
import { useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
||||
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
||||
@@ -33,11 +34,7 @@ export default function InstantEventController({
|
||||
const [instantEventState, setInstantEventState] = useState<boolean>(eventType?.isInstantEvent ?? false);
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
|
||||
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
const { shouldLockDisableProps } = useLockedFieldsManager({ eventType, translate: t, formMethods });
|
||||
|
||||
const instantLocked = shouldLockDisableProps("isInstantEvent");
|
||||
|
||||
@@ -104,6 +101,8 @@ export default function InstantEventController({
|
||||
const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
|
||||
const { data: webhooks } = trpc.viewer.webhook.list.useQuery({
|
||||
eventTypeId: eventType.id,
|
||||
eventTriggers: [WebhookTriggerEvents.INSTANT_MEETING],
|
||||
@@ -179,11 +178,11 @@ const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) =>
|
||||
);
|
||||
};
|
||||
|
||||
const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager(
|
||||
const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager({
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
formMethods,
|
||||
translate: t,
|
||||
});
|
||||
const webhookLockedStatus = shouldLockDisableProps("webhooks");
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetup } from "pages/event-types/[type]";
|
||||
import { useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Frequency } from "@calcom/prisma/zod-utils";
|
||||
@@ -31,11 +32,7 @@ export default function RecurringEventController({
|
||||
value: value.toString(),
|
||||
}));
|
||||
|
||||
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
const { shouldLockDisableProps } = useLockedFieldsManager({ eventType, translate: t, formMethods });
|
||||
|
||||
const recurringLocked = shouldLockDisableProps("recurringEvent");
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||
import type { UnitTypeLongPlural } from "dayjs";
|
||||
import { Trans } from "next-i18next";
|
||||
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetup } from "pages/event-types/[type]";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import type z from "zod";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
@@ -42,11 +43,7 @@ export default function RequiresConfirmationController({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [requiresConfirmation]);
|
||||
|
||||
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
const { shouldLockDisableProps } = useLockedFieldsManager({ eventType, translate: t, formMethods });
|
||||
const requiresConfirmationLockedProps = shouldLockDisableProps("requiresConfirmation");
|
||||
|
||||
const options = [
|
||||
|
||||
@@ -90,6 +90,17 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
},
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
parent: {
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
isPrivate: true,
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!unpublishedTeam) return { notFound: true } as const;
|
||||
@@ -103,19 +114,23 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
} as const;
|
||||
}
|
||||
|
||||
const isTeamOrParentOrgPrivate = team.isPrivate || (team.parent?.isOrganization && team.parent?.isPrivate);
|
||||
|
||||
team.eventTypes =
|
||||
team.eventTypes?.map((type) => ({
|
||||
...type,
|
||||
users: type.users.map((user) => ({
|
||||
...user,
|
||||
avatar: `/${user.username}/avatar.png`,
|
||||
})),
|
||||
users: !isTeamOrParentOrgPrivate
|
||||
? type.users.map((user) => ({
|
||||
...user,
|
||||
avatar: `/${user.username}/avatar.png`,
|
||||
}))
|
||||
: [],
|
||||
descriptionAsSafeHTML: markdownToSafeHTML(type.description),
|
||||
})) ?? null;
|
||||
|
||||
const safeBio = markdownToSafeHTML(team.bio) || "";
|
||||
|
||||
const members = !team.isPrivate
|
||||
const members = !isTeamOrParentOrgPrivate
|
||||
? team.members.map((member) => {
|
||||
return {
|
||||
name: member.name,
|
||||
@@ -139,7 +154,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
|
||||
return {
|
||||
props: {
|
||||
team: { ...serializableTeam, safeBio, members, metadata },
|
||||
team: {
|
||||
...serializableTeam,
|
||||
safeBio,
|
||||
members,
|
||||
metadata,
|
||||
children: isTeamOrParentOrgPrivate ? [] : team.children,
|
||||
},
|
||||
themeBasis: serializableTeam.slug,
|
||||
trpcState: ssr.dehydrate(),
|
||||
markdownStrippedBio,
|
||||
|
||||
@@ -24,7 +24,11 @@ import {
|
||||
useIsEmbed,
|
||||
} from "@calcom/embed-core/embed-iframe";
|
||||
import { Price } from "@calcom/features/bookings/components/event-meta/Price";
|
||||
import { SMS_REMINDER_NUMBER_FIELD, SystemField } from "@calcom/features/bookings/lib/SystemField";
|
||||
import {
|
||||
SMS_REMINDER_NUMBER_FIELD,
|
||||
SystemField,
|
||||
TITLE_FIELD,
|
||||
} from "@calcom/features/bookings/lib/SystemField";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import {
|
||||
formatToLocalizedDate,
|
||||
@@ -91,6 +95,7 @@ export default function Success(props: PageProps) {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { eventType, bookingInfo, requiresLoginToUpdate } = props;
|
||||
|
||||
const {
|
||||
allRemainingBookings,
|
||||
isSuccessBookingPage,
|
||||
@@ -345,7 +350,7 @@ export default function Success(props: PageProps) {
|
||||
!giphyImage && !isCancelled && needsConfirmation
|
||||
? "bg-subtle h-12 w-12 rounded-full"
|
||||
: "",
|
||||
isCancelled ? "bg-error h-12 w-12 rounded-full" : ""
|
||||
isCancelled ? "bg-error h-12 w-12 rounded-full " : ""
|
||||
)}>
|
||||
{giphyImage && !needsConfirmation && !isCancelled && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
@@ -518,7 +523,13 @@ export default function Success(props: PageProps) {
|
||||
if (!field) return null;
|
||||
const isSystemField = SystemField.safeParse(field.name);
|
||||
// SMS_REMINDER_NUMBER_FIELD is a system field but doesn't have a dedicated place in the UI. So, it would be shown through the following responses list
|
||||
if (isSystemField.success && field.name !== SMS_REMINDER_NUMBER_FIELD) return null;
|
||||
// TITLE is also an identifier for booking question "What is this meeting about?"
|
||||
if (
|
||||
isSystemField.success &&
|
||||
field.name !== SMS_REMINDER_NUMBER_FIELD &&
|
||||
field.name !== TITLE_FIELD
|
||||
)
|
||||
return null;
|
||||
|
||||
const label = field.label || t(field.defaultLabel || "");
|
||||
|
||||
|
||||
@@ -803,11 +803,10 @@ const CreateFirstEventTypeView = ({ slug }: { slug: string }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const CTA = ({ data }: { data: GetByViewerResponse }) => {
|
||||
const CTA = ({ data, isOrganization }: { data: GetByViewerResponse; isOrganization: boolean }) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
const profileOptions = data.profiles
|
||||
.filter((profile) => !profile.readOnly)
|
||||
.map((profile) => {
|
||||
@@ -825,7 +824,9 @@ const CTA = ({ data }: { data: GetByViewerResponse }) => {
|
||||
data-testid="new-event-type"
|
||||
subtitle={t("create_event_on").toUpperCase()}
|
||||
options={profileOptions}
|
||||
createDialog={() => <CreateEventTypeDialog profileOptions={profileOptions} />}
|
||||
createDialog={() => (
|
||||
<CreateEventTypeDialog profileOptions={profileOptions} isOrganization={isOrganization} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -983,7 +984,7 @@ const EventTypesPage: React.FC & {
|
||||
hideHeadingOnMobile
|
||||
subtitle={t("event_types_page_subtitle")}
|
||||
beforeCTAactions={<Actions />}
|
||||
CTA={<CTA data={data} />}>
|
||||
CTA={<CTA data={data} isOrganization={!!user?.organizationId} />}>
|
||||
<HeadSeo
|
||||
title="Event Types"
|
||||
description="Create events to share for people to book on your calendar."
|
||||
|
||||
@@ -12,8 +12,8 @@ import { z } from "zod";
|
||||
import checkForMultiplePaymentApps from "@calcom/app-store/_utils/payments/checkForMultiplePaymentApps";
|
||||
import { getEventLocationType } from "@calcom/app-store/locations";
|
||||
import { validateCustomEventName } from "@calcom/core/event";
|
||||
import type { EventLocationType } from "@calcom/core/location";
|
||||
import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { validateIntervalLimitOrder } from "@calcom/lib";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -22,21 +22,14 @@ import { HttpError } from "@calcom/lib/http-error";
|
||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import type { PeriodType, SchedulingType } from "@calcom/prisma/enums";
|
||||
import type {
|
||||
BookerLayoutSettings,
|
||||
customInputSchema,
|
||||
EventTypeMetaDataSchema,
|
||||
} from "@calcom/prisma/zod-utils";
|
||||
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { IntervalLimit, RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { Form, showToast } from "@calcom/ui";
|
||||
|
||||
import type { AppProps } from "@lib/app-providers";
|
||||
|
||||
import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
|
||||
import { EventTypeSingleLayout } from "@components/eventtype/EventTypeSingleLayout";
|
||||
|
||||
import { type PageProps } from "~/event-types/views/event-types-single-view.getServerSideProps";
|
||||
@@ -84,74 +77,6 @@ const ManagedEventTypeDialog = dynamic(() => import("@components/eventtype/Manag
|
||||
|
||||
export type Host = { isFixed: boolean; userId: number; priority: number };
|
||||
|
||||
export type FormValues = {
|
||||
id: number;
|
||||
title: string;
|
||||
eventTitle: string;
|
||||
eventName: string;
|
||||
slug: string;
|
||||
isInstantEvent: boolean;
|
||||
length: number;
|
||||
offsetStart: number;
|
||||
description: string;
|
||||
disableGuests: boolean;
|
||||
lockTimeZoneToggleOnBookingPage: boolean;
|
||||
requiresConfirmation: boolean;
|
||||
requiresBookerEmailVerification: boolean;
|
||||
recurringEvent: RecurringEvent | null;
|
||||
schedulingType: SchedulingType | null;
|
||||
hidden: boolean;
|
||||
hideCalendarNotes: boolean;
|
||||
hashedLink: string | undefined;
|
||||
locations: {
|
||||
type: EventLocationType["type"];
|
||||
address?: string;
|
||||
attendeeAddress?: string;
|
||||
link?: string;
|
||||
hostPhoneNumber?: string;
|
||||
displayLocationPublicly?: boolean;
|
||||
phone?: string;
|
||||
hostDefault?: string;
|
||||
credentialId?: number;
|
||||
teamName?: string;
|
||||
}[];
|
||||
customInputs: CustomInputParsed[];
|
||||
schedule: number | null;
|
||||
periodType: PeriodType;
|
||||
periodDays: number;
|
||||
periodCountCalendarDays: boolean;
|
||||
periodDates: { startDate: Date; endDate: Date };
|
||||
seatsPerTimeSlot: number | null;
|
||||
seatsShowAttendees: boolean | null;
|
||||
seatsShowAvailabilityCount: boolean | null;
|
||||
seatsPerTimeSlotEnabled: boolean;
|
||||
scheduleName: string;
|
||||
minimumBookingNotice: number;
|
||||
minimumBookingNoticeInDurationType: number;
|
||||
beforeEventBuffer: number;
|
||||
afterEventBuffer: number;
|
||||
slotInterval: number | null;
|
||||
metadata: z.infer<typeof EventTypeMetaDataSchema>;
|
||||
destinationCalendar: {
|
||||
integration: string;
|
||||
externalId: string;
|
||||
};
|
||||
successRedirectUrl: string;
|
||||
durationLimits?: IntervalLimit;
|
||||
bookingLimits?: IntervalLimit;
|
||||
onlyShowFirstAvailableSlot: boolean;
|
||||
children: ChildrenEventType[];
|
||||
hosts: Host[];
|
||||
bookingFields: z.infer<typeof eventTypeBookingFields>;
|
||||
availability?: AvailabilityOption;
|
||||
bookerLayouts: BookerLayoutSettings;
|
||||
multipleDurationEnabled: boolean;
|
||||
users: EventTypeSetup["users"];
|
||||
assignAllTeamMembers: boolean;
|
||||
useEventTypeDestinationCalendarEmail: boolean;
|
||||
secondaryEmailId?: number;
|
||||
};
|
||||
|
||||
export type CustomInputParsed = typeof customInputSchema._output;
|
||||
|
||||
const querySchema = z.object({
|
||||
@@ -438,7 +363,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
),
|
||||
availability: <EventAvailabilityTab eventType={eventType} isTeamEvent={!!team} />,
|
||||
team: <EventTeamTab teamMembers={teamMembers} team={team} eventType={eventType} />,
|
||||
limits: <EventLimitsTab />,
|
||||
limits: <EventLimitsTab eventType={eventType} />,
|
||||
advanced: <EventAdvancedTab eventType={eventType} team={team} />,
|
||||
instant: <EventInstantTab eventType={eventType} isTeamEvent={!!team} />,
|
||||
recurring: <EventRecurringTab eventType={eventType} />,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.8.7",
|
||||
"version": "3.8.8",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { BulkEditDefaultForEventsModal } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
|
||||
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
|
||||
@@ -21,6 +22,7 @@ import SkeletonLoader from "@components/availability/SkeletonLoader";
|
||||
|
||||
export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) {
|
||||
const { t } = useLocale();
|
||||
const [bulkUpdateModal, setBulkUpdateModal] = useState(false);
|
||||
const utils = trpc.useContext();
|
||||
|
||||
const meQuery = trpc.viewer.me.useQuery();
|
||||
@@ -65,6 +67,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
}),
|
||||
"success"
|
||||
);
|
||||
setBulkUpdateModal(true);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
@@ -74,6 +77,15 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
},
|
||||
});
|
||||
|
||||
const bulkUpdateDefaultAvailabilityMutation =
|
||||
trpc.viewer.availability.schedule.bulkUpdateToDefaultAvailability.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.viewer.availability.list.invalidate();
|
||||
setBulkUpdateModal(false);
|
||||
showToast(t("success"), "success");
|
||||
},
|
||||
});
|
||||
|
||||
const duplicateMutation = trpc.viewer.availability.schedule.duplicate.useMutation({
|
||||
onSuccess: async ({ schedule }) => {
|
||||
await router.push(`/availability/${schedule.id}`);
|
||||
@@ -129,6 +141,14 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
{t("add_a_redirect")}
|
||||
</Link>
|
||||
</div>
|
||||
{bulkUpdateModal && (
|
||||
<BulkEditDefaultForEventsModal
|
||||
isPending={bulkUpdateDefaultAvailabilityMutation.isPending}
|
||||
open={bulkUpdateModal}
|
||||
setOpen={setBulkUpdateModal}
|
||||
bulkUpdateFunction={bulkUpdateDefaultAvailabilityMutation.mutate}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -7,9 +7,9 @@ export type {
|
||||
CustomInputParsed,
|
||||
EventTypeSetup,
|
||||
EventTypeSetupProps,
|
||||
FormValues,
|
||||
Host,
|
||||
} from "~/event-types/views/event-types-single-view";
|
||||
export type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
|
||||
EventTypePageWrapper.PageWrapper = PageWrapper;
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import PrivacyView from "@calcom/features/ee/organizations/pages/settings/privacy";
|
||||
|
||||
import type { CalPageWrapper } from "@components/PageWrapper";
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
const Page = PrivacyView as CalPageWrapper;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
@@ -20,6 +20,7 @@ import { useFlagMap } from "@calcom/features/flags/context/provider";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { APP_NAME, URL_PROTOCOL_REGEX, IS_CALCOM, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
||||
import { pushGTMEvent } from "@calcom/lib/gtm";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -164,7 +165,6 @@ export default function Signup({
|
||||
const [premiumUsername, setPremiumUsername] = useState(false);
|
||||
const [usernameTaken, setUsernameTaken] = useState(false);
|
||||
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
|
||||
|
||||
const searchParams = useCompatSearchParams();
|
||||
const telemetry = useTelemetry();
|
||||
const { t, i18n } = useLocale();
|
||||
@@ -223,6 +223,9 @@ export default function Signup({
|
||||
})
|
||||
.then(handleErrorsAndStripe)
|
||||
.then(async () => {
|
||||
if (process.env.NEXT_PUBLIC_GTM_ID)
|
||||
pushGTMEvent("create_account", { email: data.email, user: data.username, lang: data.language });
|
||||
|
||||
telemetry.event(telemetryEventTypes.signup, collectPageParameters());
|
||||
const verifyOrGettingStarted = flags["email-verification"] ? "auth/verify-email" : "getting-started";
|
||||
const callBackUrl = `${
|
||||
|
||||
@@ -50,6 +50,8 @@ function TeamPage({
|
||||
const isBioEmpty = !team.bio || !team.bio.replace("<p><br></p>", "").length;
|
||||
const metadata = teamMetadataSchema.parse(team.metadata);
|
||||
|
||||
const teamOrOrgIsPrivate = team.isPrivate || (team?.parent?.isOrganization && team.parent?.isPrivate);
|
||||
|
||||
useEffect(() => {
|
||||
telemetry.event(
|
||||
telemetryEventTypes.pageView,
|
||||
@@ -194,11 +196,17 @@ function TeamPage({
|
||||
)}
|
||||
</div>
|
||||
{team.isOrganization ? (
|
||||
<SubTeams />
|
||||
!teamOrOrgIsPrivate ? (
|
||||
<SubTeams />
|
||||
) : (
|
||||
<div className="w-full text-center">
|
||||
<h2 className="text-emphasis font-semibold">{t("you_cannot_see_teams_of_org")}</h2>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{(showMembers.isOn || !team.eventTypes?.length) &&
|
||||
(team.isPrivate ? (
|
||||
(teamOrOrgIsPrivate ? (
|
||||
<div className="w-full text-center">
|
||||
<h2 data-testid="you-cannot-see-team-members" className="text-emphasis font-semibold">
|
||||
{t("you_cannot_see_team_members")}
|
||||
@@ -212,7 +220,7 @@ function TeamPage({
|
||||
<EventTypes eventTypes={team.eventTypes} />
|
||||
|
||||
{/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */}
|
||||
{!team.hideBookATeamMember && !team.isPrivate && (
|
||||
{!team.hideBookATeamMember && !teamOrOrgIsPrivate && (
|
||||
<div>
|
||||
<div className="relative mt-12">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { randomString } from "@calcom/lib/random";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/client";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
import { createTeamEventType } from "./fixtures/users";
|
||||
import type { Fixtures } from "./lib/fixtures";
|
||||
import { test } from "./lib/fixtures";
|
||||
import { assertBookingVisibleFor, bookTeamEvent, bookUserEvent } from "./lib/testUtils";
|
||||
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
|
||||
@@ -61,6 +67,137 @@ test.describe("Bookings", () => {
|
||||
secondUpcomingBooking.locator(`text=${bookingWhereFirstUserIsOrganizer!.title}`)
|
||||
).toBeVisible();
|
||||
});
|
||||
test("collective eventType booking should be visible to team admin", async ({ page, users }) => {
|
||||
const { owner1, owner2, commonUser, team1_teammate1 } = await createTeams(users);
|
||||
|
||||
const { team: team1 } = await owner1.getFirstTeamMembership();
|
||||
const scenario = {
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
teamEventTitle: `collective-team-event`,
|
||||
teamEventSlug: slugify(`collective-team-event-${randomString(5)}`),
|
||||
};
|
||||
|
||||
const eventType = await createTeamEventType(owner1, team1, scenario);
|
||||
const { id: eventId, slug: evnetSlug } = eventType;
|
||||
|
||||
await prisma.host.createMany({
|
||||
data: [
|
||||
{
|
||||
userId: commonUser.id,
|
||||
eventTypeId: eventId,
|
||||
isFixed: true,
|
||||
},
|
||||
{
|
||||
userId: team1_teammate1.id,
|
||||
eventTypeId: eventId,
|
||||
isFixed: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// to test if booking is visible to team admin/owner even if he is not part of the booking
|
||||
await prisma.host.deleteMany({
|
||||
where: {
|
||||
userId: owner1.id,
|
||||
eventTypeId: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
await bookTeamEvent(page, team1, eventType);
|
||||
|
||||
// booking should be visible for the team host even though he is not part of the booking
|
||||
await assertBookingVisibleFor(owner1, page, eventType, true);
|
||||
// booking should not be visible for other team host
|
||||
await assertBookingVisibleFor(owner2, page, eventType, false);
|
||||
// booking should be visible for commonUser as he is part of the booking
|
||||
await assertBookingVisibleFor(commonUser, page, eventType, true);
|
||||
// booking should be visible for team1_teammate1 as he is part of the booking
|
||||
await assertBookingVisibleFor(team1_teammate1, page, eventType, true);
|
||||
});
|
||||
test("round-robin eventType booking should be visible to team admin", async ({ page, users }) => {
|
||||
const { owner1, owner2, commonUser, team1_teammate1 } = await createTeams(users);
|
||||
|
||||
const { team: team1 } = await owner1.getFirstTeamMembership();
|
||||
const scenario = {
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
teamEventTitle: `round-robin-team-event`,
|
||||
teamEventSlug: slugify(`round-robin-team-event-${randomString(5)}`),
|
||||
};
|
||||
|
||||
const eventType = await createTeamEventType(owner1, team1, scenario);
|
||||
const { id: eventId } = eventType;
|
||||
|
||||
await prisma.host.createMany({
|
||||
data: [
|
||||
{
|
||||
userId: commonUser.id,
|
||||
eventTypeId: eventId,
|
||||
},
|
||||
{
|
||||
userId: team1_teammate1.id,
|
||||
eventTypeId: eventId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await prisma.host.deleteMany({
|
||||
where: {
|
||||
userId: owner1.id,
|
||||
eventTypeId: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
await bookTeamEvent(page, team1, eventType);
|
||||
|
||||
// booking should be visible for the team host even though he is not part of the booking
|
||||
await assertBookingVisibleFor(owner1, page, eventType, true);
|
||||
// bookings should not be visible for other team host
|
||||
await assertBookingVisibleFor(owner2, page, eventType, false);
|
||||
});
|
||||
test("managed eventType booking should be visible to team admin", async ({ page, users }) => {
|
||||
const { owner1, owner2, commonUser, team1_teammate1 } = await createTeams(users);
|
||||
|
||||
const { team: team1 } = await owner1.getFirstTeamMembership();
|
||||
|
||||
await owner1.apiLogin();
|
||||
await page.goto(`/event-types?dialog=new&eventPage=team%2F${team1.slug}&teamId=${team1.id}`);
|
||||
await page.getByTestId("managed-event-type").click();
|
||||
await page.getByTestId("event-type-quick-chat").click();
|
||||
await page.getByTestId("event-type-quick-chat").fill("managed-event");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByTestId("vertical-tab-assignment").click();
|
||||
|
||||
await page.getByRole("combobox", { name: "assignment-dropdown" }).fill("commonUser");
|
||||
await page.getByRole("combobox", { name: "assignment-dropdown" }).press("Enter");
|
||||
await page.getByRole("combobox", { name: "assignment-dropdown" }).fill("team1_teammate1");
|
||||
await page.getByRole("combobox", { name: "assignment-dropdown" }).press("Enter");
|
||||
await page.getByTestId("update-eventtype").click();
|
||||
|
||||
const eventType = await owner1.getFirstTeamEvent(team1.id);
|
||||
|
||||
await bookUserEvent(page, commonUser, eventType);
|
||||
|
||||
// booking should be visible for the team host even though he is not part of the booking
|
||||
await assertBookingVisibleFor(owner1, page, eventType, true);
|
||||
// booking should not be visible for other team host
|
||||
await assertBookingVisibleFor(owner2, page, eventType, false);
|
||||
// booking should be visible for commonUser as he is part of the booking
|
||||
await assertBookingVisibleFor(commonUser, page, eventType, true);
|
||||
// booking should not be visible for team1_teammate1 as we booked for commonUser
|
||||
await assertBookingVisibleFor(team1_teammate1, page, eventType, false);
|
||||
});
|
||||
test("individual team members booking should not be visible to team admin", async ({ page, users }) => {
|
||||
const { owner1, owner2, commonUser, team1_teammate1 } = await createTeams(users);
|
||||
|
||||
const eventType = await commonUser.getFirstEventAsOwner();
|
||||
await bookUserEvent(page, commonUser, eventType);
|
||||
// non-team bookings should not be visible for the team host
|
||||
await assertBookingVisibleFor(owner1, page, eventType, false);
|
||||
// non-team bookings should not be visible for other team host
|
||||
await assertBookingVisibleFor(owner2, page, eventType, false);
|
||||
// booking should be visible for commonUser as he is part of the booking
|
||||
await assertBookingVisibleFor(commonUser, page, eventType, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,3 +248,75 @@ async function createBooking({
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const createTeams = async (userFixture: Fixtures["users"]) => {
|
||||
const owner1 = await userFixture.create({ name: "team-owner-1" });
|
||||
const owner2 = await userFixture.create({ name: "team-owner-2" });
|
||||
const commonUser = await userFixture.create({ name: "commonUser" });
|
||||
const team1_teammate1 = await userFixture.create({ name: "team1_teammate1" });
|
||||
const team2_teammate1 = await userFixture.create({ name: "team2_teammate1" });
|
||||
const teamOne = await prisma.team.create({
|
||||
data: {
|
||||
name: "bookings-test-team-1",
|
||||
slug: slugify(`bookings-test-team-2-${randomString(5)}`),
|
||||
},
|
||||
});
|
||||
|
||||
const teamTwo = await prisma.team.create({
|
||||
data: {
|
||||
name: "bookings-test-team-2",
|
||||
slug: slugify(`bookings-test-team-2-${randomString(5)}`),
|
||||
},
|
||||
});
|
||||
|
||||
// create memberships
|
||||
await prisma.membership.createMany({
|
||||
data: [
|
||||
{
|
||||
userId: owner1.id,
|
||||
teamId: teamOne.id,
|
||||
accepted: true,
|
||||
role: "OWNER",
|
||||
},
|
||||
{
|
||||
userId: commonUser.id,
|
||||
teamId: teamOne.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
},
|
||||
{
|
||||
userId: team1_teammate1.id,
|
||||
teamId: teamOne.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
},
|
||||
{
|
||||
userId: owner2.id,
|
||||
teamId: teamTwo.id,
|
||||
accepted: true,
|
||||
role: "OWNER",
|
||||
},
|
||||
{
|
||||
userId: commonUser.id,
|
||||
teamId: teamTwo.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
},
|
||||
{
|
||||
userId: team2_teammate1.id,
|
||||
teamId: teamTwo.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
},
|
||||
],
|
||||
});
|
||||
return {
|
||||
owner1,
|
||||
owner2,
|
||||
commonUser,
|
||||
team1_teammate1,
|
||||
team2_teammate1,
|
||||
teamOne,
|
||||
teamTwo,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -58,7 +58,7 @@ const createTeamWorkflow = async (user: { id: number }, team: { id: number }) =>
|
||||
});
|
||||
};
|
||||
|
||||
const createTeamEventType = async (
|
||||
export const createTeamEventType = async (
|
||||
user: { id: number },
|
||||
team: { id: number },
|
||||
scenario?: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Frame, Page } from "@playwright/test";
|
||||
import type { APIResponse } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
import { createHash } from "crypto";
|
||||
import EventEmitter from "events";
|
||||
@@ -369,3 +370,53 @@ export async function doOnOrgDomain(
|
||||
// When App directory is there, this is the 404 page text. We should work on fixing the 404 page as it changed due to app directory.
|
||||
export const NotFoundPageTextAppDir = "This page does not exist.";
|
||||
// export const NotFoundPageText = "ERROR 404";
|
||||
|
||||
export type Team = {
|
||||
id: number;
|
||||
slug: string | null;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type EventType = {
|
||||
id: number;
|
||||
title: string;
|
||||
slug: string;
|
||||
};
|
||||
export type User = {
|
||||
id: number;
|
||||
name: string | null;
|
||||
username: string | null;
|
||||
};
|
||||
|
||||
export async function bookTeamEvent(page: Page, team: Team, eventType: EventType) {
|
||||
await page.goto(`/team/${team.slug}/${eventType.slug}/`);
|
||||
await bookEventOnThisPage(page);
|
||||
const bookingTitle = `${eventType.title} between ${team.name} and ${testName}`;
|
||||
await assertBookingIsCorrect(page, bookingTitle);
|
||||
}
|
||||
export async function bookUserEvent(page: Page, user: User, eventType: EventType) {
|
||||
await page.goto(`/${user.username}/${eventType.slug}/`);
|
||||
await bookEventOnThisPage(page);
|
||||
const bookingTitle = `${eventType.title} between ${user.name} and ${testName}`;
|
||||
await assertBookingIsCorrect(page, bookingTitle);
|
||||
}
|
||||
|
||||
export async function assertBookingIsCorrect(page: Page, bookingTitle: string) {
|
||||
await expect(page.locator("[data-testid=booking-title]")).toHaveText(bookingTitle);
|
||||
// The booker should be in the attendee list
|
||||
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
|
||||
}
|
||||
|
||||
export const assertBookingVisibleFor = async (
|
||||
user: { apiLogin: () => Promise<APIResponse> },
|
||||
pageFixture: Fixtures["page"],
|
||||
eventType: EventType,
|
||||
shouldBeVisible: boolean
|
||||
) => {
|
||||
await user.apiLogin();
|
||||
await pageFixture.goto(`/bookings/upcoming`);
|
||||
const upcomingBookingList = pageFixture.locator('[data-testid="booking-item"]');
|
||||
shouldBeVisible
|
||||
? await expect(upcomingBookingList.locator(`text=${eventType.title}`)).toBeVisible()
|
||||
: await expect(upcomingBookingList.locator(`text=${eventType.title}`)).toBeHidden();
|
||||
};
|
||||
|
||||
@@ -11,9 +11,9 @@ import {
|
||||
selectFirstAvailableTimeSlotNextMonth,
|
||||
} from "./lib/testUtils";
|
||||
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
test.afterAll(({ users }) => users.deleteAll());
|
||||
|
||||
test.describe("Managed Event Types tests", () => {
|
||||
test.describe("Managed Event Types", () => {
|
||||
test("Can create managed event type", async ({ page, users }) => {
|
||||
// Creating the owner user of the team
|
||||
const adminUser = await users.create();
|
||||
@@ -61,7 +61,7 @@ test.describe("Managed Event Types tests", () => {
|
||||
await expect(page.locator('input[name="title"]')).toBeEditable();
|
||||
await expect(page.locator('input[name="slug"]')).toBeEditable();
|
||||
await expect(page.locator('input[name="length"]')).toBeEditable();
|
||||
await adminUser.logout();
|
||||
await adminUser.apiLogin();
|
||||
});
|
||||
|
||||
await test.step("Managed event type exists for added member", async () => {
|
||||
@@ -73,7 +73,7 @@ test.describe("Managed Event Types tests", () => {
|
||||
await page.locator('button[data-testid^="accept-invitation"]').click();
|
||||
await page.getByText("Member").waitFor();
|
||||
|
||||
await memberUser.logout();
|
||||
await page.goto("/auth/logout");
|
||||
|
||||
// Coming back as team owner to assign member user to managed event
|
||||
await adminUser.apiLogin();
|
||||
@@ -107,7 +107,6 @@ test.describe("Managed Event Types tests", () => {
|
||||
|
||||
await test.step("Managed event type has locked fields for added member", async () => {
|
||||
await adminUser.logout();
|
||||
|
||||
// Coming back as member user to see if there is a managed event present after assignment
|
||||
await memberUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
@@ -118,6 +117,90 @@ test.describe("Managed Event Types tests", () => {
|
||||
await expect(page.locator('input[name="title"]')).not.toBeEditable();
|
||||
await expect(page.locator('input[name="slug"]')).not.toBeEditable();
|
||||
await expect(page.locator('input[name="length"]')).not.toBeEditable();
|
||||
|
||||
await page.goto("/auth/logout");
|
||||
});
|
||||
|
||||
await test.step("Managed event type provides discrete field lock/unlock state for admin", async () => {
|
||||
await adminUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
|
||||
await page.getByTestId("event-types").locator('a[title="managed"]').click();
|
||||
await page.waitForURL("event-types/**");
|
||||
|
||||
// Locked by default
|
||||
const titleLockIndicator = page.getByTestId("locked-indicator-title");
|
||||
await expect(titleLockIndicator).toBeVisible();
|
||||
await expect(titleLockIndicator.locator("[data-state='checked']")).toHaveCount(1);
|
||||
|
||||
// Proceed to unlock and check that it got unlocked
|
||||
titleLockIndicator.click();
|
||||
await expect(titleLockIndicator.locator("[data-state='checked']")).toHaveCount(0);
|
||||
await expect(titleLockIndicator.locator("[data-state='unchecked']")).toHaveCount(1);
|
||||
|
||||
// Save changes
|
||||
await page.locator('[type="submit"]').click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await page.goto("/auth/logout");
|
||||
});
|
||||
|
||||
await test.step("Managed event type shows discretionally unlocked field to member", async () => {
|
||||
await memberUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
|
||||
await page.getByTestId("event-types").locator('a[title="managed"]').click();
|
||||
await page.waitForURL("event-types/**");
|
||||
|
||||
await expect(page.locator('input[name="title"]')).toBeEditable();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await page.goto("/auth/logout");
|
||||
});
|
||||
|
||||
await test.step("Managed event type should only update the unlocked fields modified by Admin", async () => {
|
||||
await memberUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
|
||||
await page.getByTestId("event-types").locator('a[title="managed"]').click();
|
||||
await page.waitForURL("event-types/**");
|
||||
|
||||
await expect(page.locator('input[name="title"]')).toBeEditable();
|
||||
await page.locator('input[name="title"]').fill(`Managed Event Title`);
|
||||
// Save changes
|
||||
await page.locator('[type="submit"]').click();
|
||||
await page.getByTestId("toast-success").waitFor();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await page.goto("/auth/logout");
|
||||
|
||||
await adminUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
await page.getByTestId("event-types").locator('a[title="managed"]').click();
|
||||
await page.waitForURL("event-types/**");
|
||||
|
||||
await page.locator('input[name="length"]').fill(`45`);
|
||||
// Save changes
|
||||
await page.locator('[type="submit"]').click();
|
||||
await page.getByTestId("toast-success").waitFor();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await page.goto("/auth/logout");
|
||||
|
||||
await memberUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
|
||||
await page.getByTestId("event-types").locator('a[title="Managed Event Title"]').click();
|
||||
await page.waitForURL("event-types/**");
|
||||
|
||||
//match length
|
||||
expect(await page.locator("[data-testid=duration]").getAttribute("value")).toBe("45");
|
||||
//ensure description didn't update
|
||||
expect(await page.locator(`input[name="title"]`).getAttribute("value")).toBe(`Managed Event Title`);
|
||||
await page.locator('input[name="title"]').fill(`managed`);
|
||||
// Save changes
|
||||
await page.locator('[type="submit"]').click();
|
||||
await page.getByTestId("toast-success").waitFor();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,8 @@ test.beforeEach(async ({ page, users, bookingPage }) => {
|
||||
|
||||
await page.goto("/event-types");
|
||||
await bookingPage.goToEventType(teamEventTitle);
|
||||
await page.getByTestId("location-select").click();
|
||||
await page.locator(`text="Cal Video (Global)"`).click();
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
});
|
||||
|
||||
@@ -19,7 +21,6 @@ test.describe("Check advanced options in a managed team event type", () => {
|
||||
await bookingPage.checkRequiresBookerEmailVerification();
|
||||
await bookingPage.checkHideNotes();
|
||||
await bookingPage.checkRedirectOnBooking();
|
||||
await bookingPage.checkEnablePrivateUrl();
|
||||
await bookingPage.checkLockTimezone();
|
||||
await bookingPage.updateEventType();
|
||||
await bookingPage.goToEventTypesPage();
|
||||
@@ -32,7 +33,6 @@ test.describe("Check advanced options in a managed team event type", () => {
|
||||
await bookingPage.checkRequiresBookerEmailVerification();
|
||||
await bookingPage.checkHideNotes();
|
||||
await bookingPage.checkRedirectOnBooking();
|
||||
await bookingPage.checkEnablePrivateUrl();
|
||||
await bookingPage.toggleOfferSeats();
|
||||
await bookingPage.checkLockTimezone();
|
||||
await bookingPage.updateEventType();
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { randomString } from "@calcom/lib/random";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
import { createTeamEventType } from "../fixtures/users";
|
||||
import { test } from "../lib/fixtures";
|
||||
import type { Fixtures } from "../lib/fixtures";
|
||||
import {
|
||||
assertBookingVisibleFor,
|
||||
bookTimeSlot,
|
||||
doOnOrgDomain,
|
||||
NotFoundPageTextAppDir,
|
||||
selectFirstAvailableTimeSlotNextMonth,
|
||||
testName,
|
||||
} from "../lib/testUtils";
|
||||
|
||||
test.describe("Bookings list for organizations", () => {
|
||||
test.afterEach(({ orgs, users }) => {
|
||||
orgs.deleteAll();
|
||||
users.deleteAll();
|
||||
});
|
||||
|
||||
test.describe("Team Event", () => {
|
||||
test("Collective EventType booking list", async ({ page, users, orgs }) => {
|
||||
const { org, orgOwner, orgAdmin, team1owner, team2owner, commonUser, team1_teammate1 } =
|
||||
await createOrg(orgs, users);
|
||||
|
||||
const { team: team1 } = await team1owner.getFirstTeamMembership();
|
||||
const scenario = {
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
teamEventTitle: `collective-team-event`,
|
||||
teamEventSlug: slugify(`collective-team-event-${randomString(5)}`),
|
||||
};
|
||||
const eventType = await createTeamEventType(team1owner, team1, scenario);
|
||||
const { id: eventId, slug: evnetSlug } = eventType;
|
||||
|
||||
await prisma.host.createMany({
|
||||
data: [
|
||||
{
|
||||
userId: commonUser.id,
|
||||
eventTypeId: eventId,
|
||||
isFixed: true,
|
||||
},
|
||||
{
|
||||
userId: team1_teammate1.id,
|
||||
eventTypeId: eventId,
|
||||
isFixed: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// to test if booking is visible to team admin/owner even if he is not part of the booking
|
||||
await prisma.host.deleteMany({
|
||||
where: {
|
||||
userId: team1owner.id,
|
||||
eventTypeId: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
await expectPageToBeNotFound({ page, url: `/team/${team1.slug}/${eventType.slug}` });
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
page,
|
||||
},
|
||||
async () => {
|
||||
await bookTeamEvent({ page, team: team1, event: eventType });
|
||||
}
|
||||
);
|
||||
// booking should be visible for the Org OWNER even though he is not part of the booking
|
||||
await assertBookingVisibleFor(orgOwner, page, eventType, true);
|
||||
// booking should be visible for the Org ADMIN even though he is not part of the booking
|
||||
await assertBookingVisibleFor(orgAdmin, page, eventType, true);
|
||||
// booking should be visible for the team host even though he is not part of the booking
|
||||
await assertBookingVisibleFor(team1owner, page, eventType, true);
|
||||
// booking should not be visible for other team host, because its a team1 team-event
|
||||
await assertBookingVisibleFor(team2owner, page, eventType, false);
|
||||
// booking should be visible for commonUser as he is part of the booking
|
||||
await assertBookingVisibleFor(commonUser, page, eventType, true);
|
||||
// booking should be visible for team1_teammate1 as he is part of the booking
|
||||
await assertBookingVisibleFor(team1_teammate1, page, eventType, true);
|
||||
});
|
||||
|
||||
test("Round Robin EventType booking list", async ({ page, users, orgs }) => {
|
||||
const { org, orgOwner, orgAdmin, team1owner, team2owner, commonUser, team1_teammate1 } =
|
||||
await createOrg(orgs, users);
|
||||
|
||||
const { team: team1 } = await team1owner.getFirstTeamMembership();
|
||||
const scenario = {
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
teamEventTitle: `round-robin-team-event`,
|
||||
teamEventSlug: slugify(`round-robin-team-event-${randomString(5)}`),
|
||||
};
|
||||
|
||||
const eventType = await createTeamEventType(team1owner, team1, scenario);
|
||||
const { id: eventId } = eventType;
|
||||
await prisma.host.createMany({
|
||||
data: [
|
||||
{
|
||||
userId: commonUser.id,
|
||||
eventTypeId: eventId,
|
||||
},
|
||||
{
|
||||
userId: team1_teammate1.id,
|
||||
eventTypeId: eventId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await prisma.host.deleteMany({
|
||||
where: {
|
||||
userId: team1owner.id,
|
||||
eventTypeId: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
await expectPageToBeNotFound({ page, url: `/team/${team1.slug}/${eventType.slug}` });
|
||||
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
page,
|
||||
},
|
||||
async () => {
|
||||
await bookTeamEvent({ page, team: team1, event: eventType });
|
||||
}
|
||||
);
|
||||
// booking should be visible for the Org OWNER even though he is not part of the booking
|
||||
await assertBookingVisibleFor(orgOwner, page, eventType, true);
|
||||
// booking should be visible for the Org ADMIN even though he is not part of the booking
|
||||
await assertBookingVisibleFor(orgAdmin, page, eventType, true);
|
||||
// booking should be visible for the team host even though he is not part of the booking
|
||||
await assertBookingVisibleFor(team1owner, page, eventType, true);
|
||||
// booking should not be visible for other team host
|
||||
await assertBookingVisibleFor(team2owner, page, eventType, false);
|
||||
});
|
||||
|
||||
test("Managed EventType booking list", async ({ page, users, orgs }) => {
|
||||
const { org, orgOwner, orgAdmin, team1owner, team2owner, commonUser, team1_teammate1 } =
|
||||
await createOrg(orgs, users);
|
||||
|
||||
const { team: team1 } = await team1owner.getFirstTeamMembership();
|
||||
|
||||
await team1owner.apiLogin();
|
||||
await page.goto(`/event-types?dialog=new&eventPage=team%2F${team1.slug}&teamId=${team1.id}`);
|
||||
await page.getByTestId("managed-event-type").click();
|
||||
await page.getByTestId("event-type-quick-chat").click();
|
||||
await page.getByTestId("event-type-quick-chat").fill("managed-event");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByTestId("vertical-tab-assignment").click();
|
||||
|
||||
await page.getByRole("combobox", { name: "assignment-dropdown" }).fill("commonUser");
|
||||
await page.getByRole("combobox", { name: "assignment-dropdown" }).press("Enter");
|
||||
await page.getByRole("combobox", { name: "assignment-dropdown" }).fill("team1_teammate1");
|
||||
await page.getByRole("combobox", { name: "assignment-dropdown" }).press("Enter");
|
||||
await page.getByTestId("update-eventtype").click();
|
||||
|
||||
const eventType = await team1owner.getFirstTeamEvent(team1.id);
|
||||
|
||||
await expectPageToBeNotFound({ page, url: `/${commonUser.username}/${eventType.slug}` });
|
||||
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
page,
|
||||
},
|
||||
async () => {
|
||||
await bookUserEvent({ page, user: commonUser, event: eventType });
|
||||
}
|
||||
);
|
||||
|
||||
// booking should be visible for the Org OWNER even though he is not part of the booking
|
||||
await assertBookingVisibleFor(orgOwner, page, eventType, true);
|
||||
// booking should be visible for the Org ADMIN even though he is not part of the booking
|
||||
await assertBookingVisibleFor(orgAdmin, page, eventType, true);
|
||||
// booking should be visible for the team host even though he is not part of the booking
|
||||
await assertBookingVisibleFor(team1owner, page, eventType, true);
|
||||
// booking should not be visible for other team host
|
||||
await assertBookingVisibleFor(team2owner, page, eventType, false);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("User Event", () => {
|
||||
test("User personal booking list", async ({ page, users, orgs }) => {
|
||||
const { org, orgOwner, orgAdmin, team1owner, team2owner, commonUser, team1_teammate1 } =
|
||||
await createOrg(orgs, users);
|
||||
|
||||
const event = await commonUser.getFirstEventAsOwner();
|
||||
|
||||
await page.goto(`/${commonUser.username}/${event.slug}`);
|
||||
// Shouldn't be servable on the non-org domain
|
||||
await expect(page.locator(`text=${NotFoundPageTextAppDir}`)).toBeVisible();
|
||||
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
page,
|
||||
},
|
||||
async () => {
|
||||
await bookUserEvent({ page, user: commonUser, event });
|
||||
}
|
||||
);
|
||||
// booking should be visible for the Org OWNER
|
||||
await assertBookingVisibleFor(orgOwner, page, event, true);
|
||||
// booking should be visible for the Org ADMIN
|
||||
await assertBookingVisibleFor(orgAdmin, page, event, true);
|
||||
// booking should be visible for the team host
|
||||
await assertBookingVisibleFor(team1owner, page, event, true);
|
||||
// booking should be visible for other team host because commonUser is part of both teams
|
||||
await assertBookingVisibleFor(team2owner, page, event, true);
|
||||
// booking should be visible for commonUser as he is part of the booking
|
||||
await assertBookingVisibleFor(commonUser, page, event, true);
|
||||
// booking should not be visible for team1_teammate1
|
||||
await assertBookingVisibleFor(team1_teammate1, page, event, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function bookUserEvent({
|
||||
page,
|
||||
user,
|
||||
event,
|
||||
}: {
|
||||
page: Page;
|
||||
user: {
|
||||
username: string | null;
|
||||
name: string | null;
|
||||
};
|
||||
event: { slug: string; title: string };
|
||||
}) {
|
||||
await page.goto(`/${user.username}/${event.slug}`);
|
||||
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await bookTimeSlot(page);
|
||||
await expect(page.getByTestId("success-page")).toBeVisible();
|
||||
|
||||
// The title of the booking
|
||||
const BookingTitle = `${event.title} between ${user.name} and ${testName}`;
|
||||
await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle);
|
||||
// The booker should be in the attendee list
|
||||
await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName);
|
||||
}
|
||||
|
||||
async function bookTeamEvent({
|
||||
page,
|
||||
team,
|
||||
event,
|
||||
}: {
|
||||
page: Page;
|
||||
team: {
|
||||
slug: string | null;
|
||||
name: string | null;
|
||||
};
|
||||
event: { slug: string; title: string };
|
||||
}) {
|
||||
// Note that even though the default way to access a team booking in an organization is to not use /team in the URL, but it isn't testable with playwright as the rewrite is taken care of by Next.js config which can't handle on the fly org slug's handling
|
||||
// So, we are using /team in the URL to access the team booking
|
||||
// There are separate tests to verify that the next.config.js rewrites are working
|
||||
// Also there are additional checkly tests that verify absolute e2e flow. They are in __checks__/organization.spec.ts
|
||||
await page.goto(`/team/${team.slug}/${event.slug}`);
|
||||
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await bookTimeSlot(page);
|
||||
await expect(page.getByTestId("success-page")).toBeVisible();
|
||||
|
||||
// The title of the booking
|
||||
const BookingTitle = `${event.title} between ${team.name} and ${testName}`;
|
||||
await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle);
|
||||
// The booker should be in the attendee list
|
||||
await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName);
|
||||
}
|
||||
|
||||
async function expectPageToBeNotFound({ page, url }: { page: Page; url: string }) {
|
||||
await page.goto(`${url}`);
|
||||
await expect(page.locator(`text=${NotFoundPageTextAppDir}`)).toBeVisible();
|
||||
}
|
||||
|
||||
const createOrg = async (orgs: Fixtures["orgs"], userFixture: Fixtures["users"]) => {
|
||||
const org = await orgs.create({
|
||||
name: "TestOrg",
|
||||
});
|
||||
const orgOwner = await userFixture.create({
|
||||
name: "org-owner",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: MembershipRole.OWNER,
|
||||
});
|
||||
|
||||
const orgAdmin = await userFixture.create({
|
||||
name: "org-admin",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: MembershipRole.ADMIN,
|
||||
});
|
||||
const team1owner = await userFixture.create({
|
||||
name: "team-owner-1",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: MembershipRole.MEMBER,
|
||||
});
|
||||
const team2owner = await userFixture.create({
|
||||
name: "team-owner-2",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: MembershipRole.MEMBER,
|
||||
});
|
||||
const commonUser = await userFixture.create({
|
||||
name: "commonUser",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: MembershipRole.MEMBER,
|
||||
});
|
||||
const team1_teammate1 = await userFixture.create({
|
||||
name: "team1_teammate1",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: MembershipRole.MEMBER,
|
||||
});
|
||||
const team2_teammate1 = await userFixture.create({
|
||||
name: "team2_teammate1",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: MembershipRole.MEMBER,
|
||||
});
|
||||
const teamOne = await prisma.team.create({
|
||||
data: {
|
||||
name: "bookings-test-team-1",
|
||||
slug: slugify(`bookings-test-team-2-${randomString(5)}`),
|
||||
parentId: org.id,
|
||||
},
|
||||
});
|
||||
|
||||
const teamTwo = await prisma.team.create({
|
||||
data: {
|
||||
name: "bookings-test-team-2",
|
||||
slug: slugify(`bookings-test-team-2-${randomString(5)}`),
|
||||
parentId: org.id,
|
||||
},
|
||||
});
|
||||
|
||||
// create memberships
|
||||
await prisma.membership.createMany({
|
||||
data: [
|
||||
{
|
||||
userId: team1owner.id,
|
||||
teamId: teamOne.id,
|
||||
accepted: true,
|
||||
role: "OWNER",
|
||||
},
|
||||
{
|
||||
userId: commonUser.id,
|
||||
teamId: teamOne.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
},
|
||||
{
|
||||
userId: team1_teammate1.id,
|
||||
teamId: teamOne.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
},
|
||||
{
|
||||
userId: team2owner.id,
|
||||
teamId: teamTwo.id,
|
||||
accepted: true,
|
||||
role: "OWNER",
|
||||
},
|
||||
{
|
||||
userId: commonUser.id,
|
||||
teamId: teamTwo.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
},
|
||||
{
|
||||
userId: team2_teammate1.id,
|
||||
teamId: teamTwo.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
},
|
||||
],
|
||||
});
|
||||
return {
|
||||
org,
|
||||
orgOwner,
|
||||
orgAdmin,
|
||||
team1owner,
|
||||
team2owner,
|
||||
commonUser,
|
||||
team1_teammate1,
|
||||
team2_teammate1,
|
||||
teamOne,
|
||||
teamTwo,
|
||||
};
|
||||
};
|
||||
@@ -33,9 +33,10 @@ test.describe("UploadAvatar", async () => {
|
||||
|
||||
const response = await prisma.avatar.findUniqueOrThrow({
|
||||
where: {
|
||||
teamId_userId: {
|
||||
teamId_userId_isBanner: {
|
||||
userId: user.id,
|
||||
teamId: 0,
|
||||
isBanner: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -307,8 +307,6 @@ testBothFutureAndLegacyRoutes.describe("Teams - NonOrg", (routeVariant) => {
|
||||
await page.getByTestId("email").click();
|
||||
await page.getByTestId("incrementMonth").click();
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await expect(page.getByTestId("no-slots-available")).toBeHidden();
|
||||
|
||||
// Check Team Url
|
||||
|
||||
@@ -1823,10 +1823,8 @@
|
||||
"requires_at_least_one_schedule": "يجب أن يكون لديك جدول واحد على الأقل",
|
||||
"default_conferencing_bulk_description": "تحديث المواقع لأنواع الأحداث المحددة",
|
||||
"locked_for_members": "مقفل للأعضاء",
|
||||
"locked_apps_description": "سيتمكن الأعضاء من رؤية التطبيقات النشطة، ولكن لن يتمكنوا من تعديل أي إعدادات للتطبيق",
|
||||
"locked_webhooks_description": "سيكون الأعضاء قادرين على رؤية webhooks النشطة، ولكن من دون القدرة على تعديل أي إعدادات Webhook",
|
||||
"locked_workflows_description": "سيكون الأعضاء قادرين على رؤية مسارات العمل النشطة، ولكن من دون القدرة على تعديل أي إعدادات لمسار العمل",
|
||||
"locked_by_admin": "مقفل من قبل المشرف",
|
||||
"apps_locked_for_members_description": "سيتمكن الأعضاء من رؤية التطبيقات النشطة، ولكن لن يتمكنوا من تعديل أي إعدادات للتطبيق",
|
||||
"locked_by_team_admin": "مقفل من قبل المشرف",
|
||||
"app_not_connected": "أنت غير متصل بحساب {{appName}}.",
|
||||
"connect_now": "اتصل الآن",
|
||||
"managed_event_dialog_confirm_button_one": "الاستبدال وإخطار {{count}} عضو",
|
||||
@@ -2098,5 +2096,6 @@
|
||||
"extensive_whitelabeling": "عملية انضمام ودعم هندسي مخصصين",
|
||||
"need_help": "هل تحتاج إلى مساعدة؟",
|
||||
"show_more": "إظهار المزيد",
|
||||
"privacy": "الخصوصية",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{}
|
||||
{}
|
||||
@@ -1 +1 @@
|
||||
{}
|
||||
{}
|
||||
@@ -21,4 +21,4 @@
|
||||
"load_more_results": "Carrega més resultats",
|
||||
"integration_meeting_id": "{{integrationName}} Identificador de la reunió: {{meetingId}}",
|
||||
"confirmed_event_type_subject": "Confirmat: {{eventType}} amb {{name}} el {{date}}"
|
||||
}
|
||||
}
|
||||
@@ -1823,10 +1823,8 @@
|
||||
"requires_at_least_one_schedule": "Musíte mít alespoň jeden rozvrh",
|
||||
"default_conferencing_bulk_description": "Aktualizujte místa pro vybrané typy událostí",
|
||||
"locked_for_members": "Zamčeno pro členy",
|
||||
"locked_apps_description": "Členové uvidí aktivní aplikace, ale nastavení aplikací upravovat nemohou",
|
||||
"locked_webhooks_description": "Členové uvidí aktivní webhooky, ale nastavení webhooků upravovat nemohou",
|
||||
"locked_workflows_description": "Členové uvidí aktivní pracovní postupy, ale nastavení pracovních postupů upravovat nemohou",
|
||||
"locked_by_admin": "Uzamčeno správcem týmu",
|
||||
"apps_locked_for_members_description": "Členové uvidí aktivní aplikace, ale nastavení aplikací upravovat nemohou",
|
||||
"locked_by_team_admin": "Uzamčeno správcem týmu",
|
||||
"app_not_connected": "Nemáte připojený účet {{appName}}.",
|
||||
"connect_now": "Připojte se",
|
||||
"managed_event_dialog_confirm_button_one": "Nahradit a upozornit {{count}} člena",
|
||||
@@ -2098,5 +2096,6 @@
|
||||
"extensive_whitelabeling": "Vyhrazená podpora zaškolovací a inženýrská podpora",
|
||||
"need_help": "Potřebujete pomoc?",
|
||||
"show_more": "Zobrazit více",
|
||||
"privacy": "Ochrana soukromí",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1535,4 +1535,4 @@
|
||||
"confirm_your_details": "Bekræft dine oplysninger",
|
||||
"overlay_my_calendar": "Vis min kalender",
|
||||
"need_help": "Brug for hjælp?"
|
||||
}
|
||||
}
|
||||
@@ -808,6 +808,7 @@
|
||||
"label": "Bezeichnung",
|
||||
"placeholder": "Platzhalter",
|
||||
"display_add_to_calendar_organizer": "„Zum Kalender hinzufügen“-E-Mail als Organisator anzeigen",
|
||||
"reconnect_calendar_to_use": "Bitte beachten Sie, dass Sie möglicherweise die Verbindung trennen und anschließend Ihr „Zum Kalender hinzufügen“-Konto erneut verbinden müssen, um diese Funktion nutzen zu können.",
|
||||
"type": "Typ",
|
||||
"edit": "Bearbeiten",
|
||||
"add_input": "Eingabe hinzufügen",
|
||||
@@ -1474,6 +1475,8 @@
|
||||
"copy_link_to_form": "Link in Formular kopieren",
|
||||
"theme": "Theme",
|
||||
"theme_applies_note": "Dies gilt nur für Ihre öffentlichen Buchungsseiten",
|
||||
"app_theme": "Dashboard-Theme",
|
||||
"app_theme_applies_note": "Dies gilt nur für Ihr eingeloggtes Dashboard",
|
||||
"theme_system": "Systemstandard",
|
||||
"add_a_team": "Team hinzufügen",
|
||||
"add_webhook_description": "Erhalten Sie Meetingdaten in Echtzeit, wenn etwas in {{appName}} passiert",
|
||||
@@ -1599,6 +1602,7 @@
|
||||
"attendee_email_info": "Die E-Mail-Adresse der buchenden Person",
|
||||
"kbar_search_placeholder": "Geben Sie einen Befehl ein oder suchen Sie ...",
|
||||
"invalid_credential": "Oh nein! Es sieht so aus, als ob die Berechtigung abgelaufen ist oder entzogen wurden. Bitte erneut installieren.",
|
||||
"invalid_credential_action": "App neu installieren",
|
||||
"reschedule_reason": "Verlegungsgrund",
|
||||
"choose_common_schedule_team_event": "Gemeinsamen Zeitplan auswählen",
|
||||
"choose_common_schedule_team_event_description": "Aktivieren Sie dies, wenn Sie einen gemeinsamen Zeitplan zwischen Hosts verwenden möchten. Wenn deaktiviert, ist jeder Host basierend auf seinen Standardplan buchbar.",
|
||||
@@ -1884,10 +1888,8 @@
|
||||
"requires_at_least_one_schedule": "Sie müssen mindestens einen Verfügbarkeitsplan haben",
|
||||
"default_conferencing_bulk_description": "Orte für die ausgewählten Termintypen aktualisieren",
|
||||
"locked_for_members": "Für Mitglieder gesperrt",
|
||||
"locked_apps_description": "Mitglieder können die aktiven Apps sehen, können aber keine App-Einstellungen anpassen",
|
||||
"locked_webhooks_description": "Mitglieder können die aktiven Webhooks sehen, können aber keine Webhook-Einstellungen anpassen",
|
||||
"locked_workflows_description": "Mitglieder können die aktiven Workflows sehen, können aber keine Workflow-Einstellungen anpassen",
|
||||
"locked_by_admin": "Vom Team-Admin gesperrt",
|
||||
"apps_locked_for_members_description": "Mitglieder können die aktiven Apps sehen, können aber keine App-Einstellungen anpassen",
|
||||
"locked_by_team_admin": "Vom Team-Admin gesperrt",
|
||||
"app_not_connected": "Sie haben kein {{appName}}-Konto verbunden.",
|
||||
"connect_now": "Jetzt verbinden",
|
||||
"managed_event_dialog_confirm_button_one": "{{count}} Mitglied ersetzen & benachrichtigen",
|
||||
@@ -2263,5 +2265,13 @@
|
||||
"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",
|
||||
"privacy": "Datenschutz",
|
||||
"primary": "Primär",
|
||||
"make_primary": "Zur Primäradresse machen",
|
||||
"add_email": "E-Mail-Adresse hinzufügen",
|
||||
"add_email_description": "Fügen Sie eine E-Mail-Adresse hinzu, um die Ihre primäre E-Mail-Adresse ersetzt, oder als alternative E-Mail in Ihren Ereignistypen verwendet wird.",
|
||||
"confirm_email": "Bestätigen Sie Ihre E-Mail-Adresse",
|
||||
"confirm_email_description": "Wir haben eine E-Mail an <strong>{{email}}</strong> gesendet. Klicken Sie auf den Link in der E-Mail, um diese Adresse zu bestätigen.",
|
||||
"send_event_details_to": "Termindetails senden an",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -278,4 +278,4 @@
|
||||
"location_variable": "Τοποθεσία",
|
||||
"additional_notes_variable": "Πρόσθετες σημειώσεις",
|
||||
"already_have_account": "Έχετε ήδη λογαριασμό;"
|
||||
}
|
||||
}
|
||||
@@ -706,8 +706,10 @@
|
||||
"assigned_to": "Assigned to",
|
||||
"you_must_be_logged_in_to":"You must be logged in to {{url}}",
|
||||
"start_assigning_members_above": "Start assigning members above",
|
||||
"locked_fields_admin_description": "Members will not be able to edit this",
|
||||
"locked_fields_member_description": "This option was locked by the team admin",
|
||||
"locked_fields_admin_description": "Members can not edit",
|
||||
"unlocked_fields_admin_description": "Members can edit",
|
||||
"locked_fields_member_description": "Locked by the team admin",
|
||||
"unlocked_fields_member_description": "Unlocked by team admin",
|
||||
"url": "URL",
|
||||
"hidden": "Hidden",
|
||||
"readonly": "Readonly",
|
||||
@@ -1083,6 +1085,7 @@
|
||||
"make_team_private": "Make team private",
|
||||
"make_team_private_description": "Your team members won't be able to see other team members when this is turned on.",
|
||||
"you_cannot_see_team_members": "You cannot see all the team members of a private team.",
|
||||
"you_cannot_see_teams_of_org":"You cannot see teams of a private organization.",
|
||||
"allow_booker_to_select_duration": "Allow booker to select duration",
|
||||
"impersonate_user_tip": "All uses of this feature is audited.",
|
||||
"impersonating_user_warning": "Impersonating username \"{{user}}\".",
|
||||
@@ -1274,7 +1277,7 @@
|
||||
"recordings_are_part_of_the_teams_plan": "Recordings are part of the teams plan",
|
||||
"team_feature_teams": "This is a Team feature. Upgrade to Team to see your team's availability.",
|
||||
"team_feature_workflows": "This is a Team feature. Upgrade to Team to automate your event notifications and reminders with Workflows.",
|
||||
"show_eventtype_on_profile": "Show on profile",
|
||||
"show_eventtype_on_profile": "Show on Profile",
|
||||
"embed": "Embed",
|
||||
"new_username": "New username",
|
||||
"current_username": "Current username",
|
||||
@@ -1618,6 +1621,7 @@
|
||||
"test_routing": "Test Routing",
|
||||
"payment_app_disabled": "An admin has disabled a payment app",
|
||||
"edit_event_type": "Edit event type",
|
||||
"only_admin_can_see_members_of_org": "This Organization is private, and only the organization's admin or owner can view its members.",
|
||||
"collective_scheduling": "Collective Scheduling",
|
||||
"make_it_easy_to_book": "Make it easy to book your team when everyone is available.",
|
||||
"find_the_best_person": "Find the best person available and cycle through your team.",
|
||||
@@ -1890,10 +1894,16 @@
|
||||
"requires_at_least_one_schedule": "You are required to have at least one schedule",
|
||||
"default_conferencing_bulk_description": "Update the locations for the selected event types",
|
||||
"locked_for_members": "Locked for members",
|
||||
"locked_apps_description": "Members will be able to see the active apps but will not be able to edit any app settings",
|
||||
"locked_webhooks_description": "Members will be able to see the active webhooks but will not be able to edit any webhook settings",
|
||||
"locked_workflows_description": "Members will be able to see the active workflows but will not be able to edit any workflow settings",
|
||||
"locked_by_admin": "Locked by team admin",
|
||||
"unlocked_for_members": "Unlocked for members",
|
||||
"apps_locked_for_members_description": "Members will be able to see the active apps but will not be able to edit any app settings",
|
||||
"apps_unlocked_for_members_description": "Members will be able to see the active apps and will be able to edit any app settings",
|
||||
"apps_locked_by_team_admins_description": "You will be able to see the active apps but will not be able to edit any app settings",
|
||||
"apps_unlocked_by_team_admins_description": "You will be able to see the active apps and will be able to edit any app settings",
|
||||
"workflows_locked_for_members_description": "Members can not add their personal workflows to this event type. Members will be able to see the active team workflows but will not be able to edit any workflow settings.",
|
||||
"workflows_unlocked_for_members_description": "Members will be able to add their personal workflows to this event type. Members will be able to see the active team workflows but will not be able to edit any workflow settings.",
|
||||
"workflows_locked_by_team_admins_description": "You will be able to see the active team workflows but will not be able to edit any workflow settings or add your personal workflows to this event type.",
|
||||
"workflows_unlocked_by_team_admins_description": "You will be able to enable/disable personal workflows on this event type. You will be able to see the active team workflows but will not be able to edit any team workflow settings.",
|
||||
"locked_by_team_admin": "Locked by team admin",
|
||||
"app_not_connected": "You have not connected a {{appName}} account.",
|
||||
"connect_now": "Connect now",
|
||||
"managed_event_dialog_confirm_button_one": "Replace & notify {{count}} member",
|
||||
@@ -2192,7 +2202,9 @@
|
||||
"overlay_my_calendar":"Overlay my calendar",
|
||||
"overlay_my_calendar_toc":"By connecting to your calendar, you accept our privacy policy and terms of use. You may revoke access at any time.",
|
||||
"view_overlay_calendar_events":"View your calendar events to prevent clashed booking.",
|
||||
"join_event_location":"Join {{eventLocationType}}",
|
||||
"locked": "Locked",
|
||||
"unlocked": "Unlocked",
|
||||
"join_event_location": "Join {{eventLocationType}}",
|
||||
"troubleshooting":"Troubleshooting",
|
||||
"calendars_were_checking_for_conflicts":"Calendars we’re checking for conflicts",
|
||||
"availabilty_schedules":"Availability schedules",
|
||||
@@ -2287,6 +2299,8 @@
|
||||
"field_identifiers_as_variables": "Use field identifiers as variables for your custom event redirect",
|
||||
"field_identifiers_as_variables_with_example": "Use field identifiers as variables for your custom event redirect (e.g. {{variable}})",
|
||||
"account_already_linked": "Account is already linked",
|
||||
"privacy_organization_description": "Manage privacy settings for your organization",
|
||||
"privacy": "Privacy",
|
||||
"team_will_be_under_org": "New teams will be under your organization",
|
||||
"add_group_name": "Add group name",
|
||||
"group_name": "Group Name",
|
||||
@@ -2299,4 +2313,4 @@
|
||||
"confirm_email_description": "We sent an email to <strong>{{email}}</strong>. Click the link in the email to verify this address.",
|
||||
"send_event_details_to": "Send event details to",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1822,10 +1822,8 @@
|
||||
"requires_at_least_one_schedule": "Es necesario tener al menos un horario",
|
||||
"default_conferencing_bulk_description": "Actualizar las ubicaciones para los tipos de eventos seleccionados",
|
||||
"locked_for_members": "Bloqueado para miembros",
|
||||
"locked_apps_description": "Los miembros podrán ver las aplicaciones activas, pero no podrán editar ninguna configuración de la aplicación",
|
||||
"locked_webhooks_description": "Los miembros podrán ver los webhooks activos, pero no podrán editar ninguna configuración de webhooks",
|
||||
"locked_workflows_description": "Los miembros podrán ver los flujos de trabajo activos, pero no podrán editar ninguna configuración de flujo de trabajo",
|
||||
"locked_by_admin": "Bloqueado por el administrador del equipo",
|
||||
"apps_locked_for_members_description": "Los miembros podrán ver las aplicaciones activas, pero no podrán editar ninguna configuración de la aplicación",
|
||||
"locked_by_team_admin": "Bloqueado por el administrador del equipo",
|
||||
"app_not_connected": "No ha conectado una cuenta de {{appName}}.",
|
||||
"connect_now": "Conectar ahora",
|
||||
"managed_event_dialog_confirm_button_one": "Reemplazar y notificar a {{count}} miembro",
|
||||
@@ -2097,5 +2095,6 @@
|
||||
"extensive_whitelabeling": "Asistencia dedicada en materia de incorporación e ingeniería",
|
||||
"need_help": "¿Necesita ayuda?",
|
||||
"show_more": "Mostrar más",
|
||||
"privacy": "Privacidad",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agregue sus nuevas cadenas arriba ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -761,4 +761,4 @@
|
||||
"radio": "Irratia",
|
||||
"all_bookings_filter_label": "Erreserba guztiak",
|
||||
"timezone_variable": "Ordu-eremua"
|
||||
}
|
||||
}
|
||||
@@ -1889,10 +1889,8 @@
|
||||
"requires_at_least_one_schedule": "Vous devez avoir au moins un planning",
|
||||
"default_conferencing_bulk_description": "Mettez à jour les emplacements pour les types d'événements sélectionnés.",
|
||||
"locked_for_members": "Verrouillé pour les membres",
|
||||
"locked_apps_description": "Les membres pourront voir les applications actives, mais ne pourront pas modifier leurs paramètres.",
|
||||
"locked_webhooks_description": "Les membres pourront voir les webhooks actifs, mais ne pourront pas modifier leurs paramètres.",
|
||||
"locked_workflows_description": "Les membres pourront voir les workflows actifs, mais ne pourront pas modifier leurs paramètres.",
|
||||
"locked_by_admin": "Verrouillé par l'administrateur de l'équipe",
|
||||
"apps_locked_for_members_description": "Les membres pourront voir les applications actives, mais ne pourront pas modifier leurs paramètres.",
|
||||
"locked_by_team_admin": "Verrouillé par l'administrateur de l'équipe",
|
||||
"app_not_connected": "Vous n'avez pas connecté de compte {{appName}}.",
|
||||
"connect_now": "Connecter maintenant",
|
||||
"managed_event_dialog_confirm_button_one": "Remplacer et notifier {{count}} membre",
|
||||
@@ -2286,6 +2284,7 @@
|
||||
"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é",
|
||||
"privacy": "Confidentialité",
|
||||
"routers": "Routeurs",
|
||||
"primary": "Primaire",
|
||||
"make_primary": "Rendre primaire",
|
||||
@@ -2295,4 +2294,4 @@
|
||||
"confirm_email_description": "Nous avons envoyé un e-mail à <strong>{{email}}</strong>. Cliquez sur le lien dans l'e-mail pour vérifier cette adresse.",
|
||||
"send_event_details_to": "Envoyer les détails de l'événement à",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -707,7 +707,9 @@
|
||||
"you_must_be_logged_in_to": "חובה להיכנס אל {{url}}",
|
||||
"start_assigning_members_above": "התחל/י להקצות חברים למעלה",
|
||||
"locked_fields_admin_description": "חברים לא יוכלו לערוך את זה",
|
||||
"unlocked_fields_admin_description": "חברים יכולים לערוך",
|
||||
"locked_fields_member_description": "מנהל הצוות נעל את האפשרות הזו",
|
||||
"unlocked_fields_member_description": "שוחרר על ידי הנהלת הצוות",
|
||||
"url": "כתובת URL",
|
||||
"hidden": "מוסתר",
|
||||
"readonly": "לקריאה בלבד",
|
||||
@@ -1890,10 +1892,12 @@
|
||||
"requires_at_least_one_schedule": "אתה חייב שיהיה לך לפחות לוח זמנים אחד",
|
||||
"default_conferencing_bulk_description": "עדכן את המיקומים עבור סוגי הארועים שנבחרו",
|
||||
"locked_for_members": "נעול לחברים",
|
||||
"locked_apps_description": "החברים יוכלו לראות את האפליקציות הפעילות, אבל לא יוכלו לערוך את הגדרות האפליקציה",
|
||||
"locked_webhooks_description": "החברים יוכלו לראות את רכיבי ה-Webhook הפעילים, אבל לא יוכלו לערוך הגדרות של רכיבי Webhook",
|
||||
"locked_workflows_description": "החברים יוכלו לראות את תהליכי העבודה הפעילים, אבל לא יוכלו לערוך הגדרות של תהליכי עבודה",
|
||||
"locked_by_admin": "ננעל על ידי מנהל/ת הצוות",
|
||||
"unlocked_for_members": "שוחרר לטובת החברים",
|
||||
"apps_locked_for_members_description": "החברים יוכלו לראות את האפליקציות הפעילות, אבל לא יוכלו לערוך את הגדרות האפליקציה",
|
||||
"apps_unlocked_for_members_description": "החברים יוכלו לראות את היישומים הפעילים ויוכלו לערוך את כל הגדרות היישומים",
|
||||
"apps_locked_by_team_admins_description": "יתאפשר לך לראות את היישומים הפעילים, אבל לא לערוך הגדרות יישומים כלשהן",
|
||||
"apps_unlocked_by_team_admins_description": "יתאפשר לך לראות את היישומים הפעילים ולערוך את כל הגדרות היישומים",
|
||||
"locked_by_team_admin": "ננעל על ידי מנהל/ת הצוות",
|
||||
"app_not_connected": "לא קישרת חשבון {{appName}}.",
|
||||
"connect_now": "להתחבר עכשיו",
|
||||
"managed_event_dialog_confirm_button_one": "החלפה ועדכון של חבר/ה {{count}}",
|
||||
@@ -2178,6 +2182,8 @@
|
||||
"overlay_my_calendar": "הצג את לוח השנה שלי בשכבת-על",
|
||||
"overlay_my_calendar_toc": "על ידי חיבור אל לוח השנה שלך, את/ה מקבל/ת את מדיניות הפרטיות ואת תנאי השימוש שלנו. אפשר לשלול את הגישה בכל שלב.",
|
||||
"view_overlay_calendar_events": "ראה/י את האירועים שלך בלוח השנה כדי למנוע התנגשות בהזמנות.",
|
||||
"locked": "נעול",
|
||||
"unlocked": "משוחרר",
|
||||
"join_event_location": "הצטרפות אל {{eventLocationType}}",
|
||||
"troubleshooting": "פתרון בעיות",
|
||||
"calendars_were_checking_for_conflicts": "לוחות השנה לא בודקים סתירות",
|
||||
@@ -2273,6 +2279,10 @@
|
||||
"field_identifiers_as_variables": "להשתמש במזהי השדות כמשתנים להפניות האירועים בהתאמה אישית",
|
||||
"field_identifiers_as_variables_with_example": "להשתמש במזהי השדות כמשתנים להפניות האירועים בהתאמה אישית (למשל: {{variable}})",
|
||||
"account_already_linked": "החשבון כבר מקושר",
|
||||
"privacy": "פרטיות",
|
||||
"team_will_be_under_org": "צוותים חדשים יהיו תחת הארגון שלך",
|
||||
"add_group_name": "הוספת שם קבוצה",
|
||||
"group_name": "שם קבוצה",
|
||||
"routers": "נתבים",
|
||||
"primary": "עיקרי",
|
||||
"make_primary": "הפיכה לעיקרי",
|
||||
@@ -2282,4 +2292,4 @@
|
||||
"confirm_email_description": "שלחנו הודעה בדוא״ל אל <strong>{{email}}</strong>. נא ללחוץ על הקישור בהודעה כדי לאמת את הכתובת הזאת.",
|
||||
"send_event_details_to": "שליחת פרטי האירוע אל",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -335,4 +335,4 @@
|
||||
"user_dynamic_booking_disabled": "Neki od korisnika u grupi trenutno su onemogućili dinamičke grupne rezervacije",
|
||||
"full_name": "Puno ime",
|
||||
"already_have_account": "Već imate račun?"
|
||||
}
|
||||
}
|
||||
@@ -386,6 +386,9 @@
|
||||
"view_overlay_calendar_events": "Nézd meg a naptáreseményeidet, hogy megelőzd az ütköző foglalásokat.",
|
||||
"need_help": "Segítségre van szükséged?",
|
||||
"booking_redirect_email_subject": "Foglalás átirányítás értesítő",
|
||||
"team_will_be_under_org": "Új csapatok a szervezeted alá fognak tartozni",
|
||||
"add_group_name": "Adj hozzá csapatnevet",
|
||||
"group_name": "Csapat név",
|
||||
"primary": "Elsődleges",
|
||||
"make_primary": "Legyen elsődleges",
|
||||
"add_email": "Email hozzáadása",
|
||||
@@ -393,4 +396,4 @@
|
||||
"confirm_email": "Igazold az email címed",
|
||||
"confirm_email_description": "Küldtünk egy emailt a <strong>{{email}}</strong> címre. Kattints a linkre a levélben, hogy igazold az email címed.",
|
||||
"send_event_details_to": "Esemény részleteinek küldése a következőnek"
|
||||
}
|
||||
}
|
||||
@@ -127,4 +127,4 @@
|
||||
"incorrect_2fa_code": "Kode dua faktor salah.",
|
||||
"no_account_exists": "Tidak ada akun dengan email tersebut.",
|
||||
"need_help": "Butuh bantuan?"
|
||||
}
|
||||
}
|
||||
@@ -808,6 +808,7 @@
|
||||
"label": "Etichetta",
|
||||
"placeholder": "Segnaposto",
|
||||
"display_add_to_calendar_organizer": "Visualizza l'email \"Aggiungi al calendario\" come organizzatore",
|
||||
"reconnect_calendar_to_use": "Nota che potrebbe essere necessario disconnettere e poi riconnettere l'account \"Aggiungi al calendario\" per utilizzare questa funzione.",
|
||||
"type": "Tipo",
|
||||
"edit": "Modifica",
|
||||
"add_input": "Aggiungi un input",
|
||||
@@ -1474,6 +1475,8 @@
|
||||
"copy_link_to_form": "Copia il link del modulo",
|
||||
"theme": "Tema",
|
||||
"theme_applies_note": "Si applica solo alle pagine di prenotazione pubbliche",
|
||||
"app_theme": "Tema Pannello di controllo",
|
||||
"app_theme_applies_note": "Questo riguarda solo il pannello di controllo per cui hai effettuato l'accesso",
|
||||
"theme_system": "Predefinito di sistema",
|
||||
"add_a_team": "Aggiungi un team",
|
||||
"add_webhook_description": "Ricevi in tempo reale le informazioni sulle riunioni quando succede qualcosa su {{appName}}",
|
||||
@@ -1599,6 +1602,7 @@
|
||||
"attendee_email_info": "E-mail della persona che prenota",
|
||||
"kbar_search_placeholder": "Digita un comando o esegui una ricerca...",
|
||||
"invalid_credential": "Errore! Sembra che l'autorizzazione sia scaduta o che sia stata revocata. Prova a reinstallare.",
|
||||
"invalid_credential_action": "Reinstalla app",
|
||||
"reschedule_reason": "Motivo della riprogrammazione",
|
||||
"choose_common_schedule_team_event": "Scegli un programma comune",
|
||||
"choose_common_schedule_team_event_description": "Abilita questa opzione se desideri utilizzare un programma comune per tutti gli organizzatori. Quando l'opzione è disabilitata, ciascun organizzatore verrà prenotato in base al proprio programma predefinito.",
|
||||
@@ -1884,10 +1888,8 @@
|
||||
"requires_at_least_one_schedule": "È necessario avere almeno un programma",
|
||||
"default_conferencing_bulk_description": "Aggiorna le posizioni per i tipi di eventi selezionati",
|
||||
"locked_for_members": "Bloccato per i membri",
|
||||
"locked_apps_description": "I membri potranno vedere le app attive, ma non potranno modificare le impostazioni delle app",
|
||||
"locked_webhooks_description": "I membri potranno vedere i webhook attivi, ma non potranno modificare le impostazioni dei webhook",
|
||||
"locked_workflows_description": "I membri potranno vedere i flussi di lavoro attivi, ma non potranno modificare le impostazioni dei flussi di lavoro",
|
||||
"locked_by_admin": "Bloccato dall'amministratore del team",
|
||||
"apps_locked_for_members_description": "I membri potranno vedere le app attive, ma non potranno modificare le impostazioni delle app",
|
||||
"locked_by_team_admin": "Bloccato dall'amministratore del team",
|
||||
"app_not_connected": "Non è stato connesso un account di {{appName}}.",
|
||||
"connect_now": "Connetti ora",
|
||||
"managed_event_dialog_confirm_button_one": "Sostituisci e invia notifica a {{count}} membro",
|
||||
@@ -2263,5 +2265,13 @@
|
||||
"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",
|
||||
"privacy": "Privacy",
|
||||
"primary": "Principale",
|
||||
"make_primary": "Rendi principale",
|
||||
"add_email": "Aggiungi Email",
|
||||
"add_email_description": "Aggiungi un indirizzo email con cui sostituire quello principale o da utilizzare come email alternativa per i tipi di evento.",
|
||||
"confirm_email": "Conferma la tua email",
|
||||
"confirm_email_description": "Abbiamo inviato un'email a <strong>{{email}}</strong>. Clicca sul link nell'email per verificare questo indirizzo.",
|
||||
"send_event_details_to": "Invia dettagli evento a",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Aggiungi le tue nuove stringhe qui sopra ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1823,10 +1823,8 @@
|
||||
"requires_at_least_one_schedule": "少なくとも 1 件のスケジュールが必要です",
|
||||
"default_conferencing_bulk_description": "選択したイベントの種類の場所を更新します",
|
||||
"locked_for_members": "メンバーに対してロックされています",
|
||||
"locked_apps_description": "メンバーは有効なアプリを確認できますが、アプリの設定を編集することはできません",
|
||||
"locked_webhooks_description": "メンバーは有効なウェブフックを確認できますが、ウェブフックの設定を編集することはできません",
|
||||
"locked_workflows_description": "メンバーは有効なワークフローを確認できますが、ワークフローの設定を編集することはできません",
|
||||
"locked_by_admin": "チームの管理者によりロックされています",
|
||||
"apps_locked_for_members_description": "メンバーは有効なアプリを確認できますが、アプリの設定を編集することはできません",
|
||||
"locked_by_team_admin": "チームの管理者によりロックされています",
|
||||
"app_not_connected": "{{appName}} のアカウントに接続していません。",
|
||||
"connect_now": "今すぐ接続",
|
||||
"managed_event_dialog_confirm_button_one": "{{count}} 人のメンバーを置き換えて通知する",
|
||||
@@ -2098,5 +2096,6 @@
|
||||
"extensive_whitelabeling": "専用のオンボーディングサポートとエンジニアリングサポート",
|
||||
"need_help": "サポートが必要ですか?",
|
||||
"show_more": "さらに表示",
|
||||
"privacy": "プライバシー",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -253,4 +253,4 @@
|
||||
"user": "អ្នកប្រើប្រាស់",
|
||||
"general_description": "គ្រប់គ្រងការកំណត់សម្រាប់ភាសា និងល្វែងម៉ោងរបស់អ្នក។",
|
||||
"timezone_variable": "ល្វែងម៉ោង"
|
||||
}
|
||||
}
|
||||
@@ -1823,10 +1823,8 @@
|
||||
"requires_at_least_one_schedule": "적어도 하나 이상의 일정이 필요합니다",
|
||||
"default_conferencing_bulk_description": "선택한 이벤트 타입의 위치 업데이트",
|
||||
"locked_for_members": "회원에 대해 잠김",
|
||||
"locked_apps_description": "회원은 활성 앱을 볼 수 있지만 앱 설정을 편집할 수는 없습니다",
|
||||
"locked_webhooks_description": "회원은 활성 웹훅을 볼 수 있지만 웹훅 설정을 편집할 수는 없습니다",
|
||||
"locked_workflows_description": "회원은 활성 워크플로를 볼 수 있지만 워크플로 설정을 편집할 수는 없습니다",
|
||||
"locked_by_admin": "팀 관리자에 의해 잠김",
|
||||
"apps_locked_for_members_description": "회원은 활성 앱을 볼 수 있지만 앱 설정을 편집할 수는 없습니다",
|
||||
"locked_by_team_admin": "팀 관리자에 의해 잠김",
|
||||
"app_not_connected": "{{appName}} 계정을 연결하지 않았습니다.",
|
||||
"connect_now": "지금 연결",
|
||||
"managed_event_dialog_confirm_button_one": "회원 {{count}}명 바꾸기 및 알림",
|
||||
@@ -2098,5 +2096,6 @@
|
||||
"extensive_whitelabeling": "전담 온보딩 및 엔지니어링 지원",
|
||||
"need_help": "도움이 필요하세요?",
|
||||
"show_more": "더 보기",
|
||||
"privacy": "개인정보보호",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 여기에 새 문자열을 추가하세요 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -126,4 +126,4 @@
|
||||
"confirm_password": "Apstiprināt paroli",
|
||||
"confirm_auth_change": "Šis mainīs veidu, kā jūs autorizējaties",
|
||||
"already_have_account": "Vai jums jau ir konts?"
|
||||
}
|
||||
}
|
||||
@@ -1823,10 +1823,8 @@
|
||||
"requires_at_least_one_schedule": "U moet ten minste één planning hebben",
|
||||
"default_conferencing_bulk_description": "Werk de locaties voor de geselecteerde gebeurtenistypen bij",
|
||||
"locked_for_members": "Vergrendeld voor leden",
|
||||
"locked_apps_description": "Leden kunnen de actieve apps zien, maar kunnen geen appinstellingen bewerken",
|
||||
"locked_webhooks_description": "Leden kunnen de actieve webhooks zien, maar kunnen geen webhookinstellingen bewerken",
|
||||
"locked_workflows_description": "Leden kunnen de actieve werkstromen zien, maar kunnen geen werkstroominstellingen bewerken",
|
||||
"locked_by_admin": "Vergrendeld door teambeheerder",
|
||||
"apps_locked_for_members_description": "Leden kunnen de actieve apps zien, maar kunnen geen appinstellingen bewerken",
|
||||
"locked_by_team_admin": "Vergrendeld door teambeheerder",
|
||||
"app_not_connected": "U hebt geen {{appName}}-account gekoppeld.",
|
||||
"connect_now": "Nu koppelen",
|
||||
"managed_event_dialog_confirm_button_one": "{{count}} lid vervangen en informeren",
|
||||
@@ -2098,5 +2096,6 @@
|
||||
"extensive_whitelabeling": "Speciale onboarding en technische ondersteuning",
|
||||
"need_help": "Hulp nodig?",
|
||||
"show_more": "Meer weergeven",
|
||||
"privacy": "Privacy",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Voeg uw nieuwe strings hierboven toe ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1389,4 +1389,4 @@
|
||||
"booking_confirmation_failed": "Booking-bekreftelse feilet",
|
||||
"timezone_variable": "Tidssone",
|
||||
"need_help": "Trenger du hjelp?"
|
||||
}
|
||||
}
|
||||
@@ -1823,10 +1823,8 @@
|
||||
"requires_at_least_one_schedule": "Musisz mieć co najmniej jeden harmonogram",
|
||||
"default_conferencing_bulk_description": "Aktualizuj lokalizacje wybranych typów wydarzeń",
|
||||
"locked_for_members": "Zablokowany dla członków",
|
||||
"locked_apps_description": "Członkowie będą mogli sprawdzić aktywne aplikacje, ale nie będą mogli edytować ich ustawień",
|
||||
"locked_webhooks_description": "Członkowie będą mogli sprawdzić aktywne webhooki, ale nie będą mogli edytować ich ustawień",
|
||||
"locked_workflows_description": "Członkowie będą mogli sprawdzić aktywne przepływy pracy, ale nie będą mogli edytować ich ustawień",
|
||||
"locked_by_admin": "Zablokowane przez administratora zespołu",
|
||||
"apps_locked_for_members_description": "Członkowie będą mogli sprawdzić aktywne aplikacje, ale nie będą mogli edytować ich ustawień",
|
||||
"locked_by_team_admin": "Zablokowane przez administratora zespołu",
|
||||
"app_not_connected": "Nie podłączono konta {{appName}}.",
|
||||
"connect_now": "Podłącz teraz",
|
||||
"managed_event_dialog_confirm_button_one": "Zastąp i poinformuj {{count}} członka",
|
||||
@@ -2098,5 +2096,6 @@
|
||||
"extensive_whitelabeling": "Dedykowane wsparcie w zakresie wdrożenia i obsługi technicznej",
|
||||
"need_help": "Potrzebujesz pomocy?",
|
||||
"show_more": "Wyświetl więcej",
|
||||
"privacy": "Prywatność",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodaj nowe ciągi powyżej ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1858,10 +1858,8 @@
|
||||
"requires_at_least_one_schedule": "Você precisa ter pelo menos uma agenda",
|
||||
"default_conferencing_bulk_description": "Atualize as localizações para os tipos de evento selecionados",
|
||||
"locked_for_members": "Bloqueado para membros",
|
||||
"locked_apps_description": "Os membros poderão ver os aplicativos ativos, mas não poderão editar as configurações",
|
||||
"locked_webhooks_description": "Os membros poderão ver os webhooks ativos, mas não poderão editar as configurações",
|
||||
"locked_workflows_description": "Os membros poderão ver os fluxos de trabalho ativos, mas não poderão editar as configurações",
|
||||
"locked_by_admin": "Bloqueado pelo administrador da equipe",
|
||||
"apps_locked_for_members_description": "Os membros poderão ver os aplicativos ativos, mas não poderão editar as configurações",
|
||||
"locked_by_team_admin": "Bloqueado pelo administrador da equipe",
|
||||
"app_not_connected": "Você não conectou uma conta do {{appName}}.",
|
||||
"connect_now": "Conectar agora",
|
||||
"managed_event_dialog_confirm_button_one": "Substituir e notificar {{count}} membro",
|
||||
@@ -2133,5 +2131,6 @@
|
||||
"extensive_whitelabeling": "Marca própria abrangente",
|
||||
"need_help": "Precisa de ajuda?",
|
||||
"show_more": "Mostrar mais",
|
||||
"privacy": "Privacidade",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adicione suas novas strings aqui em cima ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -808,6 +808,7 @@
|
||||
"label": "Etiqueta",
|
||||
"placeholder": "Preenchimento",
|
||||
"display_add_to_calendar_organizer": "Mostrar o e-mail 'Adicionar ao calendário' como organizador",
|
||||
"reconnect_calendar_to_use": "Note que poderá ser necessário desligar e voltar a ligar a sua conta 'Adicionar ao Calendário' para utilizar esta funcionalidade.",
|
||||
"type": "Tipo",
|
||||
"edit": "Editar",
|
||||
"add_input": "Adicionar um Campo",
|
||||
@@ -1474,6 +1475,8 @@
|
||||
"copy_link_to_form": "Copiar ligação para o formulário",
|
||||
"theme": "Tema",
|
||||
"theme_applies_note": "Isto só se aplica às suas páginas de reservas públicas",
|
||||
"app_theme": "Tema do Painel",
|
||||
"app_theme_applies_note": "Isto aplica-se apenas ao seu painel de administração quando tem a sessão iniciada",
|
||||
"theme_system": "Predefinição do sistema",
|
||||
"add_a_team": "Adicionar uma equipa",
|
||||
"add_webhook_description": "Receba os dados da reunião em tempo real quando algo acontecer em {{appName}}",
|
||||
@@ -1599,6 +1602,7 @@
|
||||
"attendee_email_info": "O e-mail do responsável pela reserva",
|
||||
"kbar_search_placeholder": "Digite um comando ou pesquise...",
|
||||
"invalid_credential": "Oh não! Parece que a permissão expirou ou foi revogada. Por favor, reinstale novamente.",
|
||||
"invalid_credential_action": "Reinstalar aplicação",
|
||||
"reschedule_reason": "Motivo do reagendamento",
|
||||
"choose_common_schedule_team_event": "Escolha uma agenda comum",
|
||||
"choose_common_schedule_team_event_description": "Ative esta opção se quiser utilizar uma agenda comum entre anfitriões. Quando desativada, cada anfitrião será reservado com base na respetiva agenda pré-definida.",
|
||||
@@ -1884,10 +1888,8 @@
|
||||
"requires_at_least_one_schedule": "É necessário que tenha pelo menos uma agenda",
|
||||
"default_conferencing_bulk_description": "Atualizar os locais para os tipos de evento selecionados",
|
||||
"locked_for_members": "Bloqueado para os membros",
|
||||
"locked_apps_description": "Os membros poderão ver as aplicações ativas, mas não poderão editar quaisquer configurações das aplicações",
|
||||
"locked_webhooks_description": "Os membros poderão ver os webhooks ativos, mas não poderão editar quaisquer configurações dos webhooks",
|
||||
"locked_workflows_description": "Os membros poderão ver os fluxos de trabalho ativos, mas não poderão editar quaisquer configurações dos fluxos de trabalho",
|
||||
"locked_by_admin": "Bloqueado pelo administrador da equipa",
|
||||
"apps_locked_for_members_description": "Os membros poderão ver as aplicações ativas, mas não poderão editar quaisquer configurações das aplicações",
|
||||
"locked_by_team_admin": "Bloqueado pelo administrador da equipa",
|
||||
"app_not_connected": "Não associou uma conta {{appName}}.",
|
||||
"connect_now": "Associar agora",
|
||||
"managed_event_dialog_confirm_button_one": "Substituir e notificar {{count}} membro",
|
||||
@@ -2263,5 +2265,13 @@
|
||||
"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",
|
||||
"privacy": "Privacidade",
|
||||
"primary": "Primário",
|
||||
"make_primary": "Tornar primário",
|
||||
"add_email": "Adicionar e-mail",
|
||||
"add_email_description": "Adicione um endereço de e-mail para substituir o seu primário ou para utilizar como um e-mail alternativo nos seus tipos de evento.",
|
||||
"confirm_email": "Confirme o seu e-mail",
|
||||
"confirm_email_description": "Enviámos um e-mail para <strong>{{email}}</strong>. Clique na ligação nesse e-mail para verificar este endereço.",
|
||||
"send_event_details_to": "Enviar detalhes do evento para",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1823,10 +1823,8 @@
|
||||
"requires_at_least_one_schedule": "Este necesar să aveți cel puțin un program",
|
||||
"default_conferencing_bulk_description": "Actualizați locațiile pentru tipurile de evenimente selectate",
|
||||
"locked_for_members": "Blocat pentru membri",
|
||||
"locked_apps_description": "Membrii vor putea vedea aplicațiile active, însă nu le vor putea modifica deloc setările",
|
||||
"locked_webhooks_description": "Membrii vor putea vedea webhook-urile active, însă nu le vor putea modifica deloc setările",
|
||||
"locked_workflows_description": "Membrii vor putea vedea fluxurile de lucru active, însă nu le vor putea modifica deloc setările",
|
||||
"locked_by_admin": "Blocat de administratorul echipei",
|
||||
"apps_locked_for_members_description": "Membrii vor putea vedea aplicațiile active, însă nu le vor putea modifica deloc setările",
|
||||
"locked_by_team_admin": "Blocat de administratorul echipei",
|
||||
"app_not_connected": "Nu ați conectat un cont {{appName}}.",
|
||||
"connect_now": "Conectați-l acum",
|
||||
"managed_event_dialog_confirm_button_one": "Înlocuiți și anunțați {{count}} membru",
|
||||
@@ -2098,5 +2096,6 @@
|
||||
"extensive_whitelabeling": "Asistență dedicată pentru integrare și inginerie",
|
||||
"need_help": "Aveți nevoie de ajutor?",
|
||||
"show_more": "Afișare mai mult",
|
||||
"privacy": "Confidențialitate",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adăugați stringurile noi deasupra acestui rând ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1824,10 +1824,8 @@
|
||||
"requires_at_least_one_schedule": "У вас должно быть по крайней мере одно расписание",
|
||||
"default_conferencing_bulk_description": "Обновить местоположение для выбранных типов событий",
|
||||
"locked_for_members": "Заблокировано для участников",
|
||||
"locked_apps_description": "Участники смогут видеть активные приложения, но не смогут редактировать их настройки",
|
||||
"locked_webhooks_description": "Участники смогут видеть активные вебхуки, но не смогут редактировать их настройки",
|
||||
"locked_workflows_description": "Участники смогут видеть активные рабочие процессы, но не смогут редактировать их настройки",
|
||||
"locked_by_admin": "Заблокировано администратором команды",
|
||||
"apps_locked_for_members_description": "Участники смогут видеть активные приложения, но не смогут редактировать их настройки",
|
||||
"locked_by_team_admin": "Заблокировано администратором команды",
|
||||
"app_not_connected": "Вы не подключили аккаунт {{appName}}.",
|
||||
"connect_now": "Подключить",
|
||||
"managed_event_dialog_confirm_button_one": "Заменить и уведомить {{count}} участника",
|
||||
@@ -2099,5 +2097,6 @@
|
||||
"extensive_whitelabeling": "Индивидуальная техподдержка и помощь при онбординге",
|
||||
"need_help": "Нужна помощь?",
|
||||
"show_more": "Показать больше",
|
||||
"privacy": "Конфиденциальность",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Добавьте строки выше ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -29,4 +29,4 @@
|
||||
"organizer": "Organizátor",
|
||||
"need_to_reschedule_or_cancel": "Preplánovať alebo zrušiť?",
|
||||
"no_options_available": "Nie sú k dispozícii žiadne možnosti"
|
||||
}
|
||||
}
|
||||
@@ -1823,10 +1823,8 @@
|
||||
"requires_at_least_one_schedule": "Potrebno je da imate barem jedan raspored",
|
||||
"default_conferencing_bulk_description": "Ažuriranje lokacija za izabrane tipove događaja",
|
||||
"locked_for_members": "Zaključano za članove",
|
||||
"locked_apps_description": "Članovi će moći da vide aktivne aplikacije, ali neće moći da uređuju podešavanja aplikacije",
|
||||
"locked_webhooks_description": "Članovi će moći da vide aktivne webhook-ove, ali neće moći da uređuju podešavanja webhook-ova",
|
||||
"locked_workflows_description": "Članovi će moći da vide aktivne radne tokove, ali neće moći da uređuju podešavanja radnih tokova",
|
||||
"locked_by_admin": "Zaključao administrator tima",
|
||||
"apps_locked_for_members_description": "Članovi će moći da vide aktivne aplikacije, ali neće moći da uređuju podešavanja aplikacije",
|
||||
"locked_by_team_admin": "Zaključao administrator tima",
|
||||
"app_not_connected": "Niste povezali {{appName}} nalog.",
|
||||
"connect_now": "Povežite odmah",
|
||||
"managed_event_dialog_confirm_button_one": "Zameni i obavesti {{count}} člana",
|
||||
@@ -2098,5 +2096,6 @@
|
||||
"extensive_whitelabeling": "Posvećena podrška za uvodnu obuku i inženjering",
|
||||
"need_help": "Potrebna vam je pomoć?",
|
||||
"show_more": "Prikaži više",
|
||||
"privacy": "Privatnost",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodajte svoje nove stringove iznad ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1823,10 +1823,8 @@
|
||||
"requires_at_least_one_schedule": "Du måste ha minst ett schema",
|
||||
"default_conferencing_bulk_description": "Uppdatera platserna för de valda händelsetyperna",
|
||||
"locked_for_members": "Låst för medlemmar",
|
||||
"locked_apps_description": "Medlemmarna kan se de aktiva apparna men kan inte ändra appinställningarna",
|
||||
"locked_webhooks_description": "Medlemmarna kan se aktiva webhooks men kan inte redigera inställningar för webhooks",
|
||||
"locked_workflows_description": "Medlemmarna kan se aktiva arbetsflöden men kan inte ändra några arbetsflödesinställningar",
|
||||
"locked_by_admin": "Låst av teamadministratör",
|
||||
"apps_locked_for_members_description": "Medlemmarna kan se de aktiva apparna men kan inte ändra appinställningarna",
|
||||
"locked_by_team_admin": "Låst av teamadministratör",
|
||||
"app_not_connected": "Du har inte anslutit ett {{appName}}-konto.",
|
||||
"connect_now": "Anslut nu",
|
||||
"managed_event_dialog_confirm_button_one": "Ersätt och meddela {{count}} medlem",
|
||||
@@ -2098,5 +2096,6 @@
|
||||
"extensive_whitelabeling": "Dedikerad registrering och teknisk support",
|
||||
"need_help": "Behöver du hjälp?",
|
||||
"show_more": "Visa mer",
|
||||
"privacy": "Sekretess",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -156,7 +156,7 @@
|
||||
"select_all": "அனைத்தையும் தேர்வுசெய்",
|
||||
"default_conferencing_bulk_title": "தற்போதுள்ள நிகழ்வு வகைகளை மொத்தமாகப் புதுப்பிக்கவும்",
|
||||
"default_conferencing_bulk_description": "தேர்ந்தெடுக்கப்பட்ட நிகழ்வு வகைகளுக்கான இருப்பிடங்களைப் புதுப்பிக்கவும்",
|
||||
"locked_by_admin": "குழு நிர்வாகியால் பூட்டப்பட்டது",
|
||||
"locked_by_team_admin": "குழு நிர்வாகியால் பூட்டப்பட்டது",
|
||||
"looking_for_more_analytics": "மேலும் பகுப்பாய்வுகளைத் தேடுகிறீர்களா?",
|
||||
"add_filter": "வடிகட்டிச் சேர்க்கவும்",
|
||||
"select_user": "பயனரைத் தேர்ந்தெடுக்கவும்",
|
||||
@@ -173,4 +173,4 @@
|
||||
"from_last_period": "கடந்த காலத்திலிருந்து",
|
||||
"from_to_date_period": "{{startDate}} லிருந்து {{endDate}} வரை",
|
||||
"event_trends": "நிகழ்வுப் போக்குகள்"
|
||||
}
|
||||
}
|
||||
@@ -1823,10 +1823,8 @@
|
||||
"requires_at_least_one_schedule": "En az bir plana sahip olmanız gerekiyor",
|
||||
"default_conferencing_bulk_description": "Seçilen etkinlik türleri için konumları güncelleyin",
|
||||
"locked_for_members": "Üyeler için kilitli",
|
||||
"locked_apps_description": "Üyeler aktif uygulamaları görebilir, ancak hiçbir uygulama ayarını düzenleyemez",
|
||||
"locked_webhooks_description": "Üyeler aktif web kancalarını görebilir, ancak hiçbir web kancası ayarını düzenleyemez",
|
||||
"locked_workflows_description": "Üyeler aktif iş akışlarını görebilir, ancak hiçbir iş akışı ayarını düzenleyemez",
|
||||
"locked_by_admin": "Ekip yöneticisi tarafından kilitlendi",
|
||||
"apps_locked_for_members_description": "Üyeler aktif uygulamaları görebilir, ancak hiçbir uygulama ayarını düzenleyemez",
|
||||
"locked_by_team_admin": "Ekip yöneticisi tarafından kilitlendi",
|
||||
"app_not_connected": "{{appName}} hesabı bağlamadınız.",
|
||||
"connect_now": "Hemen bağlan",
|
||||
"managed_event_dialog_confirm_button_one": "{{count}} üyeyi değiştirin ve bilgilendirin",
|
||||
@@ -2098,5 +2096,6 @@
|
||||
"extensive_whitelabeling": "Özel işe alıştırma ve mühendislik desteği",
|
||||
"need_help": "Yardıma mı ihtiyacınız var?",
|
||||
"show_more": "Daha fazla göster",
|
||||
"privacy": "Gizlilik",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni dizelerinizi yukarıya ekleyin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1855,10 +1855,8 @@
|
||||
"requires_at_least_one_schedule": "У вас має бути принаймні один розклад",
|
||||
"default_conferencing_bulk_description": "Оновіть розташування для вибраних типів заходів",
|
||||
"locked_for_members": "Заблоковано для учасників",
|
||||
"locked_apps_description": "Учасники бачитимуть активні додатки, але не зможуть змінювати їх налаштування",
|
||||
"locked_webhooks_description": "Учасники бачитимуть активні вебгуки, але не зможуть змінювати їх налаштування",
|
||||
"locked_workflows_description": "Учасники бачитимуть активні робочі процеси, але не зможуть змінювати їх налаштування",
|
||||
"locked_by_admin": "Заблоковано адміністратором команди",
|
||||
"apps_locked_for_members_description": "Учасники бачитимуть активні додатки, але не зможуть змінювати їх налаштування",
|
||||
"locked_by_team_admin": "Заблоковано адміністратором команди",
|
||||
"app_not_connected": "Ви не під’єднали обліковий запис {{appName}}.",
|
||||
"connect_now": "Під’єднати",
|
||||
"managed_event_dialog_confirm_button_one": "Замінити й повідомити {{count}} учасника",
|
||||
@@ -2130,5 +2128,6 @@
|
||||
"extensive_whitelabeling": "Технічна підтримка й підтримка під час ознайомлення",
|
||||
"need_help": "Потрібна допомога?",
|
||||
"show_more": "Більше",
|
||||
"privacy": "Політика конфіденційності",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1827,10 +1827,8 @@
|
||||
"requires_at_least_one_schedule": "Bạn cần phải có ít nhất một lịch biểu",
|
||||
"default_conferencing_bulk_description": "Cập nhật địa điểm cho những loại sự kiện đã chọn",
|
||||
"locked_for_members": "Bị khoá đối với thành viên",
|
||||
"locked_apps_description": "Các thành viên có thể thấy được những ứng dụng đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của ứng dụng",
|
||||
"locked_webhooks_description": "Các thành viên có thể thấy được những webhook đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của webhook",
|
||||
"locked_workflows_description": "Các thành viên có thể thấy được những tiến độ công việc đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của tiến độ công việc",
|
||||
"locked_by_admin": "Bị khoá bởi quản trị viên của nhóm",
|
||||
"apps_locked_for_members_description": "Các thành viên có thể thấy được những ứng dụng đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của ứng dụng",
|
||||
"locked_by_team_admin": "Bị khoá bởi quản trị viên của nhóm",
|
||||
"app_not_connected": "Bạn không có tài khoản {{appName}} đã được kết nối.",
|
||||
"connect_now": "Kết nối ngay",
|
||||
"managed_event_dialog_confirm_button_one": "Thay thế & thông báo cho {{count}} thành viên",
|
||||
@@ -2106,5 +2104,6 @@
|
||||
"need_help": "Cần hỗ trợ?",
|
||||
"instant_tab_title": "Đặt Cuộc Họp Ngay",
|
||||
"show_more": "Hiện thêm",
|
||||
"privacy": "Quyền riêng tư",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1864,10 +1864,8 @@
|
||||
"requires_at_least_one_schedule": "您需要至少有一个日程安排",
|
||||
"default_conferencing_bulk_description": "更新选定活动类型的位置",
|
||||
"locked_for_members": "已为成员锁定",
|
||||
"locked_apps_description": "成员将能够查看活动的应用,但不能编辑任何应用设置",
|
||||
"locked_webhooks_description": "成员将能够查看活动的 Webhook,但不能编辑任何 Webhook 设置",
|
||||
"locked_workflows_description": "成员将能够查看活动的工作流程,但不能编辑任何工作流程设置",
|
||||
"locked_by_admin": "被团队管理员锁定",
|
||||
"apps_locked_for_members_description": "成员将能够查看活动的应用,但不能编辑任何应用设置",
|
||||
"locked_by_team_admin": "被团队管理员锁定",
|
||||
"app_not_connected": "您尚未连接 {{appName}} 帐户。",
|
||||
"connect_now": "立即连接",
|
||||
"managed_event_dialog_confirm_button_one": "替换并通知 {{count}} 个成员",
|
||||
@@ -2139,5 +2137,6 @@
|
||||
"extensive_whitelabeling": "专门的入门和工程支持",
|
||||
"need_help": "需要帮助?",
|
||||
"show_more": "显示更多",
|
||||
"privacy": "隐私",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 在此上方添加您的新字符串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1823,10 +1823,8 @@
|
||||
"requires_at_least_one_schedule": "您必須擁有至少一個行程表",
|
||||
"default_conferencing_bulk_description": "更新所選活動類型的地點",
|
||||
"locked_for_members": "已為成員鎖定",
|
||||
"locked_apps_description": "成員將能查看啟用的應用程式,但無法編輯任何應用程式設定",
|
||||
"locked_webhooks_description": "成員將能查看啟用的 Webhook,但無法編輯任何 Webhook 設定",
|
||||
"locked_workflows_description": "成員將能查看啟用的工作流程,但無法編輯任何工作流程設定",
|
||||
"locked_by_admin": "已由團隊管理員鎖定",
|
||||
"apps_locked_for_members_description": "成員將能查看啟用的應用程式,但無法編輯任何應用程式設定",
|
||||
"locked_by_team_admin": "已由團隊管理員鎖定",
|
||||
"app_not_connected": "您尚未連結 {{appName}} 帳號。",
|
||||
"connect_now": "立即連結",
|
||||
"managed_event_dialog_confirm_button_one": "取代並通知 {{count}} 位成員",
|
||||
@@ -2098,5 +2096,6 @@
|
||||
"extensive_whitelabeling": "專屬的入門和工程支援",
|
||||
"need_help": "需要協助?",
|
||||
"show_more": "顯示更多",
|
||||
"privacy": "隱私權",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 請在此處新增您的字串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
--cal-bg-info: hsla(218,83%,98%,1);
|
||||
--cal-bg-success: hsla(134,76%,94%,1);
|
||||
--cal-bg-attention: hsla(37, 86%, 92%, 1);
|
||||
--cal-bg-error: hsla(3,66,93,1);
|
||||
--cal-bg-error: hsla(3,66%,93%,1);
|
||||
--cal-bg-dark-error: hsla(2, 55%, 30%, 1);
|
||||
|
||||
/* Borders */
|
||||
@@ -95,7 +95,7 @@
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-default
|
||||
@apply border-default
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { expect, beforeEach, afterEach, it, vi, describe } from "vitest";
|
||||
|
||||
import { filterByCities, addCitiesToDropdown, handleOptionLabel } from "@calcom/lib/timezone";
|
||||
|
||||
@@ -33,12 +33,22 @@ const option = {
|
||||
altName: "Pacific Standard Time",
|
||||
};
|
||||
|
||||
it("should return empty array for an empty string", () => {
|
||||
expect(filterByCities("", cityData)).toMatchInlineSnapshot(`[]`);
|
||||
});
|
||||
describe("getTimezone", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers().setSystemTime(new Date("2020-01-01"));
|
||||
});
|
||||
|
||||
it("should filter cities for a valid city name", () => {
|
||||
expect(filterByCities("San Francisco", cityData)).toMatchInlineSnapshot(`
|
||||
afterEach(() => {
|
||||
vi.runOnlyPendingTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should return empty array for an empty string", () => {
|
||||
expect(filterByCities("", cityData)).toMatchInlineSnapshot(`[]`);
|
||||
});
|
||||
|
||||
it("should filter cities for a valid city name", () => {
|
||||
expect(filterByCities("San Francisco", cityData)).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"city": "San Francisco",
|
||||
@@ -58,10 +68,10 @@ it("should filter cities for a valid city name", () => {
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return appropriate timezone(s) for a given city name array", () => {
|
||||
expect(addCitiesToDropdown(cityData)).toMatchInlineSnapshot(`
|
||||
it("should return appropriate timezone(s) for a given city name array", () => {
|
||||
expect(addCitiesToDropdown(cityData)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"America/Argentina/Cordoba": "San Francisco",
|
||||
"America/El_Salvador": "San Francisco Gotera",
|
||||
@@ -70,12 +80,17 @@ it("should return appropriate timezone(s) for a given city name array", () => {
|
||||
"America/Sao_Paulo": "Sao Francisco do Sul",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it("should render city name as option label if cityData is not empty", () => {
|
||||
expect(handleOptionLabel(option, cityData)).toMatchInlineSnapshot(`"San Francisco GMT -8:00"`);
|
||||
});
|
||||
it("should render city name as option label if cityData is not empty", () => {
|
||||
expect(handleOptionLabel(option, cityData)).toMatchInlineSnapshot(`"San Francisco GMT -8:00"`);
|
||||
vi.setSystemTime(new Date("2020-06-01"));
|
||||
expect(handleOptionLabel(option, cityData)).toMatchInlineSnapshot(`"San Francisco GMT -7:00"`);
|
||||
});
|
||||
|
||||
it("should return timezone as option label if cityData is empty", () => {
|
||||
expect(handleOptionLabel(option, [])).toMatchInlineSnapshot(`"America/Los Angeles GMT -8:00"`);
|
||||
it("should return timezone as option label if cityData is empty", () => {
|
||||
expect(handleOptionLabel(option, [])).toMatchInlineSnapshot(`"America/Los Angeles GMT -8:00"`);
|
||||
vi.setSystemTime(new Date("2020-06-01"));
|
||||
expect(handleOptionLabel(option, [])).toMatchInlineSnapshot(`"America/Los Angeles GMT -7:00"`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,9 @@ import type { CompleteEventType, CompleteWorkflowsOnEventTypes } from "@calcom/p
|
||||
|
||||
const mockFindFirstEventType = (data?: Partial<CompleteEventType>) => {
|
||||
const eventType = buildEventType(data as Partial<EventType>);
|
||||
// const { scheduleId, destinationCalendar, ...restEventType } = eventType;
|
||||
prismaMock.eventType.findFirst.mockResolvedValue(eventType as EventType);
|
||||
|
||||
return eventType;
|
||||
};
|
||||
|
||||
@@ -52,7 +54,10 @@ describe("handleChildrenEventTypes", () => {
|
||||
});
|
||||
|
||||
it("Returns message 'No managed event metadata'", async () => {
|
||||
mockFindFirstEventType();
|
||||
mockFindFirstEventType({
|
||||
metadata: {},
|
||||
locations: [],
|
||||
});
|
||||
const result = await updateChildrenEventTypes({
|
||||
eventTypeId: 1,
|
||||
oldEventType: { children: [], team: { name: "" } },
|
||||
@@ -136,6 +141,8 @@ describe("handleChildrenEventTypes", () => {
|
||||
...evType,
|
||||
parentId: 1,
|
||||
users: { connect: [{ id: 4 }] },
|
||||
lockTimeZoneToggleOnBookingPage: false,
|
||||
requiresBookerEmailVerification: false,
|
||||
bookingLimits: undefined,
|
||||
durationLimits: undefined,
|
||||
recurringEvent: undefined,
|
||||
@@ -183,10 +190,14 @@ describe("handleChildrenEventTypes", () => {
|
||||
bookingLimits: undefined,
|
||||
},
|
||||
});
|
||||
const { profileId, ...rest } = evType;
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
hidden: false,
|
||||
bookingLimits: undefined,
|
||||
...rest,
|
||||
locations: [],
|
||||
scheduleId: null,
|
||||
lockTimeZoneToggleOnBookingPage: false,
|
||||
requiresBookerEmailVerification: false,
|
||||
hashedLink: { create: { link: expect.any(String) } },
|
||||
},
|
||||
where: {
|
||||
@@ -291,7 +302,10 @@ describe("handleChildrenEventTypes", () => {
|
||||
durationLimits: undefined,
|
||||
recurringEvent: undefined,
|
||||
hashedLink: undefined,
|
||||
lockTimeZoneToggleOnBookingPage: false,
|
||||
requiresBookerEmailVerification: false,
|
||||
userId: 4,
|
||||
workflows: undefined,
|
||||
},
|
||||
});
|
||||
expect(result.newUserIds).toEqual([4]);
|
||||
@@ -334,11 +348,13 @@ describe("handleChildrenEventTypes", () => {
|
||||
length: 30,
|
||||
},
|
||||
});
|
||||
const { profileId, ...rest } = evType;
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
hidden: false,
|
||||
length: 30,
|
||||
hashedLink: undefined,
|
||||
...rest,
|
||||
locations: [],
|
||||
lockTimeZoneToggleOnBookingPage: false,
|
||||
requiresBookerEmailVerification: false,
|
||||
},
|
||||
where: {
|
||||
userId_parentId: {
|
||||
@@ -403,6 +419,8 @@ describe("handleChildrenEventTypes", () => {
|
||||
recurringEvent: undefined,
|
||||
hashedLink: undefined,
|
||||
locations: [],
|
||||
lockTimeZoneToggleOnBookingPage: false,
|
||||
requiresBookerEmailVerification: false,
|
||||
parentId: 1,
|
||||
userId: 5,
|
||||
users: {
|
||||
@@ -417,9 +435,14 @@ describe("handleChildrenEventTypes", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const { profileId, ...rest } = evType;
|
||||
if ("workflows" in rest) delete rest.workflows;
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
hidden: false,
|
||||
...rest,
|
||||
locations: [],
|
||||
lockTimeZoneToggleOnBookingPage: false,
|
||||
requiresBookerEmailVerification: false,
|
||||
hashedLink: undefined,
|
||||
},
|
||||
where: {
|
||||
|
||||
@@ -21,6 +21,7 @@ export default function RoutingNavBar({
|
||||
},
|
||||
{
|
||||
name: "Reporting",
|
||||
target: "_blank",
|
||||
href: `${appUrl}/reporting/${form?.id}`,
|
||||
className: "pointer-events-none opacity-30 lg:pointer-events-auto lg:opacity-100",
|
||||
},
|
||||
|
||||
@@ -166,18 +166,16 @@ const Reporter = ({ form }: { form: inferSSRProps<typeof getServerSideProps>["fo
|
||||
[]
|
||||
);
|
||||
return (
|
||||
<div className="flex flex-col-reverse md:flex-row">
|
||||
<div className="cal-query-builder bg-default w-full ltr:mr-2 rtl:ml-2">
|
||||
<Query
|
||||
{...config}
|
||||
value={query.state.tree}
|
||||
onChange={(immutableTree, config) => {
|
||||
onChange(immutableTree, config as QueryBuilderUpdatedConfig);
|
||||
}}
|
||||
renderBuilder={renderBuilder}
|
||||
/>
|
||||
<Result formId={form.id} jsonLogicQuery={jsonLogicQuery as JsonLogicQuery} />
|
||||
</div>
|
||||
<div className="cal-query-builder bg-default fixed inset-0 w-full overflow-scroll pt-12 ltr:mr-2 rtl:ml-2 sm:pt-0">
|
||||
<Query
|
||||
{...config}
|
||||
value={query.state.tree}
|
||||
onChange={(immutableTree, config) => {
|
||||
onChange(immutableTree, config as QueryBuilderUpdatedConfig);
|
||||
}}
|
||||
renderBuilder={renderBuilder}
|
||||
/>
|
||||
<Result formId={form.id} jsonLogicQuery={jsonLogicQuery as JsonLogicQuery} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -261,6 +261,7 @@ test.describe("Routing Forms", () => {
|
||||
["custom-page", ""],
|
||||
]);
|
||||
|
||||
await page.goto(`apps/routing-forms/route-builder/${routingForm.id}`);
|
||||
const [download] = await Promise.all([
|
||||
// Start waiting for the download
|
||||
page.waitForEvent("download"),
|
||||
@@ -399,6 +400,7 @@ async function fillSeededForm(page: Page, routingFormId: string) {
|
||||
await gotoRoutingLink({ page, formId: routingFormId });
|
||||
await page.fill('[data-testid="form-field-Test field"]', "event-routing");
|
||||
page.click('button[type="submit"]');
|
||||
|
||||
await page.waitForURL((url) => {
|
||||
return url.pathname.endsWith("/pro/30min");
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ export function UserFieldsResponses(props: { calEvent: CalendarEvent; t: TFuncti
|
||||
labelValueMap[key] !== "" ? (
|
||||
<Info
|
||||
key={key}
|
||||
label={key}
|
||||
label={t(key)}
|
||||
description={
|
||||
typeof labelValueMap[key] === "boolean"
|
||||
? labelValueMap[key]
|
||||
|
||||
@@ -12,3 +12,4 @@ export const SystemField = z.enum([
|
||||
]);
|
||||
|
||||
export const SMS_REMINDER_NUMBER_FIELD = "smsReminderNumber";
|
||||
export const TITLE_FIELD = "title";
|
||||
|
||||
@@ -2069,6 +2069,7 @@ async function handler(
|
||||
calEvent: getPiiFreeCalendarEvent(evt),
|
||||
})
|
||||
);
|
||||
|
||||
await sendScheduledEmails(
|
||||
{
|
||||
...evt,
|
||||
|
||||
@@ -94,7 +94,11 @@ const ConfigureDirectorySync = ({ organizationId }: { organizationId: number | n
|
||||
</div>
|
||||
<DialogFooter showDivider>
|
||||
<DialogClose />
|
||||
<Button color="primary" data-testid="delete-account-confirm" onClick={onDeleteConfirmation}>
|
||||
<Button
|
||||
color="primary"
|
||||
data-testid="delete-account-confirm"
|
||||
onClick={onDeleteConfirmation}
|
||||
loading={deleteMutation.isPending}>
|
||||
{t("directory_sync_delete_connection")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -57,7 +57,7 @@ const CreateDirectory = ({ orgId }: { orgId: number | null }) => {
|
||||
handleSubmit={(values) => {
|
||||
mutation.mutate({
|
||||
...values,
|
||||
orgId,
|
||||
organizationId: orgId,
|
||||
});
|
||||
}}>
|
||||
<div className="mb-5 mt-1">
|
||||
@@ -112,7 +112,7 @@ const CreateDirectory = ({ orgId }: { orgId: number | null }) => {
|
||||
tabIndex={-1}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
<Button type="submit" loading={form.formState.isSubmitting || mutation.isPending}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -1,27 +1,102 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { get } from "lodash";
|
||||
import React from "react";
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { useState } from "react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import type z from "zod";
|
||||
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { _EventTypeModel } from "@calcom/prisma/zod/eventtype";
|
||||
import { Tooltip } from "@calcom/ui";
|
||||
import { Lock } from "@calcom/ui/components/icon";
|
||||
import { Tooltip, Badge, Switch } from "@calcom/ui";
|
||||
import { Lock, Unlock } from "@calcom/ui/components/icon";
|
||||
|
||||
export const LockedIndicator = (label: string) => (
|
||||
<Tooltip content={<>{label}</>}>
|
||||
<div className="bg -mt-0.5 ml-1 inline-flex h-4 w-4 rounded-sm p-0.5">
|
||||
<Lock className="text-subtle hover:text-muted h-3 w-3" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const useLockedFieldsManager = (
|
||||
eventType: Pick<z.infer<typeof _EventTypeModel>, "schedulingType" | "userId" | "metadata">,
|
||||
adminLabel: string,
|
||||
memberLabel: string
|
||||
export const LockedSwitch = (
|
||||
isManagedEventType: boolean,
|
||||
[isLocked, setIsLocked]: [boolean, Dispatch<SetStateAction<boolean>>],
|
||||
fieldName: string,
|
||||
setUnlockedFields: (fieldName: string, val: boolean | undefined) => void,
|
||||
options = { simple: false }
|
||||
) => {
|
||||
return isManagedEventType ? (
|
||||
<Switch
|
||||
data-testid={`locked-indicator-${fieldName}`}
|
||||
onCheckedChange={(enabled) => {
|
||||
setIsLocked(enabled);
|
||||
setUnlockedFields(fieldName, !enabled || undefined);
|
||||
}}
|
||||
checked={isLocked}
|
||||
small={!options.simple}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const LockedIndicator = (
|
||||
isChildrenManagedEventType: boolean,
|
||||
isManagedEventType: boolean,
|
||||
[isLocked, setIsLocked]: [boolean, Dispatch<SetStateAction<boolean>>],
|
||||
t: TFunction,
|
||||
fieldName: string,
|
||||
setUnlockedFields: (fieldName: string, val: boolean | undefined) => void,
|
||||
options = { simple: false }
|
||||
) => {
|
||||
const stateText = t(isLocked ? "locked" : "unlocked");
|
||||
const tooltipText = t(
|
||||
`${isLocked ? "locked" : "unlocked"}_fields_${isManagedEventType ? "admin" : "member"}_description`
|
||||
);
|
||||
|
||||
return (
|
||||
(isManagedEventType || isChildrenManagedEventType) && (
|
||||
<Tooltip content={<>{tooltipText}</>}>
|
||||
<div className="inline">
|
||||
<Badge
|
||||
variant={isLocked ? "gray" : "green"}
|
||||
className={classNames(
|
||||
"ml-2 transform justify-between gap-1.5 p-1",
|
||||
isManagedEventType && !options.simple && "w-28"
|
||||
)}>
|
||||
{!options.simple && (
|
||||
<span className="inline-flex">
|
||||
{isLocked ? (
|
||||
<Lock className="text-subtle h-3 w-3" />
|
||||
) : (
|
||||
<Unlock className="text-subtle h-3 w-3" />
|
||||
)}
|
||||
<span className="ml-1 font-medium">{stateText}</span>
|
||||
</span>
|
||||
)}
|
||||
{isManagedEventType && (
|
||||
<Switch
|
||||
data-testid={`locked-indicator-${fieldName}`}
|
||||
onCheckedChange={(enabled) => {
|
||||
setIsLocked(enabled);
|
||||
setUnlockedFields(fieldName, !enabled || undefined);
|
||||
}}
|
||||
checked={isLocked}
|
||||
small={!options.simple}
|
||||
/>
|
||||
)}
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const useLockedFieldsManager = ({
|
||||
eventType,
|
||||
translate,
|
||||
formMethods,
|
||||
}: {
|
||||
eventType: Pick<z.infer<typeof _EventTypeModel>, "schedulingType" | "userId" | "metadata" | "id">;
|
||||
translate: TFunction;
|
||||
formMethods: UseFormReturn<FormValues>;
|
||||
}) => {
|
||||
const { setValue, getValues } = formMethods;
|
||||
const fieldStates: Record<string, [boolean, Dispatch<SetStateAction<boolean>>]> = {};
|
||||
const unlockedFields =
|
||||
(eventType.metadata?.managedEventConfig?.unlockedFields !== undefined &&
|
||||
eventType.metadata?.managedEventConfig?.unlockedFields) ||
|
||||
@@ -32,28 +107,106 @@ const useLockedFieldsManager = (
|
||||
eventType.metadata?.managedEventConfig !== undefined &&
|
||||
eventType.schedulingType !== SchedulingType.MANAGED;
|
||||
|
||||
const shouldLockIndicator = (fieldName: string) => {
|
||||
const setUnlockedFields = (fieldName: string, val: boolean | undefined) => {
|
||||
const path = "metadata.managedEventConfig.unlockedFields";
|
||||
const metaUnlockedFields = getValues(path);
|
||||
if (!metaUnlockedFields) return;
|
||||
if (val === undefined) {
|
||||
delete metaUnlockedFields[fieldName as keyof typeof metaUnlockedFields];
|
||||
setValue(path, { ...metaUnlockedFields }, { shouldDirty: true });
|
||||
} else {
|
||||
setValue(
|
||||
path,
|
||||
{
|
||||
...metaUnlockedFields,
|
||||
[fieldName]: val,
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getLockedInitState = (fieldName: string): boolean => {
|
||||
let locked = isManagedEventType || isChildrenManagedEventType;
|
||||
// Supports "metadata.fieldName"
|
||||
const unlockedFieldList = getValues("metadata")?.managedEventConfig?.unlockedFields;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
const fieldIsUnlocked = unlockedFieldList && unlockedFieldList[fieldName] === true;
|
||||
if (fieldName.includes(".")) {
|
||||
locked = locked && get(unlockedFields, fieldName) === undefined;
|
||||
} else {
|
||||
locked = locked && unlockedFields[fieldName as keyof Omit<Prisma.EventTypeSelect, "id">] === undefined;
|
||||
locked = locked && !fieldIsUnlocked;
|
||||
}
|
||||
return locked && LockedIndicator(isManagedEventType ? adminLabel : memberLabel);
|
||||
return locked;
|
||||
};
|
||||
|
||||
const shouldLockDisableProps = (fieldName: string) => {
|
||||
const useShouldLockIndicator = (fieldName: string, options?: { simple: true }) => {
|
||||
if (!fieldStates[fieldName]) {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
fieldStates[fieldName] = useState(getLockedInitState(fieldName));
|
||||
}
|
||||
|
||||
return LockedIndicator(
|
||||
isChildrenManagedEventType,
|
||||
isManagedEventType,
|
||||
fieldStates[fieldName],
|
||||
translate,
|
||||
fieldName,
|
||||
setUnlockedFields,
|
||||
options
|
||||
);
|
||||
};
|
||||
|
||||
const useLockedLabel = (fieldName: string, options?: { simple: true }) => {
|
||||
if (!fieldStates[fieldName]) {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
fieldStates[fieldName] = useState(getLockedInitState(fieldName));
|
||||
}
|
||||
const isLocked = fieldStates[fieldName][0];
|
||||
|
||||
return {
|
||||
disabled:
|
||||
!isManagedEventType &&
|
||||
eventType.metadata?.managedEventConfig !== undefined &&
|
||||
unlockedFields[fieldName as keyof Omit<Prisma.EventTypeSelect, "id">] === undefined,
|
||||
LockedIcon: shouldLockIndicator(fieldName),
|
||||
LockedIcon: useShouldLockIndicator(fieldName, options),
|
||||
isLocked,
|
||||
};
|
||||
};
|
||||
|
||||
return { shouldLockIndicator, shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType };
|
||||
const useLockedSwitch = (fieldName: string, options = { simple: false }) => {
|
||||
if (!fieldStates[fieldName]) {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
fieldStates[fieldName] = useState(getLockedInitState(fieldName));
|
||||
}
|
||||
|
||||
return () =>
|
||||
LockedSwitch(isManagedEventType, fieldStates[fieldName], fieldName, setUnlockedFields, options);
|
||||
};
|
||||
|
||||
const useShouldLockDisableProps = (fieldName: string, options?: { simple: true }) => {
|
||||
if (!fieldStates[fieldName]) {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
fieldStates[fieldName] = useState(getLockedInitState(fieldName));
|
||||
}
|
||||
return {
|
||||
disabled:
|
||||
!isManagedEventType &&
|
||||
eventType.metadata?.managedEventConfig !== undefined &&
|
||||
unlockedFields[fieldName as keyof Omit<Prisma.EventTypeSelect, "id">] === undefined,
|
||||
LockedIcon: useShouldLockIndicator(fieldName, options),
|
||||
isLocked: fieldStates[fieldName][0],
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
shouldLockIndicator: useShouldLockIndicator,
|
||||
shouldLockDisableProps: useShouldLockDisableProps,
|
||||
useLockedLabel,
|
||||
useLockedSwitch,
|
||||
isManagedEventType,
|
||||
isChildrenManagedEventType,
|
||||
};
|
||||
};
|
||||
|
||||
export default useLockedFieldsManager;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import type { DeepMockProxy } from "vitest-mock-extended";
|
||||
|
||||
import { sendSlugReplacementEmail } from "@calcom/emails/email-manager";
|
||||
@@ -121,7 +122,7 @@ export default async function handleChildrenEventTypes({
|
||||
bookingFields: _EventTypeModel.shape.bookingFields.nullish(),
|
||||
});
|
||||
|
||||
const allManagedEventTypePropsZod = _ManagedEventTypeModel.pick(allManagedEventTypeProps); //FIXME
|
||||
const allManagedEventTypePropsZod = _ManagedEventTypeModel.pick(allManagedEventTypeProps);
|
||||
const managedEventTypeValues = allManagedEventTypePropsZod
|
||||
.omit(unlockedManagedEventTypeProps)
|
||||
.parse(eventType);
|
||||
@@ -169,7 +170,6 @@ export default async function handleChildrenEventTypes({
|
||||
userIds: newUserIds,
|
||||
teamName: oldEventType.team?.name ?? null,
|
||||
});
|
||||
|
||||
// Create event types for new users added
|
||||
await prisma.$transaction(
|
||||
newUserIds.map((userId) => {
|
||||
@@ -220,6 +220,28 @@ export default async function handleChildrenEventTypes({
|
||||
teamName: oldEventType.team?.name || null,
|
||||
});
|
||||
|
||||
const { unlockedFields } = managedEventTypeValues.metadata?.managedEventConfig;
|
||||
const unlockedFieldProps = !unlockedFields
|
||||
? {}
|
||||
: Object.keys(unlockedFields).reduce((acc, key) => {
|
||||
const filteredKey =
|
||||
key === "afterBufferTime"
|
||||
? "afterEventBuffer"
|
||||
: key === "beforeBufferTime"
|
||||
? "beforeEventBuffer"
|
||||
: key;
|
||||
// @ts-expect-error Element implicitly has any type
|
||||
acc[filteredKey] = true;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Add to payload all eventType values that belong to locked fields, changed or unchanged
|
||||
// Ignore from payload any eventType values that belong to unlocked fields
|
||||
const updatePayload = allManagedEventTypePropsZod.omit(unlockedFieldProps).parse(eventType);
|
||||
const updatePayloadFiltered = Object.entries(updatePayload)
|
||||
.filter(([key, _]) => key !== "children")
|
||||
.reduce((newObj, [key, value]) => ({ ...newObj, [key]: value }), {});
|
||||
console.log({ unlockedFieldProps });
|
||||
// Update event types for old users
|
||||
const oldEventTypes = await prisma.$transaction(
|
||||
oldUserIds.map((userId) => {
|
||||
@@ -231,9 +253,8 @@ export default async function handleChildrenEventTypes({
|
||||
},
|
||||
},
|
||||
data: {
|
||||
...updatedValues,
|
||||
hashedLink: hashedLinkQuery(userId),
|
||||
hidden: children?.find((ch) => ch.owner.id === userId)?.hidden ?? false,
|
||||
...updatePayloadFiltered,
|
||||
hashedLink: "hashedLink" in unlockedFieldProps ? undefined : hashedLinkQuery(userId),
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import MakeTeamPrivateSwitch from "@calcom/features/ee/teams/components/MakeTeamPrivateSwitch";
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { UserListTable } from "@calcom/features/users/components/UserTable/UserListTable";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -11,29 +10,24 @@ import { Meta } from "@calcom/ui";
|
||||
|
||||
const MembersView = () => {
|
||||
const { t } = useLocale();
|
||||
const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery();
|
||||
const { data: currentOrg, isPending } = trpc.viewer.organizations.listCurrent.useQuery();
|
||||
|
||||
const isInviteOpen = !currentOrg?.user.accepted;
|
||||
const isOrgAdminOrOwner =
|
||||
currentOrg &&
|
||||
(currentOrg.user.role === MembershipRole.OWNER || currentOrg.user.role === MembershipRole.ADMIN);
|
||||
|
||||
const canLoggedInUserSeeMembers =
|
||||
(currentOrg?.isPrivate && isOrgAdminOrOwner) || isOrgAdminOrOwner || !currentOrg?.isPrivate;
|
||||
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<Meta title={t("organization_members")} description={t("organization_description")} />
|
||||
<div>
|
||||
{((currentOrg?.isPrivate && isOrgAdminOrOwner) || isOrgAdminOrOwner || !currentOrg?.isPrivate) && (
|
||||
<UserListTable />
|
||||
)}
|
||||
{currentOrg && isOrgAdminOrOwner && (
|
||||
<MakeTeamPrivateSwitch
|
||||
isOrg={true}
|
||||
teamId={currentOrg.id}
|
||||
isPrivate={currentOrg.isPrivate}
|
||||
disabled={isInviteOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>{!isPending && canLoggedInUserSeeMembers && <UserListTable />}</div>
|
||||
{!canLoggedInUserSeeMembers && (
|
||||
<div className="border-subtle rounded-xl border p-6">
|
||||
<h2 className="text-default">{t("only_admin_can_see_members_of_org")}</h2>
|
||||
</div>
|
||||
)}
|
||||
</LicenseRequired>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import MakeTeamPrivateSwitch from "@calcom/features/ee/teams/components/MakeTeamPrivateSwitch";
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Meta } from "@calcom/ui";
|
||||
|
||||
const PrivacyView = () => {
|
||||
const { t } = useLocale();
|
||||
const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery();
|
||||
const isOrgAdminOrOwner =
|
||||
currentOrg &&
|
||||
(currentOrg.user.role === MembershipRole.OWNER || currentOrg.user.role === MembershipRole.ADMIN);
|
||||
const isInviteOpen = !currentOrg?.user.accepted;
|
||||
|
||||
const isDisabled = isInviteOpen || !isOrgAdminOrOwner;
|
||||
|
||||
if (!currentOrg) return null;
|
||||
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<Meta
|
||||
borderInShellHeader={false}
|
||||
title={t("privacy")}
|
||||
description={t("privacy_organization_description")}
|
||||
/>
|
||||
<div>
|
||||
<MakeTeamPrivateSwitch
|
||||
isOrg={true}
|
||||
teamId={currentOrg.id}
|
||||
isPrivate={currentOrg.isPrivate}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
</LicenseRequired>
|
||||
);
|
||||
};
|
||||
PrivacyView.getLayout = getLayout;
|
||||
|
||||
export default PrivacyView;
|
||||
@@ -1,9 +1,12 @@
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import { isTextMessageToAttendeeAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
@@ -157,7 +160,7 @@ const WorkflowListItem = (props: ItemProps) => {
|
||||
content={
|
||||
t(
|
||||
workflow.readOnly && props.isChildrenManagedEventType
|
||||
? "locked_by_admin"
|
||||
? "locked_by_team_admin"
|
||||
: isActive
|
||||
? "turn_off"
|
||||
: "turn_on"
|
||||
@@ -200,11 +203,17 @@ type Props = {
|
||||
function EventWorkflowsTab(props: Props) {
|
||||
const { workflows, eventType } = props;
|
||||
const { t } = useLocale();
|
||||
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const { shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager({
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
translate: t,
|
||||
formMethods,
|
||||
});
|
||||
|
||||
const workflowsDisableProps = shouldLockDisableProps("workflows", { simple: true });
|
||||
|
||||
const lockedText = workflowsDisableProps.isLocked ? "locked" : "unlocked";
|
||||
|
||||
const { data, isPending } = trpc.viewer.workflows.list.useQuery({
|
||||
teamId: eventType.team?.id,
|
||||
userId: !isChildrenManagedEventType ? eventType.userId || undefined : undefined,
|
||||
@@ -223,14 +232,20 @@ function EventWorkflowsTab(props: Props) {
|
||||
});
|
||||
const disabledWorkflows = data.workflows.filter(
|
||||
(workflow) =>
|
||||
(!workflow.teamId || eventType.teamId === workflow.teamId) &&
|
||||
!workflows
|
||||
.map((workflow) => {
|
||||
return workflow.id;
|
||||
})
|
||||
.includes(workflow.id)
|
||||
);
|
||||
setSortedWorkflows(activeWorkflows.concat(disabledWorkflows));
|
||||
const allSortedWorkflows =
|
||||
workflowsDisableProps.isLocked && !isManagedEventType
|
||||
? activeWorkflows
|
||||
: activeWorkflows.concat(disabledWorkflows);
|
||||
setSortedWorkflows(allSortedWorkflows);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isPending]);
|
||||
|
||||
const createMutation = trpc.viewer.workflows.create.useMutation({
|
||||
@@ -254,15 +269,31 @@ function EventWorkflowsTab(props: Props) {
|
||||
<LicenseRequired>
|
||||
{!isPending ? (
|
||||
<>
|
||||
{isManagedEventType && (
|
||||
{(isManagedEventType || isChildrenManagedEventType) && (
|
||||
<Alert
|
||||
severity="neutral"
|
||||
severity={workflowsDisableProps.isLocked ? "neutral" : "green"}
|
||||
className="mb-2"
|
||||
title={t("locked_for_members")}
|
||||
message={t("locked_workflows_description")}
|
||||
title={
|
||||
<Trans i18nKey={`${lockedText}_${isManagedEventType ? "for_members" : "by_team_admins"}`}>
|
||||
{lockedText[0].toUpperCase()}
|
||||
{lockedText.slice(1)} {isManagedEventType ? "for members" : "by team admins"}
|
||||
</Trans>
|
||||
}
|
||||
actions={<div className="flex h-full items-center">{workflowsDisableProps.LockedIcon}</div>}
|
||||
message={
|
||||
<Trans
|
||||
i18nKey={`workflows_${lockedText}_${
|
||||
isManagedEventType ? "for_members" : "by_team_admins"
|
||||
}_description`}>
|
||||
{isManagedEventType ? "Members" : "You"}{" "}
|
||||
{workflowsDisableProps.isLocked
|
||||
? "will be able to see the active workflows but will not be able to edit any workflow settings"
|
||||
: "will be able to see the active workflow and will be able to edit any workflow settings"}
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{data?.workflows && data?.workflows.length > 0 ? (
|
||||
{data?.workflows && sortedWorkflows.length > 0 ? (
|
||||
<div>
|
||||
<div className="space-y-4">
|
||||
{sortedWorkflows.map((workflow) => {
|
||||
@@ -285,6 +316,7 @@ function EventWorkflowsTab(props: Props) {
|
||||
description={t("no_workflows_description")}
|
||||
buttonRaw={
|
||||
<Button
|
||||
disabled={workflowsDisableProps.isLocked && !isManagedEventType}
|
||||
target="_blank"
|
||||
color="secondary"
|
||||
onClick={() => createMutation.mutate({ teamId: eventType.team?.id })}
|
||||
|
||||
@@ -54,12 +54,19 @@ export default function WorkflowDetailsPage(props: Props) {
|
||||
if (teamId && teamId !== group.teamId) return options;
|
||||
return [
|
||||
...options,
|
||||
...group.eventTypes.map((eventType) => ({
|
||||
value: String(eventType.id),
|
||||
label: `${eventType.title} ${
|
||||
eventType.children && eventType.children.length ? `(+${eventType.children.length})` : ``
|
||||
}`,
|
||||
})),
|
||||
...group.eventTypes
|
||||
.filter(
|
||||
(evType) =>
|
||||
!evType.metadata?.managedEventConfig ||
|
||||
!!evType.metadata?.managedEventConfig.unlockedFields?.workflows ||
|
||||
!!teamId
|
||||
)
|
||||
.map((eventType) => ({
|
||||
value: String(eventType.id),
|
||||
label: `${eventType.title} ${
|
||||
eventType.children && eventType.children.length ? `(+${eventType.children.length})` : ``
|
||||
}`,
|
||||
})),
|
||||
];
|
||||
}, [] as Option[]) || [],
|
||||
[data]
|
||||
|
||||
+8
-10
@@ -10,7 +10,12 @@ export const BulkUpdateEventSchema = z.object({
|
||||
eventTypeIds: z.array(z.number()),
|
||||
});
|
||||
|
||||
export function BulkEditDefaultConferencingModal(props: { open: boolean; setOpen: (open: boolean) => void }) {
|
||||
export function BulkEditDefaultForEventsModal(props: {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
bulkUpdateFunction: ({ eventTypeIds }: { eventTypeIds: number[] }) => void;
|
||||
isPending: boolean;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const { data, isFetching } = trpc.viewer.eventTypes.bulkEventFetch.useQuery();
|
||||
@@ -21,13 +26,6 @@ export function BulkEditDefaultConferencingModal(props: { open: boolean; setOpen
|
||||
},
|
||||
});
|
||||
|
||||
const updateLocationsMutation = trpc.viewer.eventTypes.bulkUpdateToDefaultLocation.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
||||
props.setOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
const eventTypesSelected = form.watch("eventTypeIds");
|
||||
|
||||
if (isFetching || !open || !data?.eventTypes) return null;
|
||||
@@ -42,7 +40,7 @@ export function BulkEditDefaultConferencingModal(props: { open: boolean; setOpen
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={(values) => {
|
||||
updateLocationsMutation.mutate(values);
|
||||
props.bulkUpdateFunction(values);
|
||||
}}>
|
||||
<div className="flex flex-col space-y-2">
|
||||
{data.eventTypes.length > 0 && (
|
||||
@@ -81,7 +79,7 @@ export function BulkEditDefaultConferencingModal(props: { open: boolean; setOpen
|
||||
utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
||||
}}
|
||||
/>
|
||||
<Button type="submit" color="primary" loading={updateLocationsMutation.isPending}>
|
||||
<Button type="submit" color="primary" loading={props.isPending}>
|
||||
{t("update")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -7,6 +7,7 @@ import { z } from "zod";
|
||||
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
@@ -69,6 +70,7 @@ const querySchema = z.object({
|
||||
|
||||
export default function CreateEventTypeDialog({
|
||||
profileOptions,
|
||||
isOrganization,
|
||||
}: {
|
||||
profileOptions: {
|
||||
teamId: number | null | undefined;
|
||||
@@ -76,6 +78,7 @@ export default function CreateEventTypeDialog({
|
||||
image: string | undefined;
|
||||
membershipRole: MembershipRole | null | undefined;
|
||||
}[];
|
||||
isOrganization: boolean;
|
||||
}) {
|
||||
const utils = trpc.useContext();
|
||||
const { t } = useLocale();
|
||||
@@ -86,8 +89,10 @@ export default function CreateEventTypeDialog({
|
||||
const {
|
||||
data: { teamId, eventPage: pageSlug },
|
||||
} = useTypedQuery(querySchema);
|
||||
const teamProfile = profileOptions.find((profile) => profile.teamId === teamId);
|
||||
|
||||
const teamProfile = profileOptions.find((profile) => profile.teamId === teamId);
|
||||
const isSelfHosted = !HOSTED_CAL_FEATURES;
|
||||
const isEE = !!(isSelfHosted || isOrganization);
|
||||
const form = useForm<z.infer<typeof createEventTypeInput>>({
|
||||
defaultValues: {
|
||||
length: 15,
|
||||
@@ -283,7 +288,7 @@ export default function CreateEventTypeDialog({
|
||||
<p>{t("round_robin_description")}</p>
|
||||
</RadioArea.Item>
|
||||
<>
|
||||
{isAdmin && (
|
||||
{isAdmin && isEE && (
|
||||
<RadioArea.Item
|
||||
{...register("schedulingType")}
|
||||
value={SchedulingType.MANAGED}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -10,6 +11,7 @@ import { HttpError } from "@calcom/lib/http-error";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import turndown from "@calcom/lib/turndownService";
|
||||
import { EventTypeDuplicateInput } from "@calcom/prisma/zod/custom/eventtype";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Button,
|
||||
@@ -24,7 +26,7 @@ import {
|
||||
} from "@calcom/ui";
|
||||
|
||||
const querySchema = z.object({
|
||||
title: z.string(),
|
||||
title: z.string().min(1),
|
||||
description: z.string().default(""),
|
||||
slug: z.string(),
|
||||
id: z.coerce.number(),
|
||||
@@ -47,6 +49,7 @@ const DuplicateDialog = () => {
|
||||
slug: t("event_type_duplicate_copy_text", { slug }),
|
||||
...defaultValues,
|
||||
},
|
||||
resolver: zodResolver(EventTypeDuplicateInput),
|
||||
});
|
||||
const { register } = form;
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { z } from "zod";
|
||||
|
||||
import type { EventLocationType } from "@calcom/core/location";
|
||||
import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
|
||||
import type { PeriodType, SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { BookerLayoutSettings, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { customInputSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import type { IntervalLimit, RecurringEvent } from "@calcom/types/Calendar";
|
||||
|
||||
export type CustomInputParsed = typeof customInputSchema._output;
|
||||
|
||||
export type AvailabilityOption = {
|
||||
label: string;
|
||||
value: number;
|
||||
isDefault: boolean;
|
||||
isManaged?: boolean;
|
||||
};
|
||||
export type EventTypeSetupProps = RouterOutputs["viewer"]["eventTypes"]["get"];
|
||||
export type EventTypeSetup = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"];
|
||||
export type Host = { isFixed: boolean; userId: number; priority: number };
|
||||
|
||||
export type FormValues = {
|
||||
id: number;
|
||||
title: string;
|
||||
eventTitle: string;
|
||||
eventName: string;
|
||||
slug: string;
|
||||
isInstantEvent: boolean;
|
||||
length: number;
|
||||
offsetStart: number;
|
||||
description: string;
|
||||
disableGuests: boolean;
|
||||
lockTimeZoneToggleOnBookingPage: boolean;
|
||||
requiresConfirmation: boolean;
|
||||
requiresBookerEmailVerification: boolean;
|
||||
recurringEvent: RecurringEvent | null;
|
||||
schedulingType: SchedulingType | null;
|
||||
hidden: boolean;
|
||||
hideCalendarNotes: boolean;
|
||||
hashedLink: string | undefined;
|
||||
locations: {
|
||||
type: EventLocationType["type"];
|
||||
address?: string;
|
||||
attendeeAddress?: string;
|
||||
link?: string;
|
||||
hostPhoneNumber?: string;
|
||||
displayLocationPublicly?: boolean;
|
||||
phone?: string;
|
||||
hostDefault?: string;
|
||||
credentialId?: number;
|
||||
teamName?: string;
|
||||
}[];
|
||||
customInputs: CustomInputParsed[];
|
||||
schedule: number | null;
|
||||
periodType: PeriodType;
|
||||
periodDays: number;
|
||||
periodCountCalendarDays: boolean;
|
||||
periodDates: { startDate: Date; endDate: Date };
|
||||
seatsPerTimeSlot: number | null;
|
||||
seatsShowAttendees: boolean | null;
|
||||
seatsShowAvailabilityCount: boolean | null;
|
||||
seatsPerTimeSlotEnabled: boolean;
|
||||
scheduleName: string;
|
||||
minimumBookingNotice: number;
|
||||
minimumBookingNoticeInDurationType: number;
|
||||
beforeEventBuffer: number;
|
||||
afterEventBuffer: number;
|
||||
slotInterval: number | null;
|
||||
metadata: z.infer<typeof EventTypeMetaDataSchema>;
|
||||
destinationCalendar: {
|
||||
integration: string;
|
||||
externalId: string;
|
||||
};
|
||||
successRedirectUrl: string;
|
||||
durationLimits?: IntervalLimit;
|
||||
bookingLimits?: IntervalLimit;
|
||||
onlyShowFirstAvailableSlot: boolean;
|
||||
children: ChildrenEventType[];
|
||||
hosts: Host[];
|
||||
bookingFields: z.infer<typeof eventTypeBookingFields>;
|
||||
availability?: AvailabilityOption;
|
||||
bookerLayouts: BookerLayoutSettings;
|
||||
multipleDurationEnabled: boolean;
|
||||
users: EventTypeSetup["users"];
|
||||
assignAllTeamMembers: boolean;
|
||||
useEventTypeDestinationCalendarEmail: boolean;
|
||||
secondaryEmailId?: number;
|
||||
};
|
||||
@@ -92,6 +92,10 @@ const tabs: VerticalTabItemProps[] = [
|
||||
name: "members",
|
||||
href: "/settings/organizations/members",
|
||||
},
|
||||
{
|
||||
name: "privacy",
|
||||
href: "/settings/organizations/privacy",
|
||||
},
|
||||
{
|
||||
name: "appearance",
|
||||
href: "/settings/organizations/appearance",
|
||||
|
||||
@@ -5,6 +5,14 @@ import { localStorage } from "@calcom/lib/webstorage";
|
||||
import { Card } from "@calcom/ui";
|
||||
|
||||
export const tips = [
|
||||
{
|
||||
id: 13,
|
||||
thumbnailUrl: "https://img.youtube.com/vi/ihE8OYTdklg/0.jpg",
|
||||
mediaLink: "https://go.cal.com/ooo",
|
||||
title: "ooo.new",
|
||||
description: "Easily go out-of-office",
|
||||
href: "https://ooo.new",
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
thumbnailUrl: "https://cal.com/og-image-cal-ai.jpg",
|
||||
|
||||
@@ -207,6 +207,12 @@ export function UserListTable() {
|
||||
);
|
||||
},
|
||||
filterFn: (rows, id, filterValue) => {
|
||||
if (filterValue.includes("PENDING")) {
|
||||
if (filterValue.length === 1) return !rows.original.accepted;
|
||||
else return !rows.original.accepted || filterValue.includes(rows.getValue(id));
|
||||
}
|
||||
|
||||
// Show only the selected roles
|
||||
return filterValue.includes(rows.getValue(id));
|
||||
},
|
||||
},
|
||||
@@ -224,7 +230,10 @@ export function UserListTable() {
|
||||
data-testid2={`member-${username}-pending`}
|
||||
variant="red"
|
||||
className="text-xs"
|
||||
data-testid={`email-${email.replace("@", "")}-pending`}>
|
||||
data-testid={`email-${email.replace("@", "")}-pending`}
|
||||
onClick={() => {
|
||||
table.getColumn("role")?.setFilterValue(["PENDING"]);
|
||||
}}>
|
||||
Pending
|
||||
</Badge>
|
||||
)}
|
||||
@@ -351,6 +360,7 @@ export function UserListTable() {
|
||||
{ label: "Owner", value: "OWNER" },
|
||||
{ label: "Admin", value: "ADMIN" },
|
||||
{ label: "Member", value: "MEMBER" },
|
||||
{ label: "Pending", value: "PENDING" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import type z from "zod";
|
||||
|
||||
import { TITLE_FIELD } from "@calcom/features/bookings/lib/SystemField";
|
||||
import type { bookingResponse } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
export default function getLabelValueMapFromResponses(calEvent: CalendarEvent) {
|
||||
const { customInputs, userFieldsResponses } = calEvent;
|
||||
const { customInputs, userFieldsResponses, responses, eventTypeId } = calEvent;
|
||||
|
||||
const isDynamicEvent = !eventTypeId;
|
||||
|
||||
let labelValueMap: Record<string, z.infer<typeof bookingResponse>> = {};
|
||||
if (userFieldsResponses) {
|
||||
if (!!responses?.[TITLE_FIELD] && !isDynamicEvent) {
|
||||
userFieldsResponses[TITLE_FIELD] = responses[TITLE_FIELD];
|
||||
}
|
||||
|
||||
for (const [, value] of Object.entries(userFieldsResponses)) {
|
||||
if (!value.label) {
|
||||
continue;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
declare const window: Window & { dataLayer: Record<string, unknown>[] };
|
||||
|
||||
export const pushGTMEvent = (event: string, data?: Record<string, unknown>) => {
|
||||
window.dataLayer?.push({
|
||||
event,
|
||||
...data,
|
||||
});
|
||||
};
|
||||
@@ -99,6 +99,8 @@ export async function getTeamWithMembers(args: {
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
isPrivate: true,
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
children: {
|
||||
|
||||
@@ -5,23 +5,27 @@ import { prisma } from "@calcom/prisma";
|
||||
export const uploadLogo = async ({
|
||||
teamId,
|
||||
logo: data,
|
||||
isBanner = false,
|
||||
}: {
|
||||
teamId: number;
|
||||
logo: string;
|
||||
isBanner?: boolean;
|
||||
}): Promise<string> => {
|
||||
const objectKey = uuidv4();
|
||||
|
||||
await prisma.avatar.upsert({
|
||||
where: {
|
||||
teamId_userId: {
|
||||
teamId_userId_isBanner: {
|
||||
teamId,
|
||||
userId: 0,
|
||||
isBanner,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
teamId,
|
||||
data,
|
||||
objectKey,
|
||||
isBanner,
|
||||
},
|
||||
update: {
|
||||
data,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ITimezoneOption } from "react-timezone-select";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import type { ICity } from "@calcom/ui/components/form/timezone-select";
|
||||
|
||||
import isProblematicTimezone from "./isProblematicTimezone";
|
||||
@@ -32,9 +33,15 @@ export const addCitiesToDropdown = (cities: ICity[]) => {
|
||||
return cityTimezones || {};
|
||||
};
|
||||
|
||||
const formatOffset = (offset: string) =>
|
||||
offset.replace(/^([-+])(0)(\d):00$/, (_, sign, _zero, hour) => `${sign}${hour}:00`);
|
||||
|
||||
export const handleOptionLabel = (option: ITimezoneOption, cities: ICity[]) => {
|
||||
const timezoneValue = option.label.split(")")[0].replace("(", " ").replace("T", "T ");
|
||||
const offsetUnit = option.label.split("-")[0].substring(1);
|
||||
const cityName = option.label.split(") ")[1];
|
||||
const refactoredOption = option.value.replace(/_/g, " ");
|
||||
return cities.length > 0 ? `${cityName}${timezoneValue}` : `${refactoredOption}${timezoneValue}`;
|
||||
|
||||
const timezoneValue = ` ${offsetUnit} ${formatOffset(dayjs.tz(undefined, option.value).format("Z"))}`;
|
||||
return cities.length > 0
|
||||
? `${cityName}${timezoneValue}`
|
||||
: `${option.value.replace(/_/g, " ")}${timezoneValue}`;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[teamId,userId,isBanner]` on the table `avatars` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- DropIndex
|
||||
DROP INDEX "avatars_teamId_userId_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "avatars" ADD COLUMN "isBanner" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "avatars_teamId_userId_isBanner_key" ON "avatars"("teamId", "userId", "isBanner");
|
||||
@@ -1125,7 +1125,9 @@ model Avatar {
|
||||
// different every time to pop the cache.
|
||||
objectKey String @unique
|
||||
|
||||
@@unique([teamId, userId])
|
||||
isBanner Boolean @default(false)
|
||||
|
||||
@@unique([teamId, userId, isBanner])
|
||||
@@map(name: "avatars")
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user