Compare commits

..
27 Commits
Author SHA1 Message Date
Keith Williams b06e46342c v3.8.8
release.yml / release (push) Canceled after 0s
2024-03-12 13:03:00 -03:00
Crowdin Bot 4cf482f860 New Crowdin translations by Github Action 2024-03-12 16:00:52 +00:00
f2478ea7d3 feat: new privacy settings for orgs (#13693)
* moved privacy setting to new tab

* i18n

* added more privacy switches

* fix: hide org teams and members

* fix: use i18n

* fix: privacy page

* feat: also hide team mebers when org is private

* chore: remove log

* fix type error

* chore: type err

* chore: feedback

* chore: type err

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2024-03-12 15:56:43 +00:00
0e110022fb fix: open reporting in new tab full screen (#13659)
* open reporting in new tab full screen

* nit

* fixed type

* Fix failing test

* Update packages/app-store/routing-forms/pages/reporting/[...appPages].tsx

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
2024-03-12 11:46:24 -04:00
Somay Chauhan ee80577cc3 fix: username inconsistency (#14034)
* fix: username inconsistency

* added comment

* code refactor
2024-03-12 15:37:53 +00:00
Udit Takkar 8402dfa3a2 fix: display all responses (#13999)
* fix: display all responses

* fix: type err

* chore: rename variable
2024-03-12 15:30:02 +00:00
Somay ChauhanandUdit Takkar 4585199b5e fix: view team bookings (#13591)
* fix: view team bookings

* fix: edge case- common user across 2 different teams

* @SomayChauhan fix: edge case- common user across 2 different teams v2.0

* feat: added booking list test for collective event-types

* feat: added booking list test for round-robin event-types

* feat: added test for managed event types bookings

* feat: added test for individual events bookings

* refactor code

* added comments

* added isFixed to collective event hosts

* better function naming

* code refactor

* fix: assertBookingVisibleFor ignoring today's meetings

* fix: handling case for organizations

* fix: non org bookings-list test

* feat: added tests for organisations booking list

* added comments for better clarity

* booking-list in org tests

* uncommented doOnOrgDomain

* removed extra selectors

* fix: failing tests

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-03-12 11:54:07 -03:00
Udit TakkarandJoe Au-Yeung ae2446f42f fix: pending payment in mobile (#14011)
* fix: pending payment in mobile

* fix: pending paymeny badge

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2024-03-12 14:36:25 +00:00
Crowdin Bot 2f6d109a70 New Crowdin translations by Github Action 2024-03-12 14:30:43 +00:00
Udit Takkar 7ff884ca7d chore: remove idle state from test (#14063) 2024-03-12 14:26:10 +00:00
9230b2d2ca feat: update event types for default availability change (#13964)
* feat: update event types for default availability change

* chore: add toast

---------

Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-03-12 13:42:57 +00:00
Albert Borsos 1a645f3110 fix: update to cancel_event dictionary key in an event related context (#14047)
to fix context for other languages than english
2024-03-12 11:48:39 +00:00
4eb11e76ea fix: Booking confirmation: Cancelled event icon regression (#14018)
* fix: #14016

* fix: #14016

* Update yarn.lock

* fix: #14016

* fix: cancelled icon

* chore: remove svg

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-03-12 10:08:12 +00:00
Alex van Andel e3a5ce5f6e fix: Timezone Select invalid labels as DST unsupported in spacetime (#14056)
* Fix: Timezone Select invalid labels as DST unsupported in spacetime

* Missed a _ -> (empty string) replace causing invalid TZs

* fix: getTimezone tests didnt support DST
2024-03-12 04:58:52 +00:00
Crowdin Bot 12e171823b New Crowdin translations by Github Action 2024-03-12 00:14:16 +00:00
3c723d6f5b feat: Managed Events V2 (#12320)
* init

* Fix event limit interlinked switches

* wip

* WIP

* fix bugs

* lock hashed link until further notice

* lock private url for managed type to stop confusion

* fix recurring

* prettier fix

* early review fixes ...

* --WIP to send only changed form fields in updateMutation

* WIP with some type fixes

* Revert "WIP with some type fixes"

This reverts commit 00f10b772d6d08af17e7c9bd2d3601d93035d9b3.

* post merge conflict resolution fixes

* further type fix

* fixed test

* fixing e2e tests

* fix test --WIP

* attempt fix test & type --WIP

* fix duplicate locale en entry

* fix locked state persistence

* adds private URL locked/unlocked functionality

* fix tests

* Fix issue where locked labels showed up in non-managed events

* adds e2e test step

* update new test

* minor fixes

* fix test

* address changes request Part 1

fixed width on the lock badge
members default location as default selection
bugfix for offset toggle duplicate within itself

* fixes locked/unlocked label for child event types

* enable choice of destination calendar for children

* Fixes width for simple lock badge

* fix type

* fixes workflows list and apps for managed event type

* restricts creation of managed event types to EE only

* further fixes --WIP

* fix unit test for handleChildrenEventTypes

* fix test --WIP

* fix type err --WIP

* fix type err in test

* fix childevent payload

* fixes hashedLink bug

* fix test step title

* lock workflow create button when workflow is locked

* fix workflow detail

* Don't rely on parameter ordering, use object instead

* Removed console.log

* Missed one

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-03-12 00:10:08 +00:00
Udit Takkar 1482d9a9af fix: duplicate dialog (#14055) 2024-03-11 15:16:32 +00:00
Udit Takkar 0efe25feb4 fix: org banner (#14041)
* fix: org banner

* fix: type error
2024-03-11 12:16:27 +00:00
Crowdin Bot 602192e026 New Crowdin translations by Github Action 2024-03-11 10:06:54 +00:00
Joe Au-Yeung aeb54e6401 fix: Dsync bugs (#14027)
* Fix dsync create mutation

* Small fixes

* Don't throw error when deleting dsync data

* Change to deleteMany
2024-03-11 10:02:06 +00:00
Peer Richelsen b8c1d623f3 Update Tips.tsx (#14038) 2024-03-08 12:45:49 -03:00
sean-brydonandPeer Richelsen 30e4d769e3 feat: gtm push signup event (#14031)
* feat: gtm push signup event

* chore: fire only if gtm id is defined

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-03-08 15:13:29 +00:00
3babfa7cf3 feat: add pending to role filter (#14013)
* feat: add pending to role filter

* fix: logic

* chore: remove log

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-03-08 15:09:45 +00:00
sean-brydonandAlex van Andel e45ee8fab3 fix: update handler (#14032)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-03-08 14:41:05 +00:00
Alex van Andel 3c96e122b4 fix: Broken timezone-select.test.tsx after DST change (#14035)
* fix: Broken timezone-select.test.tsx after DST change

* Surpress minor lint error
2024-03-08 13:37:53 +00:00
Crowdin Bot f21a915814 New Crowdin translations by Github Action 2024-03-08 10:50:48 +00:00
Udit Takkar 3467e2272f fix: seed organization (#14030) 2024-03-08 10:47:11 +00:00
122 changed files with 2165 additions and 613 deletions
+20
View File
@@ -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};
+13 -2
View File
@@ -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)}
+32 -15
View File
@@ -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}&nbsp;</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);
+44 -30
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.8.7",
"version": "3.8.8",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+21 -1
View File
@@ -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}
/>
)}
</>
)}
</>
+1 -1
View File
@@ -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;
+4 -1
View File
@@ -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 = `${
+11 -3
View File
@@ -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">
+209
View File
@@ -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,
};
};
+1 -1
View File
@@ -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?: {
+51
View File
@@ -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();
};
+88 -5
View File
@@ -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,
},
},
});
-2
View File
@@ -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?"
}
}
+15 -5
View File
@@ -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": "Έχετε ήδη λογαριασμό;"
}
}
+23 -9
View File
@@ -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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
+15 -5
View File
@@ -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?"
}
}
+15 -5
View File
@@ -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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
+15 -5
View File
@@ -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": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 請在此處新增您的字串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
+2 -2
View File
@@ -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
}
}
+30 -15
View File
@@ -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]
@@ -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;
+90
View File
@@ -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",
+8
View File
@@ -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;
+8
View File
@@ -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 -1
View File
@@ -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,
+10 -3
View File
@@ -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");
+3 -1
View File
@@ -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