Merge branch 'main' into platform

This commit is contained in:
Ryukemeister 2023-11-28 15:28:24 +05:30
commit 3d24aab4b9
69 changed files with 2998 additions and 2118 deletions

View File

@ -2,6 +2,7 @@ import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { MembershipRole } from "@calcom/prisma/enums";
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
@ -89,7 +90,9 @@ async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
if (req.isAdmin) return true;
if (eventType?.teamId) {
req.query.teamId = String(eventType.teamId);
await canAccessTeamEventOrThrow(req, "MEMBER");
await canAccessTeamEventOrThrow(req, {
in: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER],
});
}
if (eventType?.userId === req.userId) return true; // is owner.
throw new HttpError({ statusCode: 403, message: "Forbidden" });

View File

@ -299,7 +299,7 @@ async function postHandler(req: NextApiRequest) {
data.hosts = { createMany: { data: hosts } };
}
const eventType = await prisma.eventType.create({ data });
const eventType = await prisma.eventType.create({ data, include: { hosts: true } });
return {
event_type: schemaEventTypeReadPublic.parse(eventType),

View File

@ -22,7 +22,11 @@ export async function checkPermissions(
role: Prisma.MembershipWhereInput["role"] = MembershipRole.OWNER
) {
const { userId, prisma, isAdmin } = req;
const { teamId } = schemaQueryTeamId.parse(req.query);
const { teamId } = schemaQueryTeamId.parse({
teamId: req.query.teamId,
version: req.query.version,
apiKey: req.query.apiKey,
});
const args: Prisma.TeamFindFirstArgs = { where: { id: teamId } };
/** If not ADMIN then we check if the actual user belongs to team and matches the required role */
if (!isAdmin) args.where = { ...args.where, members: { some: { userId, role } } };

View File

@ -56,6 +56,14 @@ async function getHandler(req: NextApiRequest) {
members: { some: { userId } },
},
},
include: {
customInputs: true,
team: { select: { slug: true } },
users: true,
hosts: { select: { userId: true, isFixed: true } },
owner: { select: { username: true, id: true } },
children: { select: { id: true, userId: true } },
},
};
const data = await prisma.eventType.findMany(args);

View File

@ -60,7 +60,7 @@ export const InstallAppButtonChild = ({
disabled={shouldDisableInstallation}
color="primary"
size="base">
{paid.trial ? t("start_paid_trial") : t("install_paid_app")}
{paid.trial ? t("start_paid_trial") : t("subscribe")}
</Button>
);
}

View File

@ -71,13 +71,21 @@ if (process.env.GOOGLE_API_CREDENTIALS && !validJson(process.env.GOOGLE_API_CRED
}
const informAboutDuplicateTranslations = () => {
const valueSet = new Set();
const valueMap = {};
for (const key in englishTranslation) {
if (valueSet.has(englishTranslation[key])) {
console.warn("\x1b[33mDuplicate value found in:", "\x1b[0m", key);
const value = englishTranslation[key];
if (valueMap[value]) {
console.warn(
"\x1b[33mDuplicate value found in common.json keys:",
"\x1b[0m ",
key,
"and",
valueMap[value]
);
} else {
valueSet.add(englishTranslation[key]);
valueMap[value] = key;
}
}
};

View File

@ -1,19 +1,337 @@
import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { Linter } from "eslint";
import { parse } from "node-html-parser";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { MembershipRole } from "@calcom/prisma/client";
import { test } from "./lib/fixtures";
function chooseEmbedType(page: Page, embedType: string) {
const linter = new Linter();
const eslintRules = {
"no-undef": "error",
"no-unused-vars": "off",
} as const;
test.describe.configure({ mode: "parallel" });
test.afterEach(({ users }) => users.deleteAll());
test.describe("Embed Code Generator Tests", () => {
test.describe("Non-Organization", () => {
test.beforeEach(async ({ users }) => {
const pro = await users.create();
await pro.apiLogin();
});
test.describe("Event Types Page", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/event-types");
});
test("open Embed Dialog and choose Inline for First Event Type", async ({ page, users }) => {
const [pro] = users.get();
const embedUrl = await clickFirstEventTypeEmbedButton(page);
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage: "/event-types",
});
chooseEmbedType(page, "inline");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
embedType: "inline",
basePage: "/event-types",
});
await expectToContainValidCode(page, {
language: "html",
embedType: "inline",
orgSlug: null,
});
await goToReactCodeTab(page);
await expectToContainValidCode(page, {
language: "react",
embedType: "inline",
orgSlug: null,
});
await goToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "inline",
calLink: `${pro.username}/30-min`,
});
});
test("open Embed Dialog and choose floating-popup for First Event Type", async ({ page, users }) => {
const [pro] = users.get();
const embedUrl = await clickFirstEventTypeEmbedButton(page);
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage: "/event-types",
});
chooseEmbedType(page, "floating-popup");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
embedType: "floating-popup",
basePage: "/event-types",
});
await expectToContainValidCode(page, {
language: "html",
embedType: "floating-popup",
orgSlug: null,
});
await goToReactCodeTab(page);
await expectToContainValidCode(page, {
language: "react",
embedType: "floating-popup",
orgSlug: null,
});
await goToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "floating-popup",
calLink: `${pro.username}/30-min`,
});
});
test("open Embed Dialog and choose element-click for First Event Type", async ({ page, users }) => {
const [pro] = users.get();
const embedUrl = await clickFirstEventTypeEmbedButton(page);
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage: "/event-types",
});
chooseEmbedType(page, "element-click");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
embedType: "element-click",
basePage: "/event-types",
});
await expectToContainValidCode(page, {
language: "html",
embedType: "element-click",
orgSlug: null,
});
await goToReactCodeTab(page);
await expectToContainValidCode(page, {
language: "react",
embedType: "element-click",
orgSlug: null,
});
await goToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "element-click",
calLink: `${pro.username}/30-min`,
});
});
});
test.describe("Event Type Edit Page", () => {
test.beforeEach(async ({ page }) => {
await page.goto(`/event-types`);
await Promise.all([
page.locator('a[href*="/event-types/"]').first().click(),
page.waitForURL((url) => url.pathname.startsWith("/event-types/")),
]);
});
test("open Embed Dialog for the Event Type", async ({ page }) => {
const basePage = new URL(page.url()).pathname;
const embedUrl = await clickEmbedButton(page);
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage,
});
chooseEmbedType(page, "inline");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
basePage,
embedType: "inline",
});
await expectToContainValidCode(page, {
language: "html",
embedType: "inline",
orgSlug: null,
});
await goToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "inline",
calLink: decodeURIComponent(embedUrl),
});
});
});
});
test.describe("Organization", () => {
test.beforeEach(async ({ users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const user = await users.create({
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
});
await user.apiLogin();
});
test.describe("Event Types Page", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/event-types");
});
test("open Embed Dialog and choose Inline for First Event Type", async ({ page, users }) => {
const [user] = users.get();
const { team: org } = await user.getOrgMembership();
const embedUrl = await clickFirstEventTypeEmbedButton(page);
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage: "/event-types",
});
chooseEmbedType(page, "inline");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
embedType: "inline",
basePage: "/event-types",
});
// Default tab is HTML code tab
await expectToContainValidCode(page, {
language: "html",
embedType: "inline",
orgSlug: org.slug,
});
await goToReactCodeTab(page);
await expectToContainValidCode(page, {
language: "react",
embedType: "inline",
orgSlug: org.slug,
});
await goToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "inline",
calLink: `${user.username}/30-min`,
bookerUrl: getOrgFullOrigin(org?.slug ?? ""),
});
});
test("open Embed Dialog and choose floating-popup for First Event Type", async ({ page, users }) => {
const [user] = users.get();
const { team: org } = await user.getOrgMembership();
const embedUrl = await clickFirstEventTypeEmbedButton(page);
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage: "/event-types",
});
chooseEmbedType(page, "floating-popup");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
embedType: "floating-popup",
basePage: "/event-types",
});
await expectToContainValidCode(page, {
language: "html",
embedType: "floating-popup",
orgSlug: org.slug,
});
await goToReactCodeTab(page);
await expectToContainValidCode(page, {
language: "react",
embedType: "floating-popup",
orgSlug: org.slug,
});
await goToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "floating-popup",
calLink: `${user.username}/30-min`,
bookerUrl: getOrgFullOrigin(org?.slug ?? ""),
});
});
test("open Embed Dialog and choose element-click for First Event Type", async ({ page, users }) => {
const [user] = users.get();
const embedUrl = await clickFirstEventTypeEmbedButton(page);
const { team: org } = await user.getOrgMembership();
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage: "/event-types",
});
chooseEmbedType(page, "element-click");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
embedType: "element-click",
basePage: "/event-types",
});
await expectToContainValidCode(page, {
language: "html",
embedType: "element-click",
orgSlug: org.slug,
});
await goToReactCodeTab(page);
await expectToContainValidCode(page, {
language: "react",
embedType: "element-click",
orgSlug: org.slug,
});
await goToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "element-click",
calLink: `${user.username}/30-min`,
bookerUrl: getOrgFullOrigin(org?.slug ?? ""),
});
});
});
});
});
type EmbedType = "inline" | "floating-popup" | "element-click";
function chooseEmbedType(page: Page, embedType: EmbedType) {
page.locator(`[data-testid=${embedType}]`).click();
}
async function gotToPreviewTab(page: Page) {
async function goToPreviewTab(page: Page) {
// To prevent early timeouts
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1000);
await page.locator("[data-testid=embed-tabs]").locator("text=Preview").click();
await page.locator("[data-testid=horizontal-tab-Preview]").click();
}
async function goToReactCodeTab(page: Page) {
// To prevent early timeouts
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1000);
await page.locator("[data-testid=horizontal-tab-React]").click();
}
async function clickEmbedButton(page: Page) {
@ -55,7 +373,7 @@ async function expectToBeNavigatingToEmbedCodeAndPreviewDialog(
basePage,
}: {
embedUrl: string | null;
embedType: string;
embedType: EmbedType;
basePage: string;
}
) {
@ -73,10 +391,108 @@ async function expectToBeNavigatingToEmbedCodeAndPreviewDialog(
});
}
async function expectToContainValidCode(page: Page, { embedType }: { embedType: string }) {
async function expectToContainValidCode(
page: Page,
{
embedType,
language,
orgSlug,
}: { embedType: EmbedType; language: "html" | "react"; orgSlug: string | null }
) {
if (language === "react") {
return expectValidReactEmbedSnippet(page, { embedType, orgSlug });
}
if (language === "html") {
return expectValidHtmlEmbedSnippet(page, { embedType, orgSlug });
}
throw new Error("Unknown language");
}
async function expectValidHtmlEmbedSnippet(
page: Page,
{ embedType, orgSlug }: { embedType: EmbedType; orgSlug: string | null }
) {
const embedCode = await page.locator("[data-testid=embed-code]").inputValue();
expect(embedCode.includes("(function (C, A, L)")).toBe(true);
expect(embedCode.includes(`Cal ${embedType} embed code begins`)).toBe(true);
expect(embedCode).toContain("function (C, A, L)");
expect(embedCode).toContain(`Cal ${embedType} embed code begins`);
if (orgSlug) {
expect(embedCode).toContain(orgSlug);
}
const dom = parse(embedCode);
const scripts = dom.getElementsByTagName("script");
assertThatCodeIsValidVanillaJsCode(scripts[0].innerText);
return {
message: () => `passed`,
pass: true,
};
}
function assertThatCodeIsValidVanillaJsCode(code: string) {
const lintResult = linter.verify(code, {
env: {
browser: true,
},
parserOptions: {
ecmaVersion: 2021,
},
globals: {
Cal: "readonly",
},
rules: eslintRules,
});
if (lintResult.length) {
console.log(
JSON.stringify({
lintResult,
code,
})
);
}
expect(lintResult.length).toBe(0);
}
function assertThatCodeIsValidReactCode(code: string) {
const lintResult = linter.verify(code, {
env: {
browser: true,
},
parserOptions: {
ecmaVersion: 2021,
ecmaFeatures: {
jsx: true,
},
sourceType: "module",
},
rules: eslintRules,
});
if (lintResult.length) {
console.log(
JSON.stringify({
lintResult,
code,
})
);
}
expect(lintResult.length).toBe(0);
}
async function expectValidReactEmbedSnippet(
page: Page,
{ embedType, orgSlug }: { embedType: EmbedType; orgSlug: string | null }
) {
const embedCode = await page.locator("[data-testid=embed-react]").inputValue();
expect(embedCode).toContain("export default function MyApp(");
expect(embedCode).toContain(
embedType === "floating-popup" ? "floatingButton" : embedType === "inline" ? `<Cal` : "data-cal-link"
);
if (orgSlug) {
expect(embedCode).toContain(orgSlug);
}
assertThatCodeIsValidReactCode(embedCode);
return {
message: () => `passed`,
pass: true,
@ -88,141 +504,10 @@ async function expectToContainValidCode(page: Page, { embedType }: { embedType:
*/
async function expectToContainValidPreviewIframe(
page: Page,
{ embedType, calLink }: { embedType: string; calLink: string }
{ embedType, calLink, bookerUrl }: { embedType: EmbedType; calLink: string; bookerUrl?: string }
) {
const bookerUrl = `${WEBAPP_URL}`;
bookerUrl = bookerUrl || `${WEBAPP_URL}`;
expect(await page.locator("[data-testid=embed-preview]").getAttribute("src")).toContain(
`/preview.html?embedType=${embedType}&calLink=${calLink}&embedLibUrl=${EMBED_LIB_URL}&bookerUrl=${bookerUrl}`
);
}
test.describe.configure({ mode: "parallel" });
test.afterEach(({ users }) => users.deleteAll());
test.describe("Embed Code Generator Tests", () => {
test.beforeEach(async ({ users }) => {
const pro = await users.create();
await pro.apiLogin();
});
test.describe("Event Types Page", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/event-types");
});
test("open Embed Dialog and choose Inline for First Event Type", async ({ page, users }) => {
const [pro] = users.get();
const embedUrl = await clickFirstEventTypeEmbedButton(page);
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage: "/event-types",
});
chooseEmbedType(page, "inline");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
embedType: "inline",
basePage: "/event-types",
});
await expectToContainValidCode(page, { embedType: "inline" });
await gotToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "inline",
calLink: `${pro.username}/30-min`,
});
});
test("open Embed Dialog and choose floating-popup for First Event Type", async ({ page, users }) => {
const [pro] = users.get();
const embedUrl = await clickFirstEventTypeEmbedButton(page);
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage: "/event-types",
});
chooseEmbedType(page, "floating-popup");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
embedType: "floating-popup",
basePage: "/event-types",
});
await expectToContainValidCode(page, { embedType: "floating-popup" });
await gotToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "floating-popup",
calLink: `${pro.username}/30-min`,
});
});
test("open Embed Dialog and choose element-click for First Event Type", async ({ page, users }) => {
const [pro] = users.get();
const embedUrl = await clickFirstEventTypeEmbedButton(page);
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage: "/event-types",
});
chooseEmbedType(page, "element-click");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
embedType: "element-click",
basePage: "/event-types",
});
await expectToContainValidCode(page, { embedType: "element-click" });
await gotToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "element-click",
calLink: `${pro.username}/30-min`,
});
});
});
test.describe("Event Type Edit Page", () => {
test.beforeEach(async ({ page }) => {
await page.goto(`/event-types`);
await Promise.all([
page.locator('a[href*="/event-types/"]').first().click(),
page.waitForURL((url) => url.pathname.startsWith("/event-types/")),
]);
});
test("open Embed Dialog for the Event Type", async ({ page }) => {
const basePage = new URL(page.url()).pathname;
const embedUrl = await clickEmbedButton(page);
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage,
});
chooseEmbedType(page, "inline");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
basePage,
embedType: "inline",
});
await expectToContainValidCode(page, {
embedType: "inline",
});
await gotToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "inline",
calLink: decodeURIComponent(embedUrl),
});
});
});
});

View File

@ -73,10 +73,10 @@ const fillQuestion = async (eventTypePage: Page, questionType: string, customLoc
},
multiselect: async () => {
if (customLocators.shouldChangeMultiSelectLocator) {
await eventTypePage.locator("form svg").nth(1).click();
await eventTypePage.getByLabel("multi-select-dropdown").click();
await eventTypePage.getByTestId("select-option-Option 1").click();
} else {
await eventTypePage.locator("form svg").last().click();
await eventTypePage.getByLabel("multi-select-dropdown").last().click();
await eventTypePage.getByTestId("select-option-Option 1").click();
}
},
@ -88,10 +88,10 @@ const fillQuestion = async (eventTypePage: Page, questionType: string, customLoc
},
select: async () => {
if (customLocators.shouldChangeSelectLocator) {
await eventTypePage.locator("form svg").nth(1).click();
await eventTypePage.getByLabel("select-dropdown").first().click();
await eventTypePage.getByTestId("select-option-Option 1").click();
} else {
await eventTypePage.locator("form svg").last().click();
await eventTypePage.getByLabel("select-dropdown").last().click();
await eventTypePage.getByTestId("select-option-Option 1").click();
}
},
@ -138,11 +138,12 @@ const fillAllQuestions = async (eventTypePage: Page, questions: string[], option
await eventTypePage.getByPlaceholder("Textarea test").fill("This is a sample text for textarea.");
break;
case "select":
await eventTypePage.locator("form svg").last().click();
await eventTypePage.getByLabel("select-dropdown").last().click();
await eventTypePage.getByTestId("select-option-Option 1").click();
break;
case "multiselect":
await eventTypePage.locator("form svg").nth(4).click();
// select-dropdown
await eventTypePage.getByLabel("multi-select-dropdown").click();
await eventTypePage.getByTestId("select-option-Option 1").click();
break;
case "number":

View File

@ -460,7 +460,7 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
}
return membership;
},
getOrg: async () => {
getOrgMembership: async () => {
return prisma.membership.findFirstOrThrow({
where: {
userId: user.id,
@ -471,7 +471,13 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
},
},
},
include: { team: { select: { children: true, metadata: true, name: true } } },
include: {
team: {
include: {
children: true,
},
},
},
});
},
getFirstEventAsOwner: async () =>

View File

@ -14,7 +14,7 @@ test.afterEach(async ({ users, emails, clipboard }) => {
test.describe("Organization", () => {
test("Invitation (non verified)", async ({ browser, page, users, emails, clipboard }) => {
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true });
const { team: org } = await orgOwner.getOrg();
const { team: org } = await orgOwner.getOrgMembership();
await orgOwner.apiLogin();
await page.goto("/settings/organizations/members");
await page.waitForLoadState("networkidle");
@ -92,7 +92,7 @@ test.describe("Organization", () => {
test("Invitation (verified)", async ({ browser, page, users, emails }) => {
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, isOrgVerified: true });
const { team: org } = await orgOwner.getOrg();
const { team: org } = await orgOwner.getOrgMembership();
await orgOwner.apiLogin();
await page.goto("/settings/organizations/members");
await page.waitForLoadState("networkidle");

View File

@ -168,7 +168,7 @@ test.describe("Teams - NonOrg", () => {
await expect(page.locator("[data-testid=alert]")).toBeVisible();
// cleanup
const org = await owner.getOrg();
const org = await owner.getOrgMembership();
await prisma.team.delete({ where: { id: org.teamId } });
}
});

View File

@ -45,7 +45,7 @@ test.describe("Unpublished", () => {
test("Organization profile", async ({ users, page }) => {
const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, isOrg: true });
const { team: org } = await owner.getOrg();
const { team: org } = await owner.getOrgMembership();
const { requestedSlug } = org.metadata as { requestedSlug: string };
await page.goto(`/org/${requestedSlug}`);
await page.waitForLoadState("networkidle");
@ -62,7 +62,7 @@ test.describe("Unpublished", () => {
isOrg: true,
hasSubteam: true,
});
const { team: org } = await owner.getOrg();
const { team: org } = await owner.getOrgMembership();
const { requestedSlug } = org.metadata as { requestedSlug: string };
const [{ slug: subteamSlug }] = org.children as { slug: string }[];
await page.goto(`/org/${requestedSlug}/team/${subteamSlug}`);
@ -80,7 +80,7 @@ test.describe("Unpublished", () => {
isOrg: true,
hasSubteam: true,
});
const { team: org } = await owner.getOrg();
const { team: org } = await owner.getOrgMembership();
const { requestedSlug } = org.metadata as { requestedSlug: string };
const [{ slug: subteamSlug, id: subteamId }] = org.children as { slug: string; id: number }[];
const { slug: subteamEventSlug } = await owner.getFirstTeamEvent(subteamId);
@ -95,7 +95,7 @@ test.describe("Unpublished", () => {
test("Organization user", async ({ users, page }) => {
const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, isOrg: true });
const { team: org } = await owner.getOrg();
const { team: org } = await owner.getOrgMembership();
const { requestedSlug } = org.metadata as { requestedSlug: string };
await page.goto(`/org/${requestedSlug}/${owner.username}`);
await page.waitForLoadState("networkidle");
@ -107,7 +107,7 @@ test.describe("Unpublished", () => {
test("Organization user event-type", async ({ users, page }) => {
const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, isOrg: true });
const { team: org } = await owner.getOrg();
const { team: org } = await owner.getOrgMembership();
const { requestedSlug } = org.metadata as { requestedSlug: string };
const [{ slug: ownerEventType }] = owner.eventTypes;
await page.goto(`/org/${requestedSlug}/${owner.username}/${ownerEventType}`);

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "سيتم ملء \"اسم المستخدم\" باسم المستخدم للأعضاء المعينين",
"assign_to": "تعيين إلى",
"add_members": "إضافة أعضاء...",
"count_members_one": "{{count}} عضو",
"count_members_other": "{{count}} من الأعضاء",
"no_assigned_members": "لا يوجد أعضاء معينين",
"assigned_to": "تم التعيين إلى",
"start_assigning_members_above": "بدء تعيين الأعضاء أعلاه",
@ -849,7 +847,6 @@
"next_step": "تخطي الخطوة",
"prev_step": "الخطوة السابقة",
"install": "تثبيت",
"install_paid_app": "اشتراك",
"installed": "تم التثبيت",
"active_install_one": "{{count}} تثبيت نشط",
"active_install_other": "{{count}} تثبيت نشط",
@ -1218,9 +1215,6 @@
"attendee_name_variable": "الحاضر",
"event_date_variable": "تاريخ الحدث",
"event_time_variable": "وقت الحدث",
"timezone_variable": "المنطقة الزمنية",
"location_variable": "الموقع",
"additional_notes_variable": "ملاحظات إضافية",
"organizer_name_variable": "اسم المنظم",
"app_upgrade_description": "لاستخدام هذه الميزة، تحتاج إلى الترقية إلى حساب Pro.",
"invalid_number": "رقم الهاتف غير صالح",
@ -1986,14 +1980,11 @@
"google_new_spam_policy": "يمكن أن تمنعك سياسة Google الجديدة للرسائل غير المرغوب فيها من تلقي أي إشعارات بالبريد الإلكتروني والتقويم حول هذا الحجز.",
"resolve": "حل",
"no_organization_slug": "حدث خطأ أثناء إنشاء فرق لهذه المنظمة. الكلمة اللطيفة للرابط مفقودة.",
"org_name": "اسم المنظمة",
"org_url": "رابط المنظمة",
"copy_link_org": "نسخ الرابط إلى المنظمة",
"404_the_org": "المنظمة",
"404_the_team": "الفريق",
"404_claim_entity_org": "المطالبة بنطاقك الفرعي لمنظمتك",
"404_claim_entity_team": "انضم لهذا الفريق وابدأ في إدارة الجداول الزمنية بشكل جماعي",
"insights_all_org_filter": "الكل",
"insights_team_filter": "الفريق: {{teamName}}",
"insights_user_filter": "المستخدم: {{userName}}",
"insights_subtitle": "عرض Insights الحجز عبر أحداثك",
@ -2005,7 +1996,6 @@
"select_date": "اختيار التاريخ",
"see_all_available_times": "رؤية كل الأوقات المتاحة",
"org_team_names_example": "مثال، فريق التسويق",
"org_team_names_example_1": "مثال، فريق التسويق",
"org_team_names_example_2": "مثال، فريق المبيعات",
"org_team_names_example_3": "مثال، فريق التصميم",
"org_team_names_example_4": "مثال، الفريق الهندسي",
@ -2101,5 +2091,6 @@
"lock_timezone_toggle_on_booking_page": "قفل المنطقة الزمنية في صفحة الحجز",
"description_lock_timezone_toggle_on_booking_page": "تقفل المنطقة الزمنية على صفحة الحجز، وهذا مفيد للأحداث وجهاً لوجه.",
"extensive_whitelabeling": "عملية انضمام ودعم هندسي مخصصين",
"need_help": "هل تحتاج إلى مساعدة؟",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "„uživatelské jméno“ bude vyplněno uživatelským jménem přiřazených členů",
"assign_to": "Přiřadit k",
"add_members": "Přidat členy...",
"count_members_one": "Počet členů: {{count}}",
"count_members_other": "Počet členů: {{count}}",
"no_assigned_members": "Žádní přiřazení členové",
"assigned_to": "Přiřazeno k",
"start_assigning_members_above": "Začněte přiřazovat členy výše",
@ -849,7 +847,6 @@
"next_step": "Přeskočit krok",
"prev_step": "Předchozí krok",
"install": "Nainstalovat",
"install_paid_app": "Odebírat",
"installed": "Nainstalováno",
"active_install_one": "Aktivní instalace: {{count}}",
"active_install_other": "Aktivní instalace: {{count}}",
@ -1218,9 +1215,6 @@
"attendee_name_variable": "Jméno účastníka",
"event_date_variable": "Datum události",
"event_time_variable": "Čas události",
"timezone_variable": "Časová zóna",
"location_variable": "Místo",
"additional_notes_variable": "Doplňující poznámky",
"organizer_name_variable": "Jméno organizátora",
"app_upgrade_description": "Pokud chcete použít tuto funkci, musíte provést aktualizaci na účet Pro.",
"invalid_number": "Neplatné telefonní číslo",
@ -1986,14 +1980,11 @@
"google_new_spam_policy": "Nové zásady společnosti Google týkající se nevyžádané pošty by mohly zabránit tomu, abyste dostávali e-mailová oznámení a oznámení kalendáře o této rezervaci.",
"resolve": "Vyřešit",
"no_organization_slug": "Při vytváření týmů pro tuto organizaci došlo k chybě. Chybějící slug adresy URL.",
"org_name": "Název organizace",
"org_url": "Adresa URL organizace",
"copy_link_org": "Zkopírovat odkaz na organizaci",
"404_the_org": "Organizace",
"404_the_team": "Tým",
"404_claim_entity_org": "Vyžádejte si subdoménu pro svou organizaci",
"404_claim_entity_team": "Vyžádejte si přístup k tomuto týmu a začněte kolektivně spravovat rozvrhy",
"insights_all_org_filter": "Všechny aplikace",
"insights_team_filter": "Tým: {{teamName}}",
"insights_user_filter": "Uživatel: {{userName}}",
"insights_subtitle": "Zobrazte si Insight rezervací napříč vašimi událostmi",
@ -2005,7 +1996,6 @@
"select_date": "Vyberte datum",
"see_all_available_times": "Zobrazit všechny dostupné časy",
"org_team_names_example": "Např. marketingový tým",
"org_team_names_example_1": "Např. marketingový tým",
"org_team_names_example_2": "Např. obchodní tým",
"org_team_names_example_3": "Např. designérský tým",
"org_team_names_example_4": "Např. inženýrský tým",
@ -2101,5 +2091,6 @@
"lock_timezone_toggle_on_booking_page": "Uzamčení časového pásma na stránce rezervace",
"description_lock_timezone_toggle_on_booking_page": "Uzamčení časového pásma na stránce rezervace (užitečné pro osobní události).",
"extensive_whitelabeling": "Vyhrazená podpora zaškolovací a inženýrská podpora",
"need_help": "Potřebujete pomoc?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -717,7 +717,6 @@
"next_step": "Spring trin over",
"prev_step": "Forrige trin",
"install": "Installér",
"install_paid_app": "Abonnér",
"installed": "Installeret",
"active_install_one": "{{count}} aktiv installation",
"active_install_other": "{{count}} aktive installationer",
@ -1055,9 +1054,6 @@
"attendee_name_variable": "Deltager",
"event_date_variable": "Dato for begivenhed",
"event_time_variable": "Tidspunkt for begivenhed",
"timezone_variable": "Tidszone",
"location_variable": "Placering",
"additional_notes_variable": "Yderligere bemærkninger",
"app_upgrade_description": "For at bruge denne funktion skal du opgradere til en Pro-konto.",
"invalid_number": "Ugyldigt telefonnummer",
"navigate": "Navigér",
@ -1538,5 +1534,5 @@
"this_will_be_the_placeholder": "Dette vil være pladsholderen",
"verification_code": "Bekræftelseskode",
"verify": "Bekræft",
"insights_all_org_filter": "Alle"
"need_help": "Brug for hjælp?"
}

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "„Benutzername“ wird mit dem Benutzernamen der zugewiesenen Mitglieder ausgefüllt",
"assign_to": "Zuordnen zu",
"add_members": "Mitglieder hinzufügen...",
"count_members_one": "{{count}} Mitglied",
"count_members_other": "{{count}} Mitglieder",
"no_assigned_members": "Keine zugeordneten Mitglieder",
"assigned_to": "Zugeordnet zu",
"start_assigning_members_above": "Mit der Zuordnung von Mitgliedern oben beginnen",
@ -849,7 +847,6 @@
"next_step": "Schritt überspringen",
"prev_step": "Vorheriger Schritt",
"install": "Installieren",
"install_paid_app": "Abonnieren",
"installed": "Installiert",
"active_install_one": "{{count}} aktive Installation",
"active_install_other": "{{count}} aktive Installationen",
@ -1218,9 +1215,6 @@
"attendee_name_variable": "Teilnehmer",
"event_date_variable": "Event Datum",
"event_time_variable": "Event Zeit",
"timezone_variable": "Zeitzone",
"location_variable": "Ort",
"additional_notes_variable": "Zusätzliche Notizen",
"organizer_name_variable": "Organisator Name",
"app_upgrade_description": "Um diese Funktion nutzen zu können, müssen Sie ein Upgrade auf einen Pro-Account durchführen.",
"invalid_number": "Ungültige Telefonnummer",
@ -1986,14 +1980,11 @@
"google_new_spam_policy": "Die neuen Spam-Richtlinien von Google könnten verhindern, dass Sie E-Mail- und Kalenderbenachrichtigungen zu dieser Buchung erhalten.",
"resolve": "Problem lösen",
"no_organization_slug": "Beim Erstellen von Teams für diese Organization ist ein Fehler aufgetreten. URL-Slug fehlt.",
"org_name": "Name der Organization",
"org_url": "URL der Organization",
"copy_link_org": "Link zur Organization kopieren",
"404_the_org": "Die Organization",
"404_the_team": "Das Team",
"404_claim_entity_org": "Beanspruchen Sie Ihre Subdomain für Ihre Organization",
"404_claim_entity_team": "Dieses Team beanspruchen und gemeinsam mit der Verwaltung von Verfügbarkeitsplänen beginnen",
"insights_all_org_filter": "Alle Apps",
"insights_team_filter": "Team: {{teamName}}",
"insights_user_filter": "Benutzer: {{userName}}",
"insights_subtitle": "Erfahren Sie mehr über Ihre Termine und Ihr Team",
@ -2005,7 +1996,6 @@
"select_date": "Datum auswählen",
"see_all_available_times": "Alle verfügbaren Zeiten ansehen",
"org_team_names_example": "z.B. Marketing-Team",
"org_team_names_example_1": "z.B. Marketing-Team",
"org_team_names_example_2": "z.B. Vertriebsteam",
"org_team_names_example_3": "z. B. Design-Team",
"org_team_names_example_4": "z.B. Engineering-Team",
@ -2101,5 +2091,6 @@
"lock_timezone_toggle_on_booking_page": "Zeitzone auf der Buchungsseite sperren",
"description_lock_timezone_toggle_on_booking_page": "Um die Zeitzone auf der Buchungsseite zu sperren, nützlich für Termine in Person.",
"extensive_whitelabeling": "Dedizierte Onboarding- und Engineeringsupport",
"need_help": "Brauchst du Hilfe?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -274,8 +274,5 @@
"event_name_tooltip": "Το όνομα που θα εμφανίζεται στα ημερολόγια",
"label": "Ετικέτα",
"edit": "Επεξεργασία",
"disable_guests": "Απενεργοποίηση επισκεπτών",
"location_variable": "Τοποθεσία",
"additional_notes_variable": "Πρόσθετες σημειώσεις",
"insights_all_org_filter": "Όλα"
"disable_guests": "Απενεργοποίηση επισκεπτών"
}

View File

@ -678,8 +678,6 @@
"managed_event_url_clarification": "\"username\" will be filled by the username of the members assigned",
"assign_to": "Assign to",
"add_members": "Add members...",
"count_members_one": "{{count}} member",
"count_members_other": "{{count}} members",
"no_assigned_members": "No assigned members",
"assigned_to": "Assigned to",
"start_assigning_members_above": "Start assigning members above",
@ -861,7 +859,6 @@
"next_step": "Skip step",
"prev_step": "Prev step",
"install": "Install",
"install_paid_app": "Subscribe",
"start_paid_trial": "Start free Trial",
"installed": "Installed",
"active_install_one": "{{count}} active install",
@ -1232,9 +1229,6 @@
"attendee_name_variable": "Attendee",
"event_date_variable": "Event date",
"event_time_variable": "Event time",
"timezone_variable": "Timezone",
"location_variable": "Location",
"additional_notes_variable": "Additional notes",
"organizer_name_variable": "Organizer name",
"app_upgrade_description": "In order to use this feature, you need to upgrade to a Pro account.",
"invalid_number": "Invalid phone number",
@ -2002,14 +1996,11 @@
"google_new_spam_policy": "Googles new spam policy could prevent you from receiving any email and calendar notifications about this booking.",
"resolve": "Resolve",
"no_organization_slug": "There was an error creating teams for this organization. Missing URL slug.",
"org_name": "Organization name",
"org_url": "Organization URL",
"copy_link_org": "Copy link to organization",
"404_the_org": "The organization",
"404_the_team": "The team",
"404_claim_entity_org": "Claim your subdomain for your organization",
"404_claim_entity_team": "Claim this team and start managing schedules collectively",
"insights_all_org_filter": "All",
"insights_team_filter": "Team: {{teamName}}",
"insights_user_filter": "User: {{userName}}",
"insights_subtitle": "View booking insights across your events",
@ -2021,7 +2012,6 @@
"select_date": "Select Date",
"see_all_available_times": "See all available times",
"org_team_names_example": "e.g. Marketing Team",
"org_team_names_example_1": "e.g. Marketing Team",
"org_team_names_example_2": "e.g. Sales Team",
"org_team_names_example_3": "e.g. Design Team",
"org_team_names_example_4": "e.g. Engineering Team",
@ -2121,6 +2111,7 @@
"manage_availability_schedules":"Manage availability schedules",
"lock_timezone_toggle_on_booking_page": "Lock timezone on booking page",
"description_lock_timezone_toggle_on_booking_page" : "To lock the timezone on booking page, useful for in-person events.",
"number_in_international_format": "Please enter number in international format.",
"install_calendar":"Install Calendar",
"branded_subdomain": "Branded Subdomain",
"branded_subdomain_description": "Get your own branded subdomain, such as acme.cal.com",
@ -2137,5 +2128,9 @@
"enterprise_description": "Upgrade to Enterprise to create your Organization",
"create_your_org": "Create your Organization",
"create_your_org_description": "Upgrade to Enterprise and receive a subdomain, unified billing, Insights, extensive whitelabeling and more",
"troubleshooter_tooltip": "Open the troubleshooter and figure out what is wrong with your schedule",
"need_help": "Need help?",
"troubleshooter": "Troubleshooter",
"please_install_a_calendar": "Please install a calendar",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "El \"nombre de usuario\" se completará con el nombre de usuario de los miembros asignados",
"assign_to": "Asignar a",
"add_members": "Agregar miembros...",
"count_members_one": "{{count}} miembro",
"count_members_other": "{{count}} miembros",
"no_assigned_members": "No hay miembros asignados",
"assigned_to": "Asignado a",
"start_assigning_members_above": "Empezar a asignar miembros más arriba",
@ -849,7 +847,6 @@
"next_step": "Saltar paso",
"prev_step": "Paso anterior",
"install": "Instalar",
"install_paid_app": "Suscribirse",
"installed": "Instalado",
"active_install_one": "{{count}} instalación activa",
"active_install_other": "{{count}} instalaciones activas",
@ -1218,9 +1215,6 @@
"attendee_name_variable": "Nombre del asistente",
"event_date_variable": "Fecha del evento",
"event_time_variable": "Hora del evento",
"timezone_variable": "Zona Horaria",
"location_variable": "Lugar",
"additional_notes_variable": "Notas Adicionales",
"organizer_name_variable": "Nombre del organizador",
"app_upgrade_description": "Para poder usar esta función, necesita actualizarse a una cuenta Pro.",
"invalid_number": "Número de teléfono no válido",
@ -1986,14 +1980,11 @@
"google_new_spam_policy": "La nueva política de spam de Google podría evitar que reciba notificaciones por correo electrónico y calendario sobre esta reserva.",
"resolve": "Resolver",
"no_organization_slug": "Hubo un error al crear equipos para esta organización. Falta el slug de URL.",
"org_name": "Nombre de la organización",
"org_url": "URL de la organización",
"copy_link_org": "Copiar enlace a la organización",
"404_the_org": "La organización",
"404_the_team": "El equipo",
"404_claim_entity_org": "Reclame su subdominio para su organización",
"404_claim_entity_team": "Reclame este equipo y empiece a gestionar los horarios de forma colectiva",
"insights_all_org_filter": "Todas las aplicaciones",
"insights_team_filter": "Equipo: {{teamName}}",
"insights_user_filter": "Usuario: {{userName}}",
"insights_subtitle": "Mire información de insights en todos sus eventos",
@ -2005,7 +1996,6 @@
"select_date": "Seleccione la fecha",
"see_all_available_times": "Ver todas las horas disponibles",
"org_team_names_example": "ej. Equipo de marketing",
"org_team_names_example_1": "ej. Equipo de marketing",
"org_team_names_example_2": "ej. Equipo de ventas",
"org_team_names_example_3": "ej. Equipo de diseño",
"org_team_names_example_4": "ej. Equipo de ingeniería",
@ -2101,5 +2091,6 @@
"lock_timezone_toggle_on_booking_page": "Bloquear la zona horaria en la página de reserva",
"description_lock_timezone_toggle_on_booking_page": "Bloquear la zona horaria en la página de reserva, es útil para eventos en persona.",
"extensive_whitelabeling": "Asistencia dedicada en materia de incorporación e ingeniería",
"need_help": "¿Necesita ayuda?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agregue sus nuevas cadenas arriba ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -439,8 +439,6 @@
"default_duration": "Lehenetsitako iraupena",
"minutes": "minutu",
"username_placeholder": "erabiltzaile izena",
"count_members_one": "kide {{count}}",
"count_members_other": "{{count}} kide",
"url": "URLa",
"hidden": "Ezkutuan",
"readonly": "Irakurtzeko bakarrik",
@ -506,7 +504,6 @@
"next_step": "Saltatu pausoa",
"prev_step": "Aurreko pausoa",
"install": "Instalatu",
"install_paid_app": "Harpidetu",
"installed": "Instalatua",
"disconnect": "Deskonektatu",
"automation": "Automatizazioa",
@ -644,9 +641,6 @@
"attendee_name_variable": "Partaidea",
"event_date_variable": "Gertaeraren data",
"event_time_variable": "Gertaeraren ordua",
"timezone_variable": "Ordu-eremua",
"location_variable": "Kokapena",
"additional_notes_variable": "Ohar gehigarriak",
"organizer_name_variable": "Antolatzailearen izena",
"invalid_number": "Telefono zenbaki baliogabea",
"navigate": "Nabigatu",

View File

@ -665,8 +665,6 @@
"managed_event_url_clarification": "« nom d'utilisateur » sera rempli par le nom d'utilisateur des membres assignés",
"assign_to": "Assigner à",
"add_members": "Ajouter des membres...",
"count_members_one": "{{count}} membre",
"count_members_other": "{{count}} membres",
"no_assigned_members": "Aucun membre assigné",
"assigned_to": "Assigné à",
"start_assigning_members_above": "Commencez à assigner des membres ci-dessus",
@ -848,7 +846,6 @@
"next_step": "Passer l'étape",
"prev_step": "Étape précédente",
"install": "Installer",
"install_paid_app": "S'abonner",
"start_paid_trial": "Démarrer l'essai gratuit",
"installed": "Installée",
"active_install_one": "{{count}} installation active",
@ -1219,9 +1216,6 @@
"attendee_name_variable": "Nom du participant",
"event_date_variable": "Date de l'événement",
"event_time_variable": "Heure de l'événement",
"timezone_variable": "Fuseau horaire",
"location_variable": "Lieu",
"additional_notes_variable": "Notes supplémentaires",
"organizer_name_variable": "Nom de l'organisateur",
"app_upgrade_description": "Pour pouvoir utiliser cette fonctionnalité, vous devez passer à un compte Pro.",
"invalid_number": "Numéro de téléphone invalide",
@ -1983,14 +1977,11 @@
"google_new_spam_policy": "La nouvelle politique anti-spam de Google peut vous empêcher de recevoir des notifications par e-mail ou sur votre calendrier concernant cette réservation.",
"resolve": "Résoudre",
"no_organization_slug": "Une erreur s'est produite lors de la création des équipes pour cette organisation. Le slug du lien est manquant.",
"org_name": "Nom de l'organisation",
"org_url": "Lien de l'organisation",
"copy_link_org": "Copier le lien vers lorganisation",
"404_the_org": "L'organisation",
"404_the_team": "L'équipe",
"404_claim_entity_org": "Réservez votre sous-domaine pour votre organisation",
"404_claim_entity_team": "Revendiquez cette équipe et commencez à gérer vos plannings collectivement",
"insights_all_org_filter": "Tous",
"insights_team_filter": "Équipe : {{teamName}}",
"insights_user_filter": "Utilisateur : {{userName}}",
"insights_subtitle": "Visualisez les statistiques de réservation à travers vos événements",
@ -2002,7 +1993,6 @@
"select_date": "Sélectionner une date",
"see_all_available_times": "Voir tous les créneaux disponibles",
"org_team_names_example": "p. ex. Équipe marketing",
"org_team_names_example_1": "p. ex. Équipe marketing",
"org_team_names_example_2": "p. ex. Équipe de vente",
"org_team_names_example_3": "p. ex. Équipe de design",
"org_team_names_example_4": "p. ex. Équipe d'ingénierie",
@ -2085,7 +2075,9 @@
"view_overlay_calendar_events": "Consultez les événements de votre calendrier afin d'éviter les réservations incompatibles.",
"lock_timezone_toggle_on_booking_page": "Verrouiller le fuseau horaire sur la page de réservation",
"description_lock_timezone_toggle_on_booking_page": "Pour verrouiller le fuseau horaire sur la page de réservation, utile pour les événements en personne.",
"number_in_international_format": "Veuillez entrer le numéro au format international.",
"extensive_whitelabeling": "Marque blanche étendue",
"unlimited_teams": "Équipes illimitées",
"need_help": "Besoin d'aide ?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "יש למלא את שדה \"username\" בשמות המשתמשים של החברים שהוקצו",
"assign_to": "הקצאה ל",
"add_members": "הוספת חברים...",
"count_members_one": "חבר {{count}}",
"count_members_other": "{{count}} חברים",
"no_assigned_members": "לא הוקצה אף חבר",
"assigned_to": "הוקצה ל",
"start_assigning_members_above": "התחל/י להקצות חברים למעלה",
@ -849,7 +847,6 @@
"next_step": "לדלג על שלב זה",
"prev_step": "לשלב הקודם",
"install": "התקנה",
"install_paid_app": "הרשמה למינוי",
"installed": "מותקן",
"active_install_one": "התקנה פעילה {{count}}",
"active_install_other": "{{count}} התקנות פעילות",
@ -1218,9 +1215,6 @@
"attendee_name_variable": "שם המשתתף",
"event_date_variable": "תאריך האירוע",
"event_time_variable": "מועד האירוע",
"timezone_variable": "אזור זמן",
"location_variable": "מיקום",
"additional_notes_variable": "הערות נוספות",
"organizer_name_variable": "שם המארגן/ת",
"app_upgrade_description": "כדי להשתמש בתכונה זו, עליך לשדרג לחשבון Pro.",
"invalid_number": "מספר טלפון לא תקין",
@ -1986,14 +1980,11 @@
"google_new_spam_policy": "מדיניות הספאם החדשה של Google עלולה לגרום לכך שלא תקבל/י עדכונים בדוא״ל ובלוח השנה לגבי הפגישה הזו.",
"resolve": "פתרון",
"no_organization_slug": "אירעה שגיאה במהלך הניסיון ליצור צוותים עבור הארגון הזה. חסר רכיב slug של URL.",
"org_name": "שם הארגון",
"org_url": "כתובת ה-URL של הארגון",
"copy_link_org": "העתקת הקישור לארגון",
"404_the_org": "הארגון",
"404_the_team": "הצוות",
"404_claim_entity_org": "קבל/י את תת-הדומיין עבור הארגון שלך",
"404_claim_entity_team": "הצטרף/י לצוות הזה והתחל/התחילי לנהל קביעת מועדים ביחד",
"insights_all_org_filter": "כל האפליקציות",
"insights_team_filter": "צוות: {{teamName}}",
"insights_user_filter": "משתמש: {{userName}}",
"insights_subtitle": "הצגת insights לגבי ההזמנות באירועים שלך",
@ -2005,7 +1996,6 @@
"select_date": "בחירת תאריך",
"see_all_available_times": "לצפייה בכל המועדים הפנויים",
"org_team_names_example": "לדוגמה, מחלקת שיווק",
"org_team_names_example_1": "לדוגמה, מחלקת שיווק",
"org_team_names_example_2": "לדוגמה, מחלקת מכירות",
"org_team_names_example_3": "לדוגמה, מחלקת עיצוב",
"org_team_names_example_4": "לדוגמה, מחלקת הנדסה",
@ -2101,5 +2091,6 @@
"lock_timezone_toggle_on_booking_page": "נעילת אזור הזמן בדף ההזמנות",
"description_lock_timezone_toggle_on_booking_page": "כדי לנעול את אזור הזמן בדף ההזמנות שימושי לאירועים אישיים.",
"extensive_whitelabeling": "תהליך הטמעה והנדסת תמיכה אישי",
"need_help": "צריך עזרה?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -333,6 +333,5 @@
"dark": "Tamna",
"automatically_adjust_theme": "Automatski prilagodite temu na temelju preferencija pozvanih osoba",
"user_dynamic_booking_disabled": "Neki od korisnika u grupi trenutno su onemogućili dinamičke grupne rezervacije",
"full_name": "Puno ime",
"insights_all_org_filter": "Sve"
"full_name": "Puno ime"
}

View File

@ -205,9 +205,5 @@
"minute_timeUnit": "Perc",
"remove_app": "Alkalmazás eltávolítása",
"yes_remove_app": "Igen, távolítsd el az alkalmazást",
"web_conference": "Online konferencia",
"timezone_variable": "Időzóna",
"location_variable": "Helyszín",
"additional_notes_variable": "Egyéb jegyzetek",
"insights_all_org_filter": "Minden alkalmazás"
"web_conference": "Online konferencia"
}

View File

@ -125,5 +125,6 @@
"logged_out": "Telah keluar",
"please_try_again_and_contact_us": "Silakan coba lagi dan hubungi kami jika masalah terulang.",
"incorrect_2fa_code": "Kode dua faktor salah.",
"no_account_exists": "Tidak ada akun dengan email tersebut."
"no_account_exists": "Tidak ada akun dengan email tersebut.",
"need_help": "Butuh bantuan?"
}

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "il campo \"username\" può essere compilato solo con il nome utente dei membri assegnati",
"assign_to": "Assegna a",
"add_members": "Aggiungi membri...",
"count_members_one": "{{count}} membro",
"count_members_other": "{{count}} membri",
"no_assigned_members": "Nessun membro assegnato",
"assigned_to": "Assegnato a",
"start_assigning_members_above": "Inizia ad assegnare membri in alto",
@ -849,7 +847,6 @@
"next_step": "Salta passo",
"prev_step": "Passo precedente",
"install": "Installa",
"install_paid_app": "Abbonati",
"installed": "Installato",
"active_install_one": "{{count}} installazione attiva",
"active_install_other": "{{count}} installazioni attive",
@ -1218,9 +1215,6 @@
"attendee_name_variable": "Partecipante",
"event_date_variable": "Data evento",
"event_time_variable": "Ora evento",
"timezone_variable": "Timezone",
"location_variable": "Luogo",
"additional_notes_variable": "Note aggiuntive",
"organizer_name_variable": "Nome organizzatore",
"app_upgrade_description": "Per poter utilizzare questa funzionalità, è necessario passare a un account Pro.",
"invalid_number": "Numero di telefono non valido",
@ -1986,14 +1980,11 @@
"google_new_spam_policy": "Le nuove norme sullo spam di Google potrebbero impedirti di ricevere email e notifiche di calendario in merito a questa prenotazione.",
"resolve": "Risolvi",
"no_organization_slug": "Si è verificato un errore durante la creazione dei team per questa organizzazione. Slug URL mancante.",
"org_name": "Nome dell'organizzazione",
"org_url": "URL dell'organizzazione",
"copy_link_org": "Copia link all'organizzazione",
"404_the_org": "L'organizzazione",
"404_the_team": "Il team",
"404_claim_entity_org": "Richiedi il sottodominio per la tua organizzazione",
"404_claim_entity_team": "Richiedi l'accesso a questo team per iniziare a gestire le pianificazioni collettivamente",
"insights_all_org_filter": "Tutte le app",
"insights_team_filter": "Team: {{teamName}}",
"insights_user_filter": "Utente: {{userName}}",
"insights_subtitle": "Visualizza insight sulle prenotazioni per tutti i tuoi eventi",
@ -2005,7 +1996,6 @@
"select_date": "Seleziona la data",
"see_all_available_times": "Vedi tutti gli orari disponibili",
"org_team_names_example": "ad es., team di marketing",
"org_team_names_example_1": "ad es., team di marketing",
"org_team_names_example_2": "ad es., team vendite",
"org_team_names_example_3": "ad es., team di progettazione",
"org_team_names_example_4": "ad es., team di ingegneria",
@ -2101,5 +2091,6 @@
"lock_timezone_toggle_on_booking_page": "Blocca fuso orario nella pagina di prenotazione",
"description_lock_timezone_toggle_on_booking_page": "Per bloccare il fuso orario nella pagina di prenotazione, utile per gli eventi di persona.",
"extensive_whitelabeling": "Assistenza per l'onboarding e supporto tecnico dedicati",
"need_help": "Hai bisogno di aiuto?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Aggiungi le tue nuove stringhe qui sopra ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "\"ユーザー名\" には割り当てられたメンバーのユーザー名が入ります",
"assign_to": "割り当て先",
"add_members": "メンバーを追加…",
"count_members_one": "{{count}} 人のメンバー",
"count_members_other": "{{count}} 人のメンバー",
"no_assigned_members": "割り当てられたメンバーはいません",
"assigned_to": "割り当て先",
"start_assigning_members_above": "上記のメンバーに対する割り当てを開始する",
@ -849,7 +847,6 @@
"next_step": "手順をスキップ",
"prev_step": "前の手順",
"install": "インストール",
"install_paid_app": "サブスクライブ",
"installed": "インストール済み",
"active_install_one": "{{count}} 件のアクティブなインストール",
"active_install_other": "{{count}} 件のアクティブなインストール",
@ -1218,9 +1215,6 @@
"attendee_name_variable": "出席者の名前",
"event_date_variable": "イベントの日付",
"event_time_variable": "イベントの時間",
"timezone_variable": "タイムゾーン",
"location_variable": "場所",
"additional_notes_variable": "備考",
"organizer_name_variable": "主催者名",
"app_upgrade_description": "この機能を利用するには、Pro アカウントへのアップグレードが必要です。",
"invalid_number": "電話番号が無効です",
@ -1986,14 +1980,11 @@
"google_new_spam_policy": "Google の新しい迷惑メールポリシーにより、この予約に関するメールやカレンダー通知を受け取れなくなる可能性があります。",
"resolve": "解決する",
"no_organization_slug": "この組織でチームを作成する際にエラーが発生しました。URL スラッグがありません。",
"org_name": "組織名",
"org_url": "組織の URL",
"copy_link_org": "組織へのリンクをコピーする",
"404_the_org": "組織",
"404_the_team": "チーム",
"404_claim_entity_org": "組織のサブドメインを取得する",
"404_claim_entity_team": "このチームの一員になって、今後はスケジュールをまとめて管理しましょう",
"insights_all_org_filter": "すべて",
"insights_team_filter": "チーム: {{teamName}}",
"insights_user_filter": "ユーザー: {{userName}}",
"insights_subtitle": "イベント全体での予約に関する Insights を表示する",
@ -2005,7 +1996,6 @@
"select_date": "日付を選ぶ",
"see_all_available_times": "出席できる時間帯をすべて表示",
"org_team_names_example": "例:マーケティングチーム",
"org_team_names_example_1": "例:マーケティングチーム",
"org_team_names_example_2": "例:営業チーム",
"org_team_names_example_3": "例:デザインチーム",
"org_team_names_example_4": "例:エンジニアリングチーム",
@ -2101,5 +2091,6 @@
"lock_timezone_toggle_on_booking_page": "予約ページのタイムゾーンを固定する",
"description_lock_timezone_toggle_on_booking_page": "予約ページのタイムゾーンを固定するためのもので、対面のイベントに役立ちます。",
"extensive_whitelabeling": "専用のオンボーディングサポートとエンジニアリングサポート",
"need_help": "サポートが必要ですか?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "\"username\"은 할당된 회원의 사용자 이름으로 채워집니다",
"assign_to": "할당 대상",
"add_members": "회원 추가...",
"count_members_one": "회원 {{count}}명",
"count_members_other": "회원 {{count}}명",
"no_assigned_members": "할당된 회원 없음",
"assigned_to": "할당된 이벤트 유형",
"start_assigning_members_above": "위에서 회원 할당 시작",
@ -849,7 +847,6 @@
"next_step": "건너뛰기",
"prev_step": "이전 단계",
"install": "설치",
"install_paid_app": "구독",
"installed": "설치됨",
"active_install_one": "{{count}}개 활성 설치",
"active_install_other": "{{count}}개 활성 설치",
@ -1218,9 +1215,6 @@
"attendee_name_variable": "참석자",
"event_date_variable": "이벤트 날짜",
"event_time_variable": "이벤트 시간",
"timezone_variable": "시간대",
"location_variable": "위치",
"additional_notes_variable": "추가 참고 사항",
"organizer_name_variable": "주최자 이름",
"app_upgrade_description": "이 기능을 사용하려면 Pro 계정으로 업그레이드해야 합니다.",
"invalid_number": "유효하지 않은 전화 번호",
@ -1986,14 +1980,11 @@
"google_new_spam_policy": "Google의 새로운 스팸 정책으로 인해 이 예약에 대한 이메일 및 캘린더 알림을 받지 못할 수 있습니다.",
"resolve": "해결",
"no_organization_slug": "이 조직의 팀을 만드는 중 오류가 발생했습니다. URL 슬러그가 없습니다.",
"org_name": "조직 이름",
"org_url": "조직 URL",
"copy_link_org": "조직 링크 복사",
"404_the_org": "조직",
"404_the_team": "팀",
"404_claim_entity_org": "조직에 대한 하위 도메인 요청",
"404_claim_entity_team": "이 팀을 요청하고 일괄적으로 일정 관리 시작",
"insights_all_org_filter": "모두",
"insights_team_filter": "팀: {{teamName}}",
"insights_user_filter": "사용자: {{userName}}",
"insights_subtitle": "이벤트 전반에 걸친 예약 Insights 보기",
@ -2005,7 +1996,6 @@
"select_date": "날짜 선택",
"see_all_available_times": "모든 사용 가능한 시간 보기",
"org_team_names_example": "예: 마케팅 팀",
"org_team_names_example_1": "예: 마케팅 팀",
"org_team_names_example_2": "예: 세일즈 팀",
"org_team_names_example_3": "예: 디자인 팀",
"org_team_names_example_4": "예: 엔지니어링 팀",
@ -2101,5 +2091,6 @@
"lock_timezone_toggle_on_booking_page": "예약 페이지의 시간대 잠금",
"description_lock_timezone_toggle_on_booking_page": "예약 페이지에서 시간대를 잠그는 기능은 대면 이벤트에 유용합니다.",
"extensive_whitelabeling": "전담 온보딩 및 엔지니어링 지원",
"need_help": "도움이 필요하세요?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 여기에 새 문자열을 추가하세요 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "\"username\" wordt gevuld met de gebruikersnaam van de toegewezen leden",
"assign_to": "Toewijzen aan",
"add_members": "Leden toevoegen...",
"count_members_one": "{{count}} lid",
"count_members_other": "{{count}} leden",
"no_assigned_members": "Geen toegewezen leden",
"assigned_to": "Toegewezen aan",
"start_assigning_members_above": "Begin met het toewijzen van leden hierboven",
@ -849,7 +847,6 @@
"next_step": "Stap overslaan",
"prev_step": "Vorige stap",
"install": "Installeren",
"install_paid_app": "Abonneren",
"installed": "Geinstalleerd",
"active_install_one": "{{count}} actieve installatie",
"active_install_other": "{{count}} actieve installaties",
@ -1218,9 +1215,6 @@
"attendee_name_variable": "Deelnemer",
"event_date_variable": "Datum gebeurtenis",
"event_time_variable": "Tijd gebeurtenis",
"timezone_variable": "Tijdzone",
"location_variable": "Locatie",
"additional_notes_variable": "Aanvullende opmerkingen",
"organizer_name_variable": "Naam organisator",
"app_upgrade_description": "Om deze functie te gebruiken, moet u upgraden naar een Pro-account.",
"invalid_number": "Ongeldig telefoonnummer",
@ -1986,14 +1980,11 @@
"google_new_spam_policy": "Het nieuwe spambeleid van Google kan ervoor zorgen dat u geen e-mail- en agendameldingen over deze boeking ontvangt.",
"resolve": "Oplossen",
"no_organization_slug": "Er is een fout opgetreden bij het maken van teams voor deze organisatie. Ontbrekende URL-slug.",
"org_name": "Organisatienaam",
"org_url": "Organisatie-URL",
"copy_link_org": "Link kopiëren naar organisatie",
"404_the_org": "De organisatie",
"404_the_team": "Het team",
"404_claim_entity_org": "Claim uw subdomein voor uw organisatie",
"404_claim_entity_team": "Claim dit team en begin met het gezamenlijk beheren van planningen",
"insights_all_org_filter": "Alle apps",
"insights_team_filter": "Team: {{teamName}}",
"insights_user_filter": "Gebruiker: {{userName}}",
"insights_subtitle": "Bekijk boekingsinsights voor al uw gebeurtenissen",
@ -2005,7 +1996,6 @@
"select_date": "Datum selecteren",
"see_all_available_times": "Bekijk alle beschikbare tijden",
"org_team_names_example": "bijvoorbeeld Marketingteam",
"org_team_names_example_1": "bijvoorbeeld Marketingteam",
"org_team_names_example_2": "bijvoorbeeld Verkoopteam",
"org_team_names_example_3": "bijvoorbeeld Ontwerpteam",
"org_team_names_example_4": "bijvoorbeeld Engineeringteam",
@ -2101,5 +2091,6 @@
"lock_timezone_toggle_on_booking_page": "Tijdzone vergrendelen op boekingspagina",
"description_lock_timezone_toggle_on_booking_page": "Om de tijdzone op de boekingspagina te vergrendelen, handig voor persoonlijke gebeurtenissen.",
"extensive_whitelabeling": "Speciale onboarding en technische ondersteuning",
"need_help": "Hulp nodig?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Voeg uw nieuwe strings hierboven toe ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -706,7 +706,6 @@
"next_step": "Hopp over trinn",
"prev_step": "Forrige trinn",
"install": "Installer",
"install_paid_app": "Abonner",
"installed": "Installert",
"active_install_one": "{{count}} aktiv installasjon",
"active_install_other": "{{count}} aktive installasjoner",
@ -1035,9 +1034,6 @@
"attendee_name_variable": "Deltaker",
"event_date_variable": "Hendelsesdato",
"event_time_variable": "Tidspunkt for hendelse",
"timezone_variable": "Tidssone",
"location_variable": "Sted",
"additional_notes_variable": "Tilleggsinformasjon",
"app_upgrade_description": "For å bruke denne funksjonen må du oppgradere til en Pro-konto.",
"invalid_number": "Ugyldig telefonnummer",
"navigate": "Naviger",
@ -1391,5 +1387,5 @@
"configure": "Konfigurer",
"sso_configuration": "Enkel Pålogging",
"booking_confirmation_failed": "Booking-bekreftelse feilet",
"insights_all_org_filter": "Alt"
"need_help": "Trenger du hjelp?"
}

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "Pole „username” zostanie wypełnione nazwą przypisanego użytkownika",
"assign_to": "Przypisz do",
"add_members": "Dodaj członków...",
"count_members_one": "{{count}} członek",
"count_members_other": "{{count}} członków",
"no_assigned_members": "Brak przypisanych członków",
"assigned_to": "Przypisano do:",
"start_assigning_members_above": "Rozpocznij przypisywanie członków powyżej",
@ -849,7 +847,6 @@
"next_step": "Pomiń krok",
"prev_step": "Poprzedni krok",
"install": "Zainstaluj",
"install_paid_app": "Subskrybuj",
"installed": "Zainstalowane",
"active_install_one": "Aktywne instalacje: {{count}}",
"active_install_other": "Aktywne instalacje: {{count}}",
@ -1218,9 +1215,6 @@
"attendee_name_variable": "Uczestnik",
"event_date_variable": "Data wydarzenia",
"event_time_variable": "Godzina wydarzenia",
"timezone_variable": "Strefa Czasowa",
"location_variable": "Lokalizacja",
"additional_notes_variable": "Dodatkowe uwagi",
"organizer_name_variable": "Nazwa organizatora",
"app_upgrade_description": "Aby korzystać z tej funkcji, musisz uaktualnić do konta Pro.",
"invalid_number": "Nieprawidłowy numer telefonu",
@ -1986,14 +1980,11 @@
"google_new_spam_policy": "Nowa polityka firmy Google dotycząca spamu może uniemożliwić Ci otrzymywanie powiadomień na temat tej rezerwacji w wiadomościach e-mail i z kalendarza.",
"resolve": "Rozwiąż problem",
"no_organization_slug": "Podczas tworzenia zespołów tej organizacji wystąpił błąd. Brakujący slug adresu URL.",
"org_name": "Nazwa organizacji",
"org_url": "Adres URL organizacji",
"copy_link_org": "Skopiuj link do organizacji",
"404_the_org": "Organizacja",
"404_the_team": "Zespół",
"404_claim_entity_org": "Zajmij subdomenę dla Twojej organizacji",
"404_claim_entity_team": "Dołącz do tego zespołu i zacznij wspólnie zarządzać harmonogramami",
"insights_all_org_filter": "Wszystkie",
"insights_team_filter": "Zespół: {{teamName}}",
"insights_user_filter": "Użytkownik: {{userName}}",
"insights_subtitle": "Wyświetl wskaźniki Insights rezerwacji z różnych wydarzeń",
@ -2005,7 +1996,6 @@
"select_date": "Wybierz datę",
"see_all_available_times": "Zobacz wszystkie dostępne godziny",
"org_team_names_example": "np. zespół marketingowy",
"org_team_names_example_1": "np. zespół marketingowy",
"org_team_names_example_2": "np. zespół ds. sprzedaży",
"org_team_names_example_3": "np. zespół projektowy",
"org_team_names_example_4": "np. zespół ds. inżynierii",
@ -2101,5 +2091,6 @@
"lock_timezone_toggle_on_booking_page": "Zablokuj strefę czasową na stronie rezerwacji",
"description_lock_timezone_toggle_on_booking_page": "Aby zablokować strefę czasową na stronie rezerwacji (przydatne dla wydarzeń stacjonarnych).",
"extensive_whitelabeling": "Dedykowane wsparcie w zakresie wdrożenia i obsługi technicznej",
"need_help": "Potrzebujesz pomocy?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodaj nowe ciągi powyżej ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "o \"nome de usuário\" será preenchido com o nome de usuário dos membros atribuídos",
"assign_to": "Atribuir a",
"add_members": "Adicionar membros...",
"count_members_one": "{{count}} membro",
"count_members_other": "{{count}} membros",
"no_assigned_members": "Nenhum membro atribuído",
"assigned_to": "Atribuído a",
"start_assigning_members_above": "Começar a atribuir os membros acima",
@ -849,7 +847,6 @@
"next_step": "Ignorar passo",
"prev_step": "Passo anterior",
"install": "Instalar",
"install_paid_app": "Assinar",
"installed": "Instalado",
"active_install_one": "{{count}} instalação ativa",
"active_install_other": "{{count}} instalações ativas",
@ -1218,9 +1215,6 @@
"attendee_name_variable": "Nome do participante",
"event_date_variable": "Data do evento",
"event_time_variable": "Horário do evento",
"timezone_variable": "Fuso Horário",
"location_variable": "Local",
"additional_notes_variable": "Observações adicionais",
"organizer_name_variable": "Nome do organizador",
"app_upgrade_description": "Para usar este recurso, atualize para uma conta Pro.",
"invalid_number": "Número de telefone inválido",
@ -1986,14 +1980,11 @@
"google_new_spam_policy": "A nova política de spams do Google pode impedir o recebimento de notificações do calendário e e-mail sobre esta reserva.",
"resolve": "Resolver",
"no_organization_slug": "Houve um erro ao criar equipes para esta organizazção. Falta o campo de dados dinâmico do URL.",
"org_name": "Nome da organização",
"org_url": "URL da organização",
"copy_link_org": "Copiar link para a organização",
"404_the_org": "A organização",
"404_the_team": "A equipe",
"404_claim_entity_org": "Solicite o subdomínio para a sua organização",
"404_claim_entity_team": "Solicite esta equipe e comece a gerenciar agendamentos coletivamente",
"insights_all_org_filter": "Todos os apps",
"insights_team_filter": "Equipe: {{teamName}}",
"insights_user_filter": "Usuário: {{userName}}",
"insights_subtitle": "Veja insights de reserva em seus eventos",
@ -2005,7 +1996,6 @@
"select_date": "Selecione a data",
"see_all_available_times": "Veja todos os horários disponíveis",
"org_team_names_example": "ex.: Time de Marketing",
"org_team_names_example_1": "ex.: Time de Marketing",
"org_team_names_example_2": "ex: Time de Vendas",
"org_team_names_example_3": "ex: Time de Design",
"org_team_names_example_4": "ex: Time de Engenharia",
@ -2101,5 +2091,6 @@
"lock_timezone_toggle_on_booking_page": "Bloquear fuso horário na página de reserva",
"description_lock_timezone_toggle_on_booking_page": "Bloquear o fuso horário na página de reservas, útil para eventos presenciais.",
"extensive_whitelabeling": "Marca própria abrangente",
"need_help": "Precisa de ajuda?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adicione suas novas strings aqui em cima ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "\"nome-de-utilizador\" será substituído pelo nome de utilizador dos membros atribuídos",
"assign_to": "Atribuir a",
"add_members": "Adicionar membros...",
"count_members_one": "{{count}} membro",
"count_members_other": "{{count}} membros",
"no_assigned_members": "Sem membros atribuídos",
"assigned_to": "Atribuído a",
"start_assigning_members_above": "Comece a atribuir membros acima",
@ -849,7 +847,6 @@
"next_step": "Ignorar passo",
"prev_step": "Passo anterior",
"install": "Instalar",
"install_paid_app": "Subscrever",
"installed": "Instalado",
"active_install_one": "{{count}} instalação activa",
"active_install_other": "{{count}} instalações activas",
@ -1218,9 +1215,6 @@
"attendee_name_variable": "Participante",
"event_date_variable": "Data do evento",
"event_time_variable": "Hora do evento",
"timezone_variable": "Fuso Horário",
"location_variable": "Localização",
"additional_notes_variable": "Notas Adicionais",
"organizer_name_variable": "Nome do organizador",
"app_upgrade_description": "Para usar esta funcionalidade, tem de actualizar para uma conta Pro.",
"invalid_number": "Número de telefone inválido",
@ -1986,14 +1980,11 @@
"google_new_spam_policy": "A nova política de SPAM do Google pode impedir que receba notificações de e-mail e de agenda sobre esta reserva.",
"resolve": "Resolver",
"no_organization_slug": "Ocorreu um erro ao criar equipas para esta organização. Falta o slug do URL.",
"org_name": "Nome da organização",
"org_url": "URL da Organização",
"copy_link_org": "Copiar a ligação para a organização",
"404_the_org": "A organização",
"404_the_team": "A equipa",
"404_claim_entity_org": "Reivindicar o seu subdomínio para a sua organização",
"404_claim_entity_team": "Reivindicar esta equipa e começar a gerir os horários em grupo",
"insights_all_org_filter": "Todos as aplicações",
"insights_team_filter": "Equipa: {{teamName}}",
"insights_user_filter": "Utilizador: {{userName}}",
"insights_subtitle": "Ver informações sobre reservas nos seus eventos",
@ -2005,7 +1996,6 @@
"select_date": "Selecionar data",
"see_all_available_times": "Ver todos os horários disponíveis",
"org_team_names_example": "Por exemplo, Equipa de Marketing",
"org_team_names_example_1": "Por exemplo, Equipa de Marketing",
"org_team_names_example_2": "Por exemplo, Equipa de Vendas",
"org_team_names_example_3": "Por exemplo, Equipa de Design",
"org_team_names_example_4": "Por exemplo, Equipa de Engenharia",
@ -2101,5 +2091,6 @@
"lock_timezone_toggle_on_booking_page": "Bloquear fuso horário na página de reserva",
"description_lock_timezone_toggle_on_booking_page": "Para bloquear o fuso horário na página de reservas. Útil para eventos presenciais.",
"extensive_whitelabeling": "Apoio dedicado à integração e engenharia",
"need_help": "Precisa de ajuda?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "Câmpul „username” va fi completat cu numele de utilizator ale membrilor desemnați",
"assign_to": "Alocați la",
"add_members": "Adăugare membri...",
"count_members_one": "{{count}} membru",
"count_members_other": "{{count}} (de) membri",
"no_assigned_members": "Niciun membru alocat",
"assigned_to": "Alocat la",
"start_assigning_members_above": "Începeți să alocați membri mai sus",
@ -849,7 +847,6 @@
"next_step": "Sari peste",
"prev_step": "Pas anterior",
"install": "Instalați",
"install_paid_app": "Abonare",
"installed": "Instalat",
"active_install_one": "{{count}} instalare activă",
"active_install_other": "{{count}} (de) instalări active",
@ -1218,9 +1215,6 @@
"attendee_name_variable": "Participant",
"event_date_variable": "Dată eveniment",
"event_time_variable": "Oră eveniment",
"timezone_variable": "Timezone",
"location_variable": "Loc",
"additional_notes_variable": "Note suplimentare",
"organizer_name_variable": "Nume organizator",
"app_upgrade_description": "Pentru a utiliza această caracteristică, trebuie să faceți upgrade la un cont Pro.",
"invalid_number": "Număr de telefon nevalid",
@ -1986,14 +1980,11 @@
"google_new_spam_policy": "Noua politică Google privind spamul vă poate împiedica să primiți orice notificare prin e-mail sau calendar cu privire la această rezervare.",
"resolve": "Rezolvați",
"no_organization_slug": "A survenit o eroare la crearea echipelor pentru această organizație. Slug URL lipsă.",
"org_name": "Denumire organizație",
"org_url": "Adresă URL organizație",
"copy_link_org": "Copiați linkul către organizație",
"404_the_org": "Organizația",
"404_the_team": "Echipa",
"404_claim_entity_org": "Revendicați subdomeniul pentru organizația dvs.",
"404_claim_entity_team": "Solicitați să faceți parte din această echipă și începeți să gestionați programe în mod colectiv",
"insights_all_org_filter": "Toate aplicațiile",
"insights_team_filter": "Echipă: {{teamName}}",
"insights_user_filter": "Utilizator: {{userName}}",
"insights_subtitle": "Vizualizați date Insights cu privire la rezervări pentru toate evenimentele dvs.",
@ -2005,7 +1996,6 @@
"select_date": "Selectați data",
"see_all_available_times": "Vedeți toate orele disponibile",
"org_team_names_example": "de ex. echipa de marketing",
"org_team_names_example_1": "de ex. echipa de marketing",
"org_team_names_example_2": "de ex. echipa de vânzări",
"org_team_names_example_3": "de ex. echipa de design",
"org_team_names_example_4": "de ex. echipa de inginerie",
@ -2101,5 +2091,6 @@
"lock_timezone_toggle_on_booking_page": "Blocare fus orar pe pagina de rezervare",
"description_lock_timezone_toggle_on_booking_page": "Pentru a bloca fusul orar pe pagina de rezervare, util pentru evenimentele în persoană.",
"extensive_whitelabeling": "Asistență dedicată pentru integrare și inginerie",
"need_help": "Aveți nevoie de ajutor?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adăugați stringurile noi deasupra acestui rând ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "в «username» указываются имена пользователей, которым будет назначен тип события",
"assign_to": "Назначить участнику",
"add_members": "Добавить участников...",
"count_members_one": "Участники: {{count}}",
"count_members_other": "Участники: {{count}}",
"no_assigned_members": "Нет назначенных участников",
"assigned_to": "Назначено участнику",
"start_assigning_members_above": "Начните назначать участников",
@ -849,7 +847,6 @@
"next_step": "Пропустить шаг",
"prev_step": "Предыдущий шаг",
"install": "Установить",
"install_paid_app": "Подписаться",
"installed": "Установлено",
"active_install_one": "Активные установки: {{count}}",
"active_install_other": "Активные установки: {{count}}",
@ -1218,9 +1215,6 @@
"attendee_name_variable": "Участник",
"event_date_variable": "Дата события",
"event_time_variable": "Время события",
"timezone_variable": "Часовой пояс",
"location_variable": "Местоположение",
"additional_notes_variable": "Дополнительная информация",
"organizer_name_variable": "Имя организатора",
"app_upgrade_description": "Чтобы использовать эту функцию, необходимо перейти на аккаунт Pro.",
"invalid_number": "Неверный номер телефона",
@ -1986,14 +1980,11 @@
"google_new_spam_policy": "В связи с новой политикой Google в отношении спама существует вероятность, что вы не сможете увидеть уведомления об этом забронированном событии по электронной почте или в календаре.",
"resolve": "Как решить эту проблему",
"no_organization_slug": "При создании команд для этой организации произошла ошибка. Отсутствует слаг URL-адреса.",
"org_name": "Название организации",
"org_url": "URL-адрес организации",
"copy_link_org": "Копировать ссылку на организацию",
"404_the_org": "Организация",
"404_the_team": "Команда",
"404_claim_entity_org": "Получите поддомен для вашей организации",
"404_claim_entity_team": "Присоединитесь к команде и начните составлять расписания вместе",
"insights_all_org_filter": "Все приложения",
"insights_team_filter": "Команда: {{teamName}}",
"insights_user_filter": "Пользователь: {{userName}}",
"insights_subtitle": "Insights: просматривайте информацию о бронировании по всем вашим событиям",
@ -2005,7 +1996,6 @@
"select_date": "Выбрать дату",
"see_all_available_times": "Посмотреть все доступные интервалы времени",
"org_team_names_example": "например, команда по маркетингу",
"org_team_names_example_1": "например, команда по маркетингу",
"org_team_names_example_2": "например, Отдел продаж",
"org_team_names_example_3": "например, отдел дизайна",
"org_team_names_example_4": "например, технический отдел",
@ -2101,5 +2091,6 @@
"lock_timezone_toggle_on_booking_page": "Заблокируйте часовой пояс на странице бронирования",
"description_lock_timezone_toggle_on_booking_page": "Блокировка часового пояса на странице бронирования подходит для личных событий.",
"extensive_whitelabeling": "Индивидуальная техподдержка и помощь при онбординге",
"need_help": "Нужна помощь?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Добавьте строки выше ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "„username“ će biti ispunjeno korisničkim imenom dodeljenog člana",
"assign_to": "Dodelite",
"add_members": "Dodajte članove...",
"count_members_one": "{{count}} član",
"count_members_other": "{{count}} članova",
"no_assigned_members": "Nema dodeljenih članova",
"assigned_to": "Dodeljeno",
"start_assigning_members_above": "Počnite da dodeljujete članove iznad",
@ -849,7 +847,6 @@
"next_step": "Preskoči korak",
"prev_step": "Prethodni korak",
"install": "Instaliraj",
"install_paid_app": "Prijavite se",
"installed": "Instalirano",
"active_install_one": "{{count}} aktivna instalacija",
"active_install_other": "{{count}} aktivnih instalacija",
@ -1218,9 +1215,6 @@
"attendee_name_variable": "Učesnik",
"event_date_variable": "Datum događaja",
"event_time_variable": "Vreme događaja",
"timezone_variable": "Vremenska zona",
"location_variable": "Lokacija",
"additional_notes_variable": "Dodatne beleške",
"organizer_name_variable": "Ime organizatora",
"app_upgrade_description": "Da biste koristili ovu funkciju, morate izvršiti nadogradnju na Pro nalog.",
"invalid_number": "Neispravan broj telefona",
@ -1986,14 +1980,11 @@
"google_new_spam_policy": "Google-ova nova politika neželjene pošte može da spreči da primate bilo koje imejlove ili obaveštenja kalendara u vezi sa ovom rezervacijom.",
"resolve": "Kako rešiti taj problem",
"no_organization_slug": "Desila se greška kod kreiranja timova za ovu organizaciju. Nedostaje URL slug.",
"org_name": "Naziv organizacije",
"org_url": "URL organizacije",
"copy_link_org": "Kopiraj link do organizacije",
"404_the_org": "Organizacija",
"404_the_team": "Tim",
"404_claim_entity_org": "Zatražite poddomen za svoju organizaciju",
"404_claim_entity_team": "Postanite deo ovog tima i počnite da uređujete kolektivni raspored",
"insights_all_org_filter": "Sve aplikacije",
"insights_team_filter": "Tim: {{teamName}}",
"insights_user_filter": "Korisnik: {{userName}}",
"insights_subtitle": "Pogledajte insights rezervacije za vaše događaje",
@ -2005,7 +1996,6 @@
"select_date": "Izaberite datum",
"see_all_available_times": "Pogledajte sva dostupna vremena",
"org_team_names_example": "npr. Marketing tim",
"org_team_names_example_1": "npr. Marketing tim",
"org_team_names_example_2": "npr. Prodajni tim",
"org_team_names_example_3": "npr. Dizajnerski tim",
"org_team_names_example_4": "npr. Inženjerski tim",
@ -2101,5 +2091,6 @@
"lock_timezone_toggle_on_booking_page": "Zaključajte vremensku zonu na stranici za zakazivanja",
"description_lock_timezone_toggle_on_booking_page": "Za zaključavanje vremenske zone na stranici za zakazivanja, korisno za događaje licem u lice.",
"extensive_whitelabeling": "Posvećena podrška za uvodnu obuku i inženjering",
"need_help": "Potrebna vam je pomoć?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodajte svoje nove stringove iznad ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "\"användarnamn\" fylls i med användarnamnet för de medlemmar som tilldelats",
"assign_to": "Tilldela till",
"add_members": "Lägg till medlemmar ...",
"count_members_one": "{{count}} medlem",
"count_members_other": "{{count}} medlemmar",
"no_assigned_members": "Inga tilldelade medlemmar",
"assigned_to": "Tilldelad till",
"start_assigning_members_above": "Börja tilldela medlemmar ovan",
@ -849,7 +847,6 @@
"next_step": "Hoppa över steg",
"prev_step": "Föregående steg",
"install": "Installera",
"install_paid_app": "Prenumerera",
"installed": "Installerad",
"active_install_one": "{{count}} aktiv installation",
"active_install_other": "{{count}} aktiva installationer",
@ -1218,9 +1215,6 @@
"attendee_name_variable": "Deltagare",
"event_date_variable": "Händelsedatum",
"event_time_variable": "Händelsetid",
"timezone_variable": "Tidszon",
"location_variable": "Plats",
"additional_notes_variable": "Ytterligare inmatning",
"organizer_name_variable": "Arrangörens namn",
"app_upgrade_description": "För att kunna använda den här funktionen måste du uppgradera till ett Pro-konto.",
"invalid_number": "Ogiltigt telefonnummer",
@ -1986,14 +1980,11 @@
"google_new_spam_policy": "Googles nya skräppostpolicy kan hindra dig från att få e-post och kalenderaviseringen om denna bokning.",
"resolve": "Lös",
"no_organization_slug": "Ett fel uppstod när team skapades för den här organisationen. URL-sluggen saknas.",
"org_name": "Organisationens namn",
"org_url": "Organisationens URL",
"copy_link_org": "Kopiera länk till organisation",
"404_the_org": "Organisationen",
"404_the_team": "Teamet",
"404_claim_entity_org": "Registrera underdomänen för din organisation",
"404_claim_entity_team": "Registrera det här teamet och börja hantera scheman tillsammans",
"insights_all_org_filter": "Alla",
"insights_team_filter": "Team: {{teamName}}",
"insights_user_filter": "Användare: {{userName}}",
"insights_subtitle": "Visa Insights-bokningar för dina händelser",
@ -2005,7 +1996,6 @@
"select_date": "Välj datum",
"see_all_available_times": "Se alla tillgängliga tider",
"org_team_names_example": "t.ex. marknadsföringsteam",
"org_team_names_example_1": "t.ex. marknadsföringsteam",
"org_team_names_example_2": "t.ex. säljteam",
"org_team_names_example_3": "t.ex. designteam",
"org_team_names_example_4": "t.ex. ingenjörsteam",
@ -2101,5 +2091,6 @@
"lock_timezone_toggle_on_booking_page": "Lås tidszon på bokningssidan",
"description_lock_timezone_toggle_on_booking_page": "För att låsa tidszonen på bokningssidan, användbart för personliga händelser.",
"extensive_whitelabeling": "Dedikerad registrering och teknisk support",
"need_help": "Behöver du hjälp?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "\"username\" alanı, atanan üyelerin kullanıcı adı ile doldurulur",
"assign_to": "Şuraya ata:",
"add_members": "Üye ekle...",
"count_members_one": "{{count}} üye",
"count_members_other": "{{count}} üye",
"no_assigned_members": "Atanan üye yok",
"assigned_to": "Şuraya atandı:",
"start_assigning_members_above": "Üyeleri en üstten itibaren atamaya başlayın",
@ -849,7 +847,6 @@
"next_step": "Adımı geç",
"prev_step": "Önceki adım",
"install": "Yükle",
"install_paid_app": "Abone Ol",
"installed": "Yüklendi",
"active_install_one": "{{count}} aktif yükleme",
"active_install_other": "{{count}} aktif yükleme",
@ -1218,9 +1215,6 @@
"attendee_name_variable": "Katılımcı",
"event_date_variable": "Etkinlik tarihi",
"event_time_variable": "Etkinlik saati",
"timezone_variable": "Saat dilimi",
"location_variable": "Konum",
"additional_notes_variable": "Ek notlar",
"organizer_name_variable": "Düzenleyenin adı",
"app_upgrade_description": "Bu özelliği kullanmak için Pro hesabına geçmeniz gerekiyor.",
"invalid_number": "Geçersiz telefon numarası",
@ -1986,14 +1980,11 @@
"google_new_spam_policy": "Google'ın yeni spam politikası nedeniyle bu rezervasyon için e-posta veya takvim bildirimleri alamayabilirsiniz.",
"resolve": "Çöz",
"no_organization_slug": "Bu kuruluş için ekipler oluşturulurken bir hata oluştu. URL bilgisi eksik.",
"org_name": "Kuruluş adı",
"org_url": "Kuruluş URL'si",
"copy_link_org": "Bağlantıyı kuruluşa kopyala",
"404_the_org": "Kuruluş",
"404_the_team": "Ekip",
"404_claim_entity_org": "Kuruluşunuz için alt alan adınızı talep edin",
"404_claim_entity_team": "Bu ekibi sahiplenin ve planlamaları toplu şekilde yönetmeye başlayın",
"insights_all_org_filter": "Tümü",
"insights_team_filter": "Ekip: {{teamName}}",
"insights_user_filter": "Kullanıcı: {{userName}}",
"insights_subtitle": "Etkinliklerinizdeki rezervasyon insights'ı görüntüleyin",
@ -2005,7 +1996,6 @@
"select_date": "Tarihi Seçin",
"see_all_available_times": "Tüm müsait saatleri görün",
"org_team_names_example": "Örneğin. Pazarlama ekibi",
"org_team_names_example_1": "Örneğin. Pazarlama ekibi",
"org_team_names_example_2": "Örn. Satış Ekibi",
"org_team_names_example_3": "Örn. Tasarım Ekibi",
"org_team_names_example_4": "Örn. Mühendislik Ekibi",
@ -2101,5 +2091,6 @@
"lock_timezone_toggle_on_booking_page": "Rezervasyon sayfasında saat dilimini kilitle",
"description_lock_timezone_toggle_on_booking_page": "Rezervasyon sayfasında saat dilimini kilitlemek kişisel etkinlikler için kullanışlıdır.",
"extensive_whitelabeling": "Özel işe alıştırma ve mühendislik desteği",
"need_help": "Yardıma mı ihtiyacınız var?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni dizelerinizi yukarıya ekleyin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "Поле «username» буде заповнено іменами користувачів призначених учасників",
"assign_to": "Кому призначити:",
"add_members": "Додати учасників…",
"count_members_one": "{{count}} учасник",
"count_members_other": "Учасників: {{count}}",
"no_assigned_members": "Немає призначених учасників",
"assigned_to": "Кому призначено:",
"start_assigning_members_above": "Почніть призначати учасників вище",
@ -849,7 +847,6 @@
"next_step": "Пропустити крок",
"prev_step": "Попередній крок",
"install": "Установити",
"install_paid_app": "Підписатися",
"installed": "Установлено",
"active_install_one": "{{count}} активна інсталяція",
"active_install_other": "Активних інсталяцій: {{count}}",
@ -1218,9 +1215,6 @@
"attendee_name_variable": "Учасник",
"event_date_variable": "Дата заходу",
"event_time_variable": "Час заходу",
"timezone_variable": "Часовий пояс",
"location_variable": "Розташування",
"additional_notes_variable": "Додаткові примітки",
"organizer_name_variable": "Ім’я організатора",
"app_upgrade_description": "Щоб користуватися цією функцією, потрібен обліковий запис Pro.",
"invalid_number": "Недійсний номер телефону",
@ -1986,14 +1980,11 @@
"google_new_spam_policy": "Через нову політику Google щодо спаму ви можете не отримати електронний лист або сповіщення календаря щодо цього бронювання.",
"resolve": "Вирішити",
"no_organization_slug": "Сталася помилка під час створення команд для цієї організації. Відсутня URL-адреса.",
"org_name": "Назва організації",
"org_url": "URL-адреса організації",
"copy_link_org": "Копіювати посилання на організацію",
"404_the_org": "Організація",
"404_the_team": "Команда",
"404_claim_entity_org": "Отримайте піддомен для своєї організації",
"404_claim_entity_team": "Приєднайтеся до цієї команди й почніть спільно керувати розкладами",
"insights_all_org_filter": "Усі",
"insights_team_filter": "Команда: {{teamName}}",
"insights_user_filter": "Користувач: {{userName}}",
"insights_subtitle": "Переглядайте дані Insights про бронювання для своїх заходів",
@ -2005,7 +1996,6 @@
"select_date": "Виберіть дату",
"see_all_available_times": "Переглянути доступні часові проміжки",
"org_team_names_example": "напр. команда маркетингу",
"org_team_names_example_1": "напр. маркетинговий відділ",
"org_team_names_example_2": "напр. відділ продажів",
"org_team_names_example_3": "напр. дизайнерський відділ",
"org_team_names_example_4": "e.g. інженерний відділ",
@ -2101,5 +2091,6 @@
"lock_timezone_toggle_on_booking_page": "Заблокуйте часовий пояс на сторінці бронювання",
"description_lock_timezone_toggle_on_booking_page": "Для блокування часового поясу на сторінці бронювання (корисне для окремих заходів)",
"extensive_whitelabeling": "Технічна підтримка й підтримка під час ознайомлення",
"need_help": "Потрібна допомога?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "\"username\" sẽ được điền vào theo tên người dùng của những thành viên được phân công",
"assign_to": "Phân công cho",
"add_members": "Thêm thành viên...",
"count_members_one": "{{count}} thành viên",
"count_members_other": "{{count}} thành viên",
"no_assigned_members": "Không thành viên nào được phân công",
"assigned_to": "Được phân công cho",
"start_assigning_members_above": "Bắt đầu phân công những thành viên bên trên",
@ -849,7 +847,6 @@
"next_step": "Bỏ qua bước",
"prev_step": "Bước trước",
"install": "Cài đặt",
"install_paid_app": "Đăng ký",
"installed": "Đã cài đặt",
"active_install_one": "{{count}} cài đặt hoạt động",
"active_install_other": "{{count}} cài đặt hoạt động",
@ -1218,9 +1215,6 @@
"attendee_name_variable": "Tên người tham dự",
"event_date_variable": "Ngày sự kiện",
"event_time_variable": "Thời gian sự kiện",
"timezone_variable": "Múi giờ",
"location_variable": "Vị trí",
"additional_notes_variable": "Ghi chú bổ sung",
"organizer_name_variable": "Tên nhà tổ chức",
"app_upgrade_description": "Để sử dụng tính năng này, bạn cần nâng cấp lên tài khoản Pro.",
"invalid_number": "Số điện thoại không hợp lệ",
@ -1986,14 +1980,11 @@
"google_new_spam_policy": "Chính sách thư rác mới của Google có thể ngăn bạn nhận bất kì thông báo nào về email và lịch liên quan đến lịch hẹn này.",
"resolve": "Giải quyết",
"no_organization_slug": "Có lỗi khi tạo nhóm cho tổ chức này. Slug URL bị thiếu.",
"org_name": "Tên tổ chức",
"org_url": "URL của tổ chức",
"copy_link_org": "Sao chép liên kết đến tổ chức",
"404_the_org": "Tổ chức",
"404_the_team": "Nhóm",
"404_claim_entity_org": "Lấy tên miền phụ cho tổ chức của bạn",
"404_claim_entity_team": "Lấy nhóm này và bắt đầu quản lý lịch hẹn theo tập thể",
"insights_all_org_filter": "Tất cả",
"insights_team_filter": "Nhóm: {{teamName}}",
"insights_user_filter": "Người dùng: {{userName}}",
"insights_subtitle": "Xem insights của đặt lịch ở mọi sự kiện của bạn",
@ -2005,7 +1996,6 @@
"select_date": "Chọn ngày",
"see_all_available_times": "Xem tất cả những thời gian trống",
"org_team_names_example": "ví dụ Nhóm Tiếp thị",
"org_team_names_example_1": "ví dụ Nhóm Tiếp thị",
"org_team_names_example_2": "ví dụ Nhóm Kinh doanh",
"org_team_names_example_3": "ví dụ Nhóm Thiết kế",
"org_team_names_example_4": "ví dụ Nhóm Kỹ thuật",
@ -2101,5 +2091,6 @@
"lock_timezone_toggle_on_booking_page": "Khoá múi giờ trên trang lịch hẹn",
"description_lock_timezone_toggle_on_booking_page": "Để khoá múi giờ trên trang lịch hẹn, hữu dụng cho những sự kiện đích thân tham dự.",
"extensive_whitelabeling": "Hướng dẫn bắt đầu và hỗ trợ kỹ thuật chuyên biệt",
"need_help": "Cần hỗ trợ?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "“用户名”将填写所分配成员的用户名",
"assign_to": "分配给",
"add_members": "添加成员...",
"count_members_one": "{{count}} 个成员",
"count_members_other": "{{count}} 个成员",
"no_assigned_members": "没有已分配的成员",
"assigned_to": "已分配给",
"start_assigning_members_above": "开始分配以上成员",
@ -849,7 +847,6 @@
"next_step": "跳过步骤",
"prev_step": "上一步",
"install": "安装",
"install_paid_app": "订阅",
"start_paid_trial": "开始免费试用",
"installed": "已安装",
"active_install_one": "{{count}} 个活动安装",
@ -1219,9 +1216,6 @@
"attendee_name_variable": "参与者",
"event_date_variable": "活动日期",
"event_time_variable": "活动时间",
"timezone_variable": "时区",
"location_variable": "位置",
"additional_notes_variable": "附加备注",
"organizer_name_variable": "组织者姓名",
"app_upgrade_description": "要使用此功能,您需要升级到专业版帐户。",
"invalid_number": "电话号码无效",
@ -1987,14 +1981,11 @@
"google_new_spam_policy": "Google 的新垃圾邮件政策可能会阻止您收到任何有关此预约的电子邮件和日历通知。",
"resolve": "解决",
"no_organization_slug": "为该组织创建团队时出错。缺少链接 slug。",
"org_name": "组织名称",
"org_url": "组织链接",
"copy_link_org": "复制组织链接",
"404_the_org": "组织",
"404_the_team": "团队",
"404_claim_entity_org": "为您的组织声明子域",
"404_claim_entity_team": "声明成为此团队的一员并开始集体管理时间表",
"insights_all_org_filter": "所有应用",
"insights_team_filter": "团队:{{teamName}}",
"insights_user_filter": "用户:{{userName}}",
"insights_subtitle": "查看您活动的预约 insights",
@ -2006,7 +1997,6 @@
"select_date": "选择日期",
"see_all_available_times": "查看所有可预约时间",
"org_team_names_example": "例如,营销团队",
"org_team_names_example_1": "例如,营销团队",
"org_team_names_example_2": "例如,销售团队",
"org_team_names_example_3": "例如,设计团队",
"org_team_names_example_4": "例如,工程团队",
@ -2102,5 +2092,6 @@
"lock_timezone_toggle_on_booking_page": "在预约页面上锁定时区",
"description_lock_timezone_toggle_on_booking_page": "在预约页面上锁定时区,这对面对面活动非常有用。",
"extensive_whitelabeling": "专门的入门和工程支持",
"need_help": "需要帮助?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 在此上方添加您的新字符串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -666,8 +666,6 @@
"managed_event_url_clarification": "「使用者名稱」會填入所指派成員的使用者名稱",
"assign_to": "指派給",
"add_members": "新增成員…",
"count_members_one": "{{count}} 名成員",
"count_members_other": "{{count}} 名成員",
"no_assigned_members": "無已指派成員",
"assigned_to": "已指派給",
"start_assigning_members_above": "開始指派以上成員",
@ -849,7 +847,6 @@
"next_step": "跳過步驟",
"prev_step": "上個步驟",
"install": "安裝",
"install_paid_app": "訂閱",
"installed": "已安裝",
"active_install_one": "{{count}} 個有效安裝",
"active_install_other": "{{count}} 個有效安裝",
@ -1218,9 +1215,6 @@
"attendee_name_variable": "與會者姓名",
"event_date_variable": "活動日期",
"event_time_variable": "活動時間",
"timezone_variable": "時區",
"location_variable": "地點",
"additional_notes_variable": "備註",
"organizer_name_variable": "主辦者姓名",
"app_upgrade_description": "您必須升級至專業版帳號才能使用此功能。",
"invalid_number": "電話號碼無效",
@ -1986,14 +1980,11 @@
"google_new_spam_policy": "新的 Google 垃圾郵件政策可能會使您收不到任何關於此預約的電子郵件和行事曆通知。",
"resolve": "解決",
"no_organization_slug": "為此組織建立團隊時發生錯誤。缺少網址 slug。",
"org_name": "組織名稱",
"org_url": "組織網址",
"copy_link_org": "複製組織連結",
"404_the_org": "組織",
"404_the_team": "團隊",
"404_claim_entity_org": "為您的組織預留子網域",
"404_claim_entity_team": "加入此團隊並開始共同管理行程表",
"insights_all_org_filter": "所有應用程式",
"insights_team_filter": "團隊:{{teamName}}",
"insights_user_filter": "使用者:{{userName}}",
"insights_subtitle": "查看所有活動的預約 Insight",
@ -2005,7 +1996,6 @@
"select_date": "選取日期",
"see_all_available_times": "查看所有可預約時段",
"org_team_names_example": "例如行銷團隊",
"org_team_names_example_1": "例如行銷團隊",
"org_team_names_example_2": "例如銷售團隊",
"org_team_names_example_3": "例如設計團隊",
"org_team_names_example_4": "例如工程團隊",
@ -2101,5 +2091,6 @@
"lock_timezone_toggle_on_booking_page": "鎖定預約頁面的時區",
"description_lock_timezone_toggle_on_booking_page": "鎖定預約頁面的時區,安排實體活動時非常實用。",
"extensive_whitelabeling": "專屬的入門和工程支援",
"need_help": "需要協助?",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 請在此處新增您的字串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -547,6 +547,45 @@ export function expectCalendarEventCreationFailureEmails({
);
}
export function expectSuccessfulRoudRobinReschedulingEmails({
emails,
newOrganizer,
prevOrganizer,
}: {
emails: Fixtures["emails"];
newOrganizer: { email: string; name: string };
prevOrganizer: { email: string; name: string };
}) {
if (newOrganizer !== prevOrganizer) {
// new organizer should recieve scheduling emails
expect(emails).toHaveEmail(
{
heading: "new_event_scheduled",
to: `${newOrganizer.email}`,
},
`${newOrganizer.email}`
);
// old organizer should recieve cancelled emails
expect(emails).toHaveEmail(
{
heading: "event_request_cancelled",
to: `${prevOrganizer.email}`,
},
`${prevOrganizer.email}`
);
} else {
// organizer should recieve rescheduled emails
expect(emails).toHaveEmail(
{
heading: "event_has_been_rescheduled",
to: `${newOrganizer.email}`,
},
`${newOrganizer.email}`
);
}
}
export function expectSuccessfulBookingRescheduledEmails({
emails,
organizer,

View File

@ -104,10 +104,9 @@ function generateFiles() {
* If a file has index.ts or index.tsx, it can be imported after removing the index.ts* part
*/
function getModulePath(path: string, moduleName: string) {
return (
`./${path.replace(/\\/g, "/")}/` +
moduleName.replace(/\/index\.ts|\/index\.tsx/, "").replace(/\.tsx$|\.ts$/, "")
);
return `./${path.replace(/\\/g, "/")}/${moduleName
.replace(/\/index\.ts|\/index\.tsx/, "")
.replace(/\.tsx$|\.ts$/, "")}`;
}
type ImportConfig =

View File

@ -166,6 +166,7 @@ const MultiSelectWidget = ({
return (
<Select
aria-label="multi-select-dropdown"
className="mb-2"
onChange={(items) => {
setValue(items?.map((item) => item.value));
@ -193,6 +194,7 @@ function SelectWidget({ listValues, setValue, value, ...remainingProps }: Select
return (
<Select
aria-label="select-dropdown"
className="data-testid-select mb-2"
onChange={(item) => {
if (!item) {

View File

@ -323,7 +323,8 @@ export default class EventManager {
public async reschedule(
event: CalendarEvent,
rescheduleUid: string,
newBookingId?: number
newBookingId?: number,
changedOrganizer?: boolean
): Promise<CreateUpdateResult> {
const originalEvt = processLocation(event);
const evt = cloneDeep(originalEvt);
@ -376,32 +377,37 @@ export default class EventManager {
// As the reschedule requires confirmation, we can't update the events and meetings to new time yet. So, just delete them and let it be handled when organizer confirms the booking.
await this.deleteEventsAndMeetings({ booking, event });
} else {
// If the reschedule doesn't require confirmation, we can "update" the events and meetings to new time.
const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null;
// If and only if event type is a dedicated meeting, update the dedicated video meeting.
if (isDedicated) {
const result = await this.updateVideoEvent(evt, booking);
const [updatedEvent] = Array.isArray(result.updatedEvent)
? result.updatedEvent
: [result.updatedEvent];
if (changedOrganizer) {
log.debug("RescheduleOrganizerChanged: Deleting Event and Meeting for previous booking");
await this.deleteEventsAndMeetings({ booking, event });
// New event is created in handleNewBooking
} else {
// If the reschedule doesn't require confirmation, we can "update" the events and meetings to new time.
const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null;
// If and only if event type is a dedicated meeting, update the dedicated video meeting.
if (isDedicated) {
const result = await this.updateVideoEvent(evt, booking);
const [updatedEvent] = Array.isArray(result.updatedEvent)
? result.updatedEvent
: [result.updatedEvent];
if (updatedEvent) {
evt.videoCallData = updatedEvent;
evt.location = updatedEvent.url;
if (updatedEvent) {
evt.videoCallData = updatedEvent;
evt.location = updatedEvent.url;
}
results.push(result);
}
results.push(result);
}
const bookingCalendarReference = booking.references.find((reference) =>
reference.type.includes("_calendar")
);
// There was a case that booking didn't had any reference and we don't want to throw error on function
if (bookingCalendarReference) {
// Update all calendar events.
results.push(...(await this.updateAllCalendarEvents(evt, booking, newBookingId)));
const bookingCalendarReference = booking.references.find((reference) =>
reference.type.includes("_calendar")
);
// There was a case that booking didn't had any reference and we don't want to throw error on function
if (bookingCalendarReference) {
// Update all calendar events.
results.push(...(await this.updateAllCalendarEvents(evt, booking, newBookingId)));
}
}
}
const bookingPayment = booking?.payment;
// Updating all payment to new

View File

@ -102,6 +102,37 @@ export const sendScheduledEmails = async (
await Promise.all(emailsToSend);
};
// for rescheduled round robin booking that assigned new members
export const sendRoundRobinScheduledEmails = async (calEvent: CalendarEvent, members: Person[]) => {
const emailsToSend: Promise<unknown>[] = [];
for (const teamMember of members) {
emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent, teamMember })));
}
await Promise.all(emailsToSend);
};
export const sendRoundRobinRescheduledEmails = async (calEvent: CalendarEvent, members: Person[]) => {
const emailsToSend: Promise<unknown>[] = [];
for (const teamMember of members) {
emailsToSend.push(sendEmail(() => new OrganizerRescheduledEmail({ calEvent, teamMember })));
}
await Promise.all(emailsToSend);
};
export const sendRoundRobinCancelledEmails = async (calEvent: CalendarEvent, members: Person[]) => {
const emailsToSend: Promise<unknown>[] = [];
for (const teamMember of members) {
emailsToSend.push(sendEmail(() => new OrganizerCancelledEmail({ calEvent, teamMember })));
}
await Promise.all(emailsToSend);
};
export const sendRescheduledEmails = async (calEvent: CalendarEvent) => {
const emailsToSend: Promise<unknown>[] = [];

View File

@ -78,13 +78,13 @@ export const TeamInviteEmail = (
marginTop: "48px",
lineHeightStep: "24px",
}}>
<>
{/* <>
{props.language("email_no_user_invite_steps_intro", {
entity: props.language(props.isOrg ? "organization" : "team").toLowerCase(),
})}
</>
</> */}
</p>
{/*
{!props.isCalcomMember && (
<div style={{ display: "flex", flexDirection: "column", gap: "32px" }}>
<EmailStep
@ -120,7 +120,7 @@ export const TeamInviteEmail = (
}
/>
</div>
)}
)} */}
<div className="">
<p

View File

@ -247,6 +247,8 @@ const BookerComponent = ({
"bg-default dark:bg-muted sticky top-0 z-10"
)}>
<Header
username={username}
eventSlug={eventSlug}
enabledLayouts={bookerLayouts.enabledLayouts}
extraDays={layout === BookerLayouts.COLUMN_VIEW ? columnViewExtraDays.current : extraDays}
isMobile={isMobile}

View File

@ -1,11 +1,13 @@
import { ChevronLeft, ChevronRight } from "lucide-react";
import { useSession } from "next-auth/react";
import { useCallback, useMemo } from "react";
import { shallow } from "zustand/shallow";
import dayjs from "@calcom/dayjs";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import { Button, ButtonGroup, ToggleGroup } from "@calcom/ui";
import { Button, ButtonGroup, ToggleGroup, Tooltip } from "@calcom/ui";
import { Calendar, Columns, Grid } from "@calcom/ui/components/icon";
import { TimeFormatToggle } from "../../components/TimeFormatToggle";
@ -18,13 +20,19 @@ export function Header({
isMobile,
enabledLayouts,
nextSlots,
username,
eventSlug,
}: {
extraDays: number;
isMobile: boolean;
enabledLayouts: BookerLayouts[];
nextSlots: number;
username: string;
eventSlug: string;
}) {
const { t, i18n } = useLocale();
const session = useSession();
const [layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow);
const selectedDateString = useBookerStore((state) => state.selectedDate);
const setSelectedDate = useBookerStore((state) => state.setSelectedDate);
@ -54,12 +62,24 @@ export function Header({
<LayoutToggle onLayoutToggle={onLayoutToggle} layout={layout} enabledLayouts={enabledLayouts} />
);
};
const isMyLink = username === session?.data?.user.username; // TODO: check for if the user is the owner of the link
// In month view we only show the layout toggle.
if (isMonthView) {
return (
<div className="flex gap-2">
<OverlayCalendarContainer />
{isMyLink ? (
<Tooltip content={t("troubleshooter_tooltip")} side="bottom">
<Button
color="primary"
target="_blank"
href={`${WEBAPP_URL}/availability/troubleshoot?eventType=${eventSlug}`}>
{t("need_help")}
</Button>
</Tooltip>
) : (
<OverlayCalendarContainer />
)}
<LayoutToggleWithData />
</div>
);

View File

@ -30,6 +30,9 @@ import {
sendOrganizerRequestEmail,
sendRescheduledEmails,
sendRescheduledSeatEmail,
sendRoundRobinCancelledEmails,
sendRoundRobinRescheduledEmails,
sendRoundRobinScheduledEmails,
sendScheduledEmails,
sendScheduledSeatsEmails,
} from "@calcom/emails";
@ -467,8 +470,26 @@ async function getOriginalRescheduledBooking(uid: string, seatsEventType?: boole
email: true,
locale: true,
timeZone: true,
destinationCalendar: true,
credentials: {
select: {
id: true,
userId: true,
key: true,
type: true,
teamId: true,
appId: true,
invalid: true,
user: {
select: {
email: true,
},
},
},
},
},
},
destinationCalendar: true,
payment: true,
references: true,
workflowReminders: true,
@ -663,8 +684,6 @@ async function handler(
};
const {
recurringCount,
allRecurringDates,
currentRecurringIndex,
noEmail,
eventTypeId,
eventTypeSlug,
@ -900,6 +919,7 @@ async function handler(
let originalRescheduledBooking: BookingType = null;
//this gets the orginal rescheduled booking
if (rescheduleUid) {
// rescheduleUid can be bookingUid and bookingSeatUid
bookingSeat = await prisma.bookingSeat.findUnique({
@ -923,6 +943,7 @@ async function handler(
}
}
//checks what users are available
if (!eventType.seatsPerTimeSlot && !skipAvailabilityCheck) {
const availableUsers = await ensureAvailableUsers(
{
@ -1897,11 +1918,16 @@ async function handler(
evt.recurringEvent = eventType.recurringEvent;
}
const changedOrganizer =
!!originalRescheduledBooking &&
eventType.schedulingType === SchedulingType.ROUND_ROBIN &&
originalRescheduledBooking.userId !== evt.organizer.id;
async function createBooking() {
if (originalRescheduledBooking) {
evt.title = originalRescheduledBooking?.title || evt.title;
evt.description = originalRescheduledBooking?.description || evt.description;
evt.location = originalRescheduledBooking?.location || evt.location;
evt.location = changedOrganizer ? evt.location : originalRescheduledBooking?.location || evt.location;
}
const eventTypeRel = !eventTypeId
@ -2146,6 +2172,7 @@ async function handler(
let videoCallUrl;
//this is the actual rescheduling logic
if (originalRescheduledBooking?.uid) {
log.silly("Rescheduling booking", originalRescheduledBooking.uid);
try {
@ -2158,9 +2185,7 @@ async function handler(
);
}
// Use EventManager to conditionally use all needed integrations.
addVideoCallDataToEvt(originalRescheduledBooking.references);
const updateManager = await eventManager.reschedule(evt, originalRescheduledBooking.uid);
//update original rescheduled booking (no seats event)
if (!eventType.seatsPerTimeSlot) {
@ -2175,6 +2200,21 @@ async function handler(
});
}
const newDesinationCalendar = evt.destinationCalendar;
evt.destinationCalendar = originalRescheduledBooking?.destinationCalendar
? [originalRescheduledBooking?.destinationCalendar]
: originalRescheduledBooking?.user?.destinationCalendar
? [originalRescheduledBooking?.user.destinationCalendar]
: evt.destinationCalendar;
const updateManager = await eventManager.reschedule(
evt,
originalRescheduledBooking.uid,
undefined,
changedOrganizer
);
// This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back
// to the default description when we are sending the emails.
evt.description = eventType.description;
@ -2200,23 +2240,179 @@ async function handler(
: calendarResult?.updatedEvent?.iCalUID || undefined;
}
const { metadata, videoCallUrl: _videoCallUrl } = getVideoCallDetails({
const { metadata: videoMetadata, videoCallUrl: _videoCallUrl } = getVideoCallDetails({
results,
});
let metadata: AdditionalInformation = {};
metadata = videoMetadata;
videoCallUrl = _videoCallUrl;
//if organizer changed we need to create a new booking (reschedule only cancels the old one)
if (changedOrganizer) {
evt.destinationCalendar = newDesinationCalendar;
evt.title = getEventName(eventNameObject);
// location might changed and will be new created in eventManager.create (organizer default location)
evt.videoCallData = undefined;
const createManager = await eventManager.create(evt);
// This gets overridden when creating the event - to check if notes have been hidden or not. We just reset this back
// to the default description when we are sending the emails.
evt.description = eventType.description;
results = createManager.results;
referencesToCreate = createManager.referencesToCreate;
videoCallUrl = evt.videoCallData && evt.videoCallData.url ? evt.videoCallData.url : null;
if (results.length > 0 && results.every((res) => !res.success)) {
const error = {
errorCode: "BookingCreatingMeetingFailed",
message: "Booking rescheduling failed",
};
loggerWithEventDetails.error(
`EventManager.create failure in some of the integrations ${organizerUser.username}`,
safeStringify({ error, results })
);
} else {
if (results.length) {
// Handle Google Meet results
// We use the original booking location since the evt location changes to daily
if (bookingLocation === MeetLocationType) {
const googleMeetResult = {
appName: GoogleMeetMetadata.name,
type: "conferencing",
uid: results[0].uid,
originalEvent: results[0].originalEvent,
};
// Find index of google_calendar inside createManager.referencesToCreate
const googleCalIndex = createManager.referencesToCreate.findIndex(
(ref) => ref.type === "google_calendar"
);
const googleCalResult = results[googleCalIndex];
if (!googleCalResult) {
loggerWithEventDetails.warn("Google Calendar not installed but using Google Meet as location");
results.push({
...googleMeetResult,
success: false,
calWarnings: [tOrganizer("google_meet_warning")],
});
}
if (googleCalResult?.createdEvent?.hangoutLink) {
results.push({
...googleMeetResult,
success: true,
});
// Add google_meet to referencesToCreate in the same index as google_calendar
createManager.referencesToCreate[googleCalIndex] = {
...createManager.referencesToCreate[googleCalIndex],
meetingUrl: googleCalResult.createdEvent.hangoutLink,
};
// Also create a new referenceToCreate with type video for google_meet
createManager.referencesToCreate.push({
type: "google_meet_video",
meetingUrl: googleCalResult.createdEvent.hangoutLink,
uid: googleCalResult.uid,
credentialId: createManager.referencesToCreate[googleCalIndex].credentialId,
});
} else if (googleCalResult && !googleCalResult.createdEvent?.hangoutLink) {
results.push({
...googleMeetResult,
success: false,
});
}
}
metadata.hangoutLink = results[0].createdEvent?.hangoutLink;
metadata.conferenceData = results[0].createdEvent?.conferenceData;
metadata.entryPoints = results[0].createdEvent?.entryPoints;
evt.appsStatus = handleAppsStatus(results, booking);
videoCallUrl =
metadata.hangoutLink ||
results[0].createdEvent?.url ||
organizerOrFirstDynamicGroupMemberDefaultLocationUrl ||
videoCallUrl;
}
}
}
evt.appsStatus = handleAppsStatus(results, booking);
// If there is an integration error, we don't send successful rescheduling email, instead broken integration email should be sent that are handled by either CalendarManager or videoClient
if (noEmail !== true && isConfirmedByDefault && !isThereAnIntegrationError) {
const copyEvent = cloneDeep(evt);
loggerWithEventDetails.debug("Emails: Sending rescheduled emails for booking confirmation");
await sendRescheduledEmails({
const copyEventAdditionalInfo = {
...copyEvent,
additionalInformation: metadata,
additionalNotes, // Resets back to the additionalNote input and not the override value
cancellationReason: `$RCH$${rescheduleReason ? rescheduleReason : ""}`, // Removable code prefix to differentiate cancellation from rescheduling for email
});
};
loggerWithEventDetails.debug("Emails: Sending rescheduled emails for booking confirmation");
/*
handle emails for round robin
- if booked rr host is the same, then rescheduling email
- if new rr host is booked, then cancellation email to old host and confirmation email to new host
*/
if (eventType.schedulingType === SchedulingType.ROUND_ROBIN) {
const originalBookingMemberEmails: Person[] = [];
for (const user of originalRescheduledBooking.attendees) {
const translate = await getTranslation(user.locale ?? "en", "common");
originalBookingMemberEmails.push({
name: user.name,
email: user.email,
timeZone: user.timeZone,
language: { translate, locale: user.locale ?? "en" },
});
}
if (originalRescheduledBooking.user) {
const translate = await getTranslation(originalRescheduledBooking.user.locale ?? "en", "common");
originalBookingMemberEmails.push({
...originalRescheduledBooking.user,
name: originalRescheduledBooking.user.name || "",
language: { translate, locale: originalRescheduledBooking.user.locale ?? "en" },
});
}
const newBookingMemberEmails: Person[] =
copyEvent.team?.members
.map((member) => member)
.concat(copyEvent.organizer)
.concat(copyEvent.attendees) || [];
// scheduled Emails
const newBookedMembers = newBookingMemberEmails.filter(
(member) =>
!originalBookingMemberEmails.find((originalMember) => originalMember.email === member.email)
);
// cancelled Emails
const cancelledMembers = originalBookingMemberEmails.filter(
(member) => !newBookingMemberEmails.find((newMember) => newMember.email === member.email)
);
// rescheduled Emails
const rescheduledMembers = newBookingMemberEmails.filter((member) =>
originalBookingMemberEmails.find((orignalMember) => orignalMember.email === member.email)
);
sendRoundRobinRescheduledEmails(copyEventAdditionalInfo, rescheduledMembers);
sendRoundRobinScheduledEmails(copyEventAdditionalInfo, newBookedMembers);
sendRoundRobinCancelledEmails(copyEventAdditionalInfo, cancelledMembers);
} else {
// send normal rescheduled emails (non round robin event, where organizers stay the same)
await sendRescheduledEmails({
...copyEvent,
additionalInformation: metadata,
additionalNotes, // Resets back to the additionalNote input and not the override value
cancellationReason: `$RCH$${rescheduleReason ? rescheduleReason : ""}`, // Removable code prefix to differentiate cancellation from rescheduling for email
});
}
}
// If it's not a reschedule, doesn't require confirmation and there's no price,
// Create a booking
@ -2376,7 +2572,7 @@ async function handler(
const metadata = videoCallUrl
? {
videoCallUrl: getVideoCallUrlFromCalEvent(evt),
videoCallUrl: getVideoCallUrlFromCalEvent(evt) || videoCallUrl,
}
: undefined;

View File

@ -1,10 +1,11 @@
// import { debounce } from "lodash";
import { useSession } from "next-auth/react";
import { useSearchParams, useRouter } from "next/navigation";
import { useRouter } from "next/navigation";
import { useState, useEffect } from "react";
import MemberInvitationModal from "@calcom/ee/teams/components/MemberInvitationModal";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import { MembershipRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import type { RouterOutputs } from "@calcom/trpc/react";
@ -57,8 +58,8 @@ function MembersList(props: MembersListProps) {
const MembersView = () => {
const { t, i18n } = useLocale();
const router = useRouter();
const searchParams = useSearchParams();
const teamId = Number(searchParams?.get("id"));
const params = useParamsWithFallback();
const teamId = Number(params.id);
const session = useSession();
const utils = trpc.useContext();
const [offset, setOffset] = useState<number>(1);

View File

@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import type { Prisma } from "@prisma/client";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { useRouter } from "next/navigation";
import { useState, useLayoutEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
@ -10,6 +10,7 @@ import { z } from "zod";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import { md } from "@calcom/lib/markdownIt";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import objectKeys from "@calcom/lib/objectKeys";
@ -76,8 +77,8 @@ const OtherTeamProfileView = () => {
const form = useForm({
resolver: zodResolver(teamProfileFormSchema),
});
const searchParams = useSearchParams();
const teamId = Number(searchParams?.get("id"));
const params = useParamsWithFallback();
const teamId = Number(params.id);
const { data: team, isLoading } = trpc.viewer.organizations.getOtherTeam.useQuery(
{ teamId: teamId },
{

View File

@ -116,7 +116,7 @@ const OrgProfileView = () => {
<div className="border-subtle flex rounded-b-md border border-t-0 px-4 py-8 sm:px-6">
<div className="flex-grow">
<div>
<Label className="text-emphasis">{t("org_name")}</Label>
<Label className="text-emphasis">{t("organization_name")}</Label>
<p className="text-default text-sm">{currentOrganisation?.name}</p>
</div>
{!isBioEmpty && (
@ -245,7 +245,7 @@ const OrgProfileForm = ({ defaultValues }: { defaultValues: FormValues }) => {
<div className="mt-8">
<TextField
name="name"
label={t("org_name")}
label={t("organization_name")}
value={value}
onChange={(e) => {
form.setValue("name", e?.target.value, { shouldDirty: true });
@ -261,7 +261,7 @@ const OrgProfileForm = ({ defaultValues }: { defaultValues: FormValues }) => {
<div className="mt-8">
<TextField
name="slug"
label={t("org_url")}
label={t("organization_url")}
value={value}
disabled
addOnSuffix={`.${subdomainSuffix()}`}

View File

@ -1,9 +1,15 @@
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
import { CAL_URL, IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
import type { PreviewState } from "../types";
import { embedLibUrl } from "./constants";
import { getDimension } from "./getDimension";
export const doWeNeedCalOriginProp = (embedCalOrigin: string) => {
// If we are self hosted, calOrigin won't be app.cal.com so we need to pass it
// If we are not self hosted but it's still different from WEBAPP_URL and CAL_URL, we need to pass it -> It happens for organization booking URL at the moment
return IS_SELF_HOSTED || (embedCalOrigin !== WEBAPP_URL && embedCalOrigin !== CAL_URL);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const Codes = {
react: {
@ -33,13 +39,9 @@ export const Codes = {
return <Cal
calLink="${calLink}"
style={{width:"${width}",height:"${height}",overflow:"scroll"}}
${previewState.layout ? `config={{layout: '${previewState.layout}'}}` : ""}${
IS_SELF_HOSTED
? `
calOrigin="${embedCalOrigin}"
calJsUrl="${embedLibUrl}"`
: ""
}
${previewState.layout ? `config={{layout: '${previewState.layout}'}}` : ""}
${doWeNeedCalOriginProp(embedCalOrigin) ? ` calOrigin="${embedCalOrigin}"` : ""}
${IS_SELF_HOSTED ? `calJsUrl="${embedLibUrl}"` : ""}
/>;
};`;
},
@ -53,7 +55,7 @@ export const Codes = {
return code`
import { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
export default function App() {
export default function MyApp() {
useEffect(()=>{
(async function () {
const cal = await getCalApi(${IS_SELF_HOSTED ? `"${embedLibUrl}"` : ""});
@ -77,7 +79,7 @@ export const Codes = {
return code`
import { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
export default function App() {
export default function MyApp() {
useEffect(()=>{
(async function () {
const cal = await getCalApi(${IS_SELF_HOSTED ? `"${embedLibUrl}"` : ""});
@ -85,7 +87,8 @@ export const Codes = {
})();
}, [])
return <button
data-cal-link="${calLink}"${IS_SELF_HOSTED ? `\ndata-cal-origin="${embedCalOrigin}"` : ""}
data-cal-link="${calLink}"
${doWeNeedCalOriginProp(embedCalOrigin) ? ` data-cal-origin="${embedCalOrigin}"` : ""}
${`data-cal-config='${JSON.stringify({
layout: previewState.layout,
})}'`}

View File

@ -2,14 +2,14 @@ import { forwardRef } from "react";
import type { MutableRefObject } from "react";
import type { BookerLayout } from "@calcom/features/bookings/Booker/types";
import { APP_NAME, IS_SELF_HOSTED } from "@calcom/lib/constants";
import { APP_NAME } from "@calcom/lib/constants";
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { TextArea } from "@calcom/ui";
import { Code, Trello } from "@calcom/ui/components/icon";
import type { EmbedType, PreviewState, EmbedFramework } from "../types";
import { Codes } from "./EmbedCodes";
import { Codes, doWeNeedCalOriginProp } from "./EmbedCodes";
import { EMBED_PREVIEW_HTML_URL, embedLibUrl } from "./constants";
import { getDimension } from "./getDimension";
import { useEmbedCalOrigin } from "./hooks";
@ -193,7 +193,7 @@ const getEmbedTypeSpecificString = ({
} else if (embedType === "floating-popup") {
const floatingButtonArg = {
calLink,
...(IS_SELF_HOSTED ? { calOrigin: embedCalOrigin } : null),
...(doWeNeedCalOriginProp(embedCalOrigin) ? { calOrigin: embedCalOrigin } : null),
...previewState.floatingPopup,
};
return frameworkCodes[embedType]({

View File

@ -20,6 +20,7 @@ import {
CheckboxField,
} from "@calcom/ui";
import { UserPlus, X } from "@calcom/ui/components/icon";
import InfoBadge from "@calcom/web/components/ui/InfoBadge";
import { ComponentForField } from "./FormBuilderField";
import { propsTypes } from "./propsTypes";
@ -395,6 +396,7 @@ export const Components: Record<FieldType, Component> = {
}
}, [options, setValue, value]);
const { t } = useLocale();
return (
<div>
<div>
@ -418,17 +420,25 @@ export const Components: Record<FieldType, Component> = {
checked={value?.value === option.value}
/>
<span className="text-emphasis me-2 ms-2 text-sm">{option.label ?? ""}</span>
<span>
{option.value === "phone" && (
<InfoBadge content={t("number_in_international_format")} />
)}
</span>
</label>
);
})
) : (
// Show option itself as label because there is just one option
<>
<Label>
<Label className="flex">
{options[0].label}
{!readOnly && optionsInputs[options[0].value]?.required ? (
<span className="text-default mb-1 ml-1 text-sm font-medium">*</span>
) : null}
{options[0].value === "phone" && (
<InfoBadge content={t("number_in_international_format")} />
)}
</Label>
</>
)}

View File

@ -7,6 +7,7 @@ import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Label } from "@calcom/ui";
import { Info } from "@calcom/ui/components/icon";
import InfoBadge from "@calcom/web/components/ui/InfoBadge";
import { Components, isValidValueProp } from "./Components";
import { fieldTypesConfigMap } from "./fieldTypes";
@ -126,6 +127,8 @@ const WithLabel = ({
readOnly: boolean;
children: React.ReactNode;
}) => {
const { t } = useLocale();
return (
<div>
{/* multiemail doesnt show label initially. It is shown on clicking CTA */}
@ -133,11 +136,12 @@ const WithLabel = ({
{/* Component itself managing it's label should remove these checks */}
{field.type !== "boolean" && field.type !== "multiemail" && field.label && (
<div className="mb-2 flex items-center">
<Label className="!mb-0">
<Label className="!mb-0 flex">
<span>{field.label}</span>
<span className="text-emphasis -mb-1 ml-1 text-sm font-medium leading-none">
{!readOnly && field.required ? "*" : ""}
</span>
{field.type === "phone" && <InfoBadge content={t("number_in_international_format")} />}
</Label>
</div>
)}

View File

@ -97,7 +97,7 @@ export const TeamAndSelfList = () => {
isAll: true,
});
}}
label={t("insights_all_org_filter")}
label={t("all")}
/>
)}

View File

@ -27,7 +27,7 @@ const TroubleshooterComponent = ({ month }: TroubleshooterProps) => {
<>
<div
className={classNames(
"text-default grid min-h-full w-full flex-col items-center overflow-clip ",
"text-default fixed inset-0 grid min-h-full w-full flex-col items-center overflow-clip ",
isMobile
? "[--troublehooster-meta-width:0px]"
: "[--troublehooster-meta-width:250px] lg:[--troubleshooter-meta-width:430px]"
@ -51,7 +51,7 @@ const TroubleshooterComponent = ({ month }: TroubleshooterProps) => {
<TroubleshooterHeader extraDays={extraDays} isMobile={isMobile} />
</div>
<StickyBox key="meta" className={classNames("relative z-10")}>
<div className="">
<div className="ps-6">
<TroubleshooterSidebar />
</div>
</StickyBox>

View File

@ -54,7 +54,8 @@ function EmptyCalendarToggleItem() {
return (
<TroubleshooterListItemContainer
title="Please install a calendar"
title={t("installed", { count: 0 })}
subtitle={t("please_install_a_calendar")}
prefixSlot={
<>
<div className="h-4 w-4 self-center rounded-[4px] bg-blue-500" />
@ -63,7 +64,7 @@ function EmptyCalendarToggleItem() {
suffixSlot={
<div>
<Badge variant="orange" withDot size="sm">
Not found
{t("unavailable")}
</Badge>
</div>
}>

View File

@ -1,8 +1,9 @@
import { useMemo, useEffect } from "react";
import { useMemo, useEffect, startTransition } from "react";
import { trpc } from "@calcom/trpc";
import { SelectField } from "@calcom/ui";
import { getQueryParam } from "../../bookings/Booker/utils/query-param";
import { useTroubleshooterStore } from "../store";
export function EventTypeSelect() {
@ -10,7 +11,7 @@ export function EventTypeSelect() {
const selectedEventType = useTroubleshooterStore((state) => state.event);
const setSelectedEventType = useTroubleshooterStore((state) => state.setEvent);
// const selectedEventQueryParam = getQueryParam("eventType");
const selectedEventQueryParam = getQueryParam("eventType");
const options = useMemo(() => {
if (!eventTypes) return [];
@ -23,7 +24,7 @@ export function EventTypeSelect() {
}, [eventTypes]);
useEffect(() => {
if (!selectedEventType && eventTypes && eventTypes[0]) {
if (!selectedEventType && eventTypes && eventTypes[0] && !selectedEventQueryParam) {
const { id, slug, length } = eventTypes[0];
setSelectedEventType({
id,
@ -34,6 +35,26 @@ export function EventTypeSelect() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [eventTypes]);
useEffect(() => {
if (selectedEventQueryParam) {
// ensure that the update is deferred until the Suspense boundary has finished hydrating
startTransition(() => {
const foundEventType = eventTypes?.find((et) => et.slug === selectedEventQueryParam);
if (foundEventType) {
const { id, slug, length } = foundEventType;
setSelectedEventType({ id, slug, duration: length });
} else if (eventTypes && eventTypes[0]) {
const { id, slug, length } = eventTypes[0];
setSelectedEventType({
id,
slug,
duration: length,
});
}
});
}
}, [eventTypes, selectedEventQueryParam, setSelectedEventType]);
return (
<SelectField
label="Event Type"

View File

@ -17,7 +17,7 @@ const BackButtonInSidebar = ({ name }: { name: string }) => {
<Skeleton
title={name}
as="p"
className="max-w-36 min-h-4 truncate text-xl font-semibold"
className="max-w-36 min-h-4 truncate font-semibold"
loadingClassName="ms-3">
{name}
</Skeleton>

View File

@ -1,28 +1,24 @@
import { randomBytes } from "crypto";
import { sendTeamInviteEmail } from "@calcom/emails";
import { updateQuantitySubscriptionFromStripe } from "@calcom/features/ee/teams/lib/payments";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
import { getTranslation } from "@calcom/lib/server/i18n";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { isEmail } from "../util";
import type { TInviteMemberInputSchema } from "./inviteMember.schema";
import {
checkPermissions,
getTeamOrThrow,
getEmailsToInvite,
getUserToInviteOrThrowIfExists,
checkInputEmailIsValid,
getOrgConnectionInfo,
createNewUserConnectToOrgIfExists,
throwIfInviteIsToOrgAndUserExists,
createProvisionalMembership,
getIsOrgVerified,
sendVerificationEmail,
createAndAutoJoinIfInOrg,
getUsersToInvite,
createNewUsersConnectToOrgIfExists,
createProvisionalMemberships,
groupUsersByJoinability,
sendTeamInviteEmails,
sendEmails,
} from "./utils";
type InviteMemberOptions = {
@ -33,10 +29,10 @@ type InviteMemberOptions = {
};
export const inviteMemberHandler = async ({ ctx, input }: InviteMemberOptions) => {
const translation = await getTranslation(input.language ?? "en", "common");
await checkRateLimitAndThrowError({
identifier: `invitedBy:${ctx.user.id}`,
});
await checkPermissions({
userId: ctx.user.id,
teamId:
@ -46,100 +42,81 @@ export const inviteMemberHandler = async ({ ctx, input }: InviteMemberOptions) =
const team = await getTeamOrThrow(input.teamId, input.isOrg);
const { autoAcceptEmailDomain, orgVerified } = getIsOrgVerified(input.isOrg, team);
const translation = await getTranslation(input.language ?? "en", "common");
const emailsToInvite = await getEmailsToInvite(input.usernameOrEmail);
for (const usernameOrEmail of emailsToInvite) {
const connectionInfo = getOrgConnectionInfo({
orgVerified,
orgAutoAcceptDomain: autoAcceptEmailDomain,
usersEmail: usernameOrEmail,
team,
isOrg: input.isOrg,
});
const invitee = await getUserToInviteOrThrowIfExists({
usernameOrEmail,
teamId: input.teamId,
isOrg: input.isOrg,
});
if (!invitee) {
checkInputEmailIsValid(usernameOrEmail);
// valid email given, create User and add to team
await createNewUserConnectToOrgIfExists({
usernameOrEmail,
input,
connectionInfo,
autoAcceptEmailDomain,
parentId: team.parentId,
});
await sendVerificationEmail({ usernameOrEmail, team, translation, ctx, input, connectionInfo });
} else {
throwIfInviteIsToOrgAndUserExists(invitee, team, input.isOrg);
const shouldAutoJoinOrgTeam = await createAndAutoJoinIfInOrg({
invitee,
role: input.role,
const orgConnectInfoByEmail = emailsToInvite.reduce((acc, email) => {
return {
...acc,
[email]: getOrgConnectionInfo({
orgVerified,
orgAutoAcceptDomain: autoAcceptEmailDomain,
usersEmail: email,
team,
});
if (shouldAutoJoinOrgTeam.autoJoined) {
// Continue here because if this is true we dont need to send an email to the user
// we also dont need to update stripe as thats handled on an ORG level and not a team level.
continue;
}
// create provisional membership
await createProvisionalMembership({
isOrg: input.isOrg,
}),
};
}, {} as Record<string, ReturnType<typeof getOrgConnectionInfo>>);
const existingUsersWithMembersips = await getUsersToInvite({
usernameOrEmail: emailsToInvite,
isInvitedToOrg: input.isOrg,
team,
});
const existingUsersEmails = existingUsersWithMembersips.map((user) => user.email);
const newUsersEmails = emailsToInvite.filter((email) => !existingUsersEmails.includes(email));
// deal with users to create and invite to team/org
if (newUsersEmails.length) {
await createNewUsersConnectToOrgIfExists({
usernamesOrEmails: newUsersEmails,
input,
connectionInfoMap: orgConnectInfoByEmail,
autoAcceptEmailDomain,
parentId: team.parentId,
});
const sendVerifEmailsPromises = newUsersEmails.map((usernameOrEmail) => {
return sendVerificationEmail({
usernameOrEmail,
team,
translation,
ctx,
input,
invitee,
connectionInfo: orgConnectInfoByEmail[usernameOrEmail],
});
});
sendEmails(sendVerifEmailsPromises);
}
let sendTo = usernameOrEmail;
if (!isEmail(usernameOrEmail)) {
sendTo = invitee.email;
}
// inform user of membership by email
if (ctx?.user?.name && team?.name) {
const inviteTeamOptions = {
joinLink: `${WEBAPP_URL}/auth/login?callbackUrl=/settings/teams`,
isCalcomMember: true,
};
/**
* Here we want to redirect to a different place if onboarding has been completed or not. This prevents the flash of going to teams -> Then to onboarding - also show a different email template.
* This only changes if the user is a CAL user and has not completed onboarding and has no password
*/
if (!invitee.completedOnboarding && !invitee.password && invitee.identityProvider === "CAL") {
const token = randomBytes(32).toString("hex");
await prisma.verificationToken.create({
data: {
identifier: usernameOrEmail,
token,
expires: new Date(new Date().setHours(168)), // +1 week
team: {
connect: {
id: team.id,
},
},
},
});
// deal with existing users invited to join the team/org
if (existingUsersWithMembersips.length) {
const [autoJoinUsers, regularUsers] = groupUsersByJoinability({
existingUsersWithMembersips,
team,
});
inviteTeamOptions.joinLink = `${WEBAPP_URL}/signup?token=${token}&callbackUrl=/getting-started`;
inviteTeamOptions.isCalcomMember = false;
}
// invited users can autojoin, create their memberships in org
if (autoJoinUsers.length) {
await prisma.membership.createMany({
data: autoJoinUsers.map((userToAutoJoin) => ({
userId: userToAutoJoin.id,
teamId: team.id,
accepted: true,
role: input.role,
})),
});
}
await sendTeamInviteEmail({
language: translation,
from: ctx.user.name,
to: sendTo,
teamName: team.name,
...inviteTeamOptions,
isOrg: input.isOrg,
});
}
// invited users cannot autojoin, create provisional memberships and send email
if (regularUsers.length) {
await createProvisionalMemberships({
input,
invitees: regularUsers,
});
await sendTeamInviteEmails({
currentUserName: ctx?.user?.name,
currentUserTeamName: team?.name,
existingUsersWithMembersips: regularUsers,
language: translation,
isOrg: input.isOrg,
teamId: team.id,
});
}
}

View File

@ -2,14 +2,39 @@ import { z } from "zod";
import { MembershipRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
export const ZInviteMemberInputSchema = z.object({
teamId: z.number(),
usernameOrEmail: z.union([z.string(), z.array(z.string())]).transform((usernameOrEmail) => {
if (typeof usernameOrEmail === "string") {
return usernameOrEmail.trim().toLowerCase();
}
return usernameOrEmail.map((item) => item.trim().toLowerCase());
}),
usernameOrEmail: z
.union([z.string(), z.array(z.string())])
.transform((usernameOrEmail) => {
if (typeof usernameOrEmail === "string") {
return usernameOrEmail.trim().toLowerCase();
}
return usernameOrEmail.map((item) => item.trim().toLowerCase());
})
.refine((value) => {
let invalidEmail;
if (Array.isArray(value)) {
if (value.length > 100) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `You are limited to inviting a maximum of 100 users at once.`,
});
}
invalidEmail = value.find((email) => !z.string().email().safeParse(email).success);
} else {
invalidEmail = !z.string().email().safeParse(value).success ? value : null;
}
if (invalidEmail) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Invite failed because '${invalidEmail}' is not a valid email address`,
});
}
return true;
}),
role: z.nativeEnum(MembershipRole),
language: z.string(),
isOrg: z.boolean().default(false),

View File

@ -2,20 +2,19 @@ import { describe, it, vi, expect } from "vitest";
import { isTeamAdmin } from "@calcom/lib/server/queries";
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
import type { User } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
import type { TeamWithParent } from "./types";
import type { Invitee, UserWithMembership } from "./utils";
import {
checkInputEmailIsValid,
checkPermissions,
getEmailsToInvite,
getIsOrgVerified,
getOrgConnectionInfo,
throwIfInviteIsToOrgAndUserExists,
createAndAutoJoinIfInOrg,
validateInviteeEligibility,
shouldAutoJoinIfInOrg,
} from "./utils";
vi.mock("@calcom/lib/server/queries", () => {
@ -60,46 +59,29 @@ const mockedTeam: TeamWithParent = {
parentId: null,
parent: null,
isPrivate: false,
logoUrl: "",
};
const mockUser: User = {
const mockUser: Invitee = {
id: 4,
username: "pro",
name: "Pro Example",
email: "pro@example.com",
emailVerified: new Date(),
password: "",
bio: null,
avatar: null,
timeZone: "Europe/London",
weekStart: "Sunday",
startTime: 0,
endTime: 1440,
bufferTime: 0,
hideBranding: false,
theme: null,
createdDate: new Date(),
trialEndsAt: null,
defaultScheduleId: null,
completedOnboarding: true,
locale: "en",
timeFormat: 12,
twoFactorSecret: null,
twoFactorEnabled: false,
identityProvider: "CAL",
identityProviderId: null,
invitedTo: null,
brandColor: "#292929",
darkBrandColor: "#fafafa",
away: false,
allowDynamicBooking: true,
metadata: null,
verified: false,
role: "USER",
disableImpersonation: false,
organizationId: null,
};
const userInTeamAccepted: UserWithMembership = {
...mockUser,
teams: [{ teamId: mockedTeam.id, accepted: true, userId: mockUser.id }],
};
const userInTeamNotAccepted: UserWithMembership = {
...mockUser,
teams: [{ teamId: mockedTeam.id, accepted: false, userId: mockUser.id }],
};
describe("Invite Member Utils", () => {
describe("checkPermissions", () => {
it("It should throw an error if the user is not an admin of the ORG", async () => {
@ -134,20 +116,7 @@ describe("Invite Member Utils", () => {
expect(result).toEqual(["test1@example.com", "test2@example.com"]);
});
});
describe("checkInputEmailIsValid", () => {
it("should throw a TRPCError with code BAD_REQUEST if the email is invalid", () => {
const invalidEmail = "invalid-email";
expect(() => checkInputEmailIsValid(invalidEmail)).toThrow(TRPCError);
expect(() => checkInputEmailIsValid(invalidEmail)).toThrowError(
"Invite failed because invalid-email is not a valid email address"
);
});
it("should not throw an error if the email is valid", () => {
const validEmail = "valid-email@example.com";
expect(() => checkInputEmailIsValid(validEmail)).not.toThrow();
});
});
describe("getOrgConnectionInfo", () => {
const orgAutoAcceptDomain = "example.com";
const usersEmail = "user@example.com";
@ -270,8 +239,8 @@ describe("Invite Member Utils", () => {
});
});
describe("throwIfInviteIsToOrgAndUserExists", () => {
const invitee: User = {
describe("validateInviteeEligibility: Check if user can be invited to the team/org", () => {
const invitee: Invitee = {
...mockUser,
id: 1,
username: "testuser",
@ -280,8 +249,8 @@ describe("Invite Member Utils", () => {
};
const isOrg = false;
it("should not throw when inviting an existing user to the same organization", () => {
const inviteeWithOrg: User = {
it("should not throw when inviting to an organization's team an existing org user", () => {
const inviteeWithOrg: Invitee = {
...invitee,
organizationId: 2,
};
@ -289,10 +258,36 @@ describe("Invite Member Utils", () => {
...mockedTeam,
parentId: 2,
};
expect(() => throwIfInviteIsToOrgAndUserExists(inviteeWithOrg, teamWithOrg, isOrg)).not.toThrow();
expect(() => validateInviteeEligibility(inviteeWithOrg, teamWithOrg, isOrg)).not.toThrow();
});
it("should throw a TRPCError when inviting a user who is already a member of the org", () => {
const inviteeWithOrg: Invitee = {
...invitee,
organizationId: 1,
};
const teamWithOrg = {
...mockedTeam,
id: 1,
};
expect(() => validateInviteeEligibility(inviteeWithOrg, teamWithOrg, isOrg)).toThrow(TRPCError);
});
it("should throw a TRPCError when inviting a user who is already a member of the team", () => {
const inviteeWithOrg: UserWithMembership = {
...invitee,
organizationId: null,
teams: [{ teamId: 1, accepted: true, userId: invitee.id }],
};
const teamWithOrg = {
...mockedTeam,
id: 1,
};
expect(() => validateInviteeEligibility(inviteeWithOrg, teamWithOrg, isOrg)).toThrow(TRPCError);
});
it("should throw a TRPCError with code FORBIDDEN if the invitee is already a member of another organization", () => {
const inviteeWithOrg: User = {
const inviteeWithOrg: Invitee = {
...invitee,
organizationId: 2,
};
@ -300,36 +295,48 @@ describe("Invite Member Utils", () => {
...mockedTeam,
parentId: 3,
};
expect(() => throwIfInviteIsToOrgAndUserExists(inviteeWithOrg, teamWithOrg, isOrg)).toThrow(TRPCError);
expect(() => validateInviteeEligibility(inviteeWithOrg, teamWithOrg, isOrg)).toThrow(TRPCError);
});
it("should throw a TRPCError with code FORBIDDEN if the invitee already exists in Cal.com and is being invited to an organization", () => {
const isOrg = true;
expect(() => throwIfInviteIsToOrgAndUserExists(invitee, mockedTeam, isOrg)).toThrow(TRPCError);
expect(() => validateInviteeEligibility(invitee, mockedTeam, isOrg)).toThrow(TRPCError);
});
it("should not throw an error if the invitee does not already belong to another organization and is not being invited to an organization", () => {
expect(() => throwIfInviteIsToOrgAndUserExists(invitee, mockedTeam, isOrg)).not.toThrow();
expect(() => validateInviteeEligibility(invitee, mockedTeam, isOrg)).not.toThrow();
});
});
describe("createAndAutoJoinIfInOrg", () => {
describe("shouldAutoJoinIfInOrg", () => {
it("should return autoJoined: false if the user is not in the same organization as the team", async () => {
const result = await createAndAutoJoinIfInOrg({
const result = await shouldAutoJoinIfInOrg({
team: mockedTeam,
role: MembershipRole.ADMIN,
invitee: mockUser,
invitee: userInTeamAccepted,
});
expect(result).toEqual({ autoJoined: false });
expect(result).toEqual(false);
});
it("should return autoJoined: false if the team does not have a parent organization", async () => {
const result = await createAndAutoJoinIfInOrg({
const result = await shouldAutoJoinIfInOrg({
team: { ...mockedTeam, parentId: null },
role: MembershipRole.ADMIN,
invitee: mockUser,
invitee: userInTeamAccepted,
});
expect(result).toEqual({ autoJoined: false });
expect(result).toEqual(false);
});
it("should return `autoJoined: false` if team has parent organization and invitee has not accepted membership to organization", async () => {
const result = await shouldAutoJoinIfInOrg({
team: { ...mockedTeam, parentId: mockedTeam.id },
invitee: { ...userInTeamNotAccepted, organizationId: mockedTeam.id },
});
expect(result).toEqual(false);
});
it("should return `autoJoined: true` if team has parent organization and invitee has accepted membership to organization", async () => {
const result = await shouldAutoJoinIfInOrg({
team: { ...mockedTeam, parentId: mockedTeam.id },
invitee: { ...userInTeamAccepted, organizationId: mockedTeam.id },
});
expect(result).toEqual(true);
});
// TODO: Add test for when the user is already a member of the organization - need to mock prisma response value
});
});

View File

@ -3,11 +3,12 @@ import type { TFunction } from "next-i18next";
import { sendTeamInviteEmail, sendOrganizationAutoJoinEmail } from "@calcom/emails";
import { WEBAPP_URL } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { isTeamAdmin } from "@calcom/lib/server/queries";
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
import slugify from "@calcom/lib/slugify";
import { prisma } from "@calcom/prisma";
import type { Team } from "@calcom/prisma/client";
import type { Membership, Team } from "@calcom/prisma/client";
import { Prisma, type User } from "@calcom/prisma/client";
import type { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
@ -18,6 +19,15 @@ import type { TrpcSessionUser } from "../../../../trpc";
import { isEmail } from "../util";
import type { InviteMemberOptions, TeamWithParent } from "./types";
export type Invitee = Pick<
User,
"id" | "email" | "organizationId" | "username" | "password" | "identityProvider" | "completedOnboarding"
>;
export type UserWithMembership = Invitee & {
teams?: Pick<Membership, "userId" | "teamId" | "accepted">[];
};
export async function checkPermissions({
userId,
teamId,
@ -53,7 +63,9 @@ export async function getTeamOrThrow(teamId: number, isOrg?: boolean) {
}
export async function getEmailsToInvite(usernameOrEmail: string | string[]) {
const emailsToInvite = Array.isArray(usernameOrEmail) ? usernameOrEmail : [usernameOrEmail];
const emailsToInvite = Array.isArray(usernameOrEmail)
? Array.from(new Set(usernameOrEmail))
: [usernameOrEmail];
if (emailsToInvite.length === 0) {
throw new TRPCError({
@ -65,43 +77,102 @@ export async function getEmailsToInvite(usernameOrEmail: string | string[]) {
return emailsToInvite;
}
export async function getUserToInviteOrThrowIfExists({
usernameOrEmail,
teamId,
isOrg,
}: {
usernameOrEmail: string;
teamId: number;
isOrg?: boolean;
}) {
// Check if user exists in ORG or exists all together
const orgWhere = isOrg && {
organizationId: teamId,
};
const invitee = await prisma.user.findFirst({
where: {
OR: [{ username: usernameOrEmail, ...orgWhere }, { email: usernameOrEmail }],
},
});
// We throw on error cause we can't have two users in the same org with the same username
if (isOrg && invitee) {
export function validateInviteeEligibility(
invitee: UserWithMembership,
team: TeamWithParent,
isOrg: boolean
) {
const alreadyInvited = invitee.teams?.find(({ teamId: membershipTeamId }) => team.id === membershipTeamId);
if (alreadyInvited) {
throw new TRPCError({
code: "NOT_FOUND",
message: `Email ${usernameOrEmail} already exists, you can't invite existing users.`,
code: "BAD_REQUEST",
message: `${invitee.email} has already been invited.`,
});
}
return invitee;
const orgMembership = invitee.teams?.find((membersip) => membersip.teamId === team.parentId);
// invitee is invited to the org's team and is already part of the organization
if (invitee.organizationId && team.parentId && invitee.organizationId === team.parentId) {
return;
}
// user invited to join a team inside an org, but has not accepted invite to org yet
if (team.parentId && orgMembership && !orgMembership.accepted) {
throw new TRPCError({
code: "FORBIDDEN",
message: `User ${invitee.username} needs to accept the invitation to join your organization first.`,
});
}
// user is invited to join a team which is not in his organization
if (invitee.organizationId && invitee.organizationId !== team.parentId) {
throw new TRPCError({
code: "FORBIDDEN",
message: `User ${invitee.username} is already a member of another organization.`,
});
}
if (invitee && isOrg) {
throw new TRPCError({
code: "FORBIDDEN",
message: `You cannot add a user that already exists in Cal.com to an organization. If they wish to join via this email address, they must update their email address in their profile to that of your organization.`,
});
}
if (team.parentId && invitee) {
throw new TRPCError({
code: "FORBIDDEN",
message: `You cannot add a user that already exists in Cal.com to an organization's team. If they wish to join via this email address, they must update their email address in their profile to that of your organization.`,
});
}
}
export function checkInputEmailIsValid(email: string) {
if (!isEmail(email))
throw new TRPCError({
code: "BAD_REQUEST",
message: `Invite failed because ${email} is not a valid email address`,
});
export async function getUsersToInvite({
usernameOrEmail,
isInvitedToOrg,
team,
}: {
usernameOrEmail: string[];
isInvitedToOrg: boolean;
team: TeamWithParent;
}): Promise<UserWithMembership[]> {
const orgWhere = isInvitedToOrg && {
organizationId: team.id,
};
const memberships = [];
if (isInvitedToOrg) {
memberships.push({ teamId: team.id });
} else {
memberships.push({ teamId: team.id });
team.parentId && memberships.push({ teamId: team.parentId });
}
const invitees: UserWithMembership[] = await prisma.user.findMany({
where: {
OR: [{ username: { in: usernameOrEmail }, ...orgWhere }, { email: { in: usernameOrEmail } }],
},
select: {
id: true,
email: true,
organizationId: true,
username: true,
password: true,
completedOnboarding: true,
identityProvider: true,
teams: {
select: { teamId: true, userId: true, accepted: true },
where: {
OR: memberships,
},
},
},
});
// Check if the users found in the database can be invited to join the team/org
invitees.forEach((invitee) => {
validateInviteeEligibility(invitee, team, isInvitedToOrg);
});
return invitees;
}
export function getOrgConnectionInfo({
@ -133,84 +204,92 @@ export function getOrgConnectionInfo({
return { orgId, autoAccept };
}
export async function createNewUserConnectToOrgIfExists({
usernameOrEmail,
export async function createNewUsersConnectToOrgIfExists({
usernamesOrEmails,
input,
parentId,
autoAcceptEmailDomain,
connectionInfo,
connectionInfoMap,
}: {
usernameOrEmail: string;
usernamesOrEmails: string[];
input: InviteMemberOptions["input"];
parentId?: number | null;
autoAcceptEmailDomain?: string;
connectionInfo: ReturnType<typeof getOrgConnectionInfo>;
connectionInfoMap: Record<string, ReturnType<typeof getOrgConnectionInfo>>;
}) {
const { orgId, autoAccept } = connectionInfo;
await prisma.$transaction(async (tx) => {
for (let index = 0; index < usernamesOrEmails.length; index++) {
const usernameOrEmail = usernamesOrEmails[index];
const { orgId, autoAccept } = connectionInfoMap[usernameOrEmail];
const [emailUser, emailDomain] = usernameOrEmail.split("@");
const username =
emailDomain === autoAcceptEmailDomain
? slugify(emailUser)
: slugify(`${emailUser}-${emailDomain.split(".")[0]}`);
const [emailUser, emailDomain] = usernameOrEmail.split("@");
const username =
emailDomain === autoAcceptEmailDomain
? slugify(emailUser)
: slugify(`${emailUser}-${emailDomain.split(".")[0]}`);
const createdUser = await prisma.user.create({
data: {
username,
email: usernameOrEmail,
verified: true,
invitedTo: input.teamId,
organizationId: orgId || null, // If the user is invited to a child team, they are automatically added to the parent org
teams: {
create: {
teamId: input.teamId,
role: input.role as MembershipRole,
accepted: autoAccept, // If the user is invited to a child team, they are automatically accepted
const createdUser = await tx.user.create({
data: {
username,
email: usernameOrEmail,
verified: true,
invitedTo: input.teamId,
organizationId: orgId || null, // If the user is invited to a child team, they are automatically added to the parent org
teams: {
create: {
teamId: input.teamId,
role: input.role as MembershipRole,
accepted: autoAccept, // If the user is invited to a child team, they are automatically accepted
},
},
},
},
},
});
});
// We also need to create the membership in the parent org if it exists
if (parentId) {
await prisma.membership.create({
data: {
teamId: parentId,
userId: createdUser.id,
role: input.role as MembershipRole,
accepted: autoAccept,
},
});
}
// We also need to create the membership in the parent org if it exists
if (parentId) {
await tx.membership.create({
data: {
teamId: parentId,
userId: createdUser.id,
role: input.role as MembershipRole,
accepted: autoAccept,
},
});
}
}
});
}
export async function createProvisionalMembership({
export async function createProvisionalMemberships({
input,
invitee,
invitees,
parentId,
}: {
input: InviteMemberOptions["input"];
invitee: User;
invitees: UserWithMembership[];
parentId?: number;
}) {
try {
await prisma.membership.create({
data: {
teamId: input.teamId,
userId: invitee.id,
role: input.role as MembershipRole,
},
});
// Create the membership in the parent also if it exists
if (parentId) {
await prisma.membership.create({
data: {
teamId: parentId,
await prisma.membership.createMany({
data: invitees.flatMap((invitee) => {
const data = [];
// membership for the team
data.push({
teamId: input.teamId,
userId: invitee.id,
role: input.role as MembershipRole,
},
});
}
});
// membership for the org
if (parentId) {
data.push({
teamId: parentId,
userId: invitee.id,
role: input.role as MembershipRole,
});
}
return data;
}),
});
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
// Don't throw an error if the user is already a member of the team when inviting multiple users
@ -219,9 +298,13 @@ export async function createProvisionalMembership({
code: "FORBIDDEN",
message: "This user is a member of this team / has a pending invitation.",
});
} else {
console.log(`User ${invitee.id} is already a member of this team.`);
} else if (Array.isArray(input.usernameOrEmail) && e.code === "P2002") {
throw new TRPCError({
code: "FORBIDDEN",
message: "Trying to invite users already members of this team / have pending invitations",
});
}
logger.error("Failed to create provisional memberships", input.teamId);
} else throw e;
}
}
@ -282,26 +365,6 @@ export async function sendVerificationEmail({
}
}
export function throwIfInviteIsToOrgAndUserExists(invitee: User, team: TeamWithParent, isOrg: boolean) {
if (invitee.organizationId && invitee.organizationId === team.parentId) {
return;
}
if (invitee.organizationId && invitee.organizationId !== team.parentId) {
throw new TRPCError({
code: "FORBIDDEN",
message: `User ${invitee.username} is already a member of another organization.`,
});
}
if ((invitee && isOrg) || (team.parentId && invitee)) {
throw new TRPCError({
code: "FORBIDDEN",
message: `You cannot add a user that already exists in Cal.com to an organization. If they wish to join via this email address, they must update their email address in their profile to that of your organization.`,
});
}
}
export function getIsOrgVerified(
isOrg: boolean,
team: Team & {
@ -331,52 +394,125 @@ export function getIsOrgVerified(
} as { isInOrgScope: false; orgVerified: never; autoAcceptEmailDomain: never };
}
export async function createAndAutoJoinIfInOrg({
export function shouldAutoJoinIfInOrg({
team,
role,
invitee,
}: {
team: TeamWithParent;
invitee: User;
role: MembershipRole;
invitee: UserWithMembership;
}) {
// Not a member of the org
if (invitee.organizationId && invitee.organizationId !== team.parentId) {
return {
autoJoined: false,
};
return false;
}
// team is an Org
if (!team.parentId) {
return {
autoJoined: false,
};
return false;
}
const orgMembership = await prisma.membership.findFirst({
where: {
userId: invitee.id,
teamId: team.parentId,
},
});
const orgMembership = invitee.teams?.find((membership) => membership.teamId === team.parentId);
if (!orgMembership?.accepted) {
return {
autoJoined: false,
};
return false;
}
// Since we early return if the user is not a member of the org. Or the team they are being invited to is an org (not having a parentID)
// We create the membership in the child team
await prisma.membership.create({
data: {
userId: invitee.id,
teamId: team.id,
accepted: true,
role: role,
},
return true;
}
// split invited users between ones that can autojoin and the others who cannot autojoin
export const groupUsersByJoinability = ({
existingUsersWithMembersips,
team,
}: {
team: TeamWithParent;
existingUsersWithMembersips: UserWithMembership[];
}) => {
const usersToAutoJoin = [];
const regularUsers = [];
for (let index = 0; index < existingUsersWithMembersips.length; index++) {
const existingUserWithMembersips = existingUsersWithMembersips[index];
const canAutojoin = shouldAutoJoinIfInOrg({
invitee: existingUserWithMembersips,
team,
});
canAutojoin
? usersToAutoJoin.push(existingUserWithMembersips)
: regularUsers.push(existingUserWithMembersips);
}
return [usersToAutoJoin, regularUsers];
};
export const sendEmails = async (emailPromises: Promise<void>[]) => {
const sentEmails = await Promise.allSettled(emailPromises);
sentEmails.forEach((sentEmail) => {
if (sentEmail.status === "rejected") {
logger.error("Could not send email to user");
}
});
};
export const sendTeamInviteEmails = async ({
existingUsersWithMembersips,
language,
currentUserTeamName,
currentUserName,
isOrg,
teamId,
}: {
language: TFunction;
existingUsersWithMembersips: UserWithMembership[];
currentUserTeamName?: string;
currentUserName?: string | null;
isOrg: boolean;
teamId: number;
}) => {
const sendEmailsPromises = existingUsersWithMembersips.map(async (user) => {
let sendTo = user.email;
if (!isEmail(user.email)) {
sendTo = user.email;
}
// inform user of membership by email
if (currentUserName && currentUserTeamName) {
const inviteTeamOptions = {
joinLink: `${WEBAPP_URL}/auth/login?callbackUrl=/settings/teams`,
isCalcomMember: true,
};
/**
* Here we want to redirect to a different place if onboarding has been completed or not. This prevents the flash of going to teams -> Then to onboarding - also show a different email template.
* This only changes if the user is a CAL user and has not completed onboarding and has no password
*/
if (!user.completedOnboarding && !user.password && user.identityProvider === "CAL") {
const token = randomBytes(32).toString("hex");
await prisma.verificationToken.create({
data: {
identifier: user.email,
token,
expires: new Date(new Date().setHours(168)), // +1 week
team: {
connect: {
id: teamId,
},
},
},
});
inviteTeamOptions.joinLink = `${WEBAPP_URL}/signup?token=${token}&callbackUrl=/getting-started`;
inviteTeamOptions.isCalcomMember = false;
}
return sendTeamInviteEmail({
language,
from: currentUserName,
to: sendTo,
teamName: currentUserTeamName,
...inviteTeamOptions,
isOrg: isOrg,
});
}
});
return {
autoJoined: true,
};
}
await sendEmails(sendEmailsPromises);
};

View File

@ -217,7 +217,7 @@ const InstallAppButtonChild = ({
StartIcon={Plus}
data-testid="install-app-button"
{...props}>
{paid.trial ? t("start_paid_trial") : t("install_paid_app")}
{paid.trial ? t("start_paid_trial") : t("subscribe")}
</Button>
);
}