Compare commits

..
29 Commits
Author SHA1 Message Date
Alex van Andel 4f14775a4d chore: Release v4.6.5 (#17174)
release.yml / release (push) Canceled after 0s
2024-10-18 19:01:15 +00:00
Hariom BalharaandMorgan acbcd59c31 perf: Attributes Query - loadTree only once (#17172)
* chore: increase pagination managed users endpoint (#17147)

* perf: Build tree once per call

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-10-18 18:45:12 +01:00
Hariom BalharaandMorgan 05c1ad885b fix: Embed - Do not reuse the modal on reopening it. (#17171)
* chore: increase pagination managed users endpoint (#17147)

* Dont reuse the existing modal

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-10-18 16:42:04 +00:00
Alex van Andel d73d0ebb6e chore: Release v4.6.4 (#17170)
release.yml / release (push) Canceled after 0s
2024-10-18 15:35:06 +00:00
Hariom BalharaandMorgan a3971c127b fix: Timings of attributes query endpoint (#17169)
* chore: increase pagination managed users endpoint (#17147)

* Log timings

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-10-18 15:10:28 +00:00
Udit Takkar 912d3cf5b2 fix: schedule no show webhooks for requires confirmation event (#17089) 2024-10-18 07:50:15 -07:00
73c35ba5a2 fix: data table fixes (#17141)
* tidy up css

* sticky header

* add sticky top and left on columns

* use group for cell hover when overflowing

* strickly type meta as override isnt working

* fix: type err

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2024-10-18 12:41:21 +00:00
Keith Williams 55f1019908 chore: v4.6.3 (#17161)
release.yml / release (push) Canceled after 0s
2024-10-18 09:01:23 -03:00
Keith Williams 863fc695ec perf: Add index for invalid column in Credential (#17162)
* perf: Add index for invalid column in Credential

* Added missing file change
2024-10-18 11:42:54 +00:00
Keith Williams d931fb1f9c perf: Add indices to Membership (#17160) 2024-10-18 08:31:16 -03:00
Hariom Balhara fd054dfb8d fix: select_any_in operator - Broken only for attributes (#17148)
* Fix select any_in operator for attributes - Broken only for attributres

* Fix ts error
2024-10-18 10:19:25 +00:00
Hariom BalharaandMorgan 699e08b2ee fix: Possible perf improvement and logging for team members routing logic (#17154)
* chore: increase pagination managed users endpoint (#17147)

* Add server-timings and used concurrency 2 for teamMembers matching

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-10-18 09:44:50 +00:00
Alex van Andel e1228ca03c fix: Hide invite_new_member_description cost from users within an organisation (#17149) 2024-10-18 00:22:40 +01:00
Morgan 9ad69c20cf fix: event-types locations output schema apiv2 (#17150) 2024-10-17 21:07:31 +03:00
Somay Chauhan 9a5efd39ce feat: event team Assignment tab atom (#17145)
* feat: event team Assignment tab atom

* Update event-types-atom.service.ts

* Update event-types-atom.service.ts
2024-10-17 17:33:59 +00:00
Morgan 497b0804d3 feat: create event type atom (#17142)
* feat: create event type atom

* fixup! feat: create event type atom

* fixup! fixup! feat: create event type atom

* docs: base doc for create event type atom
2024-10-17 21:51:23 +05:30
Morgan 8b50f419e5 chore: increase pagination managed users endpoint (#17147) 2024-10-17 14:32:35 +00:00
Alex van Andel 9208827406 chore: Release v4.6.2 (#17146)
release.yml / release (push) Canceled after 0s
2024-10-17 15:22:59 +01:00
Udit Takkar 8891381d89 chore: add logs for debugging (#17111)
* chore: add logs

* fix: filter in prisma query

* test: add test for personal event types

* chore: remove OR condition
2024-10-17 13:43:38 +00:00
Hariom Balhara ab39f0c8b6 Add support for multiselect_some_in for attributes query (#17144) 2024-10-17 12:16:34 +00:00
Vadim Rusin cd2382e091 fix: Add error message for taken username (#17129) 2024-10-17 04:44:52 +00:00
Syed Ali Shahbaz 154c54a2aa fix: Update openapi-v1.json bookings post example (#17132)
* Update openapi-v1.json

* Update openapi-v1.json
2024-10-16 20:59:10 -03:00
sean-brydon e949a4c529 fix: pass from org link to all instances of event.public (#17136)
* pass from org link to all instances of event.public

* fix type error

* fix type error
2024-10-16 14:57:39 +01:00
64b321ac9c feat: added custom class name to booker and availability atom (#17105)
* added custom class name for booker atoms wrapper

* atomsWrapper

* added props to availability atom

* allowDelete
* allowSetToDefault
* added customClassNames -> hiddenSwitchClassname, deleteButtonClassname

* Update availability.tsx

* fixup! Merge branch 'main' into add-custom-classname-to-booker-atom-wrapper

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
2024-10-16 12:04:33 +00:00
e90e060739 feat: Event availability platform wrapper (#17091)
* moved listSchedules to schedule repository

* Create EventAvailabilityTabPlatformWrapper.tsx

* Revert "moved listSchedules to schedule repository"

This reverts commit b5feacfabbadbf9d75eb205dfad8f70b7773a5cb.

* fix: active state of tabs items

* added event availability platform wrapper

* Update event-types.tsx

* better types

only pick the types we need and omit the rest

* fix: type-check failing

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2024-10-16 13:24:13 +03:00
Hariom Balhara 123074f4ba fix: Hotfix - Revert hideEventTypeDetails handling in Booker.tsx (#17130)
* Revert "fix: DatePicker not showing up in mobile mode when hideEventTypeDetails is true (#17037)"

This reverts commit 1f08bc5a4f.

* Revert "feat: Booker atom enable hiding event details (#16906)"

This reverts commit ba5b9e951a.

* Keep the playground test scenarios
2024-10-16 10:01:04 +00:00
MorganandKeith Williams 8115c86e37 chore: refactor create event -type forms (#17106)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-10-16 08:15:54 +00:00
Syed Ali Shahbaz 92a48b466c chore: developer docs updates (#17112)
* remove mention of data proxy

* fix self hosting broken app link

* fix booking doc

* deploy buttons and more options

* fix capitalization
2024-10-15 22:50:54 +00:00
Calcom BotandReplexica 7a4298dc00 feat: update translations (#17120)
Co-authored-by: Replexica <support@replexica.com>
2024-10-15 22:28:53 +00:00
118 changed files with 2346 additions and 716 deletions
+1 -1
View File
@@ -28,7 +28,7 @@
"dependencies": {
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.45",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.46",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -3,13 +3,14 @@ import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06
import { AtomsController } from "@/modules/atoms/controllers/atoms.controller";
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersService } from "@/modules/users/services/users.service";
import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, EventTypesModule_2024_06_14],
imports: [PrismaModule, EventTypesModule_2024_06_14, OrganizationsModule],
providers: [
EventTypesAtomService,
MembershipsRepository,
@@ -64,4 +64,20 @@ export class AtomsController {
data: eventType,
};
}
@Patch("/organizations/:organizationId/teams/:teamId/event-types/:eventTypeId")
@Version(VERSION_NEUTRAL)
@UseGuards(ApiAuthGuard)
async updateAtomTeamEventType(
@GetUser() user: UserWithProfile,
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
@Param("teamId", ParseIntPipe) teamId: number,
@Body() body: UpdateEventTypeReturn
): Promise<ApiResponse<UpdateEventTypeReturn>> {
const eventType = await this.eventTypesService.updateTeamEventType(eventTypeId, body, user, teamId);
return {
status: SUCCESS_STATUS,
data: eventType,
};
}
}
@@ -1,10 +1,11 @@
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, NotFoundException } from "@nestjs/common";
import { Injectable, NotFoundException, ForbiddenException } from "@nestjs/common";
import {
updateEventType,
@@ -21,11 +22,11 @@ export class EventTypesAtomService {
private readonly usersService: UsersService,
private readonly dbWrite: PrismaWriteService,
private readonly dbRead: PrismaReadService,
private readonly eventTypeService: EventTypesService_2024_06_14
private readonly eventTypeService: EventTypesService_2024_06_14,
private readonly teamEventTypeService: OrganizationsEventTypesService
) {}
async getUserEventType(user: UserWithProfile, eventTypeId: number) {
this.eventTypeService.checkUserOwnsEventType(user.id, { id: eventTypeId, userId: user.id });
const organizationId = this.usersService.getUserMainOrgId(user);
const isUserOrganizationAdmin = organizationId
@@ -45,12 +46,22 @@ export class EventTypesAtomService {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
this.eventTypeService.checkUserOwnsEventType(user.id, eventType.eventType);
if (eventType?.team?.id) {
await this.checkTeamOwnsEventType(user.id, eventType.eventType.id, eventType.team.id);
} else {
this.eventTypeService.checkUserOwnsEventType(user.id, eventType.eventType);
}
return eventType;
}
async updateEventType(eventTypeId: number, body: TUpdateEventTypeInputSchema, user: UserWithProfile) {
this.eventTypeService.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId);
async updateTeamEventType(
eventTypeId: number,
body: TUpdateEventTypeInputSchema,
user: UserWithProfile,
teamId: number
) {
await this.checkCanUpdateTeamEventType(user.id, eventTypeId, teamId, body.scheduleId);
const eventTypeUser = await this.eventTypeService.getUserToUpdateEvent(user);
const bookingFields = [...(body.bookingFields || [])];
@@ -77,4 +88,62 @@ export class EventTypesAtomService {
return eventType.eventType;
}
async updateEventType(eventTypeId: number, body: TUpdateEventTypeInputSchema, user: UserWithProfile) {
await this.eventTypeService.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId);
const eventTypeUser = await this.eventTypeService.getUserToUpdateEvent(user);
const bookingFields = [...(body.bookingFields || [])];
if (
!bookingFields.find((field) => field.type === "email") &&
!bookingFields.find((field) => field.type === "phone")
) {
bookingFields.push(systemBeforeFieldEmail);
}
const eventType = await updateEventType({
input: { id: eventTypeId, ...body, bookingFields },
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prisma: this.dbWrite.prisma,
},
});
if (!eventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
return eventType.eventType;
}
async checkCanUpdateTeamEventType(userId: number, eventTypeId: number, teamId: number, scheduleId: number) {
await this.checkTeamOwnsEventType(userId, eventTypeId, teamId);
await this.teamEventTypeService.validateEventTypeExists(teamId, eventTypeId);
await this.eventTypeService.checkUserOwnsSchedule(userId, scheduleId);
}
async checkTeamOwnsEventType(userId: number, eventTypeId: number, teamId: number) {
const membership = await this.dbRead.prisma.membership.findFirst({
where: {
userId,
teamId,
accepted: true,
OR: [{ role: "ADMIN" }, { role: "OWNER" }],
},
select: {
team: {
select: {
eventTypes: true,
},
},
},
});
if (!membership?.team?.eventTypes?.some((item) => item.id === eventTypeId)) {
throw new ForbiddenException(
`Access denied. Either the team with ID=${teamId} does not own the event type with ID=${eventTypeId}, or your MEMBER role does not have permission to access this resource.`
);
}
}
}
@@ -162,6 +162,7 @@ describe("Organizations Team Endpoints", () => {
const responseBody: ApiSuccessResponse<OrgMeTeamOutputDto[]> = response.body;
expect(responseBody.data.find((t) => t.id === teamCreatedViaApi.id)).toBeDefined();
expect(responseBody.data.some((t) => t.accepted)).toBeTruthy();
expect(responseBody.data.find((t) => t.id === teamCreatedViaApi.id)?.role).toBe("OWNER");
});
});
@@ -67,7 +67,7 @@ export class OrganizationsTeamsController {
}
@Get("/me")
@ApiOperation({ summary: "Get team membership for user" })
@ApiOperation({ summary: "Get teams membership for user" })
@Roles("ORG_MEMBER")
@PlatformPlan("ESSENTIALS")
async getMyTeams(
@@ -84,13 +84,14 @@ export class OrganizationsTeamsController {
);
return {
status: SUCCESS_STATUS,
data: teams.map((team) =>
plainToClass(
data: teams.map((team) => {
const me = team.members.find((member) => member.userId === user.id);
return plainToClass(
OrgMeTeamOutputDto,
{ ...team, accepted: team.members.find((member) => member.userId === user.id)?.accepted ?? false },
me ? { ...team, role: me.role, accepted: me.accepted } : team,
{ strategy: "excludeAll" }
)
),
);
}),
};
}
@@ -100,6 +100,7 @@ import { Module } from "@nestjs/common";
OrganizationsWebhooksService,
WebhooksRepository,
WebhooksService,
OrganizationsEventTypesService,
],
controllers: [
OrganizationsTeamsController,
@@ -1,4 +1,5 @@
import { ApiProperty } from "@nestjs/swagger";
import { MembershipRole } from "@prisma/client";
import { Expose, Type } from "class-transformer";
import { IsEnum, IsString, ValidateNested } from "class-validator";
@@ -9,6 +10,14 @@ export class OrgMeTeamOutputDto extends OrgTeamOutputDto {
@IsString()
@Expose()
readonly accepted!: boolean;
@ApiProperty({
example: MembershipRole.MEMBER,
enum: [MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER],
})
@IsEnum([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
@Expose()
readonly role!: MembershipRole;
}
export class OrgTeamsOutputResponseDto {
@@ -97,7 +97,7 @@ export class OrganizationsTeamsRepository {
},
},
include: {
members: { select: { accepted: true, userId: true } },
members: { select: { accepted: true, userId: true, role: true } },
},
skip,
take,
+12 -1
View File
@@ -2005,7 +2005,7 @@
"/v2/organizations/{orgId}/teams/me": {
"get": {
"operationId": "OrganizationsTeamsController_getMyTeams",
"summary": "Get team membership for user",
"summary": "Get teams membership for user",
"parameters": [
{
"name": "orgId",
@@ -12223,6 +12223,12 @@
"type": "string"
}
},
"meetingUrl": {
"type": "string",
"description": "Deprecated - use 'location' instead. Meeting URL just for this booking. Displayed in email and calendar event. If not provided then cal video link will be generated.",
"example": "https://example.com/meeting",
"deprecated": true
},
"location": {
"type": "string",
"description": "Location for this booking. Displayed in email and calendar event.",
@@ -12234,6 +12240,11 @@
"example": {
"customField": "customValue"
}
},
"recurrenceCount": {
"type": "number",
"description": "The number of recurrences. If not provided then event type recurrence count will be used. Can't be more than\n event type recurrence count",
"example": 5
}
},
"required": [
@@ -212,7 +212,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
autoCorrect="none"
disabled={disabled}
className={classNames(
"border-l-1 mb-0 mt-0 rounded-md rounded-l-none font-sans text-sm leading-4 focus:!ring-0",
"border-l-1 my-0 rounded-md rounded-l-none font-sans text-sm leading-4 focus:!ring-0",
isInputUsernamePremium
? "border border-orange-400 focus:border focus:border-orange-400"
: "border focus:border",
@@ -262,7 +262,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
)}
</div>
{paymentMsg}
{markAsError && <p className="mt-1 text-xs text-red-500">Username is already taken</p>}
{markAsError && <p className="mt-1 text-xs text-red-500">{t("username_already_taken")}</p>}
<Dialog open={openDialogSaveUsername}>
<DialogContent
@@ -2,9 +2,12 @@
import { useRouter } from "next/navigation";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import { TeamEventTypeForm } from "@calcom/features/ee/teams/components/TeamEventTypeForm";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useCreateEventType } from "@calcom/lib/hooks/useCreateEventType";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { WizardLayout } from "@calcom/ui";
import { Button, showToast } from "@calcom/ui";
@@ -35,13 +38,29 @@ export const CreateTeamEventType = () => {
);
};
const { data: team } = trpc.viewer.teams.get.useQuery(
{ teamId: teamId ?? -1, isOrg: false },
{ enabled: !!teamId }
);
const { form, createMutation, isManagedEventType } = useCreateEventType(onSuccessMutation, onErrorMutation);
const orgBranding = useOrgBranding();
const urlPrefix = orgBranding?.fullDomain ?? process.env.NEXT_PUBLIC_WEBSITE_URL;
return (
<TeamEventTypeForm
isTeamAdminOrOwner={true}
teamSlug={team?.slug}
teamId={teamId}
isTeamAdminOrOwner={true}
urlPrefix={urlPrefix}
isPending={createMutation.isPending}
form={form}
isManagedEventType={isManagedEventType}
handleSubmit={(values) => {
createMutation.mutate(values);
}}
SubmitButton={SubmitButton}
onSuccessMutation={onSuccessMutation}
onErrorMutation={onErrorMutation}
/>
);
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "4.6.1",
"version": "4.6.5",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "إعادة تثبيت التطبيق",
"reschedule_reason": "سبب إعادة الجدولة",
"choose_common_schedule_team_event": "اختر جدولًا زمنيًا مشتركًا",
"choose_common_schedule_team_event_description": "قم بتمكين هذا الخيار إذا كنت ترغب في استخدام جدول زمني مشترك بين المضيفين. عند تعطيل هذا الخيار، سيتم حجز كل مضيف على أساس جدوله الافتراضي.",
"choose_common_schedule_team_event_description": "قم بتمكين هذا إذا كنت ترغب في استخدام جدول مشترك بين المضيفين. عند التعطيل، سيتم حجز كل مضيف بناءً على جدوله الافتراضي أو الجدول الذي تم اختياره.",
"reason": "السبب",
"sender_id": "معرّف المرسل",
"sender_id_error_message": "يسمح بالحروف والأرقام والمسافات فقط (بحد أقصى 11 حرفًا)",
@@ -2595,6 +2595,7 @@
"new_option": "خيار جديد",
"update_profile": "تحديث العضو",
"attribute_updated_successfully": "تم تحديث السمة بنجاح",
"attribute_deleted_successfully": "تم حذف السمة بنجاح",
"attributes_edited_successfully": "تم تعديل السمات بنجاح",
"attribute_meta_description": "إدارة السمات لأعضاء فريقك",
"attributes_edit_description": "تعديل السمات لأعضاء فريقك",
@@ -2640,6 +2641,9 @@
"month_to_date": "منذ بداية الشهر",
"year_to_date": "منذ بداية السنة",
"custom_range": "نطاق مخصص",
"show_all_columns": "عرض جميع الأعمدة",
"toggle_columns": "تبديل الأعمدة",
"no_columns_found": "لم يتم العثور على أعمدة",
"salesforce_create_record_as": "عند الحجز، أضف الأحداث والمشاركين الجدد كالتالي:",
"salesforce_lead": "عميل محتمل",
"salesforce_contact_under_account": "جهة اتصال تحت حساب",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "إذا كان المشارك موجودًا في Salesforce، احجز مباشرة مع المالك",
"salesforce_check_owner_of": "الكيان الذي يجب التحقق من مالكه للحجز مباشرة",
"account": "حساب",
"choose_hosts_schedule": "اختر جداول المضيفين",
"hosts_schedule_description": "يمكنك اختيار جدول مختلف لكل مضيف. يتم اختيار جدول المضيف الافتراضي لكل مضيف بشكل افتراضي.",
"no_hosts_description": "لا يوجد مضيفون معينون، أضف مضيفين في علامة التبويب التعيين.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "Tətbiqi yenidən quraşdırın",
"reschedule_reason": "Yenidən planlaşdırma səbəbi",
"choose_common_schedule_team_event": "Ümumi cədvəli seçin",
"choose_common_schedule_team_event_description": "Əgər ev sahibləri arasında ümumi cədvəldən istifadə etmək istəyirsinizsə, bunu aktiv edin. Deaktiv edildikdə, hər bir ev sahibi öz standart cədvəlinə əsasən rezervasiya ediləcək.",
"choose_common_schedule_team_event_description": "Əgər hostlar arasında ümumi cədvəldən istifadə etmək istəyirsinizsə, bunu aktivləşdirin. Deaktiv edildikdə, hər host öz standart cədvəlinə və ya seçilmiş cədvəlinə əsasən rezervasiya ediləcək.",
"reason": "Səbəb",
"sender_id": "Göndərən ID",
"sender_id_error_message": "Yalnız hərflər, rəqəmlər və boşluqlar icazəlidir (maks. 11 simvol)",
@@ -2595,6 +2595,7 @@
"new_option": "Yeni seçim",
"update_profile": "Üzvü yenilə",
"attribute_updated_successfully": "Atribut uğurla yeniləndi",
"attribute_deleted_successfully": "Atribut uğurla silindi",
"attributes_edited_successfully": "Atributlar uğurla redaktə edildi",
"attribute_meta_description": "Komanda üzvləriniz üçün atributları idarə edin",
"attributes_edit_description": "Komanda üzvləriniz üçün atributları redaktə edin",
@@ -2640,6 +2641,9 @@
"month_to_date": "bu ayın tarixinə qədər",
"year_to_date": "bu ilin tarixinə qədər",
"custom_range": "xüsusi aralıq",
"show_all_columns": "Bütün sütunları göstər",
"toggle_columns": "Sütunları dəyiş",
"no_columns_found": "Sütun tapılmadı",
"salesforce_create_record_as": "Rezervasiya zamanı tədbirləri əlavə edin və yeni iştirakçıları belə qeyd edin:",
"salesforce_lead": "Potensial müştəri",
"salesforce_contact_under_account": "Hesab altında əlaqə",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Əgər iştirakçı Salesforce-da mövcuddursa, birbaşa sahib ilə rezervasiya edin",
"salesforce_check_owner_of": "Birbaşa rezervasiya etmək üçün sahibini yoxlayacaq obyekt",
"account": "Hesab",
"choose_hosts_schedule": "Hostların cədvəlini seçin",
"hosts_schedule_description": "Hər bir host üçün fərqli cədvəl seçə bilərsiniz. Hər hostun standart cədvəli avtomatik olaraq seçilir.",
"no_hosts_description": "Host təyin edilməyib, təyinat sekmesinde host əlavə edin.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni sətirləri bura əlavə edin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "Přeinstalovat aplikaci",
"reschedule_reason": "Důvod změny termínu",
"choose_common_schedule_team_event": "Vyberte společný plán",
"choose_common_schedule_team_event_description": "Tuto možnost povolte, pokud chcete používat společný plán mezi hostiteli. Při zakázání této možnosti bude každý hostitel rezervován na základě svého výchozího plánu.",
"choose_common_schedule_team_event_description": "Povolte tuto možnost, pokud chcete použít společný rozvrh mezi hostiteli. Pokud je zakázána, každý hostitel bude rezervován podle svého výchozího rozvrhu nebo zvoleného rozvrhu.",
"reason": "Důvod",
"sender_id": "ID odesílatele",
"sender_id_error_message": "Povolena jsou pouze písmena, číslice a mezery (max. 11 znaků)",
@@ -2595,6 +2595,7 @@
"new_option": "Nová možnost",
"update_profile": "Aktualizovat člena",
"attribute_updated_successfully": "Atribut úspěšně aktualizován",
"attribute_deleted_successfully": "Atribut byl úspěšně smazán",
"attributes_edited_successfully": "Atributy úspěšně upraveny",
"attribute_meta_description": "Spravujte atributy pro členy svého týmu",
"attributes_edit_description": "Upravte atributy pro členy svého týmu",
@@ -2640,6 +2641,9 @@
"month_to_date": "od začátku měsíce",
"year_to_date": "od začátku roku",
"custom_range": "vlastní rozsah",
"show_all_columns": "Zobrazit všechny sloupce",
"toggle_columns": "Přepnout sloupce",
"no_columns_found": "Žádné sloupce nebyly nalezeny",
"salesforce_create_record_as": "Při rezervaci přidejte události a nové účastníky jako:",
"salesforce_lead": "Lead",
"salesforce_contact_under_account": "Kontakt pod účtem",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Pokud účastník existuje v Salesforce, rezervujte přímo s vlastníkem",
"salesforce_check_owner_of": "Entita ke kontrole vlastníka pro přímou rezervaci",
"account": "Účet",
"choose_hosts_schedule": "Vyberte rozvrhy hostitelů",
"hosts_schedule_description": "Můžete vybrat jiný rozvrh pro každého hostitele. Výchozí rozvrh každého hostitele je vybrán automaticky.",
"no_hosts_description": "Žádní hostitelé nejsou přiřazeni, přidejte hostitele na kartě přiřazení.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "Geninstaller app",
"reschedule_reason": "Årsag til omlægning",
"choose_common_schedule_team_event": "Vælg en fælles tidsplan",
"choose_common_schedule_team_event_description": "Aktivér dette hvis du ønsker at bruge en fælles tidsplan mellem værter. Når deaktiveret, vil hver vært blive booket baseret på deres standardtidsplan.",
"choose_common_schedule_team_event_description": "Aktivér dette, hvis du vil bruge en fælles tidsplan mellem værter. Når det er deaktiveret, vil hver vært blive booket baseret på deres standardtidsplan eller deres valgte tidsplan.",
"reason": "Årsag",
"sender_id": "Afsender ID",
"sender_id_error_message": "Kun bogstaver, tal og mellemrum tilladt (maks. 11 tegn)",
@@ -2595,6 +2595,7 @@
"new_option": "Ny mulighed",
"update_profile": "Opdater medlem",
"attribute_updated_successfully": "Egenskab opdateret med succes",
"attribute_deleted_successfully": "Attribut slettet med succes",
"attributes_edited_successfully": "Egenskaber redigeret med succes",
"attribute_meta_description": "Administrer egenskaber for dine teammedlemmer",
"attributes_edit_description": "Rediger egenskaber for dine teammedlemmer",
@@ -2640,6 +2641,9 @@
"month_to_date": "måned til dato",
"year_to_date": "år til dato",
"custom_range": "tilpasset interval",
"show_all_columns": "Vis alle kolonner",
"toggle_columns": "Skift kolonner",
"no_columns_found": "Ingen kolonner fundet",
"salesforce_create_record_as": "Ved booking, tilføj begivenheder og nye deltagere som:",
"salesforce_lead": "Lead",
"salesforce_contact_under_account": "Kontakt under en konto",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Hvis deltageren findes i Salesforce, book direkte med ejeren",
"salesforce_check_owner_of": "Enhed til at tjekke ejeren af for at booke direkte",
"account": "Konto",
"choose_hosts_schedule": "Vælg værternes tidsplaner",
"hosts_schedule_description": "Du kan vælge en anden tidsplan for hver vært. Hver værts standardtidsplan er valgt som standard.",
"no_hosts_description": "Ingen værter tildelt, tilføj værter i tildelingsfanen.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Tilføj dine nye strenge ovenfor her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"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.",
"choose_common_schedule_team_event_description": "Aktivieren Sie dies, wenn Sie einen gemeinsamen Zeitplan für alle Gastgeber verwenden möchten. Wenn deaktiviert, wird jeder Gastgeber basierend auf seinem Standardzeitplan oder seinem gewählten Zeitplan gebucht.",
"reason": "Grund",
"sender_id": "Absender-ID",
"sender_id_error_message": "Nur Buchstaben, Zahlen und Leerzeichen erlaubt (max. 11 Zeichen)",
@@ -2595,6 +2595,7 @@
"new_option": "Neue Option",
"update_profile": "Mitglied aktualisieren",
"attribute_updated_successfully": "Attribut erfolgreich aktualisiert",
"attribute_deleted_successfully": "Attribut erfolgreich gelöscht",
"attributes_edited_successfully": "Attribute erfolgreich bearbeitet",
"attribute_meta_description": "Verwalten Sie Attribute für Ihre Teammitglieder",
"attributes_edit_description": "Bearbeiten Sie Attribute für Ihre Teammitglieder",
@@ -2640,6 +2641,9 @@
"month_to_date": "aktueller Monat",
"year_to_date": "aktuelles Jahr",
"custom_range": "benutzerdefinierter Zeitraum",
"show_all_columns": "Alle Spalten anzeigen",
"toggle_columns": "Spalten ein-/ausblenden",
"no_columns_found": "Keine Spalten gefunden",
"salesforce_create_record_as": "Bei der Buchung Ereignisse und neue Teilnehmer hinzufügen als:",
"salesforce_lead": "Lead",
"salesforce_contact_under_account": "Kontakt unter einem Account",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Wenn der Teilnehmer in Salesforce existiert, buchen Sie direkt beim Besitzer",
"salesforce_check_owner_of": "Entität, deren Besitzer für die direkte Buchung überprüft werden soll",
"account": "Account",
"choose_hosts_schedule": "Zeitpläne der Gastgeber auswählen",
"hosts_schedule_description": "Sie können für jeden Gastgeber einen anderen Zeitplan auswählen. Standardmäßig wird der Standardzeitplan jedes Gastgebers ausgewählt.",
"no_hosts_description": "Keine Gastgeber zugewiesen. Fügen Sie Gastgeber im Zuweisungs-Tab hinzu.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "Reinstalar la aplicación",
"reschedule_reason": "Razón para reprogramar",
"choose_common_schedule_team_event": "Elige un horario común",
"choose_common_schedule_team_event_description": "Habilita esto si deseas usar un horario común entre los anfitriones. Cuando está deshabilitado, cada anfitrión será reservado según su horario predeterminado.",
"choose_common_schedule_team_event_description": "Activa esto si deseas usar un horario común entre los anfitriones. Cuando está desactivado, cada anfitrión será reservado según su horario predeterminado o el horario que hayan elegido.",
"reason": "Razón",
"sender_id": "ID del remitente",
"sender_id_error_message": "Solo se permiten letras, números y espacios (máx. 11 caracteres)",
@@ -2595,6 +2595,7 @@
"new_option": "Nueva opción",
"update_profile": "Actualizar miembro",
"attribute_updated_successfully": "Atributo actualizado con éxito",
"attribute_deleted_successfully": "Atributo eliminado exitosamente",
"attributes_edited_successfully": "Atributos editados con éxito",
"attribute_meta_description": "Gestiona los atributos de los miembros de tu equipo",
"attributes_edit_description": "Edita los atributos de los miembros de tu equipo",
@@ -2640,6 +2641,9 @@
"month_to_date": "mes hasta la fecha",
"year_to_date": "año hasta la fecha",
"custom_range": "rango personalizado",
"show_all_columns": "Mostrar todas las columnas",
"toggle_columns": "Alternar columnas",
"no_columns_found": "No se encontraron columnas",
"salesforce_create_record_as": "Al reservar, agregar eventos y nuevos asistentes como:",
"salesforce_lead": "Cliente potencial",
"salesforce_contact_under_account": "Contacto bajo una cuenta",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Si el asistente existe en Salesforce, reservar directamente con el propietario",
"salesforce_check_owner_of": "Entidad de la cual verificar el propietario para reservar directamente",
"account": "Cuenta",
"choose_hosts_schedule": "Elegir horarios de los anfitriones",
"hosts_schedule_description": "Puedes seleccionar un horario diferente para cada anfitrión. Por defecto, se elige el horario predeterminado de cada anfitrión.",
"no_hosts_description": "No hay anfitriones asignados, agrega anfitriones en la pestaña de asignación.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agrega tus nuevas cadenas arriba de esta línea ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "Reinstalar aplicación",
"reschedule_reason": "Razón de la reprogramación",
"choose_common_schedule_team_event": "Elige un horario común para todos",
"choose_common_schedule_team_event_description": "Habilite esto si desea utilizar un cronograma común entre anfitriones. Cuando esté desactivado, cada anfitrión se reservará en relación a su horario predeterminado.",
"choose_common_schedule_team_event_description": "Activa esta opción si deseas utilizar un horario común entre los anfitriones. Cuando está desactivada, cada anfitrión será reservado según su horario predeterminado o el horario que haya elegido.",
"reason": "Motivo",
"sender_id": "ID del remitente",
"sender_id_error_message": "Sólo se permiten letras, números y espacios (máx. 11 caracteres)",
@@ -2595,6 +2595,7 @@
"new_option": "Nueva opción",
"update_profile": "Actualizar miembro",
"attribute_updated_successfully": "Atributo actualizado con éxito",
"attribute_deleted_successfully": "Atributo eliminado con éxito",
"attributes_edited_successfully": "Atributos editados con éxito",
"attribute_meta_description": "Gestionar atributos para los miembros de tu equipo",
"attributes_edit_description": "Editar atributos para los miembros de tu equipo",
@@ -2640,6 +2641,9 @@
"month_to_date": "mes hasta la fecha",
"year_to_date": "año hasta la fecha",
"custom_range": "rango personalizado",
"show_all_columns": "Mostrar todas las columnas",
"toggle_columns": "Alternar columnas",
"no_columns_found": "No se encontraron columnas",
"salesforce_create_record_as": "Al reservar, agregar eventos y nuevos asistentes como:",
"salesforce_lead": "Cliente potencial",
"salesforce_contact_under_account": "Contacto bajo una cuenta",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Si el asistente existe en Salesforce, reservar directamente con el propietario",
"salesforce_check_owner_of": "Entidad de la que verificar el propietario para reservar directamente",
"account": "Cuenta",
"choose_hosts_schedule": "Elegir horarios de los anfitriones",
"hosts_schedule_description": "Puedes seleccionar un horario diferente para cada anfitrión. Por defecto, se elige el horario predeterminado de cada anfitrión.",
"no_hosts_description": "No hay anfitriones asignados, añade anfitriones en la pestaña de asignación.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agregue sus nuevas cadenas arriba ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "installige rakendus uuesti",
"reschedule_reason": "Ajastamise põhjus",
"choose_common_schedule_team_event": "Valige ühine ajakava",
"choose_common_schedule_team_event_description": "Lubage see, kui soovite kasutada hostide vahel ühist ajakava. Kui see on keelatud, broneeritakse iga host nende vaikegraafiku alusel.",
"choose_common_schedule_team_event_description": "Luba see, kui soovid võõrustajate vahel kasutada ühist ajakava. Kui see on keelatud, broneeritakse iga võõrustaja aeg vastavalt nende vaikimisi ajakavale või nende valitud ajakavale.",
"reason": "Põhjus",
"sender_id": "Saatja ID",
"sender_id_error_message": "Lubatud on ainult tähed, numbrid ja tühikud (max 11 tähemärki)",
@@ -2595,6 +2595,7 @@
"new_option": "Uus valik",
"update_profile": "Uuenda liiget",
"attribute_updated_successfully": "Atribuut edukalt uuendatud",
"attribute_deleted_successfully": "Atribuut on edukalt kustutatud",
"attributes_edited_successfully": "Atribuudid edukalt muudetud",
"attribute_meta_description": "Halda atribuute oma meeskonnaliikmete jaoks",
"attributes_edit_description": "Muuda atribuute oma meeskonnaliikmete jaoks",
@@ -2640,6 +2641,9 @@
"month_to_date": "kuu algusest tänaseni",
"year_to_date": "aasta algusest tänaseni",
"custom_range": "kohandatud vahemik",
"show_all_columns": "Näita kõiki veerge",
"toggle_columns": "Lülita veerud sisse/välja",
"no_columns_found": "Veerge ei leitud",
"salesforce_create_record_as": "Broneeringu korral lisa sündmused ja uued osalejad kui:",
"salesforce_lead": "Müügivihje",
"salesforce_contact_under_account": "Kontakt konto all",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Kui osaleja eksisteerib Salesforce'is, broneeri otse omanikuga",
"salesforce_check_owner_of": "Üksus, mille omanikku kontrollida otsebroneerimiseks",
"account": "Konto",
"choose_hosts_schedule": "Vali võõrustajate ajakavad",
"hosts_schedule_description": "Saad valida igale võõrustajale erineva ajakava. Vaikimisi on valitud iga võõrustaja tavaline ajakava.",
"no_hosts_description": "Võõrustajaid pole määratud, lisa need määramise vahekaardil.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "Réinstaller l'application",
"reschedule_reason": "Raison de la replanification",
"choose_common_schedule_team_event": "Choisissez un horaire commun",
"choose_common_schedule_team_event_description": "Activez cette option si vous souhaitez utiliser des disponibilités communes entre les hôtes. Si cette option est désactivée, chaque hôte recevra les réservations en fonction de ses disponibilités par défaut.",
"choose_common_schedule_team_event_description": "Activez cette option si vous souhaitez utiliser un horaire commun entre les hôtes. Lorsqu'elle est désactivée, chaque hôte sera réservé en fonction de son horaire par défaut ou de l'horaire qu'il a choisi.",
"reason": "Raison",
"sender_id": "ID de l'expéditeur",
"sender_id_error_message": "Seules les lettres, chiffres et espaces sont autorisés (max. 11 caractères)",
@@ -2595,6 +2595,7 @@
"new_option": "Nouvelle option",
"update_profile": "Mettre à jour le membre",
"attribute_updated_successfully": "Attribut mis à jour avec succès",
"attribute_deleted_successfully": "Attribut supprimé avec succès",
"attributes_edited_successfully": "Attributs modifiés avec succès",
"attribute_meta_description": "Gérer les attributs de vos membres d'équipe",
"attributes_edit_description": "Modifier les attributs de vos membres d'équipe",
@@ -2640,6 +2641,9 @@
"month_to_date": "mois en cours",
"year_to_date": "année en cours",
"custom_range": "période personnalisée",
"show_all_columns": "Afficher toutes les colonnes",
"toggle_columns": "Basculer les colonnes",
"no_columns_found": "Aucune colonne trouvée",
"salesforce_create_record_as": "Lors de la réservation, ajouter les événements et les nouveaux participants en tant que :",
"salesforce_lead": "Prospect",
"salesforce_contact_under_account": "Contact sous un compte",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Si le participant existe dans Salesforce, réserver directement avec le propriétaire",
"salesforce_check_owner_of": "Entité dont il faut vérifier le propriétaire pour réserver directement",
"account": "Compte",
"choose_hosts_schedule": "Choisir les horaires des hôtes",
"hosts_schedule_description": "Vous pouvez sélectionner un horaire différent pour chaque hôte. L'horaire par défaut de chaque hôte est choisi par défaut.",
"no_hosts_description": "Aucun hôte assigné, ajoutez des hôtes dans l'onglet d'affectation.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "להתקין את היישום מחדש",
"reschedule_reason": "הסיבה לקביעת מועד חדש",
"choose_common_schedule_team_event": "בחירת לוח זמנים משותף",
"choose_common_schedule_team_event_description": "יש להפעיל אפשרות זו אם רוצים להשתמש בלוח זמנים משותף בין המארחים. אם האפשרות הזו מבוטלת, כל מארח יוזמן לפי לוח הזמנים המשמש כברירת המחדל שלו.",
"choose_common_schedule_team_event_description": "הפעל אפשרות זו אם ברצונך להשתמש בלוח זמנים משותף בין המארחים. כאשר האפשרות מבוטלת, כל מארח יוזמן על פי לוח הזמנים המוגדר כברירת מחדל או לוח הזמנים שנבחר.",
"reason": "סיבה",
"sender_id": "מזהה השולח/ת",
"sender_id_error_message": "מותר להשתמש רק באותיות, במספרים וברווחים (לכל היותר 11 תווים)",
@@ -2595,6 +2595,7 @@
"new_option": "אפשרות חדשה",
"update_profile": "עדכן חבר צוות",
"attribute_updated_successfully": "המאפיין עודכן בהצלחה",
"attribute_deleted_successfully": "התכונה נמחקה בהצלחה",
"attributes_edited_successfully": "המאפיינים נערכו בהצלחה",
"attribute_meta_description": "נהל מאפיינים עבור חברי הצוות שלך",
"attributes_edit_description": "ערוך מאפיינים עבור חברי הצוות שלך",
@@ -2640,6 +2641,9 @@
"month_to_date": "מתחילת החודש",
"year_to_date": "מתחילת השנה",
"custom_range": "טווח מותאם אישית",
"show_all_columns": "הצג את כל העמודות",
"toggle_columns": "החלף עמודות",
"no_columns_found": "לא נמצאו עמודות",
"salesforce_create_record_as": "בעת הזמנה, הוסף אירועים וחדשים משתתפים כ:",
"salesforce_lead": "ליד",
"salesforce_contact_under_account": "איש קשר תחת חשבון",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "אם המשתתף קיים ב-Salesforce, הזמן ישירות עם הבעלים",
"salesforce_check_owner_of": "ישות לבדוק את הבעלים שלה כדי להזמין ישירות",
"account": "חשבון",
"choose_hosts_schedule": "בחר לוחות זמנים למארחים",
"hosts_schedule_description": "ניתן לבחור לוח זמנים שונה לכל מארח. לוח הזמנים המוגדר כברירת מחדל נבחר עבור כל מארח.",
"no_hosts_description": "לא הוקצו מארחים, הוסף מארחים בלשונית ההקצאה.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "Alkalmazás újratelepítése",
"reschedule_reason": "Időpontmódosítás oka",
"choose_common_schedule_team_event": "Válasszon közös menetrendet",
"choose_common_schedule_team_event_description": "Engedélyezze ezt, ha közös ütemezést szeretne használni a szervezők között. Ha le van tiltva, minden szervező az alapértelmezett ütemezésük alapján lesz lefoglalva.",
"choose_common_schedule_team_event_description": "Engedélyezze ezt, ha közös ütemtervet szeretne használni a házigazdák között. Ha le van tiltva, minden házigazda az alapértelmezett vagy a választott ütemterv alapján lesz foglalva.",
"reason": "Ok",
"sender_id": "Feladó azonosítója",
"sender_id_error_message": "Csak betűk, számok és szóközök megengedettek (max. 11 karakter)",
@@ -2595,6 +2595,7 @@
"new_option": "Új opció",
"update_profile": "Tag frissítése",
"attribute_updated_successfully": "Az attribútum sikeresen frissítve",
"attribute_deleted_successfully": "Attribútum sikeresen törölve",
"attributes_edited_successfully": "Az attribútumok sikeresen szerkesztve",
"attribute_meta_description": "Attribútumok kezelése a csapattagok számára",
"attributes_edit_description": "Attribútumok szerkesztése a csapattagok számára",
@@ -2640,6 +2641,9 @@
"month_to_date": "hónap elejétől",
"year_to_date": "év elejétől",
"custom_range": "egyéni tartomány",
"show_all_columns": "Összes oszlop megjelenítése",
"toggle_columns": "Oszlopok váltása",
"no_columns_found": "Nincsenek oszlopok",
"salesforce_create_record_as": "Foglaláskor adja hozzá az eseményeket és az új résztvevőket mint:",
"salesforce_lead": "Érdeklődő",
"salesforce_contact_under_account": "Kapcsolat egy fiók alatt",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Ha a résztvevő létezik a Salesforce-ban, foglaljon közvetlenül a tulajdonossal",
"salesforce_check_owner_of": "Entitás, amelynek tulajdonosát ellenőrizni kell a közvetlen foglaláshoz",
"account": "Fiók",
"choose_hosts_schedule": "Válassza ki a házigazdák ütemtervét",
"hosts_schedule_description": "Minden házigazdához különböző ütemtervet választhat. Alapértelmezés szerint minden házigazda alapértelmezett ütemterve van kiválasztva.",
"no_hosts_description": "Nincsenek hozzárendelt házigazdák, adjon hozzá házigazdákat a hozzárendelési fülön.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adja hozzá az új karakterláncokat fent ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"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.",
"choose_common_schedule_team_event_description": "Abilita questa opzione se desideri utilizzare un orario comune tra gli host. Quando disabilitato, ogni host verrà prenotato in base al proprio orario predefinito o all'orario scelto.",
"reason": "Motivo",
"sender_id": "Id mittente",
"sender_id_error_message": "Sono consentiti solo lettere, numeri e spazi (massimo 11 caratteri)",
@@ -2595,6 +2595,7 @@
"new_option": "Nuova opzione",
"update_profile": "Aggiorna membro",
"attribute_updated_successfully": "Attributo aggiornato con successo",
"attribute_deleted_successfully": "Attributo eliminato con successo",
"attributes_edited_successfully": "Attributi modificati con successo",
"attribute_meta_description": "Gestisci gli attributi per i membri del tuo team",
"attributes_edit_description": "Modifica gli attributi per i membri del tuo team",
@@ -2640,6 +2641,9 @@
"month_to_date": "mese corrente",
"year_to_date": "anno corrente",
"custom_range": "intervallo personalizzato",
"show_all_columns": "Mostra tutte le colonne",
"toggle_columns": "Attiva/disattiva colonne",
"no_columns_found": "Nessuna colonna trovata",
"salesforce_create_record_as": "Al momento della prenotazione, aggiungi eventi e nuovi partecipanti come:",
"salesforce_lead": "Lead",
"salesforce_contact_under_account": "Contatto sotto un account",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Se il partecipante esiste in Salesforce, prenota direttamente con il proprietario",
"salesforce_check_owner_of": "Entità per verificare il proprietario per prenotare direttamente",
"account": "Account",
"choose_hosts_schedule": "Scegli gli orari degli host",
"hosts_schedule_description": "Puoi selezionare un orario diverso per ogni host. L'orario predefinito di ciascun host è selezionato di default.",
"no_hosts_description": "Nessun host assegnato, aggiungi host nella scheda di assegnazione.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Aggiungi le tue nuove stringhe qui sopra ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "アプリを再インストール",
"reschedule_reason": "スケジュール変更の理由",
"choose_common_schedule_team_event": "共通のスケジュールを選択",
"choose_common_schedule_team_event_description": "ホスト間で共通のスケジュールを使用する場合には、これを有効にしてください。無効にすると、各ホストに対してはデフォルトのスケジュールに基づいて予約が行われます。",
"choose_common_schedule_team_event_description": "ホスト間で共通のスケジュールを使用する場合はこれを有効にします。無効にすると、各ホストはデフォルトのスケジュールまたは選択されたスケジュールに基づいて予約されます。",
"reason": "理由",
"sender_id": "送信者 ID",
"sender_id_error_message": "文字、数字、スペースのみが使用可能です (最大 11 文字)",
@@ -2595,6 +2595,7 @@
"new_option": "新しいオプション",
"update_profile": "メンバーを更新",
"attribute_updated_successfully": "属性が正常に更新されました",
"attribute_deleted_successfully": "属性が正常に削除されました",
"attributes_edited_successfully": "属性が正常に編集されました",
"attribute_meta_description": "チームメンバーの属性を管理",
"attributes_edit_description": "チームメンバーの属性を編集",
@@ -2640,6 +2641,9 @@
"month_to_date": "月初から今日まで",
"year_to_date": "年初から今日まで",
"custom_range": "カスタム範囲",
"show_all_columns": "すべての列を表示",
"toggle_columns": "列を切り替える",
"no_columns_found": "列が見つかりません",
"salesforce_create_record_as": "予約時にイベントを追加し、新しい参加者を次のように追加します:",
"salesforce_lead": "リード",
"salesforce_contact_under_account": "アカウント下の連絡先",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "参加者がSalesforceに存在する場合、所有者と直接予約する",
"salesforce_check_owner_of": "直接予約するために所有者を確認するエンティティ",
"account": "アカウント",
"choose_hosts_schedule": "ホストのスケジュールを選択",
"hosts_schedule_description": "各ホストに異なるスケジュールを選択できます。各ホストのデフォルトスケジュールが初期設定として選択されています。",
"no_hosts_description": "ホストが割り当てられていません。割り当てタブでホストを追加してください。",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "ដំឡើងកម្មវិធីឡើងវិញ",
"reschedule_reason": "មូលហេតុការកំណត់ពេលថ្មី",
"choose_common_schedule_team_event": "ជ្រើសរើសកាលវិភាគរួម",
"choose_common_schedule_team_event_description": "បើកមុខងារនេះប្រសិនបើអ្នកចង់ប្រើកាលវិភាគរួមរវាងម្ចាស់ផ្ទះ។ នៅពេលបិទ ម្ចាស់ផ្ទះនីមួយៗនឹងត្រូវបានកក់ដោយផ្អែកលើកាលវិភាគលំនាំដើមរបស់ពួកគេ។",
"choose_common_schedule_team_event_description": "បើកមុខងារនេះប្រសិនបើអ្នកចង់ប្រើកាលវិភាគរួមរវាងម្ចាស់ផ្ទះ។ នៅពេលបិទ ម្ចាស់ផ្ទះនីមួយៗនឹងត្រូវបានកក់ដោយផ្អែកលើកាលវិភាគលំនាំដើមរបស់ពួកគេ ឬកាលវិភាគដែលពួកគេបានជ្រើសរើស។",
"reason": "មូលហេតុ",
"sender_id": "លេខសម្គាល់អ្នកផ្ញើ",
"sender_id_error_message": "អនុញ្ញាតអក្សរ លេខ និងចន្លោះតែប៉ុណ្ណោះ (ច្រើនបំផុត 11 តួអក្សរ)",
@@ -2595,6 +2595,7 @@
"new_option": "ជម្រើសថ្មី",
"update_profile": "ធ្វើបច្ចុប្បន្នភាពសមាជិក",
"attribute_updated_successfully": "គុណលក្ខណៈត្រូវបានធ្វើបច្ចុប្បន្នភាពដោយជោគជ័យ",
"attribute_deleted_successfully": "លក្ខណៈពិសេសត្រូវបានលុបដោយជោគជ័យ",
"attributes_edited_successfully": "គុណលក្ខណៈត្រូវបានកែសម្រួលដោយជោគជ័យ",
"attribute_meta_description": "គ្រប់គ្រងគុណលក្ខណៈសម្រាប់សមាជិកក្រុមរបស់អ្នក",
"attributes_edit_description": "កែសម្រួលគុណលក្ខណៈសម្រាប់សមាជិកក្រុមរបស់អ្នក",
@@ -2640,6 +2641,9 @@
"month_to_date": "ខែនេះ",
"year_to_date": "ឆ្នាំនេះ",
"custom_range": "ជួរដែលកំណត់ដោយខ្លួនឯង",
"show_all_columns": "បង្ហាញជួរឈរទាំងអស់",
"toggle_columns": "បិទបើកជួរឈរ",
"no_columns_found": "រកមិនឃើញជួរឈរ",
"salesforce_create_record_as": "នៅពេលកក់ សូមបន្ថែមព្រឹត្តិការណ៍និងអ្នកចូលរួមថ្មីជា៖",
"salesforce_lead": "អ្នកដឹកនាំ",
"salesforce_contact_under_account": "ទំនាក់ទំនងក្រោមគណនី",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "ប្រសិនបើអ្នកចូលរួមមាននៅក្នុង Salesforce សូមកក់ដោយផ្ទាល់ជាមួយម្ចាស់",
"salesforce_check_owner_of": "អង្គភាពត្រូវពិនិត្យម្ចាស់ដើម្បីកក់ដោយផ្ទាល់",
"account": "គណនី",
"choose_hosts_schedule": "ជ្រើសរើសកាលវិភាគរបស់ម្ចាស់ផ្ទះ",
"hosts_schedule_description": "អ្នកអាចជ្រើសរើសកាលវិភាគផ្សេងៗសម្រាប់ម្ចាស់ផ្ទះនីមួយៗ។ កាលវិភាគលំនាំដើមរបស់ម្ចាស់ផ្ទះនីមួយៗត្រូវបានជ្រើសរើសជាលំនាំដើម។",
"no_hosts_description": "មិនមានម្ចាស់ផ្ទះបានចាត់តាំងទេ សូមបន្ថែមម្ចាស់ផ្ទះនៅក្នុងផ្ទាំងចាត់តាំង។",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ បន្ថែមខ្សែអក្សរថ្មីរបស់អ្នកនៅខាងលើនេះ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "앱 재설치",
"reschedule_reason": "일정 변경 사유",
"choose_common_schedule_team_event": "공통 일정 선택",
"choose_common_schedule_team_event_description": "호스트 간에 공통 일정을 사용하려면 이 옵션을 활성화하세요. 비활성화되면 각 호스트는 기본 일정에 따라 예약됩니다.",
"choose_common_schedule_team_event_description": "호스트 간에 공통 일정을 사용하려면 이 옵션을 활성화하십시오. 비활성화하면 각 호스트는 기본 일정 또는 선택한 일정에 따라 예약됩니다.",
"reason": "사유",
"sender_id": "발신자 ID",
"sender_id_error_message": "영문, 숫자, 공백만 허용(최대 11자)",
@@ -2595,6 +2595,7 @@
"new_option": "새 옵션",
"update_profile": "회원 정보 업데이트",
"attribute_updated_successfully": "속성이 성공적으로 업데이트되었습니다",
"attribute_deleted_successfully": "속성이 성공적으로 삭제되었습니다",
"attributes_edited_successfully": "속성이 성공적으로 편집되었습니다",
"attribute_meta_description": "팀원의 속성을 관리하세요",
"attributes_edit_description": "팀원의 속성을 편집하세요",
@@ -2640,6 +2641,9 @@
"month_to_date": "이번 달 현재까지",
"year_to_date": "올해 현재까지",
"custom_range": "사용자 지정 범위",
"show_all_columns": "모든 열 표시",
"toggle_columns": "열 전환",
"no_columns_found": "열을 찾을 수 없습니다",
"salesforce_create_record_as": "예약 시, 이벤트를 추가하고 새로운 참석자를 다음으로 추가합니다:",
"salesforce_lead": "리드",
"salesforce_contact_under_account": "계정 내 연락처",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "참석자가 Salesforce에 존재하면, 소유자와 직접 예약합니다",
"salesforce_check_owner_of": "직접 예약하기 위해 소유자를 확인할 엔터티",
"account": "계정",
"choose_hosts_schedule": "호스트 일정 선택",
"hosts_schedule_description": "각 호스트에 대해 다른 일정을 선택할 수 있습니다. 각 호스트의 기본 일정이 기본적으로 선택됩니다.",
"no_hosts_description": "할당 탭에서 호스트를 추가하십시오. 현재 할당된 호스트가 없습니다.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 여기에 새 문자열을 추가하세요 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "Herinstalleer de app",
"reschedule_reason": "Reden voor opnieuw plannen",
"choose_common_schedule_team_event": "Kies een gezamenlijke planning",
"choose_common_schedule_team_event_description": "Schakel dit in als u een gemeenschappelijke planning tussen organisatoren wilt gebruiken. Indien uitgeschakeld, wordt elke organisator geboekt op basis van zijn standaardplanning.",
"choose_common_schedule_team_event_description": "Schakel dit in als u een gemeenschappelijk schema tussen hosts wilt gebruiken. Wanneer dit is uitgeschakeld, wordt elke host geboekt op basis van hun standaardschema of hun gekozen schema.",
"reason": "Reden",
"sender_id": "Afzender-ID",
"sender_id_error_message": "Alleen letters, cijfers en spaties toegestaan (max. 11 tekens)",
@@ -2595,6 +2595,7 @@
"new_option": "Nieuwe optie",
"update_profile": "Lid bijwerken",
"attribute_updated_successfully": "Attribuut succesvol bijgewerkt",
"attribute_deleted_successfully": "Attribuut succesvol verwijderd",
"attributes_edited_successfully": "Attributen succesvol bewerkt",
"attribute_meta_description": "Beheer attributen voor je teamleden",
"attributes_edit_description": "Bewerk attributen voor je teamleden",
@@ -2640,6 +2641,9 @@
"month_to_date": "maand tot nu toe",
"year_to_date": "jaar tot nu toe",
"custom_range": "aangepast bereik",
"show_all_columns": "Toon alle kolommen",
"toggle_columns": "Kolommen wisselen",
"no_columns_found": "Geen kolommen gevonden",
"salesforce_create_record_as": "Bij boeking, voeg evenementen toe en nieuwe deelnemers als:",
"salesforce_lead": "Lead",
"salesforce_contact_under_account": "Contact onder een account",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Als de deelnemer in Salesforce bestaat, boek direct met de eigenaar",
"salesforce_check_owner_of": "Entiteit om eigenaar van te controleren voor directe boeking",
"account": "Account",
"choose_hosts_schedule": "Kies hostschema's",
"hosts_schedule_description": "U kunt een ander schema selecteren voor elke host. Standaard is het standaardschema van elke host geselecteerd.",
"no_hosts_description": "Geen hosts toegewezen, voeg hosts toe in het toewijzingstabblad.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Voeg uw nieuwe strings hierboven toe ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "Installer appen på nytt",
"reschedule_reason": "Årsak for endring av tidspunkt",
"choose_common_schedule_team_event": "Velg en felles timeplan",
"choose_common_schedule_team_event_description": "Aktiver dette hvis du vil bruke en felles tidsplan mellom verter. Når den er deaktivert, vil hver vert bookes basert på deres standard tidsplan.",
"choose_common_schedule_team_event_description": "Aktiver dette hvis du vil bruke en felles plan mellom verter. Når det er deaktivert, vil hver vert bli booket basert på sin standardplan eller sin valgte plan.",
"reason": "Årsak",
"sender_id": "Avsender ID",
"sender_id_error_message": "Kun bokstaver, tall og mellomrom er tillatt (maks. 11 tegn)",
@@ -2595,6 +2595,7 @@
"new_option": "Nytt alternativ",
"update_profile": "Oppdater medlem",
"attribute_updated_successfully": "Egenskap oppdatert vellykket",
"attribute_deleted_successfully": "Attributet ble slettet",
"attributes_edited_successfully": "Egenskaper redigert vellykket",
"attribute_meta_description": "Administrer egenskaper for teammedlemmene dine",
"attributes_edit_description": "Rediger egenskaper for teammedlemmene dine",
@@ -2640,6 +2641,9 @@
"month_to_date": "måned til dags dato",
"year_to_date": "år til dags dato",
"custom_range": "tilpasset periode",
"show_all_columns": "Vis alle kolonner",
"toggle_columns": "Veksle kolonner",
"no_columns_found": "Ingen kolonner funnet",
"salesforce_create_record_as": "Ved booking, legg til hendelser og nye deltakere som:",
"salesforce_lead": "Lead",
"salesforce_contact_under_account": "Kontakt under en konto",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Hvis deltaker finnes i Salesforce, book direkte med eieren",
"salesforce_check_owner_of": "Enhet for å sjekke eier av for å booke direkte",
"account": "Konto",
"choose_hosts_schedule": "Velg vertsplaner",
"hosts_schedule_description": "Du kan velge en annen plan for hver vert. Hver verts standardplan er valgt som standard.",
"no_hosts_description": "Ingen verter tildelt, legg til verter i tildelingsfanen.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Legg til dine nye strenger over her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "Zainstaluj ponownie aplikację",
"reschedule_reason": "Powód zmiany terminu",
"choose_common_schedule_team_event": "Wybierz wspólny harmonogram",
"choose_common_schedule_team_event_description": "Włącz tę opcję, jeśli chcesz używać wspólnego harmonogramu gospodarzy. Jeśli ją wyłączysz, rezerwacje u każdego gospodarza będą dokonywane na podstawie jego domyślnego harmonogramu.",
"choose_common_schedule_team_event_description": "Włącz to, jeśli chcesz używać wspólnego harmonogramu między gospodarzami. Gdy jest wyłączone, każdy gospodarz będzie rezerwowany na podstawie swojego domyślnego harmonogramu lub wybranego harmonogramu.",
"reason": "Powód",
"sender_id": "Identyfikator nadawcy",
"sender_id_error_message": "Dozwolone są tylko litery, cyfry i spacje (maksymalnie 11 znaków)",
@@ -2595,6 +2595,7 @@
"new_option": "Nowa opcja",
"update_profile": "Aktualizuj członka",
"attribute_updated_successfully": "Atrybut zaktualizowany pomyślnie",
"attribute_deleted_successfully": "Atrybut został pomyślnie usunięty",
"attributes_edited_successfully": "Atrybuty edytowane pomyślnie",
"attribute_meta_description": "Zarządzaj atrybutami członków zespołu",
"attributes_edit_description": "Edytuj atrybuty członków zespołu",
@@ -2640,6 +2641,9 @@
"month_to_date": "od początku miesiąca",
"year_to_date": "od początku roku",
"custom_range": "zakres niestandardowy",
"show_all_columns": "Pokaż wszystkie kolumny",
"toggle_columns": "Przełącz kolumny",
"no_columns_found": "Nie znaleziono kolumn",
"salesforce_create_record_as": "Podczas rezerwacji dodaj wydarzenia i nowych uczestników jako:",
"salesforce_lead": "Lead",
"salesforce_contact_under_account": "Kontakt w ramach konta",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Jeśli uczestnik istnieje w Salesforce, zarezerwuj bezpośrednio z właścicielem",
"salesforce_check_owner_of": "Podmiot do sprawdzenia właściciela w celu bezpośredniej rezerwacji",
"account": "Konto",
"choose_hosts_schedule": "Wybierz harmonogramy gospodarzy",
"hosts_schedule_description": "Możesz wybrać inny harmonogram dla każdego gospodarza. Domyślny harmonogram każdego gospodarza jest wybierany automatycznie.",
"no_hosts_description": "Brak przypisanych gospodarzy, dodaj gospodarzy w zakładce przypisania.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodaj nowe ciągi powyżej ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "Reinstale o aplicativo",
"reschedule_reason": "Motivo de reagendamento",
"choose_common_schedule_team_event": "Escolha uma agenda compartilhada",
"choose_common_schedule_team_event_description": "Ative esta opção se você quiser usar uma agenda compartilhada entre os anfitriões. Ao desativar esta opção, cada anfitriã(o) será reservado conforme seu horário padrão.",
"choose_common_schedule_team_event_description": "Ative esta opção se quiser usar um horário comum entre os anfitriões. Quando desativado, cada anfitrião será agendado com base em seu horário padrão ou no horário escolhido.",
"reason": "Motivo",
"sender_id": "ID do remetente",
"sender_id_error_message": "Apenas letras, números e espaços permitidos (no máx. 11 caracteres)",
@@ -2595,6 +2595,7 @@
"new_option": "Nova opção",
"update_profile": "Atualizar membro",
"attribute_updated_successfully": "Atributo atualizado com sucesso",
"attribute_deleted_successfully": "Atributo excluído com sucesso",
"attributes_edited_successfully": "Atributos editados com sucesso",
"attribute_meta_description": "Gerencie atributos para os membros da sua equipe",
"attributes_edit_description": "Edite atributos para os membros da sua equipe",
@@ -2640,6 +2641,9 @@
"month_to_date": "mês até a data",
"year_to_date": "ano até a data",
"custom_range": "intervalo personalizado",
"show_all_columns": "Mostrar todas as colunas",
"toggle_columns": "Alternar colunas",
"no_columns_found": "Nenhuma coluna encontrada",
"salesforce_create_record_as": "Ao agendar, adicione eventos e novos participantes como:",
"salesforce_lead": "Lead",
"salesforce_contact_under_account": "Contato em uma conta",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Se o participante existir no Salesforce, agendar diretamente com o proprietário",
"salesforce_check_owner_of": "Entidade para verificar o proprietário para agendar diretamente",
"account": "Conta",
"choose_hosts_schedule": "Escolher horários dos anfitriões",
"hosts_schedule_description": "Você pode selecionar um horário diferente para cada anfitrião. O horário padrão de cada anfitrião é escolhido por padrão.",
"no_hosts_description": "Nenhum anfitrião atribuído, adicione anfitriões na aba de atribuição.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adicione suas novas strings aqui em cima ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"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.",
"choose_common_schedule_team_event_description": "Ative esta opção se quiser usar um horário comum entre os anfitriões. Quando desativado, cada anfitrião será agendado com base no seu horário padrão ou no horário escolhido.",
"reason": "Motivo",
"sender_id": "Identificação do remetente",
"sender_id_error_message": "Apenas são permitidas letras, números e espaços (máx. 11 caracteres)",
@@ -2595,6 +2595,7 @@
"new_option": "Nova opção",
"update_profile": "Atualizar membro",
"attribute_updated_successfully": "Atributo atualizado com sucesso",
"attribute_deleted_successfully": "Atributo eliminado com sucesso",
"attributes_edited_successfully": "Atributos editados com sucesso",
"attribute_meta_description": "Gerir atributos para os membros da sua equipa",
"attributes_edit_description": "Editar atributos para os membros da sua equipa",
@@ -2640,6 +2641,9 @@
"month_to_date": "mês até à data",
"year_to_date": "ano até à data",
"custom_range": "intervalo personalizado",
"show_all_columns": "Mostrar todas as colunas",
"toggle_columns": "Alternar colunas",
"no_columns_found": "Nenhuma coluna encontrada",
"salesforce_create_record_as": "Ao agendar, adicionar eventos em e novos participantes como:",
"salesforce_lead": "Lead",
"salesforce_contact_under_account": "Contacto sob uma conta",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Se o participante existir no Salesforce, agendar diretamente com o proprietário",
"salesforce_check_owner_of": "Entidade para verificar o proprietário para agendar diretamente",
"account": "Conta",
"choose_hosts_schedule": "Escolher horários dos anfitriões",
"hosts_schedule_description": "Pode selecionar um horário diferente para cada anfitrião. O horário padrão de cada anfitrião é escolhido por defeito.",
"no_hosts_description": "Nenhum anfitrião atribuído, adicione anfitriões na aba de atribuição.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "Reinstalează aplicația",
"reschedule_reason": "Motiv reprogramare",
"choose_common_schedule_team_event": "Alegeți un program comun",
"choose_common_schedule_team_event_description": "Activați această opțiune dacă doriți să folosiți un program comun între gazde. Atunci când este dezactivată, fiecare gazdă va fi rezervată în funcție de programul său implicit.",
"choose_common_schedule_team_event_description": "Activează această opțiune dacă dorești să folosești un program comun între gazde. Când este dezactivată, fiecare gazdă va fi rezervată pe baza programului său implicit sau a programului ales.",
"reason": "Motiv",
"sender_id": "ID expeditor",
"sender_id_error_message": "Sunt permise doar litere, numere și spații (maxim 11 caractere)",
@@ -2595,6 +2595,7 @@
"new_option": "Opțiune nouă",
"update_profile": "Actualizați membrul",
"attribute_updated_successfully": "Atribut actualizat cu succes",
"attribute_deleted_successfully": "Atributul a fost șters cu succes",
"attributes_edited_successfully": "Atribute editate cu succes",
"attribute_meta_description": "Gestionați atributele pentru membrii echipei dvs.",
"attributes_edit_description": "Editați atributele pentru membrii echipei dvs.",
@@ -2640,6 +2641,9 @@
"month_to_date": "luna până în prezent",
"year_to_date": "anul până în prezent",
"custom_range": "interval personalizat",
"show_all_columns": "Afișează toate coloanele",
"toggle_columns": "Comută coloanele",
"no_columns_found": "Nu au fost găsite coloane",
"salesforce_create_record_as": "La rezervare, adaugă evenimentele și noii participanți ca:",
"salesforce_lead": "Lead",
"salesforce_contact_under_account": "Contact sub un cont",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Dacă participantul există în Salesforce, rezervă direct cu proprietarul",
"salesforce_check_owner_of": "Entitatea pentru a verifica proprietarul pentru a rezerva direct",
"account": "Cont",
"choose_hosts_schedule": "Alege programele gazdelor",
"hosts_schedule_description": "Poți selecta un program diferit pentru fiecare gazdă. Programul implicit al fiecărei gazde este selectat în mod prestabilit.",
"no_hosts_description": "Nicio gazdă nu este atribuită, adaugă gazde în fila de atribuire.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adăugați stringurile noi deasupra acestui rând ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "Ponovo instalirajte aplikaciju",
"reschedule_reason": "Razlog za promenu rasporeda",
"choose_common_schedule_team_event": "Izaberite zajednički raspored",
"choose_common_schedule_team_event_description": "Omogućite to ako želite da koristite zajednički raspored između domaćina. Kada je onemogućen, svaki domaćin može rezervisati na osnovu svog podrazumevanog rasporeda.",
"choose_common_schedule_team_event_description": "Омогућите ово ако желите да користите заједнички распоред између домаћина. Када је онемогућено, сваки домаћин ће бити резервисан на основу свог подразумеваног распореда или изабраног распореда.",
"reason": "Razlog",
"sender_id": "ID pošiljaoca",
"sender_id_error_message": "Dozvoljena su samo slova, brojevi i razmaci (maks. 11 znakova)",
@@ -2595,6 +2595,7 @@
"new_option": "Nova opcija",
"update_profile": "Ažuriraj člana",
"attribute_updated_successfully": "Atribut uspešno ažuriran",
"attribute_deleted_successfully": "Атрибут је успешно обрисан",
"attributes_edited_successfully": "Atributi uspešno izmenjeni",
"attribute_meta_description": "Upravljajte atributima za članove vašeg tima",
"attributes_edit_description": "Izmenite atribute za članove vašeg tima",
@@ -2640,6 +2641,9 @@
"month_to_date": "od početka meseca do danas",
"year_to_date": "od početka godine do danas",
"custom_range": "prilagođeni opseg",
"show_all_columns": "Прикажи све колоне",
"toggle_columns": "Укључи/искључи колоне",
"no_columns_found": "Нису пронађене колоне",
"salesforce_create_record_as": "Prilikom zakazivanja, dodaj događaje i nove učesnike kao:",
"salesforce_lead": "Potencijalni klijent",
"salesforce_contact_under_account": "Kontakt pod nalogom",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Ako učesnik postoji u Salesforce-u, zakaži direktno sa vlasnikom",
"salesforce_check_owner_of": "Entitet čijeg vlasnika treba proveriti za direktno zakazivanje",
"account": "Nalog",
"choose_hosts_schedule": "Изаберите распореде домаћина",
"hosts_schedule_description": "Можете изабрати различит распоред за сваког домаћина. Подразумевано је изабран подразумевани распоред сваког домаћина.",
"no_hosts_description": "Нема додељених домаћина, додајте домаћине у картици за доделу.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodajte svoje nove stringove iznad ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "Installera om appen",
"reschedule_reason": "Anledning till ombokning",
"choose_common_schedule_team_event": "Välj ett gemensamt schema",
"choose_common_schedule_team_event_description": "Aktivera den här funktionen om du vill använda ett gemensamt schema mellan värdarna. När den är inaktiverad bokas varje värd enligt sitt standardschema.",
"choose_common_schedule_team_event_description": "Aktivera detta om du vill använda ett gemensamt schema mellan värdarna. När det är inaktiverat kommer varje värd att bokas baserat på sitt standardschema eller det valda schemat.",
"reason": "Orsak",
"sender_id": "Avsändar-ID",
"sender_id_error_message": "Endast bokstäver, siffror och mellanslag tillåts (maximalt 11 tecken)",
@@ -2595,6 +2595,7 @@
"new_option": "Nytt alternativ",
"update_profile": "Uppdatera medlem",
"attribute_updated_successfully": "Egenskapen uppdaterades framgångsrikt",
"attribute_deleted_successfully": "Attributet har raderats framgångsrikt",
"attributes_edited_successfully": "Egenskaperna redigerades framgångsrikt",
"attribute_meta_description": "Hantera egenskaper för dina teammedlemmar",
"attributes_edit_description": "Redigera egenskaper för dina teammedlemmar",
@@ -2640,6 +2641,9 @@
"month_to_date": "månad till datum",
"year_to_date": "år till datum",
"custom_range": "anpassat intervall",
"show_all_columns": "Visa alla kolumner",
"toggle_columns": "Växla kolumner",
"no_columns_found": "Inga kolumner hittades",
"salesforce_create_record_as": "Vid bokning, lägg till händelser och nya deltagare som:",
"salesforce_lead": "Lead",
"salesforce_contact_under_account": "Kontakt under ett konto",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Om deltagaren finns i Salesforce, boka direkt med ägaren",
"salesforce_check_owner_of": "Entitet att kontrollera ägare av för att boka direkt",
"account": "Konto",
"choose_hosts_schedule": "Välj värdarnas scheman",
"hosts_schedule_description": "Du kan välja ett annat schema för varje värd. Varje värds standardschema är valt som standard.",
"no_hosts_description": "Inga värdar tilldelade, lägg till värdar i tilldelningsfliken.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "Uygulamayı yeniden yükleyin",
"reschedule_reason": "Yeniden planlama nedeni",
"choose_common_schedule_team_event": "Ortak bir program seçin",
"choose_common_schedule_team_event_description": "Toplantı sahipleri arasında ortak bir plan kullanmak istiyorsanız bunu etkinleştirin. Devre dışı bırakıldığında, her toplantı sahibi varsayılan programına göre rezerve edilir.",
"choose_common_schedule_team_event_description": "Ev sahipleri arasında ortak bir program kullanmak istiyorsanız bunu etkinleştirin. Devre dışı bırakıldığında, her ev sahibi varsayılan programına veya seçilen programına göre rezerve edilir.",
"reason": "Sebep",
"sender_id": "Gönderici Kimliği",
"sender_id_error_message": "Sadece harflere, sayılara ve boşluklara izin verilir (en fazla 11 karakter)",
@@ -2595,6 +2595,7 @@
"new_option": "Yeni seçenek",
"update_profile": "Üyeyi güncelle",
"attribute_updated_successfully": "Öznitelik başarıyla güncellendi",
"attribute_deleted_successfully": "Özellik başarıyla silindi",
"attributes_edited_successfully": "Öznitelikler başarıyla düzenlendi",
"attribute_meta_description": "Takım üyeleriniz için öznitelikleri yönetin",
"attributes_edit_description": "Takım üyeleriniz için öznitelikleri düzenleyin",
@@ -2640,6 +2641,9 @@
"month_to_date": "ayın başından bugüne",
"year_to_date": "yılın başından bugüne",
"custom_range": "özel aralık",
"show_all_columns": "Tüm sütunları göster",
"toggle_columns": "Sütunları değiştir",
"no_columns_found": "Sütun bulunamadı",
"salesforce_create_record_as": "Rezervasyon sırasında, etkinlikleri ekleyin ve yeni katılımcıları şu şekilde ekleyin:",
"salesforce_lead": "Potansiyel Müşteri",
"salesforce_contact_under_account": "Bir hesap altında İletişim",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Katılımcı Salesforce'ta mevcutsa, doğrudan sahibiyle rezervasyon yap",
"salesforce_check_owner_of": "Doğrudan rezervasyon yapmak için sahibini kontrol edilecek varlık",
"account": "Hesap",
"choose_hosts_schedule": "Ev sahibi programlarını seçin",
"hosts_schedule_description": "Her ev sahibi için farklı bir program seçebilirsiniz. Her ev sahibinin varsayılan programı varsayılan olarak seçilir.",
"no_hosts_description": "Atanmış ev sahibi yok, atama sekmesinde ev sahipleri ekleyin.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni dizelerinizi yukarıya ekleyin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "Перевстановіть додаток",
"reschedule_reason": "Причина перенесення",
"choose_common_schedule_team_event": "Виберіть спільний розклад",
"choose_common_schedule_team_event_description": "Увімкніть цей параметр, щоб використовувати спільний для двох ведучих розклад. Якщо цей параметр вимкнено, бронювання для кожного з ведучих відбуватиметься за їхніми власними графіками.",
"choose_common_schedule_team_event_description": "Увімкніть це, якщо ви хочете використовувати спільний розклад між ведучими. Коли вимкнено, кожен ведучий буде заброньований відповідно до свого стандартного або обраного розкладу.",
"reason": "Причина",
"sender_id": "ID відправника",
"sender_id_error_message": "Дозволяються тільки літери, цифри та пробіли (макс. 11 символів)",
@@ -2595,6 +2595,7 @@
"new_option": "Нова опція",
"update_profile": "Оновити профіль",
"attribute_updated_successfully": "Атрибут успішно оновлено",
"attribute_deleted_successfully": "Атрибут успішно видалено",
"attributes_edited_successfully": "Атрибути успішно відредаговано",
"attribute_meta_description": "Керуйте атрибутами для членів вашої команди",
"attributes_edit_description": "Редагуйте атрибути для членів вашої команди",
@@ -2640,6 +2641,9 @@
"month_to_date": "з початку місяця",
"year_to_date": "з початку року",
"custom_range": "власний діапазон",
"show_all_columns": "Показати всі стовпці",
"toggle_columns": "Перемкнути стовпці",
"no_columns_found": "Стовпці не знайдено",
"salesforce_create_record_as": "Під час бронювання додати події та нових учасників як:",
"salesforce_lead": "Лід",
"salesforce_contact_under_account": "Контакт під обліковим записом",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Якщо учасник існує в Salesforce, бронювати безпосередньо з власником",
"salesforce_check_owner_of": "Сутність для перевірки власника для прямого бронювання",
"account": "Обліковий запис",
"choose_hosts_schedule": "Виберіть розклад для ведучих",
"hosts_schedule_description": "Ви можете вибрати інший розклад для кожного ведучого. За замовчуванням обрано стандартний розклад кожного ведучого.",
"no_hosts_description": "Ведучих не призначено, додайте ведучих на вкладці призначення.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "Cài đặt lại ứng dụng",
"reschedule_reason": "Lý do dời lịch",
"choose_common_schedule_team_event": "Chọn một lịch chung",
"choose_common_schedule_team_event_description": "Bật mục này nếu bạn muốn dùng lịch thông thường giữa các chủ sự kiện. Khi tắt đi, mỗi chủ sự kiện sẽ được đặt lịch theo lịch mặc định của họ.",
"choose_common_schedule_team_event_description": "Bật tùy chọn này nếu bạn muốn sử dụng lịch trình chung giữa các người tổ chức. Khi tắt, mỗi người tổ chức sẽ được đặt lịch dựa trên lịch trình mặc định của họ hoặc lịch trình đã chọn.",
"reason": "Lý do",
"sender_id": "ID người gửi",
"sender_id_error_message": "Chỉ được phép sử dụng chữ cái, số và khoảng trắng (tối đa 11 ký tự)",
@@ -2595,6 +2595,7 @@
"new_option": "Tùy chọn mới",
"update_profile": "Cập nhật thành viên",
"attribute_updated_successfully": "Thuộc tính đã được cập nhật thành công",
"attribute_deleted_successfully": "Thuộc tính đã được xóa thành công",
"attributes_edited_successfully": "Thuộc tính đã được chỉnh sửa thành công",
"attribute_meta_description": "Quản lý thuộc tính cho các thành viên trong nhóm của bạn",
"attributes_edit_description": "Chỉnh sửa thuộc tính cho các thành viên trong nhóm của bạn",
@@ -2640,6 +2641,9 @@
"month_to_date": "từ đầu tháng đến nay",
"year_to_date": "từ đầu năm đến nay",
"custom_range": "phạm vi tùy chỉnh",
"show_all_columns": "Hiển thị tất cả các cột",
"toggle_columns": "Chuyển đổi cột",
"no_columns_found": "Không tìm thấy cột nào",
"salesforce_create_record_as": "Khi đặt lịch, thêm sự kiện và người tham dự mới dưới dạng:",
"salesforce_lead": "Khách hàng tiềm năng",
"salesforce_contact_under_account": "Liên hệ dưới một tài khoản",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "Nếu người tham dự tồn tại trong Salesforce, đặt lịch trực tiếp với chủ sở hữu",
"salesforce_check_owner_of": "Thực thể để kiểm tra chủ sở hữu để đặt lịch trực tiếp",
"account": "Tài khoản",
"choose_hosts_schedule": "Chọn lịch trình của người tổ chức",
"hosts_schedule_description": "Bạn có thể chọn lịch trình khác nhau cho từng người tổ chức. Lịch trình mặc định của mỗi người tổ chức sẽ được chọn theo mặc định.",
"no_hosts_description": "Không có người tổ chức nào được chỉ định, hãy thêm người tổ chức trong tab phân công.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "重新安装应用",
"reschedule_reason": "重新安排原因",
"choose_common_schedule_team_event": "选择通用时间表",
"choose_common_schedule_team_event_description": "如果您希望在主持人之间使用通用时间表,请启用此选项。如果禁用,将根据主持人的默认时间表预约每个主持人。",
"choose_common_schedule_team_event_description": "如果您希望在主持人之间使用通用日程,请启用此选项。禁用时,每位主持人将根据其默认日程或选择的日程进行预订。",
"reason": "原因",
"sender_id": "发件人 ID",
"sender_id_error_message": "只允许字母、数字和空格(最多 11 个字符)",
@@ -2595,6 +2595,7 @@
"new_option": "新选项",
"update_profile": "更新成员",
"attribute_updated_successfully": "属性更新成功",
"attribute_deleted_successfully": "属性删除成功",
"attributes_edited_successfully": "属性编辑成功",
"attribute_meta_description": "管理团队成员的属性",
"attributes_edit_description": "编辑团队成员的属性",
@@ -2640,6 +2641,9 @@
"month_to_date": "本月至今",
"year_to_date": "今年至今",
"custom_range": "自定义范围",
"show_all_columns": "显示所有列",
"toggle_columns": "切换列",
"no_columns_found": "未找到列",
"salesforce_create_record_as": "预订时,将事件添加到,并将新参与者添加为:",
"salesforce_lead": "潜在客户",
"salesforce_contact_under_account": "账户下的联系人",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "如果参与者在Salesforce中存在,则直接与所有者预订",
"salesforce_check_owner_of": "检查实体的所有者以直接预订",
"account": "账户",
"choose_hosts_schedule": "选择主持人日程",
"hosts_schedule_description": "您可以为每位主持人选择不同的日程。默认情况下,每位主持人的默认日程被选中。",
"no_hosts_description": "未分配主持人,请在分配选项卡中添加主持人。",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 在此上方添加您的新字符串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1702,7 +1702,7 @@
"invalid_credential_action": "重新安裝應用程式",
"reschedule_reason": "重新預約的原因",
"choose_common_schedule_team_event": "選擇公用行程表",
"choose_common_schedule_team_event_description": "若要在多個主辦者之間使用公用行程表,請啟用此選項。停用時,會根據每個主辦者預設的行程表預約對方的時間。",
"choose_common_schedule_team_event_description": "如果您希望在主持人之間使用共同的排程,請啟用此選項。禁用時,每位主持人將根據其預設排程或選擇的排程進行預訂。",
"reason": "原因",
"sender_id": "傳送者 ID",
"sender_id_error_message": "只能使用字母、數字和空格 (最多 11 個字元)",
@@ -2595,6 +2595,7 @@
"new_option": "新選項",
"update_profile": "更新成員",
"attribute_updated_successfully": "屬性更新成功",
"attribute_deleted_successfully": "屬性已成功刪除",
"attributes_edited_successfully": "屬性編輯成功",
"attribute_meta_description": "管理您的團隊成員的屬性",
"attributes_edit_description": "編輯您的團隊成員的屬性",
@@ -2640,6 +2641,9 @@
"month_to_date": "本月至今",
"year_to_date": "本年至今",
"custom_range": "自訂範圍",
"show_all_columns": "顯示所有欄位",
"toggle_columns": "切換欄位",
"no_columns_found": "未找到欄位",
"salesforce_create_record_as": "預約時,將事件和新參加者新增為:",
"salesforce_lead": "潛在客戶",
"salesforce_contact_under_account": "帳戶下的聯絡人",
@@ -2650,5 +2654,8 @@
"salesforce_book_directly_with_attendee_owner": "如果參加者存在於 Salesforce 中,則直接與擁有者預約",
"salesforce_check_owner_of": "檢查實體的擁有者以直接預約",
"account": "帳戶",
"choose_hosts_schedule": "選擇主持人的排程",
"hosts_schedule_description": "您可以為每位主持人選擇不同的排程。每位主持人的預設排程會自動選擇。",
"no_hosts_description": "未分配主持人,請在分配標籤中添加主持人。",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 請在此處新增您的字串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+10 -7
View File
@@ -1346,6 +1346,10 @@
}
},
"description": "Meeting location"
},
"smsReminderNumber": {
"type": "number",
"description": "SMS reminder number"
}
}
},
@@ -1389,10 +1393,6 @@
"seatsShowAvailabilityCount": {
"type": "boolean",
"description": "Show the number of available seats"
},
"smsReminderNumber": {
"type": "number",
"description": "SMS reminder number"
}
}
},
@@ -1405,15 +1405,18 @@
"responses": {
"name": "Hello Hello",
"email": "hello@gmail.com",
"metadata": {},
"location": "Calcom HQ"
"smsReminderNumber": null,
"location": {
"value": "userPhone",
"optionValue": ""
}
},
"timeZone": "Europe/London",
"language": "en",
"title": "Debugging between Syed Ali Shahbaz and Hello Hello",
"description": null,
"status": "PENDING",
"smsReminderNumber": null
"metadata": {}
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.0 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg width="153" height="40" viewBox="0 0 153 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="153" height="40" fill="#8A05FF"/>
<path d="M17.5001 21L18.2051 21.705L19.5001 20.415V26.5H20.5001V20.415L21.7951 21.705L22.5001 21L20.0001 18.5L17.5001 21Z" fill="white"/>
<path d="M23.7501 23H23.5001V22H23.7501C24.3468 22.0239 24.9286 21.8097 25.3674 21.4047C25.8062 20.9996 26.0662 20.4368 26.0901 19.84C26.1139 19.2433 25.8998 18.6615 25.4947 18.2227C25.0896 17.7838 24.5268 17.5239 23.9301 17.5H23.5001L23.4501 17.09C23.3391 16.2483 22.926 15.4755 22.2876 14.9157C21.6492 14.3559 20.8291 14.0473 19.9801 14.0473C19.131 14.0473 18.3109 14.3559 17.6725 14.9157C17.0341 15.4755 16.621 16.2483 16.5101 17.09L16.5001 17.5H16.0701C15.4733 17.5239 14.9105 17.7838 14.5054 18.2227C14.1003 18.6615 13.8862 19.2433 13.9101 19.84C13.9339 20.4368 14.1939 20.9996 14.6327 21.4047C15.0715 21.8097 15.6533 22.0239 16.2501 22H16.5001V23H16.2501C15.4483 22.995 14.6766 22.6937 14.0835 22.1541C13.4905 21.6145 13.1178 20.8747 13.0371 20.077C12.9565 19.2792 13.1736 18.4798 13.6467 17.8325C14.1198 17.1851 14.8155 16.7355 15.6001 16.57C15.8159 15.5633 16.3705 14.661 17.1712 14.0138C17.9719 13.3665 18.9704 13.0134 20.0001 13.0134C21.0297 13.0134 22.0282 13.3665 22.8289 14.0138C23.6296 14.661 24.1842 15.5633 24.4001 16.57C25.1846 16.7355 25.8803 17.1851 26.3534 17.8325C26.8265 18.4798 27.0436 19.2792 26.963 20.077C26.8823 20.8747 26.5096 21.6145 25.9166 22.1541C25.3235 22.6937 24.5518 22.995 23.7501 23Z" fill="white"/>
<path d="M39.0632 25H35.4372V14.99H39.0632C41.8212 14.99 43.5152 16.782 43.5152 20.002C43.5152 23.222 41.8212 25 39.0632 25ZM36.6972 23.88H39.0632C41.6112 23.88 42.1992 21.962 42.1992 20.002C42.1992 18.042 41.6112 16.11 39.0632 16.11H36.6972V23.88ZM51.4309 21.164C51.4309 21.374 51.4309 21.612 51.4169 21.724H45.8309C45.8589 22.984 46.5589 24.244 48.1409 24.244C49.5409 24.244 50.0729 23.306 50.1989 22.802H51.3329C50.9689 24.132 49.9609 25.21 48.1269 25.21C45.8729 25.21 44.6549 23.586 44.6549 21.416C44.6549 19.134 46.0129 17.65 48.1269 17.65C50.2129 17.65 51.4309 19.078 51.4309 21.164ZM45.8309 20.744H50.2409C50.2409 19.568 49.4009 18.616 48.0709 18.616C46.8389 18.616 45.9009 19.498 45.8309 20.744ZM56.1898 17.664C57.9818 17.664 59.4098 19.078 59.4098 21.43C59.4098 23.782 57.9818 25.196 56.1898 25.196C55.0418 25.196 54.3698 24.692 53.9498 24.076H53.9218V27.478H52.8018V17.86H53.9218V18.742H53.9498C54.3698 18.196 55.0418 17.664 56.1898 17.664ZM56.0778 24.216C57.5478 24.216 58.2338 22.83 58.2338 21.43C58.2338 20.03 57.5478 18.644 56.0778 18.644C54.7478 18.644 53.9218 19.75 53.9218 21.43C53.9218 23.11 54.7478 24.216 56.0778 24.216ZM61.9094 25H60.7894V14.99H61.9094V25ZM66.7855 25.21C64.6015 25.21 63.2855 23.656 63.2855 21.43C63.2855 19.204 64.6015 17.65 66.7855 17.65C68.9555 17.65 70.2715 19.204 70.2715 21.43C70.2715 23.656 68.9555 25.21 66.7855 25.21ZM66.7855 24.23C68.3815 24.23 69.0955 22.956 69.0955 21.43C69.0955 19.904 68.3815 18.63 66.7855 18.63C65.1755 18.63 64.4615 19.904 64.4615 21.43C64.4615 22.956 65.1755 24.23 66.7855 24.23ZM72.202 27.506C71.95 27.506 71.782 27.492 71.46 27.436V26.484C71.698 26.512 71.81 26.526 72.02 26.526C72.608 26.526 73.252 26.162 73.56 24.93L70.662 17.86H71.922L74.12 23.544H74.148L76.248 17.86H77.452L74.568 25.21C73.854 27.016 73.168 27.506 72.202 27.506ZM84.237 17.86V18.84H82.837V23.138C82.837 23.894 83.019 24.09 83.649 24.09C83.887 24.09 83.999 24.076 84.237 24.048V25C83.915 25.056 83.747 25.07 83.495 25.07C82.445 25.07 81.717 24.706 81.717 23.082V18.84H80.667V17.86H81.717V15.48H82.837V17.86H84.237ZM88.5095 25.21C86.3255 25.21 85.0095 23.656 85.0095 21.43C85.0095 19.204 86.3255 17.65 88.5095 17.65C90.6795 17.65 91.9955 19.204 91.9955 21.43C91.9955 23.656 90.6795 25.21 88.5095 25.21ZM88.5095 24.23C90.1055 24.23 90.8195 22.956 90.8195 21.43C90.8195 19.904 90.1055 18.63 88.5095 18.63C86.8995 18.63 86.1855 19.904 86.1855 21.43C86.1855 22.956 86.8995 24.23 88.5095 24.23ZM97.98 25H96.72V14.99H100.738C102.936 14.99 104.084 16.026 104.084 17.874C104.084 19.12 103.314 20.072 102.264 20.324V20.352C104.518 20.786 103.692 24.748 104.224 24.916V25H102.866C102.432 24.58 103.384 20.954 100.85 20.954H97.98V25ZM97.98 19.834H100.444C101.97 19.834 102.824 19.148 102.824 17.902C102.824 16.754 102.18 16.11 100.486 16.11H97.98V19.834ZM111.962 21.164C111.962 21.374 111.962 21.612 111.948 21.724H106.362C106.39 22.984 107.09 24.244 108.672 24.244C110.072 24.244 110.604 23.306 110.73 22.802H111.864C111.5 24.132 110.492 25.21 108.658 25.21C106.404 25.21 105.186 23.586 105.186 21.416C105.186 19.134 106.544 17.65 108.658 17.65C110.744 17.65 111.962 19.078 111.962 21.164ZM106.362 20.744H110.772C110.772 19.568 109.932 18.616 108.602 18.616C107.37 18.616 106.432 19.498 106.362 20.744ZM116.833 17.664C118.135 17.664 119.227 18.378 119.227 20.016V25H118.107V20.296C118.107 19.372 117.673 18.644 116.553 18.644C115.293 18.644 114.453 19.442 114.453 20.548V25H113.333V17.86H114.453V18.714H114.481C114.859 18.21 115.615 17.664 116.833 17.664ZM126.047 18.784V14.99H127.167V25H126.047V24.118H126.019C125.599 24.664 124.927 25.196 123.779 25.196C121.987 25.196 120.559 23.782 120.559 21.43C120.559 19.078 121.987 17.664 123.779 17.664C124.927 17.664 125.599 18.168 126.019 18.784H126.047ZM123.891 24.216C125.221 24.216 126.047 23.11 126.047 21.43C126.047 19.75 125.221 18.644 123.891 18.644C122.421 18.644 121.735 20.03 121.735 21.43C121.735 22.83 122.421 24.216 123.891 24.216ZM135.323 21.164C135.323 21.374 135.323 21.612 135.309 21.724H129.723C129.751 22.984 130.451 24.244 132.033 24.244C133.433 24.244 133.965 23.306 134.091 22.802H135.225C134.861 24.132 133.853 25.21 132.019 25.21C129.765 25.21 128.547 23.586 128.547 21.416C128.547 19.134 129.905 17.65 132.019 17.65C134.105 17.65 135.323 19.078 135.323 21.164ZM129.723 20.744H134.133C134.133 19.568 133.293 18.616 131.963 18.616C130.731 18.616 129.793 19.498 129.723 20.744ZM139.816 17.664C140.082 17.664 140.278 17.678 140.474 17.706V18.826C140.306 18.798 140.18 18.784 140.012 18.784C138.696 18.784 137.814 19.82 137.814 21.192V25H136.694V17.86H137.814V19.274H137.842C138.22 18.336 138.794 17.664 139.816 17.664Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

+3
View File
@@ -129,7 +129,10 @@
"pages": [
"self-hosting/deployments/aws",
"self-hosting/deployments/azure",
"self-hosting/deployments/elastio",
"self-hosting/deployments/gcp",
"self-hosting/deployments/railway",
"self-hosting/deployments/render",
"self-hosting/deployments/vercel"
]
},
+36
View File
@@ -0,0 +1,36 @@
---
title: "Create EventType"
---
The create event-type atom enables you to display a form with the required input fields needed to create an event-type.
By specifying a team id as a prop of the component you can create an event-type for a specific team.
```js
import { CreateEventType } from "@calcom/atoms";
export default function EventType() {
return (
<>
<CreateEventType
onSuccess={(eventType) => {
console.log("EventType created", eventType);
}}
onError={(err) => {
console.log("Error while creating eventType", err);
}}
onCancel={() => {
console.log("Clicked on cancel button")
}}
customClassNames={{
atomsWrapper: "border p-4 shadow-md rounded-md",
buttons: { container: "justify-center", submit: "bg-red-500", cancel: "bg-gray-300" },
}}
/>
<>
)
}
```
<Info>
Please ensure all custom classnames are valid [Tailwind CSS](https://tailwindcss.com/) classnames.
</Info>
@@ -5,31 +5,31 @@ title: "Introduction"
Install desired apps in your self-hosted instance for better integration.
<CardGroup cols={2}>
<Card href="/self-hosting/install-apps/google" title="Google" icon="google">
<Card href="/self-hosting/apps/install-apps/google" title="Google" icon="google">
Install google for supporting Google Calendar and Google Meet integrations
</Card>
<Card href="/self-hosting/install-apps/microsoft" title="Microsoft" icon="microsoft">
<Card href="/self-hosting/apps/install-apps/microsoft" title="Microsoft" icon="microsoft">
Install microsoft to support Exchange365 integration
</Card>
<Card href="/self-hosting/install-apps/zoom" title="Zoom" icon="z">
<Card href="/self-hosting/apps/install-apps/zoom" title="Zoom" icon="z">
Install Zoom to support Zoom video conferencing integration
</Card>
<Card href="/self-hosting/install-apps/daily" title="Daily" icon="video">
<Card href="/self-hosting/apps/install-apps/daily" title="Daily" icon="video">
Install Daily to support Cal Video integration
</Card>
<Card href="/self-hosting/install-apps/hubspot" title="Hubspot" icon="hubspot">
<Card href="/self-hosting/apps/install-apps/hubspot" title="Hubspot" icon="hubspot">
Install Hubspot to support Hubspot CRM integration
</Card>
<Card href="/self-hosting/install-apps/sendgrid" title="Sendgrid" icon="envelope">
<Card href="/self-hosting/apps/install-apps/sendgrid" title="Sendgrid" icon="envelope">
Install Sendgrid to support Sendgrid Email integration
</Card>
<Card href="/self-hosting/install-apps/stripe" title="Stripe" icon="stripe">
<Card href="/self-hosting/apps/install-apps/stripe" title="Stripe" icon="stripe">
Install Stripe to support Stripe integration for paid bookings
</Card>
<Card href="/self-hosting/install-apps/twilio" title="Twilio" icon="grid-round-2" iconType="solid">
<Card href="/self-hosting/apps/install-apps/twilio" title="Twilio" icon="grid-round-2" iconType="solid">
Install Twilio to support Workflows integration for sms reminders
</Card>
<Card href="/self-hosting/install-apps/zoho" title="Zoho" icon="square-dashed">
<Card href="/self-hosting/apps/install-apps/zoho" title="Zoho" icon="square-dashed">
Install Zoho to support Zoho CRM integration
</Card>
</CardGroup>
+8
View File
@@ -6,6 +6,14 @@ icon: "aws"
Deploying Cal.com on AWS
___
## One Click Deployment
<a href="https://console.aws.amazon.com/amplify/home#/deploy?repo=https://github.com/calcom/docker" target="_blank">
<img src="https://oneclick.amplifyapp.com/button.svg" noZoom alt="Deploy on AWS" />
</a>
## Manual Deployment
<Steps>
<Step title="Prerequisites">
1. Amazon Web Services account
+10
View File
@@ -6,6 +6,16 @@ icon: "microsoft"
Deploying Cal.com on Azure
___
## One Click Deployment
<a href="https://portal.azure.com/#create/Microsoft.Template/uri/https://raw.githubusercontent.com/calcom/docker/main/azuredeploy.json" target="_blank">
<img src="https://aka.ms/deploytoazurebutton" noZoom alt="Deploy to Azure"/>
</a>
## Manual Deployment
<Steps>
<Step title="Prerequisites">
1. Microsoft Azure account
+13
View File
@@ -0,0 +1,13 @@
---
title: "Elastio"
icon: "square-e"
iconType: "solid"
---
You can deploy Cal.com on Elastio using the button below.
## One Click Deployment
<a href="https://elest.io/open-source/cal.com" target="_blank">
<img src="/images/deploy-elastio.png" noZoom alt="Deploy on Elastio" />
</a>
+8
View File
@@ -7,6 +7,14 @@ Deploying Cal.com on Google Cloud Platform (GCP)
In this guide, we will go over the steps to deploy cal.com on Google Cloud Platform (GCP). We will cover how to create a virtual machine, configure it, install Docker, and finally deploy the cal.com application.
## One Click Deployment
<a href="https://deploy.cloud.run/?git_repo=https://github.com/calcom/docker" target="_blank">
<img src="https://storage.googleapis.com/gweb-cloudblog-publish/images/run_on_google_cloud.max-300x300.png" noZoom alt="Deploy on Google Cloud" />
</a>
## Manual Deployment
<Steps>
<Step title="Creating a Virtual Machine">
### Go to the GCP Console
+13
View File
@@ -0,0 +1,13 @@
---
title: "Railway"
icon: "square-r"
---
You can deploy Cal.com on Railway using the button below.
The team at Railway also have a [detailed blog post](https://blog.railway.app/p/calendso) on deploying Cal.com on their platform.
## One Click Deployment
<a href="https://railway.app/new/template/cal" target="_blank">
<img src="/images/deploy-railway.svg" noZoom alt="Deploy on Railway" />
</a>
+13
View File
@@ -0,0 +1,13 @@
---
title: "Render"
icon: "square-r"
iconType: "solid"
---
You can deploy Cal.com on Render using the button below.
## One Click Deployment
<a href="https://render.com/deploy?repo=https://github.com/calcom/docker" target="_blank">
<img src="/images/deploy-render.svg" alt="Deploy on Render" noZoom />
</a>
+9 -1
View File
@@ -6,9 +6,17 @@ iconType: "solid"
## Requirements
Currently Vercel Pro Plan is required to be able to Deploy this application with Vercel, due to limitations on the number of serverless functions on the free plan.
You need a PostgresDB database hosted somewhere. [Supabase](https://supabase.com/) offer a great free option while [Heroku](https://www.heroku.com/) offers a low-cost option.
## Getting Started
## One Click Deployment
<a href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com&env=DATABASE_URL,NEXT_PUBLIC_WEBAPP_URL,NEXTAUTH_URL,NEXTAUTH_SECRET,CRON_API_KEY,CALENDSO_ENCRYPTION_KEY&envDescription=See%20all%20available%20env%20vars&envLink=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com%2Fblob%2Fmain%2F.env.example&project-name=cal&repo-name=cal.com&build-command=cd%20../..%20%26%26%20yarn%20build&root-directory=apps%2Fweb%2F" target="_blank">
<img src="https://vercel.com/button" noZoom alt="Deploy with Vercel" />
</a>
## Manual Deployment
### Local settings
-12
View File
@@ -17,18 +17,6 @@ To run Cal.com, you need to install a few things. **Node.js, yarn, Git and Postg
To ensure optimal performance and compatibility, we highly recommend using Node.js version 18 for your development environment. This version provides the best balance of stability, features, and security for this project. Please make sure to update your Node.js installation if necessary.
</Info>
## Prisma Data Proxy
To use cal.com with prisma data proxy, you can:
1. Set in the .env file
```
PRISMA_GENERATE_DATAPROXY=true
```
2. Set DATABASE_URL in the .env file equal to the prisma connection string
```
DATABASE_URL="prisma://****:****@****"
```
## Production Build
1. First, you git clone the repository with the following command, so you have a copy of the code.
+8 -1
View File
@@ -1704,7 +1704,7 @@ checksums:
invalid_credential_action: 127776c374b77c1b3883d5cb7f9a8817
reschedule_reason: 375399a84bfe199e31165cb63d833d3c
choose_common_schedule_team_event: 60d29b0f512b0a6482ecd6f13627b285
choose_common_schedule_team_event_description: b787458df2c3f097756d7cdb60da6ece
choose_common_schedule_team_event_description: 4d4813a44066fb6209cbcb6745447556
reason: 9b66f286b86dd0e87b1ac0604c9d1ced
sender_id: 680bc78dc8a2c837c872bf6dcca5d1ad
sender_id_error_message: 2b4e0f06152b212b2a2bd53d2d9ce7b6
@@ -2597,6 +2597,7 @@ checksums:
new_option: 53ef69223fa6d5d640121556cebfef27
update_profile: 4c76016c39de93fbfaa15d71d66055d3
attribute_updated_successfully: 0e64422156c29940cd4dab2f9d1f40b2
attribute_deleted_successfully: 397d8b28927091e8d892b02a606deae7
attributes_edited_successfully: 2f7d5f01066da918c78ce93b8caf046d
attribute_meta_description: 965d429537f63c619fa098838d873d88
attributes_edit_description: 85ca457819aa45b160cfea08cc446c34
@@ -2642,6 +2643,9 @@ checksums:
month_to_date: 7f7e4653c67cdaab178faa332007b1a0
year_to_date: 1de1bfcb57fc841987305d851383042a
custom_range: c489facbacd8b2b232c9ea6aae568ae3
show_all_columns: f8c81014fa436fa5baf3d0be325e2988
toggle_columns: 33ea1f6185497c358ae57e79e327c266
no_columns_found: c73ee3c789abf7838f3c39c661d74db0
salesforce_create_record_as: cbbfbdf5568f642bfc38dc83284c3c18
salesforce_lead: f1dee70c3b331625a29cc93338b3078c
salesforce_contact_under_account: 7311a2c181c34ad69ff9751472d4f7f3
@@ -2652,4 +2656,7 @@ checksums:
salesforce_book_directly_with_attendee_owner: 32527f7bc50272de1cb81401e0bf17a9
salesforce_check_owner_of: f9e2369739fa3b659b86ed78c1937b11
account: 01215c12fb1cdb93bd0c84c1382bef56
choose_hosts_schedule: b5405848d78c8bbf891764eb85ad6c78
hosts_schedule_description: 9fbda99f7028e2cc126d158c903af652
no_hosts_description: e9b5a1e85244a43ee254ded46c81f1ec
ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS: 18323f2f3fabb169a7cb50ff62433850
+1 -1
View File
@@ -8,7 +8,7 @@
- [x] Connect Form Response to Booking
- [x] Test with non option based attributes and form field input
- [ ] Switching from Select to MultiSelect in attribute
- [x] It immediately makes the Routing logic to not work. Fix it.
- [ ] It immediately makes the Routing logic to not work. Fix it.
- [ ] It removes the selected option from Routing. Fix it.
- [ ] Warnings are needed with recommendation to create new attribute instead in unsupported scenarios. We need to test for more such changes to Attribute types.
- [ ] When choosing 'Value of Field' option in RAQB, show a warning somewhere if all options of the Form field aren't present in the attribute option.
@@ -42,7 +42,6 @@ const assertSelectOperators = (config: any) => {
expect(config.types.select.widgets.multiselect.operators).not.toContain("select_equals");
expect(config.types.select.widgets.multiselect.operators).not.toContain("select_not_equals");
expect(config.types.select.widgets.select.operators).toContain("select_equals");
expect(config.types.select.widgets.select.operators).toContain("select_not_equals");
@@ -61,9 +60,13 @@ describe("Query Builder Config", () => {
assertCommonStructure(FormFieldsBaseConfig);
});
it("should not support multiselect_contains and multiselect_not_contains - because they are not supported in Prisma for reporting(probably)", () => {
expect(FormFieldsBaseConfig.operators).not.toHaveProperty("multiselect_contains");
expect(FormFieldsBaseConfig.operators).not.toHaveProperty("multiselect_not_contains");
it("should not support multiselect_some_in and multiselect_not_some_in - because they are not supported in Prisma for reporting(probably)", () => {
expect(FormFieldsBaseConfig.types.multiselect.widgets.multiselect.operators).not.toContain(
"multiselect_some_in"
);
expect(FormFieldsBaseConfig.types.multiselect.widgets.multiselect.operators).not.toContain(
"multiselect_not_some_in"
);
});
it("should support select_any_in, select_not_any_in, select_equals, select_not_equals - Verify both types.widgets and operators", () => {
@@ -84,9 +87,23 @@ describe("Query Builder Config", () => {
assertCommonStructure(AttributesBaseConfig);
});
it("should support multiselect_contains and multiselect_not_contains operators", () => {
expect(AttributesBaseConfig.operators).toHaveProperty("multiselect_contains");
expect(AttributesBaseConfig.operators).toHaveProperty("multiselect_not_contains");
it("should support multiselect_some_in operator for multiselect", () => {
expect(AttributesBaseConfig.types.multiselect.widgets.multiselect.operators).toContain(
"multiselect_some_in"
);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const jsonLogic = AttributesBaseConfig.operators.multiselect_some_in.jsonLogic(
["A"],
"multiselect_some_in",
["A", "B"]
);
// Verifies that it is using some-in implementation
expect(jsonLogic).toEqual({
some: [["A"], { in: [{ var: "" }, ["A", "B"]] }],
});
});
it("should support select_any_in, select_not_any_in, select_equals, select_not_equals - Verify both types.widgets and operators", () => {
@@ -179,21 +179,30 @@ const operators: Operators = {
labelForFormat: "NOT IN",
reversedOp: "select_any_in",
},
// We define this operator but use it conditionally for multiselect for Attributes only
multiselect_some_in: {
label: "Any in",
jsonLogic: (field: any, operator: any, vals: any) => {
return {
// Tested in jsonLogic.test.ts
some: [field, { in: [{ var: "" }, vals] }],
};
},
},
multiselect_equals: {
// TODO: Consider renaming it to "includes" or similar due to faulty implementation of jsonLogic and the operator is in use by users.
label: "Equals",
labelForFormat: "==",
label: "All in",
reversedOp: "multiselect_not_equals",
// jsonLogic2: "all-in",
jsonLogic: (field: any, op: any, vals: any) => ({
// This is wrongly implemented as "includes". This isn't "equals". Because if field is ["a" ] and vals is ["a", "b"], it still matches. Expectation would probably be that it should be a strict match(["a", "b"] or ["b", "a"])
all: [field, { in: [{ var: "" }, vals] }],
}),
jsonLogic: (field: any, op: any, vals: any, ...rest) => {
return {
// This is wrongly implemented as "includes". This isn't "equals". Because if field is ["a" ] and vals is ["a", "b"], it still matches. Expectation would probably be that it should be a strict match(["a", "b"] or ["b", "a"])
all: [field, { in: [{ var: "" }, vals] }],
};
},
},
multiselect_not_equals: {
isNotOp: true,
label: "Not equals",
labelForFormat: "!=",
label: "Not all in",
reversedOp: "multiselect_equals",
},
some: {
@@ -340,10 +349,7 @@ const types: Types = {
defaultOperator: "select_equals",
widgets: {
select: {
operators: [
"select_equals",
"select_not_equals",
],
operators: ["select_equals", "select_not_equals"],
widgetProps: {
customProps: {
showSearch: true,
@@ -351,10 +357,7 @@ const types: Types = {
},
},
multiselect: {
operators: [
"select_any_in",
"select_not_any_in",
],
operators: ["select_any_in", "select_not_any_in"],
},
},
},
@@ -362,14 +365,7 @@ const types: Types = {
defaultOperator: "multiselect_equals",
widgets: {
multiselect: {
operators: [
"multiselect_equals",
"multiselect_not_equals",
// "is_empty",
// "is_not_empty",
"is_null",
"is_not_null",
],
operators: ["multiselect_equals", "multiselect_not_equals", "is_null", "is_not_null"],
},
},
},
@@ -129,7 +129,15 @@ function getWidgets(_configFor: ConfigFor) {
return widgets;
}
function getTypes(_configFor: ConfigFor) {
function getTypes(configFor: ConfigFor) {
const multiSelectOperators = BasicConfig.types.multiselect.widgets.multiselect.operators || [];
if (configFor === ConfigFor.Attributes) {
// Attributes don't need reporting at the moment. So, we can support multiselect_some_in operator for attributes.
// We could probably use them in FormFields later once they are supported through Prisma query as well
multiSelectOperators.push("multiselect_some_in");
}
const types: Types = {
...BasicConfig.types,
phone: {
@@ -150,12 +158,7 @@ function getTypes(_configFor: ConfigFor) {
...BasicConfig.types.multiselect.widgets,
multiselect: {
...BasicConfig.types.multiselect.widgets.multiselect,
operators: [
...(BasicConfig.types.multiselect.widgets.multiselect.operators || []),
// TODO: First verify the definition of multiselect_contains and multiselect_not_contains and then uncomment these operators
// "multiselect_contains",
// "multiselect_not_contains",
],
operators: [...multiSelectOperators],
},
},
},
@@ -167,58 +170,6 @@ function getOperators(configFor: ConfigFor) {
// Clone to avoid mutating the original object
const operators: Operators = {
...BasicConfig.operators,
// Attributes don't need reporting at the moment. So, we can support contains and not contains operators for attributes.
// We could probably use them in FormFields if they are supported through Prisma query as well.
...(configFor === ConfigFor.Attributes
? {
multiselect_contains: {
label: "Contains",
labelForFormat: "CONTAINS",
reversedOp: "multiselect_not_contains",
// jsonLogic2: "some-in",
jsonLogic: function (e, t, r) {
return {
some: [
e,
{
in: [
{
var: "",
},
r,
],
},
],
};
},
},
multiselect_not_contains: {
isNotOp: !0,
label: "Not contains",
labelForFormat: "NOT CONTAINS",
reversedOp: "multiselect_contains",
// jsonLogic2: "!some-in",
jsonLogic: function (e, t, r) {
return {
"!": {
some: [
e,
{
in: [
{
var: "",
},
r,
],
},
],
},
};
},
_jsonLogicIsExclamationOp: !0,
},
}
: {}),
};
return operators;
@@ -12,17 +12,27 @@ export const enum RaqbLogicResult {
LOGIC_NOT_FOUND_SO_MATCHED = "LOGIC_NOT_FOUND_SO_MATCHED",
}
export const evaluateRaqbLogic = ({
queryValue,
queryBuilderConfig,
data,
beStrictWithEmptyLogic = false,
}: {
queryValue: JsonTree;
queryBuilderConfig: any;
data: Record<string, unknown>;
beStrictWithEmptyLogic?: boolean;
}): RaqbLogicResult => {
export const evaluateRaqbLogic = (
{
queryValue,
queryBuilderConfig,
data,
beStrictWithEmptyLogic = false,
}: {
queryValue: JsonTree;
queryBuilderConfig: any;
data: Record<string, unknown>;
beStrictWithEmptyLogic?: boolean;
},
config: {
// 2 - Error/Warning
// 1 - Info
// 0 - Debug
logLevel: 0 | 1 | 2;
} = {
logLevel: 1,
}
): RaqbLogicResult => {
const state = {
tree: QbUtils.checkTree(QbUtils.loadTree(queryValue), queryBuilderConfig),
config: queryBuilderConfig,
@@ -40,7 +50,10 @@ export const evaluateRaqbLogic = ({
// If no logic is provided, then consider it a match
return RaqbLogicResult.LOGIC_NOT_FOUND_SO_MATCHED;
}
console.log("Checking logic with data", safeStringify({ logic, data }));
if (config.logLevel >= 1) {
console.log("Checking logic with data", safeStringify({ logic, data }));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return !!jsonLogic.apply(logic as any, data) ? RaqbLogicResult.MATCH : RaqbLogicResult.NO_MATCH;
};
@@ -67,7 +67,45 @@ describe("jsonLogic", () => {
});
});
describe("all-in", () => {
describe("all-in -> used by multiselect_equals", () => {
it("should return true if all elements of the array in 'data' is in the jsonLogic 'in' array using case insensitivity", () => {
expect(
jsonLogic.apply(
{
and: [
{
all: [{ var: "location" }, { in: [{ var: "" }, ["nevada", "abc"]] }],
},
],
},
// Data
{
there: "no",
location: ["nevada"],
}
)
).toBe(true);
});
it("should return false if even one element of the array in 'data' is not in the jsonLogic 'in' array using case insensitivity", () => {
expect(
jsonLogic.apply(
{
and: [
{
all: [{ var: "location" }, { in: [{ var: "" }, ["India", "Canada"]] }],
},
],
},
// Data
{
there: "no",
location: ["India", "Australia"],
}
)
).toBe(false);
});
it("should return true if the variable value is an array and contains the string", () => {
expect(
jsonLogic.apply(
@@ -78,17 +116,18 @@ describe("jsonLogic", () => {
},
],
},
// Data
{
there: "no",
location: ["nevada"],
location: "nevada",
}
)
).toBe(true);
).toBe(false);
});
});
describe("some-in", () => {
it("should return true if the variable value is an array and contains the string", () => {
describe("some-in -> used by multiselect_some_in", () => {
it("should return true if the only element of the array in 'data' is in the jsonLogic 'in' array using case insensitivity", () => {
expect(
jsonLogic.apply(
{
@@ -99,17 +138,68 @@ describe("jsonLogic", () => {
{
var: "",
},
["nevada", "ABC"],
["India", "USA"],
],
},
],
},
// data
{
there: "no",
location: ["nevada"],
location: ["india"],
}
)
).toBe(true);
});
it("should return true if at least one element of the array in 'data' is in the jsonLogic 'in' array using case insensitivity", () => {
expect(
jsonLogic.apply(
{
some: [
{ var: "location" },
{
in: [
{
var: "",
},
["India", "USA"],
],
},
],
},
// Data
{
there: "no",
location: ["india", "Australia"],
}
)
).toBe(true);
});
it("should return false if none of the elements of the array in 'data' is in the jsonLogic 'in' array using case insensitivity", () => {
expect(
jsonLogic.apply(
{
some: [
{ var: "location" },
{
in: [
{
var: "",
},
["India", "Canada"],
],
},
],
},
// Data
{
there: "no",
location: ["Australia", "Iceland"],
}
)
).toBe(false);
});
});
});
@@ -31,12 +31,61 @@ function mockAttributesScenario({
);
}
function mockHugeAttributesOfTypeSingleSelect({
numAttributes,
numOptionsPerAttribute,
numTeamMembers,
numAttributesUsedPerTeamMember,
}: {
numAttributes: number;
numOptionsPerAttribute: number;
numTeamMembers: number;
numAttributesUsedPerTeamMember: number;
}) {
if (numAttributesUsedPerTeamMember > numAttributes) {
throw new Error("numAttributesUsedPerTeamMember cannot be greater than numAttributes");
}
const attributes = Array.from({ length: numAttributes }, (_, i) => ({
id: `attr${i + 1}`,
name: `Attribute ${i + 1}`,
type: "SINGLE_SELECT" as const,
slug: `attribute-${i + 1}`,
options: Array.from({ length: numOptionsPerAttribute }, (_, i) => ({
id: `opt${i + 1}`,
value: `Option ${i + 1}`,
slug: `option-${i + 1}`,
})),
}));
const assignedAttributeOptionIdForEachMember = 1;
const teamMembersWithAttributeOptionValuePerAttribute = Array.from({ length: numTeamMembers }, (_, i) => ({
userId: i + 1,
attributes: Object.fromEntries(
Array.from({ length: numAttributesUsedPerTeamMember }, (_, j) => [
attributes[j].id,
attributes[j].options[assignedAttributeOptionIdForEachMember].value,
])
),
}));
mockAttributesScenario({
attributes,
teamMembersWithAttributeOptionValuePerAttribute,
});
return {
attributes,
teamMembersWithAttributeOptionValuePerAttribute,
};
}
function buildQueryValue({
rules,
}: {
rules: {
raqbFieldId: string;
value: string | number | string[];
value: string | number | string[] | [string[]];
operator: string;
valueSrc: NonNullable<BaseWidget["valueSrc"]>[];
valueType: string[];
@@ -68,8 +117,9 @@ function buildSelectTypeFieldQueryValue({
}: {
rules: {
raqbFieldId: string;
value: string | number | string[];
value: string | number | string[] | [string[]];
operator: string;
valueType?: string[];
}[];
}) {
return buildQueryValue({
@@ -78,7 +128,7 @@ function buildSelectTypeFieldQueryValue({
value: rule.value,
operator: rule.operator,
valueSrc: ["value"],
valueType: ["select"],
valueType: rule.valueType ?? ["select"],
})),
}) as AttributesQueryValue;
}
@@ -126,7 +176,7 @@ describe("findTeamMembersMatchingAttributeLogicOfRoute", () => {
});
it("should return null if route is not found", async () => {
const result = await findTeamMembersMatchingAttributeLogicOfRoute({
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogicOfRoute({
form: { routes: [], fields: [] },
response: {},
routeId: "non-existent-route",
@@ -137,7 +187,7 @@ describe("findTeamMembersMatchingAttributeLogicOfRoute", () => {
});
it("should return null if the route does not have an attributesQueryValue set", async () => {
const result = await findTeamMembersMatchingAttributeLogicOfRoute({
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogicOfRoute({
form: {
routes: [
{
@@ -183,7 +233,7 @@ describe("findTeamMembersMatchingAttributeLogicOfRoute", () => {
],
}) as AttributesQueryValue;
const result = await findTeamMembersMatchingAttributeLogicOfRoute({
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogicOfRoute({
form: {
routes: [
{
@@ -251,7 +301,7 @@ describe("findTeamMembersMatchingAttributeLogicOfRoute", () => {
],
}) as AttributesQueryValue;
const result = await findTeamMembersMatchingAttributeLogicOfRoute({
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogicOfRoute({
form: {
routes: [
buildDefaultCustomPageRoute({
@@ -286,6 +336,78 @@ describe("findTeamMembersMatchingAttributeLogicOfRoute", () => {
]);
});
it("should return matching team members with a SINGLE_SELECT attribute when 'Any in' option is selected", async () => {
const Option1OfAttribute1HumanReadableValue = "Option 1";
const Option1OfField1HumanReadableValue = Option1OfAttribute1HumanReadableValue;
const Field1Id = "field-1";
const Option1OfAttribute1 = {
id: "attr-1-opt-1",
value: Option1OfAttribute1HumanReadableValue,
slug: "option-1",
};
const Option2OfAttribute1 = {
id: "attr-1-opt-2",
value: "Option 2",
slug: "option-2",
};
const Option3OfAttribute1 = {
id: "attr-1-opt-3",
value: "Option 3",
slug: "option-3",
};
const Attribute1 = {
id: "attr1",
name: "Attribute 1",
type: "SINGLE_SELECT" as const,
slug: "attribute-1",
options: [Option1OfAttribute1, Option2OfAttribute1, Option3OfAttribute1],
};
mockAttributesScenario({
attributes: [Attribute1],
teamMembersWithAttributeOptionValuePerAttribute: [
{ userId: 1, attributes: { [Attribute1.id]: Option1OfAttribute1.value } },
],
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: Attribute1.id,
value: [[Option1OfAttribute1.id, Option2OfAttribute1.id]],
operator: "select_any_in",
valueType: ["multiselect"],
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result } = await findTeamMembersMatchingAttributeLogicOfRoute({
form: {
routes: [
buildDefaultCustomPageRoute({
id: "test-route",
attributesQueryValue: attributesQueryValue,
}),
],
fields: [],
},
response: {},
routeId: "test-route",
teamId: 1,
});
expect(result).toEqual([
{
userId: 1,
result: RaqbLogicResult.MATCH,
},
]);
});
describe("Error handling", () => {
it("should throw an error if the attribute type is not supported", async () => {
const Option1OfAttribute1 = { id: "opt1", value: "Option 1", slug: "option-1" };
@@ -332,4 +454,86 @@ describe("findTeamMembersMatchingAttributeLogicOfRoute", () => {
).rejects.toThrow("Unsupported attribute type");
});
});
describe("Performance testing", () => {
describe("20 attributes, 4000 team members", async () => {
// In tests, the performance is actually really bad than real world. So, skipping this test for now
it("should return matching team members with a SINGLE_SELECT attribute when 'all in' option is selected", async () => {
const { attributes } = mockHugeAttributesOfTypeSingleSelect({
numAttributes: 20,
numOptionsPerAttribute: 30,
numTeamMembers: 4000,
numAttributesUsedPerTeamMember: 10,
});
const attributesQueryValue = buildSelectTypeFieldQueryValue({
rules: [
{
raqbFieldId: attributes[0].id,
value: [attributes[0].options[1].id],
operator: "select_equals",
},
],
}) as AttributesQueryValue;
const { teamMembersMatchingAttributeLogic: result, timeTaken } =
await findTeamMembersMatchingAttributeLogicOfRoute(
{
form: {
routes: [
buildDefaultCustomPageRoute({
id: "test-route",
attributesQueryValue: attributesQueryValue,
}),
],
fields: [],
},
response: {},
routeId: "test-route",
teamId: 1,
},
{
concurrency: 1,
enablePerf: true,
}
);
expect(result).toEqual(
expect.arrayContaining([
{
userId: 1,
result: RaqbLogicResult.MATCH,
},
{
userId: 2,
result: RaqbLogicResult.MATCH,
},
{
userId: 3,
result: RaqbLogicResult.MATCH,
},
{
userId: 2000,
result: RaqbLogicResult.MATCH,
},
// Last Item
{
userId: 4000,
result: RaqbLogicResult.MATCH,
},
])
);
if (!timeTaken) {
throw new Error("Looks like performance testing is not enabled");
}
const totalTimeTaken = Object.values(timeTaken).reduce((sum, time) => (sum ?? 0) + (time ?? 0), 0);
console.log("Total time taken", totalTimeTaken, {
timeTaken,
});
expect(totalTimeTaken).toBeLessThan(1000);
// All of them should match
expect(result?.length).toBe(4000);
}, 10000);
});
});
});
@@ -1,3 +1,6 @@
import type { ServerResponse } from "http";
import type { NextApiResponse } from "next";
import { entityPrismaWhereClause } from "@calcom/lib/entityPermissionUtils";
import { UserRepository } from "@calcom/lib/server/repository/user";
import type { PrismaClient } from "@calcom/prisma";
@@ -12,6 +15,7 @@ interface FindTeamMembersMatchingAttributeLogicHandlerOptions {
ctx: {
prisma: PrismaClient;
user: NonNullable<TrpcSessionUser>;
res: ServerResponse | NextApiResponse | undefined;
};
input: TFindTeamMembersMatchingAttributeLogicInputSchema;
}
@@ -21,7 +25,7 @@ export const findTeamMembersMatchingAttributeLogicHandler = async ({
input,
}: FindTeamMembersMatchingAttributeLogicHandlerOptions) => {
const { prisma, user } = ctx;
const { formId, response, routeId } = input;
const { formId, response, routeId, _enablePerf, _concurrency } = input;
const form = await prisma.app_RoutingForms_Form.findFirst({
where: {
@@ -46,18 +50,35 @@ export const findTeamMembersMatchingAttributeLogicHandler = async ({
const serializableForm = await getSerializableForm({ form });
const matchingTeamMembersWithResult = await findTeamMembersMatchingAttributeLogicOfRoute({
response,
routeId,
form: serializableForm,
teamId: form.teamId,
});
const {
teamMembersMatchingAttributeLogic: matchingTeamMembersWithResult,
timeTaken: teamMembersMatchingAttributeLogicTimeTaken,
} = await findTeamMembersMatchingAttributeLogicOfRoute(
{
response,
routeId,
form: serializableForm,
teamId: form.teamId,
},
{
enablePerf: _enablePerf,
concurrency: _concurrency,
}
);
if (!matchingTeamMembersWithResult) {
return null;
}
const matchingTeamMembersIds = matchingTeamMembersWithResult.map((member) => member.userId);
const matchingTeamMembers = await UserRepository.findByIds({ ids: matchingTeamMembersIds });
console.log('_enablePerf, _concurrency', _enablePerf, _concurrency)
if (_enablePerf) {
const serverTimingHeader = getServerTimingHeader(teamMembersMatchingAttributeLogicTimeTaken);
ctx.res?.setHeader("Server-Timing", serverTimingHeader);
console.log('Server-Timing', serverTimingHeader)
}
return matchingTeamMembers.map((user) => ({
id: user.id,
name: user.name,
@@ -65,4 +86,21 @@ export const findTeamMembersMatchingAttributeLogicHandler = async ({
}));
};
function getServerTimingHeader(timeTaken: {
gAtr: number | null;
gQryCnfg: number | null;
gMbrWtAtr: number | null;
lgcFrMbrs: number | null;
gQryVal: number | null;
}) {
const headerParts = Object.entries(timeTaken).map(([key, value]) => {
if (value !== null) {
return `${key};dur=${value}`;
}
return null;
}).filter(Boolean);
return headerParts.join(', ');
}
export default findTeamMembersMatchingAttributeLogicHandler;
@@ -4,6 +4,8 @@ export const ZFindTeamMembersMatchingAttributeLogicInputSchema = z.object({
formId: z.string(),
response: z.record(z.string(), z.any()),
routeId: z.string(),
_enablePerf: z.boolean().optional(),
_concurrency: z.number().optional(),
});
export type TFindTeamMembersMatchingAttributeLogicInputSchema = z.infer<
@@ -138,6 +138,10 @@ const replaceFieldTemplateVariableWithOptionLabel = ({
* Utilities to handle compatiblity when attribute's type changes
*/
const attributeChangeCompatibility = {
/**
* FIXME: It isn't able to handle a case where for SINGLE_SELECT attribute, the queryValue->valueType is ["multiselect"]. It happens for select_any_in operator
* So, don't use it till that is fixed.
*/
getRaqbFieldTypeCompatibleWithQueryValue: function getRaqbFieldTypeCompatibleWithQueryValue({
attributesQueryValue,
raqbField,
@@ -163,6 +167,8 @@ const attributeChangeCompatibility = {
},
/**
* Ensure the attribute value if of type same as the valueType in queryValue
* FIXME: It isn't able to handle a case where for SINGLE_SELECT attribute, the queryValue->valueType is ["multiselect"]. It happens for select_any_in operator
* So, don't use it till that is fixed.
*/
ensureAttributeValueToBeOfRaqbFieldValueType: function ensureAttributeValueToBeOfRaqbFieldValueType({
attributeValue,
@@ -197,11 +203,14 @@ function getAttributesData({
const compatibleValueForAttributeAndFormFieldMatching = compatibleForAttributeAndFormFieldMatch(value);
// We do this to ensure that correct jsonLogic is generated for an existing route even if the attribute's type changes
acc[attributeId] = attributeChangeCompatibility.ensureAttributeValueToBeOfRaqbFieldValueType({
attributeValue: compatibleValueForAttributeAndFormFieldMatching,
attributesQueryValue,
attributeId,
});
// acc[attributeId] = attributeChangeCompatibility.ensureAttributeValueToBeOfRaqbFieldValueType({
// attributeValue: compatibleValueForAttributeAndFormFieldMatching,
// attributesQueryValue,
// attributeId,
// });
// Right now we can't trust ensureAttributeValueToBeOfRaqbFieldValueType to give us the correct value
acc[attributeId] = compatibleValueForAttributeAndFormFieldMatching;
return acc;
}, {} as Record<string, string | string[]>);
@@ -231,7 +240,6 @@ function getAttributesQueryValue({
return acc;
}, {} as Record<string, Attribute>);
console.log({ attributesMap });
const attributesQueryValueCompatibleForMatchingWithFormField: AttributesQueryValue = JSON.parse(
replaceFieldTemplateVariableWithOptionLabel({
queryValueString: replaceAttributeOptionIdsWithOptionLabel({
@@ -263,11 +271,14 @@ function getAttributesQueryBuilderConfig({
const attributesQueryBuilderConfigFieldsWithCompatibleListValues = Object.fromEntries(
Object.entries(attributesQueryBuilderConfig.fields).map(([raqbFieldId, raqbField]) => {
const raqbFieldType = attributeChangeCompatibility.getRaqbFieldTypeCompatibleWithQueryValue({
attributesQueryValue,
raqbField,
raqbFieldId,
});
// const raqbFieldType = attributeChangeCompatibility.getRaqbFieldTypeCompatibleWithQueryValue({
// attributesQueryValue,
// raqbField,
// raqbFieldId,
// });
// Right now we can't trust getRaqbFieldTypeCompatibleWithQueryValue to give us the correct type
const raqbFieldType = raqbField.type;
return [
raqbFieldId,
@@ -138,8 +138,8 @@ export const responseHandler = async ({ ctx, input }: ResponseHandlerOptions) =>
safeStringify({ teamMembersMatchingAttributeLogicWithResult })
);
const teamMemberIdsMatchingAttributeLogic = teamMembersMatchingAttributeLogicWithResult
? teamMembersMatchingAttributeLogicWithResult?.map((member) => member.userId)
const teamMemberIdsMatchingAttributeLogic = teamMembersMatchingAttributeLogicWithResult?.teamMembersMatchingAttributeLogic
? teamMembersMatchingAttributeLogicWithResult.teamMembersMatchingAttributeLogic.map((member) => member.userId)
: null;
await onFormSubmission(
{ ...serializableFormWithFields, userWithEmails },
+210 -76
View File
@@ -1,4 +1,6 @@
import type { App_RoutingForms_Form, User } from "@prisma/client";
import async from "async";
import { Config, Utils as QbUtils } from "react-awesome-query-builder";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
@@ -8,13 +10,14 @@ import { safeStringify } from "@calcom/lib/safeStringify";
import { WebhookTriggerEvents } from "@calcom/prisma/client";
import type { Ensure } from "@calcom/types/utils";
import { evaluateRaqbLogic, RaqbLogicResult } from "../lib/evaluateRaqbLogic";
import { RaqbLogicResult } from "../lib/evaluateRaqbLogic";
import {
getTeamMembersWithAttributeOptionValuePerAttribute,
getAttributesForTeam,
} from "../lib/getAttributes";
import isRouter from "../lib/isRouter";
import type { SerializableField, OrderedResponses } from "../types/types";
import jsonLogic from "../lib/jsonLogic";
import type { SerializableField, OrderedResponses, AttributesQueryValue } from "../types/types";
import type { FormResponse, SerializableForm } from "../types/types";
import { acrossQueryValueCompatiblity } from "./raqbUtils";
@@ -26,6 +29,24 @@ const {
const moduleLogger = logger.getSubLogger({ prefix: ["routing-forms/trpc/utils"] });
type SelectFieldWebhookResponse = string | number | string[] | { label: string; id: string | null };
type FORM_SUBMITTED_WEBHOOK_RESPONSES = Record<
string,
{
/**
* Deprecates `value` prop as it now has both the id(that doesn't change) and the label(that can change but is human friendly)
*/
response: number | string | string[] | SelectFieldWebhookResponse | SelectFieldWebhookResponse[];
/**
* @deprecated Use `response` instead
*/
value: FormResponse[keyof FormResponse]["value"];
}
>;
type TeamMemberWithAttributeOptionValuePerAttribute = Awaited<
ReturnType<typeof getTeamMembersWithAttributeOptionValuePerAttribute>
>[number];
function isOptionsField(field: Pick<SerializableField, "type" | "options">) {
return (field.type === "select" || field.type === "multiselect") && field.options;
}
@@ -77,100 +98,213 @@ function getFieldResponse({
};
}
type SelectFieldWebhookResponse = string | number | string[] | { label: string; id: string | null };
type FORM_SUBMITTED_WEBHOOK_RESPONSES = Record<
string,
{
/**
* Deprecates `value` prop as it now has both the id(that doesn't change) and the label(that can change but is human friendly)
*/
response: number | string | string[] | SelectFieldWebhookResponse | SelectFieldWebhookResponse[];
/**
* @deprecated Use `response` instead
*/
value: FormResponse[keyof FormResponse]["value"];
}
>;
/**
* Performance wrapper for async functions
*/
async function asyncPerf<ReturnValue>(fn: () => Promise<ReturnValue>): Promise<[ReturnValue, number | null]> {
const start = performance.now();
const result = await fn();
const end = performance.now();
return [result, end - start];
}
export async function findTeamMembersMatchingAttributeLogicOfRoute({
form,
response,
routeId,
teamId,
/**
* Performance wrapper for sync functions
*/
function perf<ReturnValue>(fn: () => ReturnValue): [ReturnValue, number | null] {
const start = performance.now();
const result = fn();
const end = performance.now();
return [result, end - start];
}
function getJsonLogic({
attributesQueryValue,
attributesQueryBuilderConfig,
}: {
form: Pick<SerializableForm<App_RoutingForms_Form>, "routes" | "fields">;
response: FormResponse;
routeId: string;
teamId: number;
attributesQueryValue: AttributesQueryValue;
attributesQueryBuilderConfig: Config;
}) {
const route = form.routes?.find((route) => route.id === routeId);
if (!route) {
return null;
const state = {
tree: QbUtils.checkTree(
QbUtils.loadTree(attributesQueryValue),
// We know that attributesQueryBuilderConfig is a Config because getAttributesQueryBuilderConfig returns a Config. So, asserting it.
attributesQueryBuilderConfig as unknown as Config
),
config: attributesQueryBuilderConfig as unknown as Config,
};
const jsonLogicQuery = QbUtils.jsonLogicFormat(state.tree, state.config);
const logic = jsonLogicQuery.logic;
if (!logic) {
if (attributesQueryValue.children1 && Object.keys(attributesQueryValue.children1).length > 0) {
throw new Error("Couldn't build the logic from the query value");
}
console.log(
"No logic found",
safeStringify({ attributesQueryValue, queryBuilderConfigFields: attributesQueryBuilderConfig.fields })
);
}
const teamMembersMatchingAttributeLogic: {
userId: number;
result: RaqbLogicResult;
}[] = [];
if (!isRouter(route)) {
const attributesForTeam = await getAttributesForTeam({ teamId: teamId });
const attributesQueryValue = getAttributesQueryValue({
console.log("Using LOGIC", safeStringify(logic));
return logic;
}
export async function findTeamMembersMatchingAttributeLogicOfRoute(
{
form,
response,
routeId,
teamId,
}: {
form: Pick<SerializableForm<App_RoutingForms_Form>, "routes" | "fields">;
response: FormResponse;
routeId: string;
teamId: number;
},
config: {
enablePerf?: boolean;
concurrency?: number;
} = {}
) {
const route = form.routes?.find((route) => route.id === routeId);
// Higher value of concurrency might not be performant as it might overwhelm the system. So, use a lower value as default.
const { enablePerf = false, concurrency = 2 } = config;
if (!route) {
return {
teamMembersMatchingAttributeLogic: null,
timeTaken: null,
};
}
if (isRouter(route)) {
return {
teamMembersMatchingAttributeLogic: null,
timeTaken: null,
};
}
const teamMembersMatchingAttributeLogicMap = new Map<number, RaqbLogicResult>();
const [attributesForTeam, getAttributesForTeamTimeTaken] = await aPf(
async () => await getAttributesForTeam({ teamId: teamId })
);
const [attributesQueryValue, getAttributesQueryValueTimeTaken] = pf(() =>
getAttributesQueryValue({
attributesQueryValue: route.attributesQueryValue,
attributes: attributesForTeam,
response,
fields: form.fields,
getFieldResponse,
});
})
);
if (!attributesQueryValue) {
return null;
}
if (!attributesQueryValue) {
return {
teamMembersMatchingAttributeLogic: null,
timeTaken: {
gAtr: getAttributesForTeamTimeTaken,
gQryVal: getAttributesQueryValueTimeTaken,
gQryCnfg: null,
gMbrWtAtr: null,
lgcFrMbrs: null,
},
};
}
const attributesQueryBuilderConfig = getAttributesQueryBuilderConfig({
const [attributesQueryBuilderConfig, getAttributesQueryBuilderConfigTimeTaken] = pf(() =>
getAttributesQueryBuilderConfig({
form,
attributes: attributesForTeam,
attributesQueryValue,
});
})
);
moduleLogger.debug(
"Finding team members matching attribute logic",
safeStringify({
form,
response,
routeId,
teamId,
attributesQueryBuilderConfigFields: attributesQueryBuilderConfig.fields,
})
);
moduleLogger.debug(
"Finding team members matching attribute logic",
safeStringify({
form,
response,
routeId,
teamId,
attributesQueryBuilderConfigFields: attributesQueryBuilderConfig.fields,
})
);
const teamMembersWithAttributeOptionValuePerAttribute =
await getTeamMembersWithAttributeOptionValuePerAttribute({ teamId: teamId });
const [
teamMembersWithAttributeOptionValuePerAttribute,
getTeamMembersWithAttributeOptionValuePerAttributeTimeTaken,
] = await aPf(() => getTeamMembersWithAttributeOptionValuePerAttribute({ teamId: teamId }));
teamMembersWithAttributeOptionValuePerAttribute.forEach((member, index) => {
const attributesData = getAttributes({
attributesData: member.attributes,
attributesQueryValue,
});
moduleLogger.debug(
`Checking team member ${member.userId} with attributes logic`,
safeStringify({ attributes: attributesData, attributesQueryValue })
);
const result = evaluateRaqbLogic({
queryValue: attributesQueryValue,
queryBuilderConfig: attributesQueryBuilderConfig,
data: attributesData,
beStrictWithEmptyLogic: true,
});
const logic = getJsonLogic({
attributesQueryValue,
attributesQueryBuilderConfig: attributesQueryBuilderConfig as unknown as Config,
});
if (result === RaqbLogicResult.MATCH || result === RaqbLogicResult.LOGIC_NOT_FOUND_SO_MATCHED) {
moduleLogger.debug(`Team member ${member.userId} matches attributes logic`);
teamMembersMatchingAttributeLogic.push({ userId: member.userId, result });
} else {
moduleLogger.debug(`Team member ${member.userId} does not match attributes logic`);
}
});
if (!logic) {
return {
teamMembersMatchingAttributeLogic: [],
timeTaken: {
gAtr: getAttributesForTeamTimeTaken,
gQryCnfg: getAttributesQueryBuilderConfigTimeTaken,
gMbrWtAtr: getTeamMembersWithAttributeOptionValuePerAttributeTimeTaken,
lgcFrMbrs: null,
gQryVal: getAttributesQueryValueTimeTaken,
},
};
}
return teamMembersMatchingAttributeLogic;
const [_, teamMembersMatchingAttributeLogicTimeTaken] = await aPf(async () => {
return await async.mapLimit<TeamMemberWithAttributeOptionValuePerAttribute, Promise<void>>(
teamMembersWithAttributeOptionValuePerAttribute,
concurrency,
async (member: TeamMemberWithAttributeOptionValuePerAttribute) => {
const attributesData = getAttributes({
attributesData: member.attributes,
attributesQueryValue,
});
const result = !!jsonLogic.apply(logic as any, attributesData)
? RaqbLogicResult.MATCH
: RaqbLogicResult.NO_MATCH;
if (result !== RaqbLogicResult.MATCH) {
return;
}
teamMembersMatchingAttributeLogicMap.set(member.userId, result);
}
);
});
return {
teamMembersMatchingAttributeLogic: Array.from(teamMembersMatchingAttributeLogicMap).map((item) => ({
userId: item[0],
result: item[1],
})),
timeTaken: {
gAtr: getAttributesForTeamTimeTaken,
gQryCnfg: getAttributesQueryBuilderConfigTimeTaken,
gMbrWtAtr: getTeamMembersWithAttributeOptionValuePerAttributeTimeTaken,
lgcFrMbrs: teamMembersMatchingAttributeLogicTimeTaken,
gQryVal: getAttributesQueryValueTimeTaken,
},
};
function pf<ReturnValue>(fn: () => ReturnValue): [ReturnValue, number | null] {
if (!enablePerf) {
return [fn(), null];
}
return perf(fn);
}
async function aPf<ReturnValue>(fn: () => Promise<ReturnValue>): Promise<[ReturnValue, number | null]> {
if (!enablePerf) {
return [await fn(), null];
}
return asyncPerf(fn);
}
}
export async function onFormSubmission(
+5
View File
@@ -438,6 +438,11 @@
<h3><a href="?only=ns:columnView">Test Column View</a></h3>
<div class="place" style="width: 100%"></div>
</div>
<div class="inline-embed-container" id="cal-booking-place-columnViewHideEventTypeDetails">
<h3><a href="?only=ns:columnViewHideEventTypeDetails">Test Column View Hide Event Type Details</a></h3>
<div class="place" style="width: 100%"></div>
</div>
<div class="inline-embed-container" id="cal-booking-place-pageParamsForwarding">
<h3><a href="?only=ns:pageParamsForwarding">Test Page Params Forwarding</a></h3>
<div class="place" style="width: 100%"></div>
+35
View File
@@ -538,12 +538,47 @@ if (only === "all" || only == "ns:columnView") {
},
}
);
Cal.ns.columnView("on", {
action: "*",
callback,
});
}
if (only === "all" || only == "ns:columnViewHideEventTypeDetails") {
// Create a namespace "second". It can be accessed as Cal.ns.second with the exact same API as Cal
Cal("init", "columnViewHideEventTypeDetails", {
debug: true,
origin: origin,
});
Cal.ns.columnViewHideEventTypeDetails(
"inline",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
{
elementOrSelector: "#cal-booking-place-columnViewHideEventTypeDetails .place",
calLink: "free/30min",
config: {
iframeAttrs: {
id: "cal-booking-place-columnView-iframe",
},
"flag.coep": "true",
layout: "column_view",
},
}
);
Cal.ns.columnViewHideEventTypeDetails("ui", {
hideEventTypeDetails: true,
});
Cal.ns.columnViewHideEventTypeDetails("on", {
action: "*",
callback,
});
}
if (only === "all" || only == "ns:autoScrollTest") {
if (!calLink) {
throw new Error("cal-link parameter is required for autoScrollTest");
+4 -1
View File
@@ -690,7 +690,10 @@ class CalApi {
if (__prerender) {
this.preloadedModalUid = uid;
} else {
this.modalUid = uid;
// Intentionally not setting it to have the behaviour of reusing the same modal. Because it causes outdated content that might not be valid based on
// 1. The time difference b/w reopening(availability getting changed in b/w)
// 2. User using different query params but they not being used because of the same modal being reused. Happens in case of headless router being opened in embed
// this.modalUid = uid;
}
if (typeof config.iframeAttrs === "string" || config.iframeAttrs instanceof Array) {
+33 -47
View File
@@ -259,8 +259,6 @@ const BookerComponent = ({
return null;
}
const shouldShowMeta = !hideEventTypeDetails;
return (
<>
{event.data && !isPlatform ? <BookingPageTagManager eventType={event.data} /> : <></>}
@@ -329,46 +327,42 @@ const BookerComponent = ({
)}
</BookerSection>
)}
{shouldShowMeta && (
<StickyOnDesktop key="meta" className={classNames("relative z-10 flex [grid-area:meta]")}>
<BookerSection
area="meta"
className="max-w-screen flex w-full flex-col md:w-[var(--booker-meta-width)]">
{orgBannerUrl && !isPlatform && (
<img
loading="eager"
className="-mb-9 ltr:rounded-tl-md rtl:rounded-tr-md"
alt="org banner"
src={orgBannerUrl}
/>
)}
<EventMeta
classNames={{
eventMetaContainer: customClassNames?.eventMetaCustomClassNames?.eventMetaContainer,
eventMetaTitle: customClassNames?.eventMetaCustomClassNames?.eventMetaTitle,
eventMetaTimezoneSelect:
customClassNames?.eventMetaCustomClassNames?.eventMetaTimezoneSelect,
}}
event={event.data}
isPending={event.isPending}
isPlatform={isPlatform}
<StickyOnDesktop key="meta" className={classNames("relative z-10 flex [grid-area:meta]")}>
<BookerSection
area="meta"
className="max-w-screen flex w-full flex-col md:w-[var(--booker-meta-width)]">
{!hideEventTypeDetails && orgBannerUrl && !isPlatform && (
<img
loading="eager"
className="-mb-9 ltr:rounded-tl-md rtl:rounded-tr-md"
alt="org banner"
src={orgBannerUrl}
/>
{layout !== BookerLayouts.MONTH_VIEW &&
!(layout === "mobile" && bookerState === "booking") && (
<div className="mt-auto px-5 py-3">
<DatePicker event={event} schedule={schedule} scrollToTimeSlots={scrollToTimeSlots} />
</div>
)}
</BookerSection>
</StickyOnDesktop>
)}
)}
<EventMeta
classNames={{
eventMetaContainer: customClassNames?.eventMetaCustomClassNames?.eventMetaContainer,
eventMetaTitle: customClassNames?.eventMetaCustomClassNames?.eventMetaTitle,
eventMetaTimezoneSelect:
customClassNames?.eventMetaCustomClassNames?.eventMetaTimezoneSelect,
}}
event={event.data}
isPending={event.isPending}
isPlatform={isPlatform}
/>
{layout !== BookerLayouts.MONTH_VIEW &&
!(layout === "mobile" && bookerState === "booking") && (
<div className="mt-auto px-5 py-3">
<DatePicker event={event} schedule={schedule} scrollToTimeSlots={scrollToTimeSlots} />
</div>
)}
</BookerSection>
</StickyOnDesktop>
<BookerSection
key="book-event-form"
area="main"
className={`sticky top-0 ml-[-1px] h-full p-6 md:w-[var(--booker-main-width)] ${
!hideEventTypeDetails ? "md:border-l" : ""
}`}
className="sticky top-0 ml-[-1px] h-full p-6 md:w-[var(--booker-main-width)] md:border-l"
{...fadeInLeft}
visible={bookerState === "booking" && !shouldShowFormInDialog}>
{EventBooker}
@@ -377,18 +371,10 @@ const BookerComponent = ({
<BookerSection
key="datepicker"
area="main"
visible={
bookerState !== "booking" &&
(layout === BookerLayouts.MONTH_VIEW ||
// Meta possibly can show DatePicker but if meta is not shown, then DatePicker must be shown here
// FIXME: We need proper state management for this(depending on layout and bookerState)
!shouldShowMeta)
}
visible={bookerState !== "booking" && layout === BookerLayouts.MONTH_VIEW}
{...fadeInLeft}
initial="visible"
className={`ml-[-1px] h-full flex-shrink px-5 py-3 lg:w-[var(--booker-main-width)] ${
!hideEventTypeDetails ? "md:border-subtle md:border-l" : ""
}`}>
className="md:border-subtle ml-[-1px] h-full flex-shrink px-5 py-3 md:border-l lg:w-[var(--booker-main-width)]">
<DatePicker
classNames={{
datePickerContainer: customClassNames?.datePickerCustomClassNames?.datePickerContainer,
@@ -15,14 +15,7 @@ import { getQueryParam } from "../../utils/query-param";
export type UseBookerLayoutType = ReturnType<typeof useBookerLayout>;
type Options = {
hideEventTypeDetails?: boolean;
};
export const useBookerLayout = (
event: Pick<BookerEvent, "profile"> | undefined | null,
options?: Options
) => {
export const useBookerLayout = (event: Pick<BookerEvent, "profile"> | undefined | null) => {
const [_layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow);
const isEmbed = useIsEmbed();
const isMobile = useMediaQuery("(max-width: 768px)");
@@ -81,7 +74,7 @@ export const useBookerLayout = (
const shouldShowFormInDialog = shouldShowFormInDialogMap[layout];
const hideEventTypeDetails = isEmbed ? embedUiConfig.hideEventTypeDetails : !!options?.hideEventTypeDetails;
const hideEventTypeDetails = isEmbed ? embedUiConfig.hideEventTypeDetails : false;
return {
shouldShowFormInDialog,
@@ -25,6 +25,7 @@ export interface BookerProps {
* Whether is a team or org, we gather basic info from both
*/
entity: {
fromRedirectOfNonOrgLink?: boolean;
considerUnpublished: boolean;
isUnpublished?: boolean;
orgSlug?: string | null;
@@ -151,4 +152,5 @@ export type CustomClassNames = {
availableTimeSlotsTimeFormatToggle?: string;
availableTimes?: string;
};
atomsWrapper: string;
};
@@ -19,10 +19,11 @@ export type useScheduleForEventReturnType = ReturnType<typeof useScheduleForEven
* Using this hook means you only need to use one hook, instead
* of combining multiple conditional hooks.
*/
export const useEvent = () => {
const [username, eventSlug] = useBookerStore((state) => [state.username, state.eventSlug], shallow);
const isTeamEvent = useBookerStore((state) => state.isTeamEvent);
const org = useBookerStore((state) => state.org);
export const useEvent = (props?: { fromRedirectOfNonOrgLink?: boolean }) => {
const [username, eventSlug, isTeamEvent, org] = useBookerStore(
(state) => [state.username, state.eventSlug, state.isTeamEvent, state.org],
shallow
);
const event = trpc.viewer.public.event.useQuery(
{
@@ -30,8 +31,12 @@ export const useEvent = () => {
eventSlug: eventSlug ?? "",
isTeamEvent,
org: org ?? null,
fromRedirectOfNonOrgLink: props?.fromRedirectOfNonOrgLink,
},
{ refetchOnWindowFocus: false, enabled: Boolean(username) && Boolean(eventSlug) }
{
refetchOnWindowFocus: false,
enabled: Boolean(username) && Boolean(eventSlug),
}
);
return {
@@ -66,6 +71,7 @@ export const useScheduleForEvent = ({
selectedDate,
orgSlug,
teamMemberEmail,
fromRedirectOfNonOrgLink,
}: {
prefetchNextMonth?: boolean;
username?: string | null;
@@ -78,9 +84,10 @@ export const useScheduleForEvent = ({
selectedDate?: string | null;
orgSlug?: string;
teamMemberEmail?: string | null;
fromRedirectOfNonOrgLink?: boolean;
} = {}) => {
const { timezone } = useTimePreferences();
const event = useEvent();
const event = useEvent({ fromRedirectOfNonOrgLink });
const [usernameFromStore, eventSlugFromStore, monthFromStore, durationFromStore] = useBookerStore(
(state) => [state.username, state.eventSlug, state.month, state.selectedDuration],
shallow
@@ -27,6 +27,8 @@ import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
import { scheduleNoShowTriggers } from "./handleNewBooking/scheduleNoShowTriggers";
const log = logger.getSubLogger({ prefix: ["[handleConfirmation] book:user"] });
export async function handleConfirmation(args: {
@@ -36,6 +38,8 @@ export async function handleConfirmation(args: {
prisma: PrismaClient;
bookingId: number;
booking: {
startTime: Date;
id: number;
eventType: {
currency: string;
description: string | null;
@@ -393,6 +397,18 @@ export async function handleConfirmation(args: {
await Promise.all(scheduleTriggerPromises);
await scheduleNoShowTriggers({
booking: {
startTime: booking.startTime,
id: booking.id,
},
triggerForUser,
organizerUser: { id: booking.userId },
eventTypeId: booking.eventTypeId,
teamId,
orgId,
});
const eventTypeInfo: EventTypeInfo = {
eventTitle: eventType?.title,
eventDescription: eventType?.description,
@@ -8,15 +8,16 @@ type ScheduleNoShowTriggersArgs = {
startTime: Date;
id: number;
};
triggerForUser: number | true | null;
organizerUser: { id: number };
eventTypeId: number;
triggerForUser?: number | true | null;
organizerUser: { id: number | null };
eventTypeId: number | null;
teamId?: number | null;
orgId?: number | null;
};
export const scheduleNoShowTriggers = async (args: ScheduleNoShowTriggersArgs) => {
const { booking, triggerForUser, organizerUser, eventTypeId, teamId, orgId } = args;
// Add task for automatic no show in cal video
const noShowPromises: Promise<any>[] = [];
@@ -206,7 +206,7 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
type="creation"
title={t("invite_team_member")}
description={
IS_TEAM_BILLING_ENABLED_CLIENT ? (
IS_TEAM_BILLING_ENABLED_CLIENT && !currentOrg ? (
<span className="text-subtle text-sm leading-tight">
<Trans i18nKey="invite_new_member_description">
Note: This will <span className="text-emphasis font-medium">cost an extra seat ($15/m)</span>{" "}
@@ -1,44 +1,45 @@
import type { EventType } from "@prisma/client";
import type { ReactNode } from "react";
import type { UseFormReturn } from "react-hook-form";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import { useIsPlatform } from "@calcom/atoms/monorepo";
import { classNames } from "@calcom/lib";
import { useCreateEventType } from "@calcom/lib/hooks/useCreateEventType";
import type { CreateEventTypeFormValues } from "@calcom/lib/hooks/useCreateEventType";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import slugify from "@calcom/lib/slugify";
import { SchedulingType } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import { Form, TextField, Tooltip } from "@calcom/ui";
import { Alert, RadioGroup as RadioArea } from "@calcom/ui";
type props = {
isTeamAdminOrOwner: boolean;
teamSlug?: string | null;
teamId: number;
isPending: boolean;
urlPrefix?: string;
form: UseFormReturn<CreateEventTypeFormValues>;
handleSubmit: (values: CreateEventTypeFormValues) => void;
isManagedEventType: boolean;
SubmitButton: (isPending: boolean) => ReactNode;
onSuccessMutation: (eventType: EventType) => void;
onErrorMutation: (message: string) => void;
};
export const TeamEventTypeForm = ({
isTeamAdminOrOwner,
teamSlug,
teamId,
form,
urlPrefix,
isPending,
handleSubmit,
isManagedEventType,
SubmitButton,
onSuccessMutation,
onErrorMutation,
}: props) => {
const { t } = useLocale();
const orgBranding = useOrgBranding();
const { data: team } = trpc.viewer.teams.get.useQuery({ teamId, isOrg: false }, { enabled: !!teamId });
const urlPrefix = orgBranding?.fullDomain ?? process.env.NEXT_PUBLIC_WEBSITE_URL;
const isPlatform = useIsPlatform();
const { t } = useLocale();
const { form, createMutation, isManagedEventType } = useCreateEventType(onSuccessMutation, onErrorMutation);
const { register, setValue, formState } = form;
return (
<Form
form={form}
handleSubmit={(values) => {
createMutation.mutate(values);
}}>
<Form form={form} handleSubmit={handleSubmit}>
<div className="mt-3 space-y-6 pb-11">
<TextField
type="hidden"
@@ -61,14 +62,16 @@ export const TeamEventTypeForm = ({
{urlPrefix && urlPrefix.length >= 21 ? (
<div>
<TextField
label={`${t("url")}: ${urlPrefix}`}
label={isPlatform ? "Slug" : `${t("url")}: ${urlPrefix}`}
required
addOnLeading={
<Tooltip content={!isManagedEventType ? `team/${team?.slug}` : t("username_placeholder")}>
<span className="max-w-24 md:max-w-56">
/{!isManagedEventType ? `team/${team?.slug}` : t("username_placeholder")}/
</span>
</Tooltip>
!isPlatform ? (
<Tooltip content={!isManagedEventType ? `team/${teamSlug}` : t("username_placeholder")}>
<span className="max-w-24 md:max-w-56">
/{!isManagedEventType ? `team/${teamSlug}` : t("username_placeholder")}/
</span>
</Tooltip>
) : undefined
}
{...register("slug")}
onChange={(e) => {
@@ -76,31 +79,33 @@ export const TeamEventTypeForm = ({
}}
/>
{isManagedEventType && (
{isManagedEventType && !isPlatform && (
<p className="mt-2 text-sm text-gray-600">{t("managed_event_url_clarification")}</p>
)}
</div>
) : (
<div>
<TextField
label={t("url")}
label={isPlatform ? "Slug" : t("url")}
required
addOnLeading={
<Tooltip
content={`${urlPrefix}/${
!isManagedEventType ? `team/${team?.slug}` : t("username_placeholder")
}/`}>
<span className="max-w-24 md:max-w-56">
{urlPrefix}/{!isManagedEventType ? `team/${team?.slug}` : t("username_placeholder")}/
</span>
</Tooltip>
!isPlatform ? (
<Tooltip
content={`${urlPrefix}/${
!isManagedEventType ? `team/${teamSlug}` : t("username_placeholder")
}/`}>
<span className="max-w-24 md:max-w-56">
{urlPrefix}/{!isManagedEventType ? `team/${teamSlug}` : t("username_placeholder")}/
</span>
</Tooltip>
) : undefined
}
{...register("slug")}
onChange={(e) => {
form.setValue("slug", slugify(e?.target.value), { shouldTouch: true });
}}
/>
{isManagedEventType && (
{isManagedEventType && !isPlatform && (
<p className="mt-2 text-sm text-gray-600">{t("managed_event_url_clarification")}</p>
)}
</div>
@@ -147,7 +152,7 @@ export const TeamEventTypeForm = ({
</RadioArea.Group>
</div>
</div>
{SubmitButton(createMutation.isPending)}
{SubmitButton(isPending)}
</Form>
);
};
@@ -1,7 +1,6 @@
import type { EventType } from "@prisma/client";
import { isValidPhoneNumber } from "libphonenumber-js";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { z } from "zod";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
@@ -9,22 +8,11 @@ import { TeamEventTypeForm } from "@calcom/features/ee/teams/components/TeamEven
import { useCreateEventType } from "@calcom/lib/hooks/useCreateEventType";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { md } from "@calcom/lib/markdownIt";
import slugify from "@calcom/lib/slugify";
import turndown from "@calcom/lib/turndownService";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
Editor,
Form,
showToast,
TextField,
Tooltip,
} from "@calcom/ui";
import { trpc } from "@calcom/trpc/react";
import { Button, Dialog, DialogClose, DialogContent, DialogFooter, showToast } from "@calcom/ui";
import CreateEventTypeForm from "./CreateEventTypeForm";
// this describes the uniform data needed to create a new event type on Profile or Team
export interface EventTypeParent {
@@ -74,7 +62,6 @@ export default function CreateEventTypeDialog({
}) {
const { t } = useLocale();
const router = useRouter();
const [firstRender, setFirstRender] = useState(true);
const orgBranding = useOrgBranding();
const {
@@ -115,9 +102,13 @@ export default function CreateEventTypeDialog({
const { form, createMutation, isManagedEventType } = useCreateEventType(onSuccessMutation, onErrorMutation);
const { register } = form;
const urlPrefix = orgBranding?.fullDomain ?? process.env.NEXT_PUBLIC_WEBSITE_URL;
const { data: team } = trpc.viewer.teams.get.useQuery(
{ teamId: teamId ?? -1, isOrg: false },
{ enabled: !!teamId }
);
return (
<Dialog
name="new"
@@ -138,106 +129,30 @@ export default function CreateEventTypeDialog({
description={t("new_event_type_to_book_description")}>
{teamId ? (
<TeamEventTypeForm
isTeamAdminOrOwner={isTeamAdminOrOwner}
teamSlug={team?.slug}
teamId={teamId}
SubmitButton={SubmitButton}
onSuccessMutation={onSuccessMutation}
onErrorMutation={onErrorMutation}
/>
) : (
<Form
isTeamAdminOrOwner={isTeamAdminOrOwner}
urlPrefix={urlPrefix}
isPending={createMutation.isPending}
form={form}
isManagedEventType={isManagedEventType}
handleSubmit={(values) => {
createMutation.mutate(values);
}}>
<div className="mt-3 space-y-6 pb-11">
<TextField
label={t("title")}
placeholder={t("quick_chat")}
data-testid="event-type-quick-chat"
{...register("title")}
onChange={(e) => {
form.setValue("title", e?.target.value);
if (form.formState.touchedFields["slug"] === undefined) {
form.setValue("slug", slugify(e?.target.value));
}
}}
/>
{urlPrefix && urlPrefix.length >= 21 ? (
<div>
<TextField
label={`${t("url")}: ${urlPrefix}`}
required
addOnLeading={
<Tooltip content={!isManagedEventType ? pageSlug : t("username_placeholder")}>
<span className="max-w-24 md:max-w-56">
/{!isManagedEventType ? pageSlug : t("username_placeholder")}/
</span>
</Tooltip>
}
{...register("slug")}
onChange={(e) => {
form.setValue("slug", slugify(e?.target.value), { shouldTouch: true });
}}
/>
{isManagedEventType && (
<p className="mt-2 text-sm text-gray-600">{t("managed_event_url_clarification")}</p>
)}
</div>
) : (
<div>
<TextField
label={t("url")}
required
addOnLeading={
<Tooltip
content={`${urlPrefix}/${
!isManagedEventType ? pageSlug : t("username_placeholder")
}/`}>
<span className="max-w-24 md:max-w-56">
{urlPrefix}/{!isManagedEventType ? pageSlug : t("username_placeholder")}/
</span>
</Tooltip>
}
{...register("slug")}
onChange={(e) => {
form.setValue("slug", slugify(e?.target.value), { shouldTouch: true });
}}
/>
{isManagedEventType && (
<p className="mt-2 text-sm text-gray-600">{t("managed_event_url_clarification")}</p>
)}
</div>
)}
<>
<Editor
getText={() => md.render(form.getValues("description") || "")}
setText={(value: string) => form.setValue("description", turndown(value))}
excludedToolbarItems={["blockType", "link"]}
placeholder={t("quick_video_meeting")}
firstRender={firstRender}
setFirstRender={setFirstRender}
maxHeight="200px"
/>
<div className="relative">
<TextField
type="number"
required
min="10"
placeholder="15"
label={t("duration")}
className="pr-4"
{...register("length", { valueAsNumber: true })}
addOnSuffix={t("minutes")}
/>
</div>
</>
</div>
{SubmitButton(createMutation.isPending)}
</Form>
}}
SubmitButton={SubmitButton}
/>
) : (
<CreateEventTypeForm
urlPrefix={urlPrefix}
isPending={createMutation.isPending}
form={form}
isManagedEventType={isManagedEventType}
handleSubmit={(values) => {
createMutation.mutate(values);
}}
SubmitButton={SubmitButton}
pageSlug={pageSlug}
/>
)}
</DialogContent>
</Dialog>
@@ -0,0 +1,136 @@
import type { ReactNode } from "react";
import { useState } from "react";
import type { UseFormReturn } from "react-hook-form";
import { useIsPlatform } from "@calcom/atoms/monorepo";
import type { CreateEventTypeFormValues } from "@calcom/lib/hooks/useCreateEventType";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import slugify from "@calcom/lib/slugify";
import turndown from "@calcom/lib/turndownService";
import { Editor, Form, TextAreaField, TextField, Tooltip } from "@calcom/ui";
export default function CreateEventTypeForm({
form,
isManagedEventType,
handleSubmit,
pageSlug,
isPending,
urlPrefix,
SubmitButton,
}: {
form: UseFormReturn<CreateEventTypeFormValues>;
isManagedEventType: boolean;
handleSubmit: (values: CreateEventTypeFormValues) => void;
pageSlug?: string;
isPending: boolean;
urlPrefix?: string;
SubmitButton: (isPending: boolean) => ReactNode;
}) {
const isPlatform = useIsPlatform();
const { t } = useLocale();
const [firstRender, setFirstRender] = useState(true);
const { register } = form;
return (
<Form
form={form}
handleSubmit={(values) => {
handleSubmit(values);
}}>
<div className="mt-3 space-y-6 pb-11">
<TextField
label={t("title")}
placeholder={t("quick_chat")}
data-testid="event-type-quick-chat"
{...register("title")}
onChange={(e) => {
form.setValue("title", e?.target.value);
if (form.formState.touchedFields["slug"] === undefined) {
form.setValue("slug", slugify(e?.target.value));
}
}}
/>
{urlPrefix && urlPrefix.length >= 21 ? (
<div>
<TextField
label={isPlatform ? "Slug" : `${t("url")}: ${urlPrefix}`}
required
addOnLeading={
!isPlatform ? (
<Tooltip content={!isManagedEventType ? pageSlug : t("username_placeholder")}>
<span className="max-w-24 md:max-w-56">
/{!isManagedEventType ? pageSlug : t("username_placeholder")}/
</span>
</Tooltip>
) : undefined
}
{...register("slug")}
onChange={(e) => {
form.setValue("slug", slugify(e?.target.value), { shouldTouch: true });
}}
/>
{isManagedEventType && !isPlatform && (
<p className="mt-2 text-sm text-gray-600">{t("managed_event_url_clarification")}</p>
)}
</div>
) : (
<div>
<TextField
label={isPlatform ? "Slug" : t("url")}
required
addOnLeading={
!isPlatform ? (
<Tooltip
content={`${urlPrefix}/${!isManagedEventType ? pageSlug : t("username_placeholder")}/`}>
<span className="max-w-24 md:max-w-56">
{urlPrefix}/{!isManagedEventType ? pageSlug : t("username_placeholder")}/
</span>
</Tooltip>
) : undefined
}
{...register("slug")}
onChange={(e) => {
form.setValue("slug", slugify(e?.target.value), { shouldTouch: true });
}}
/>
{isManagedEventType && !isPlatform && (
<p className="mt-2 text-sm text-gray-600">{t("managed_event_url_clarification")}</p>
)}
</div>
)}
<>
{isPlatform ? (
<TextAreaField {...register("description")} placeholder={t("quick_video_meeting")} />
) : (
<Editor
getText={() => md.render(form.getValues("description") || "")}
setText={(value: string) => form.setValue("description", turndown(value))}
excludedToolbarItems={["blockType", "link"]}
placeholder={t("quick_video_meeting")}
firstRender={firstRender}
setFirstRender={setFirstRender}
maxHeight="200px"
/>
)}
<div className="relative">
<TextField
type="number"
required
min="10"
placeholder="15"
label={t("duration")}
className="pr-4"
{...register("length", { valueAsNumber: true })}
addOnSuffix={t("minutes")}
/>
</div>
</>
</div>
{SubmitButton(isPending)}
</Form>
);
}
@@ -21,6 +21,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SchedulingType } from "@calcom/prisma/enums";
import { Label, Select, SettingsToggle } from "@calcom/ui";
export type EventTeamAssignmentTabBaseProps = Pick<EventTypeSetupProps, "teamMembers" | "team" | "eventType">;
export const mapMemberToChildrenOption = (
member: EventTypeSetupProps["teamMembers"][number],
slug: string,
@@ -427,11 +429,7 @@ const Hosts = ({
);
};
export const EventTeamAssignmentTab = ({
team,
teamMembers,
eventType,
}: Pick<EventTypeSetupProps, "teamMembers" | "team" | "eventType">) => {
export const EventTeamAssignmentTab = ({ team, teamMembers, eventType }: EventTeamAssignmentTabBaseProps) => {
const { t } = useLocale();
const schedulingTypeOptions: {
@@ -4,10 +4,7 @@ import { Controller, useFormContext } from "react-hook-form";
import type { OptionProps, SingleValueProps } from "react-select";
import { components } from "react-select";
import type {
EventAvailabilityTabWebWrapperProps,
GetAllSchedulesByUserIdQueryType,
} from "@calcom/atoms/event-types/wrappers/EventAvailabilityTabWebWrapper";
import type { GetAllSchedulesByUserIdQueryType } from "@calcom/atoms/event-types/wrappers/EventAvailabilityTabWebWrapper";
import dayjs from "@calcom/dayjs";
import { SelectSkeletonLoader } from "@calcom/features/availability/components/SkeletonLoader";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
@@ -27,10 +24,14 @@ import type { RouterOutputs } from "@calcom/trpc/react";
import { Avatar, Badge, Button, Icon, Label, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
import { Spinner } from "@calcom/ui/components/icon/Spinner";
type ScheduleQueryData = RouterOutputs["viewer"]["availability"]["schedule"]["get"];
type EventTypeScheduleDetailsProps = {
scheduleQueryData?: RouterOutputs["viewer"]["availability"]["schedule"]["get"];
scheduleQueryData?: Pick<ScheduleQueryData, "timeZone" | "id" | "isManaged" | "readOnly"> & {
schedule: Array<Pick<ScheduleQueryData["schedule"][number], "days" | "startTime" | "endTime">>;
};
isSchedulePending?: boolean;
loggedInUser?: RouterOutputs["viewer"]["me"];
user?: Pick<RouterOutputs["viewer"]["me"], "timeFormat" | "weekStart">;
editAvailabilityRedirectUrl?: string;
};
@@ -40,13 +41,20 @@ type EventTypeTeamScheduleProps = {
};
type EventTypeScheduleProps = {
availabilityQueryData?: RouterOutputs["viewer"]["availability"]["list"];
isAvailabilityPending?: boolean;
schedulesQueryData?: Array<
Omit<RouterOutputs["viewer"]["availability"]["list"]["schedules"][number], "availability">
>;
isSchedulesPending?: boolean;
eventType: EventTypeSetup;
teamMembers: TeamMembers;
} & EventTypeScheduleDetailsProps &
EventTypeTeamScheduleProps;
type EventAvailabilityTabProps = EventAvailabilityTabWebWrapperProps & EventTypeScheduleProps;
export type EventAvailabilityTabBaserProps = {
isTeamEvent: boolean;
};
type EventAvailabilityTabProps = EventAvailabilityTabBaserProps & EventTypeScheduleProps;
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
const { label, isDefault, isManaged = false } = props.data;
@@ -99,16 +107,16 @@ const EventTypeScheduleDetails = memo(
({
scheduleQueryData,
isSchedulePending,
loggedInUser,
user,
editAvailabilityRedirectUrl,
}: EventTypeScheduleDetailsProps) => {
const timeFormat = loggedInUser?.timeFormat;
const timeFormat = user?.timeFormat;
const { t, i18n } = useLocale();
const weekStart = weekStartNum(loggedInUser?.weekStart);
const weekStart = weekStartNum(user?.weekStart);
const filterDays = (dayNum: number) =>
scheduleQueryData?.schedule.filter((item) => item.days.includes((dayNum + weekStart) % 7)) || [];
scheduleQueryData?.schedule?.filter((item) => item.days.includes((dayNum + weekStart) % 7)) || [];
return (
<div>
@@ -157,7 +165,6 @@ const EventTypeScheduleDetails = memo(
!scheduleQueryData.readOnly &&
!!editAvailabilityRedirectUrl && (
<Button
// href={`/availability/${scheduleQueryData.id}`}
href={editAvailabilityRedirectUrl}
disabled={isSchedulePending}
color="minimal"
@@ -177,8 +184,8 @@ EventTypeScheduleDetails.displayName = "EventTypeScheduleDetails";
const EventTypeSchedule = ({
eventType,
availabilityQueryData,
isAvailabilityPending,
schedulesQueryData,
isSchedulesPending,
...rest
}: EventTypeScheduleProps) => {
const { t } = useLocale();
@@ -191,25 +198,21 @@ const EventTypeSchedule = ({
useEffect(() => {
// after data is loaded.
if (availabilityQueryData && scheduleId !== 0 && !scheduleId) {
const newValue = isManagedEventType
? 0
: availabilityQueryData.schedules.find((schedule) => schedule.isDefault)?.id;
if (schedulesQueryData && scheduleId !== 0 && !scheduleId) {
const newValue = isManagedEventType ? 0 : schedulesQueryData.find((schedule) => schedule.isDefault)?.id;
if (!newValue && newValue !== 0) return;
setValue("schedule", newValue, {
shouldDirty: true,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scheduleId, availabilityQueryData]);
}, [scheduleId, schedulesQueryData]);
if (isAvailabilityPending || !availabilityQueryData) {
if (isSchedulesPending || !schedulesQueryData) {
return <SelectSkeletonLoader />;
}
const schedules = availabilityQueryData.schedules;
const options = schedules.map((schedule) => ({
const options = schedulesQueryData.map((schedule) => ({
value: schedule.id,
label: schedule.name,
isDefault: schedule.isDefault,
@@ -227,7 +230,11 @@ const EventTypeSchedule = ({
}
// We are showing a managed event for a member and team owner selected their own schedule, so adding
// the managed schedule option
if (isChildrenManagedEventType && scheduleId && !schedules.find((schedule) => schedule.id === scheduleId)) {
if (
isChildrenManagedEventType &&
scheduleId &&
!schedulesQueryData.find((schedule) => schedule.id === scheduleId)
) {
options.push({
value: scheduleId,
label: eventType.scheduleName ?? t("default_schedule_name"),
@@ -236,7 +243,7 @@ const EventTypeSchedule = ({
});
}
// We push the selected schedule from the event type if it's not part of the list response. This happens if the user is an admin but not the schedule owner.
else if (eventType.schedule && !schedules.find((schedule) => schedule.id === eventType.schedule)) {
else if (eventType.schedule && !schedulesQueryData.find((schedule) => schedule.id === eventType.schedule)) {
options.push({
value: eventType.schedule,
label: eventType.scheduleName ?? t("default_schedule_name"),
@@ -411,10 +418,7 @@ const TeamAvailability = ({
);
};
const UseCommonScheduleSettingsToggle = ({
eventType,
...rest
}: Omit<EventAvailabilityTabProps, "isTeamEvent">) => {
const UseCommonScheduleSettingsToggle = ({ eventType, ...rest }: EventTypeScheduleProps) => {
const { t } = useLocale();
const { setValue, resetField, getFieldState, getValues, watch } = useFormContext<FormValues>();
@@ -9,6 +9,7 @@ import {
useReactTable,
type ColumnDef,
} from "@tanstack/react-table";
import type { ColumnMeta } from "@tanstack/react-table";
import { useSession } from "next-auth/react";
import { useQueryState, parseAsBoolean } from "nuqs";
import { useMemo, useReducer, useRef, useState } from "react";
@@ -45,6 +46,11 @@ import { TableActions } from "./UserTableActions";
import type { UserTableState, UserTableAction, UserTableUser } from "./types";
import { useFetchMoreOnBottomReached } from "./useFetchMoreOnBottomReached";
type CustomColumnMeta<TData, TValue> = ColumnMeta<TData, TValue> & {
sticky?: boolean;
stickLeft?: number;
};
const initialState: UserTableState = {
changeMemberRole: {
showModal: false,
@@ -186,6 +192,10 @@ export function UserListTable() {
id: "select",
enableHiding: false,
enableSorting: false,
meta: {
sticky: true,
stickLeft: 0,
} as CustomColumnMeta<UserTableUser, unknown>,
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
@@ -210,6 +220,10 @@ export function UserListTable() {
header: () => {
return `Members`;
},
meta: {
sticky: true,
stickyLeft: 24,
} as CustomColumnMeta<UserTableUser, unknown>,
cell: ({ row }) => {
const { username, email, avatarUrl } = row.original;
return (
@@ -69,10 +69,20 @@ const UserPhoneSchema = z.object({
displayLocationPublicly: z.boolean().default(false),
});
const ConferencingSchema = z.object({
type: z.literal("conferencing"),
});
type InPersonLocation = z.infer<typeof InPersonSchema>;
type LinkLocation = z.infer<typeof LinkSchema>;
export type IntegrationLocation = z.infer<typeof IntegrationSchema>;
type UserPhoneLocation = z.infer<typeof UserPhoneSchema>;
const TransformedLocationSchema = z.union([InPersonSchema, LinkSchema, IntegrationSchema, UserPhoneSchema]);
const TransformedLocationSchema = z.union([
InPersonSchema,
LinkSchema,
IntegrationSchema,
UserPhoneSchema,
ConferencingSchema,
]);
export const TransformedLocationsSchema = z.array(TransformedLocationSchema);
+15 -7
View File
@@ -11,13 +11,10 @@ import { unlockedManagedEventTypeProps } from "@calcom/prisma/zod-utils";
import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype";
import { trpc } from "@calcom/trpc/react";
export const useCreateEventType = (
onSuccessMutation: (eventType: EventType) => void,
onErrorMutation: (message: string) => void
) => {
const utils = trpc.useUtils();
const { t } = useLocale();
const form = useForm<z.infer<typeof createEventTypeInput>>({
export type CreateEventTypeFormValues = z.infer<typeof createEventTypeInput>;
export const useCreateEventTypeForm = () => {
const form = useForm<CreateEventTypeFormValues>({
defaultValues: {
length: 15,
},
@@ -35,6 +32,17 @@ export const useCreateEventType = (
}
}, [schedulingTypeWatch]);
return { form, isManagedEventType };
};
export const useCreateEventType = (
onSuccessMutation: (eventType: EventType) => void,
onErrorMutation: (message: string) => void
) => {
const utils = trpc.useUtils();
const { t } = useLocale();
const { form, isManagedEventType } = useCreateEventTypeForm();
const createMutation = trpc.viewer.eventTypes.create.useMutation({
onSuccess: async ({ eventType }) => {
onSuccessMutation(eventType);
@@ -66,6 +66,11 @@ export type CustomClassNames = {
labelAndSwitchContainer?: string;
};
overridesModalClassNames?: string;
hiddenSwitchClassname?: {
container?: string;
thumb?: string;
};
deleteButtonClassname?: string;
};
export type Availability = Pick<Schedule, "days" | "startTime" | "endTime">;
@@ -85,6 +90,8 @@ type AvailabilitySettingsProps = {
};
travelSchedules?: RouterOutputs["viewer"]["getTravelSchedules"];
handleDelete: () => void;
allowDelete?: boolean;
allowSetToDefault?: boolean;
isDeleting: boolean;
isSaving: boolean;
isLoading: boolean;
@@ -252,6 +259,8 @@ export function AvailabilitySettings({
disableEditableHeading = false,
enableOverrides = false,
bulkUpdateModalProps,
allowSetToDefault = true,
allowDelete = true,
}: AvailabilitySettingsProps) {
const [openSidebar, setOpenSidebar] = useState(false);
const { t, i18n } = useLocale();
@@ -314,7 +323,7 @@ export function AvailabilitySettings({
CTA={
<div className={cn(customClassNames?.ctaClassName, "flex items-center justify-end")}>
<div className="sm:hover:bg-muted hidden items-center rounded-md px-2 transition sm:flex">
{!openSidebar ? (
{!openSidebar && allowSetToDefault ? (
<>
<Skeleton
as={Label}
@@ -330,6 +339,10 @@ export function AvailabilitySettings({
render={({ field: { value, onChange } }) => (
<Switch
id="hiddenSwitch"
classNames={{
container: cn(customClassNames?.hiddenSwitchClassname?.container),
thumb: cn(customClassNames?.hiddenSwitchClassname?.thumb),
}}
disabled={isSaving || schedule.isDefault}
checked={value}
onCheckedChange={(checked) => {
@@ -339,6 +352,7 @@ export function AvailabilitySettings({
/>
)}
/>
<VerticalDivider className="hidden sm:inline" />
</>
) : null}
</div>
@@ -353,14 +367,17 @@ export function AvailabilitySettings({
/>
)}
<VerticalDivider className="hidden sm:inline" />
<DeleteDialogButton
buttonClassName="hidden sm:inline"
disabled={schedule.isLastSchedule}
isPending={isDeleting}
handleDelete={handleDelete}
/>
<VerticalDivider className="hidden sm:inline" />
{allowDelete && (
<>
<DeleteDialogButton
buttonClassName={cn("hidden sm:inline", customClassNames?.deleteButtonClassname)}
disabled={schedule.isLastSchedule}
isPending={isDeleting}
handleDelete={handleDelete}
/>
<VerticalDivider className="hidden sm:inline" />
</>
)}
<SmallScreenSideBar open={openSidebar}>
<>
<div
@@ -377,15 +394,17 @@ export function AvailabilitySettings({
<div className="flex flex-row items-center pt-5">
<Button StartIcon="arrow-left" color="minimal" onClick={() => setOpenSidebar(false)} />
<p className="-ml-2">{t("availability_settings")}</p>
<DeleteDialogButton
buttonClassName="ml-16 inline"
disabled={schedule.isLastSchedule}
isPending={isDeleting}
handleDelete={handleDelete}
onDeleteConfirmed={() => {
setOpenSidebar(false);
}}
/>
{allowDelete && (
<DeleteDialogButton
buttonClassName={cn("ml-16 inline", customClassNames?.deleteButtonClassname)}
disabled={schedule.isLastSchedule}
isPending={isDeleting}
handleDelete={handleDelete}
onDeleteConfirmed={() => {
setOpenSidebar(false);
}}
/>
)}
</div>
<div className="flex flex-col px-2 py-2">
<Skeleton as={Label} waitForTranslation={!isPlatform}>
@@ -403,25 +422,33 @@ export function AvailabilitySettings({
/>
</div>
<div className="flex h-9 flex-row-reverse items-center justify-end gap-3 px-2">
<Skeleton
as={Label}
htmlFor="hiddenSwitch"
className="mt-2 cursor-pointer self-center pr-2 sm:inline"
waitForTranslation={!isPlatform}>
{t("set_to_default")}
</Skeleton>
<Controller
control={form.control}
name="isDefault"
render={({ field: { value, onChange } }) => (
<Switch
id="hiddenSwitch"
disabled={isSaving || value}
checked={value}
onCheckedChange={onChange}
{allowSetToDefault && (
<>
<Skeleton
as={Label}
htmlFor="hiddenSwitch"
className="mt-2 cursor-pointer self-center pr-2 sm:inline"
waitForTranslation={!isPlatform}>
{t("set_to_default")}
</Skeleton>
<Controller
control={form.control}
name="isDefault"
render={({ field: { value, onChange } }) => (
<Switch
classNames={{
container: cn(customClassNames?.hiddenSwitchClassname?.container),
thumb: cn(customClassNames?.hiddenSwitchClassname?.thumb),
}}
id="hiddenSwitch"
disabled={isSaving || value}
checked={value}
onCheckedChange={onChange}
/>
)}
/>
)}
/>
</>
)}
</div>
<div className="min-w-40 col-span-3 space-y-2 px-2 py-4 lg:col-span-1">
@@ -31,6 +31,7 @@ export function transformApiScheduleForAtom(
scheduleId: schedule.id,
days: "days" in entry ? entry.days : [],
date: "date" in entry ? entry.date : null,
id: schedule.id,
};
});
@@ -33,6 +33,8 @@ type AvailabilitySettingsPlatformWrapperProps = {
disableEditableHeading?: boolean;
enableOverrides?: boolean;
onBeforeUpdate?: (updateBody: UpdateScheduleInput_2024_06_11) => boolean | Promise<boolean>;
allowDelete?: boolean;
allowSetToDefault?: boolean;
};
export const AvailabilitySettingsPlatformWrapper = ({
@@ -45,6 +47,8 @@ export const AvailabilitySettingsPlatformWrapper = ({
disableEditableHeading = false,
enableOverrides = false,
onBeforeUpdate,
allowDelete,
allowSetToDefault,
}: AvailabilitySettingsPlatformWrapperProps) => {
const { isLoading, data: schedule } = useSchedule(id);
const { data: schedules } = useSchedules();
@@ -142,6 +146,8 @@ export const AvailabilitySettingsPlatformWrapper = ({
backPath=""
isPlatform={true}
customClassNames={customClassNames}
allowDelete={allowDelete}
allowSetToDefault={allowSetToDefault}
/>
</AtomsWrapper>
);
@@ -80,7 +80,6 @@ export type BookerPlatformWrapperAtomProps = Omit<
locationUrl?: string;
view?: VIEW_TYPE;
metadata?: Record<string, string>;
hideEventTypeDetails?: boolean;
};
type VIEW_TYPE = keyof typeof BookerLayouts;
@@ -188,7 +187,7 @@ export const BookerPlatformWrapper = (
event.data.length = props.duration;
}
const bookerLayout = useBookerLayout(event.data, { hideEventTypeDetails: props.hideEventTypeDetails });
const bookerLayout = useBookerLayout(event.data);
useInitializeBookerStore({
...props,
eventId: event.data?.id,
@@ -402,7 +401,7 @@ export const BookerPlatformWrapper = (
}, [view, isOverlayCalendarEnabled]);
return (
<AtomsWrapper>
<AtomsWrapper customClassName={props?.customClassNames?.atomsWrapper}>
<BookerComponent
customClassNames={props.customClassNames}
eventSlug={props.eventSlug}
@@ -30,7 +30,7 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const event = useEvent();
const event = useEvent({ fromRedirectOfNonOrgLink: props.entity.fromRedirectOfNonOrgLink });
const bookerLayout = useBookerLayout(event.data);
const selectedDate = searchParams?.get("date");
@@ -131,6 +131,7 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
duration: props.duration,
selectedDate,
teamMemberEmail: props.teamMemberEmail,
fromRedirectOfNonOrgLink: props.entity.fromRedirectOfNonOrgLink,
});
const bookings = useBookings({
event,
@@ -5,6 +5,7 @@ import { V2_ENDPOINTS, SUCCESS_STATUS } from "@calcom/platform-constants";
import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
import type { EventType } from "@calcom/prisma/client";
import { useAtomsContext } from "../../hooks/useAtomsContext";
import http from "../../lib/http";
export const QUERY_KEY = "use-event-by-id";
@@ -12,6 +13,7 @@ export type UseAtomUpdateEventTypeProps = {
onSuccess?: () => void;
onError?: (err: Error) => void;
onSettled?: () => void;
teamId?: number;
};
export const useAtomUpdateEventType = ({
onSuccess = () => {
@@ -23,14 +25,20 @@ export const useAtomUpdateEventType = ({
onSettled = () => {
return;
},
teamId,
}: UseAtomUpdateEventTypeProps) => {
const { organizationId } = useAtomsContext();
return useMutation({
onSuccess,
onError,
onSettled,
mutationFn: (data: EventTypeUpdateInput) => {
if (!data.id) throw new Error("Event type id is required");
const pathname = `/atoms/${V2_ENDPOINTS.eventTypes}/${data.id}`;
let pathname = `/atoms/${V2_ENDPOINTS.eventTypes}/${data.id}`;
if (teamId) {
pathname = `/atoms/organizations/${organizationId}/teams/${teamId}/${V2_ENDPOINTS.eventTypes}/${data.id}`;
}
return http?.patch<ApiResponse<EventType>>(pathname, data).then((res) => {
if (res.data.status === SUCCESS_STATUS) {
return (res.data as ApiSuccessResponse<EventType>).data;
@@ -0,0 +1,36 @@
import { useQuery } from "@tanstack/react-query";
import {
V2_ENDPOINTS,
SUCCESS_STATUS,
VERSION_2024_06_11,
CAL_API_VERSION_HEADER,
} from "@calcom/platform-constants";
import type { GetScheduleOutput_2024_06_11 } from "@calcom/platform-types";
import http from "../../lib/http";
export const QUERY_KEY = "event-type-user-schedule";
export const useSchedule = (id?: number) => {
const pathname = id ? `/${V2_ENDPOINTS.availability}/${id}` : `/${V2_ENDPOINTS.availability}/default`;
const { isLoading, error, data } = useQuery({
queryKey: [QUERY_KEY, id],
queryFn: () => {
return http
.get<GetScheduleOutput_2024_06_11>(pathname, {
headers: { [CAL_API_VERSION_HEADER]: VERSION_2024_06_11 },
})
.then((res) => {
if (res.data.status === SUCCESS_STATUS) {
return res.data.data;
}
throw new Error(res.data.error?.message);
});
},
enabled: !!id,
});
return { isLoading, error, data };
};
@@ -0,0 +1,35 @@
import { useQuery } from "@tanstack/react-query";
import {
V2_ENDPOINTS,
SUCCESS_STATUS,
VERSION_2024_06_11,
CAL_API_VERSION_HEADER,
} from "@calcom/platform-constants";
import type { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
import http from "../../lib/http";
export const QUERY_KEY = "event-type-user-schedules";
export const useSchedules = () => {
const pathname = `/${V2_ENDPOINTS.availability}`;
const { isLoading, error, data } = useQuery({
queryKey: [QUERY_KEY],
queryFn: () => {
return http
.get<GetSchedulesOutput_2024_06_11>(pathname, {
headers: { [CAL_API_VERSION_HEADER]: VERSION_2024_06_11 },
})
.then((res) => {
if (res.data.status === SUCCESS_STATUS) {
return res.data.data;
}
throw new Error(res.data.error?.message);
});
},
});
return { isLoading, error, data };
};
@@ -0,0 +1,137 @@
import { AtomsWrapper } from "@/components/atoms-wrapper";
import { TeamEventTypeForm } from "@calcom/features/ee/teams/components/TeamEventTypeForm";
import CreateEventTypeForm from "@calcom/features/eventtypes/components/CreateEventTypeForm";
import { useCreateEventTypeForm } from "@calcom/lib/hooks/useCreateEventType";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { EventType } from "@calcom/prisma/client";
import { Button } from "@calcom/ui";
import { useCreateEventType } from "../../hooks/event-types/private/useCreateEventType";
import { useCreateTeamEventType } from "../../hooks/event-types/private/useCreateTeamEventType";
import { useTeams } from "../../hooks/teams/useTeams";
import { cn } from "../../src/lib/utils";
type ActionButtonsClassNames = {
container?: string;
submit?: string;
cancel?: string;
};
type CreateEventTypeProps = {
teamId?: number;
onSuccess?: (eventType: EventType) => void;
onError?: (error: Error) => void;
customClassNames?: {
atomsWrapper?: string;
buttons?: ActionButtonsClassNames;
};
onCancel?: () => void;
};
const ActionButtons = ({
submitLabel,
cancelLabel,
isPending,
customClassNames,
onCancel,
}: {
submitLabel: string;
cancelLabel: string;
isPending: boolean;
customClassNames?: ActionButtonsClassNames;
onCancel?: () => void;
}) => {
return (
<div className={cn("flex flex-row gap-4", customClassNames?.container)}>
<Button type="submit" loading={isPending} className={customClassNames?.submit}>
{submitLabel}
</Button>
{onCancel && (
<Button
color="secondary"
type="button"
loading={isPending}
className={customClassNames?.cancel}
onClick={() => onCancel()}>
{cancelLabel}
</Button>
)}
</div>
);
};
export const CreateEventTypePlatformWrapper = ({
teamId,
onSuccess,
onError,
customClassNames,
onCancel,
}: CreateEventTypeProps) => {
const { form, isManagedEventType } = useCreateEventTypeForm();
const createEventTypeQuery = useCreateEventType({ onSuccess, onError });
const createTeamEventTypeQuery = useCreateTeamEventType({ onSuccess, onError });
const { data: teams } = useTeams();
const team = teams?.find((t) => t.id === teamId);
const { t } = useLocale();
return teamId && team ? (
<AtomsWrapper customClassName={customClassNames?.atomsWrapper}>
<TeamEventTypeForm
teamSlug={team?.slug}
teamId={teamId}
isTeamAdminOrOwner={team.role === "ADMIN" || team.role === "OWNER"}
urlPrefix=""
isPending={createTeamEventTypeQuery.isPending}
form={form}
isManagedEventType={isManagedEventType}
handleSubmit={(values) => {
createTeamEventTypeQuery.mutate({
lengthInMinutes: values.length,
title: values.title,
slug: values.slug,
description: values.description ?? "",
schedulingType: values.schedulingType ?? "COLLECTIVE",
hosts: [],
teamId,
});
}}
SubmitButton={(isPending) =>
ActionButtons({
isPending,
onCancel,
submitLabel: t("continue"),
cancelLabel: t("cancel"),
customClassNames: customClassNames?.buttons,
})
}
/>
</AtomsWrapper>
) : (
<AtomsWrapper customClassName={customClassNames?.atomsWrapper}>
<CreateEventTypeForm
urlPrefix=""
isPending={createEventTypeQuery.isPending}
form={form}
isManagedEventType={isManagedEventType}
handleSubmit={(values) => {
createEventTypeQuery.mutate({
lengthInMinutes: values.length,
title: values.title,
slug: values.slug,
description: values.description ?? "",
});
}}
SubmitButton={(isPending) =>
ActionButtons({
isPending,
onCancel,
submitLabel: t("continue"),
cancelLabel: t("cancel"),
customClassNames: customClassNames?.buttons,
})
}
/>
</AtomsWrapper>
);
};
@@ -0,0 +1,66 @@
import { useFormContext } from "react-hook-form";
import { EventAvailabilityTab } from "@calcom/features/eventtypes/components/tabs/availability/EventAvailabilityTab";
import type { EventTypeSetup, FormValues } from "@calcom/features/eventtypes/lib/types";
import type { User } from "@calcom/prisma/client";
import type { Availability } from "../../availability/AvailabilitySettings";
import { transformApiScheduleForAtom } from "../../availability/atom-api-transformers/transformApiScheduleForAtom";
import { useSchedule } from "../hooks/useSchedule";
import { useSchedules } from "../hooks/useSchedules";
type EventAvailabilityTabPlatformWrapperProps = {
user?: Pick<User, "id" | "defaultScheduleId" | "timeZone" | "timeFormat" | "weekStart">;
eventType: EventTypeSetup;
isTeamEvent: boolean;
};
const EventAvailabilityTabPlatformWrapper = ({
user,
...props
}: EventAvailabilityTabPlatformWrapperProps) => {
const formMethods = useFormContext<FormValues>();
const scheduleId = formMethods.watch("schedule");
const { isLoading: isSchedulePending, data: scheduleQueryData } = useSchedule(
scheduleId || user?.defaultScheduleId || undefined
);
const { data: schedulesQueryData, isLoading: isSchedulesPending } = useSchedules();
const atomSchedule = transformApiScheduleForAtom(user, scheduleQueryData, schedulesQueryData?.length || 0);
if (!atomSchedule) {
return <></>;
}
return (
<EventAvailabilityTab
{...props}
user={user}
schedulesQueryData={schedulesQueryData}
isSchedulesPending={isSchedulesPending}
isSchedulePending={isSchedulePending}
scheduleQueryData={{
isManaged: atomSchedule.isManaged,
readOnly: atomSchedule.readOnly,
id: atomSchedule.id,
timeZone: atomSchedule.timeZone,
schedule:
atomSchedule.schedule.reduce(
(acc: Availability[], avail: Availability) => [
...acc,
{
startTime: new Date(avail.startTime),
endTime: new Date(avail.endTime),
days: avail.days,
},
],
[]
) || [],
}}
/>
);
};
export default EventAvailabilityTabPlatformWrapper;
@@ -11,7 +11,7 @@ import { trpc } from "@calcom/trpc/react";
export type EventAvailabilityTabWebWrapperProps = {
eventType: EventTypeSetup;
isTeamEvent: boolean;
loggedInUser?: RouterOutputs["viewer"]["me"];
user?: RouterOutputs["viewer"]["me"];
teamMembers: TeamMembers;
};
@@ -32,13 +32,13 @@ const EventAvailabilityTabWebWrapper = (props: EventAvailabilityTabWebWrapperPro
const { isPending: isSchedulePending, data: scheduleQueryData } =
trpc.viewer.availability.schedule.get.useQuery(
{
scheduleId: scheduleId || props.loggedInUser?.defaultScheduleId || undefined,
scheduleId: scheduleId || props.user?.defaultScheduleId || undefined,
isManagedEventType: isManagedEventType || isChildrenManagedEventType,
},
{ enabled: !!scheduleId || !!props.loggedInUser?.defaultScheduleId }
{ enabled: !!scheduleId || !!props.user?.defaultScheduleId }
);
const { data: availabilityQueryData, isPending: isAvailabilityPending } =
const { data: schedulesQueryData, isPending: isSchedulesPending } =
trpc.viewer.availability.list.useQuery(undefined);
const hostSchedulesQuery = trpc.viewer.availability.schedule.getAllSchedulesByUserId.useQuery;
@@ -46,8 +46,8 @@ const EventAvailabilityTabWebWrapper = (props: EventAvailabilityTabWebWrapperPro
return (
<EventAvailabilityTab
{...props}
availabilityQueryData={availabilityQueryData}
isAvailabilityPending={isAvailabilityPending}
schedulesQueryData={schedulesQueryData?.schedules}
isSchedulesPending={isSchedulesPending}
isSchedulePending={isSchedulePending}
scheduleQueryData={scheduleQueryData}
editAvailabilityRedirectUrl={`/availability/${scheduleQueryData?.id}`}
@@ -0,0 +1,10 @@
import {
EventTeamAssignmentTab,
type EventTeamAssignmentTabBaseProps,
} from "@calcom/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab";
const EventTeamAssignmentTabPlatformWrapper = (props: EventTeamAssignmentTabBaseProps) => {
return <EventTeamAssignmentTab {...props} />;
};
export default EventTeamAssignmentTabPlatformWrapper;
@@ -0,0 +1,10 @@
import {
EventTeamAssignmentTab,
type EventTeamAssignmentTabBaseProps,
} from "@calcom/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab";
const EventTeamAssignmentTabWebWrapper = (props: EventTeamAssignmentTabBaseProps) => {
return <EventTeamAssignmentTab {...props} />;
};
export default EventTeamAssignmentTabWebWrapper;

Some files were not shown because too many files have changed in this diff Show More