Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a48a1bd5d8 | ||
|
|
1acd0a14ba | ||
|
|
6c9ac7636b | ||
|
|
d261a23dbc | ||
|
|
8b7947a172 | ||
|
|
89789b1e3b | ||
|
|
43dd24c480 | ||
|
|
ae6b1d402a | ||
|
|
3054ad33b2 | ||
|
|
a4dbb1a90b | ||
|
|
689150d78a | ||
|
|
fba096f23d | ||
|
|
7d5a3391f1 | ||
|
|
dcef99bf49 | ||
|
|
805fcc22a5 | ||
|
|
c5b7da8ffd | ||
|
|
97f1f7b130 | ||
|
|
0ac460fce3 | ||
|
|
82dfca8272 | ||
|
|
d1c5576d4f | ||
|
|
7090e3ed60 | ||
|
|
9926fd2e79 | ||
|
|
7793cb20d1 | ||
|
|
8ed0fb6a07 | ||
|
|
f5a1222fc5 | ||
|
|
2b892296b0 | ||
|
|
f796802ed3 | ||
|
|
b2ac227877 | ||
|
|
f0aec4efdc | ||
|
|
8d7cdf9d3b | ||
|
|
2ed0380e4f | ||
|
|
752cfa6bf9 | ||
|
|
f11c4dd7ec | ||
|
|
386f60ca6d | ||
|
|
e98f279f64 | ||
|
|
fe190f20f3 | ||
|
|
96855ea391 | ||
|
|
57150d992e | ||
|
|
8b75bf347f | ||
|
|
788fbdbe5e | ||
|
|
b2239374bc | ||
|
|
77c61abef8 | ||
|
|
c2472cc05a |
@@ -1,9 +0,0 @@
|
||||
node_modules
|
||||
apps/api/v2/dist
|
||||
packages/platform/**/dist/*
|
||||
**/**/node_modules
|
||||
**/**/.next
|
||||
**/**/public
|
||||
packages/prisma/zod
|
||||
apps/web/public/embed
|
||||
packages/ui/components/icon/dynamicIconImports.tsx
|
||||
-115
@@ -1,115 +0,0 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
extends: ["./packages/config/eslint-preset.js"],
|
||||
plugins: ["import"],
|
||||
rules: {
|
||||
"import/no-cycle": ["warn", { maxDepth: Infinity }],
|
||||
},
|
||||
overrides: [
|
||||
// WARN: features must not be imported by app-store or lib
|
||||
{
|
||||
files: ["packages/app-store/**/*.{ts,tsx,js,jsx}", "packages/lib/**/*.{ts,tsx,js,jsx}"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"warn",
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: [
|
||||
// Catch all relative paths into features
|
||||
"**/features",
|
||||
"**/features/*",
|
||||
// Catch all alias imports
|
||||
"@calcom/features",
|
||||
"@calcom/features/*",
|
||||
],
|
||||
message: "Avoid importing @calcom/features from @calcom/app-store or @calcom/lib.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// WARN: lib must not import app-store or features
|
||||
{
|
||||
files: ["packages/lib/**/*.{ts,tsx,js,jsx}"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"warn",
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: [
|
||||
// Catch all relative paths into app-store
|
||||
"**/app-store",
|
||||
"**/app-store/*",
|
||||
// Catch all relative paths into features
|
||||
"**/features",
|
||||
"**/features/*",
|
||||
// Catch alias imports
|
||||
"@calcom/app-store",
|
||||
"@calcom/app-store/*",
|
||||
"@calcom/features",
|
||||
"@calcom/features/*",
|
||||
],
|
||||
message: "@calcom/lib should not import @calcom/app-store or @calcom/features.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// ERROR: app-store must not import trpc
|
||||
{
|
||||
files: ["packages/app-store/**/*.{ts,tsx,js,jsx}"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: [
|
||||
// Catch all relative paths into trpc
|
||||
"**/trpc",
|
||||
"**/trpc/*",
|
||||
// Catch alias imports
|
||||
"@calcom/trpc",
|
||||
"@calcom/trpc/*",
|
||||
"@trpc",
|
||||
"@trpc/*",
|
||||
],
|
||||
message:
|
||||
"@calcom/app-store must not import trpc. Move UI to apps/web/components/apps or introduce an API boundary.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// ERROR: prisma must not import `features` package
|
||||
{
|
||||
files: ["packages/prisma/**/*.{ts,tsx,js,jsx}"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: [
|
||||
// Catch all relative paths into features
|
||||
"**/features",
|
||||
"**/features/*",
|
||||
// Catch all alias imports
|
||||
"@calcom/features",
|
||||
"@calcom/features/*",
|
||||
],
|
||||
message: "Avoid importing @calcom/features from @calcom/prisma.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/**/package.json @calcom/Foundation
|
||||
/apps/api/**/* @calcom/Platform @calcom/Foundation
|
||||
/apps/api/**/* @calcom/API
|
||||
/apps/ui-playground/**/* @calcom/Consumer @calcom/Foundation
|
||||
/apps/web/**/layout.tsx @calcom/Consumer @calcom/Foundation
|
||||
/apps/web/lib/csp.ts @calcom/Foundation
|
||||
|
||||
@@ -74,14 +74,8 @@ jobs:
|
||||
uses: ./.github/workflows/e2e-embed-react.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-atoms:
|
||||
name: Tests
|
||||
needs: [lint, build, build-atoms, build-api-v2]
|
||||
uses: ./.github/workflows/e2e-atoms.yml
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs: [lint, type-check, unit-test, integration-test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store, e2e-atoms]
|
||||
needs: [lint, type-check, unit-test, integration-test, build, build-api-v1, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
if: always()
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: E2E
|
||||
name: Check breaking changes and run E2E
|
||||
on:
|
||||
workflow_call:
|
||||
env:
|
||||
@@ -23,6 +23,10 @@ env:
|
||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||
SLOTS_CACHE_TTL: ${{ secrets.CI_SLOTS_CACHE_TTL }}
|
||||
NEXT_PUBLIC_VAPID_PUBLIC_KEY: ${{ secrets.NEXT_PUBLIC_VAPID_PUBLIC_KEY }}
|
||||
VAPID_PRIVATE_KEY: ${{ secrets.VAPID_PRIVATE_KEY }}
|
||||
JWT_SECRET: ${{ secrets.CI_JWT_SECRET }}
|
||||
NODE_ENV: ${{ vars.CI_NODE_ENV }}
|
||||
jobs:
|
||||
e2e:
|
||||
timeout-minutes: 20
|
||||
@@ -68,6 +72,18 @@ jobs:
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
|
||||
- name: Generate Swagger
|
||||
working-directory: apps/api/v2
|
||||
run: yarn generate-swagger
|
||||
|
||||
- name: Check breaking changes
|
||||
uses: oasdiff/oasdiff-action/breaking@main
|
||||
with:
|
||||
base: https://raw.githubusercontent.com/calcom/cal.com/refs/heads/main/docs/api-reference/v2/openapi.json
|
||||
revision: docs/api-reference/v2/openapi.json
|
||||
fail-on: WARN
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: apps/api/v2
|
||||
run: |
|
||||
@@ -75,6 +91,7 @@ jobs:
|
||||
EXIT_CODE=$?
|
||||
echo "yarn workspace @calcom/platform-libraries build && yarn test:e2e command exit code: $EXIT_CODE"
|
||||
exit $EXIT_CODE
|
||||
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
with:
|
||||
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
||||
organization-name: calcom
|
||||
ignore-labels: "admin, app-store, ai, authentication, automated-testing, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, dba, devops, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, self-hosted, teams, webhooks, workflows, zapier"
|
||||
ignore-labels: "admin, app-store, ai, api, authentication, automated-testing, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, dba, devops, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, self-hosted, sre, teams, webhooks, workflows, zapier"
|
||||
apply-labels-from-issue:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
@@ -195,13 +195,6 @@ jobs:
|
||||
uses: ./.github/workflows/e2e-embed-react.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-atoms:
|
||||
name: Tests
|
||||
needs: [changes, check-label, build, build-atoms, build-api-v2]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e-atoms.yml
|
||||
secrets: inherit
|
||||
|
||||
analyze:
|
||||
name: Analyze Build
|
||||
needs: [build]
|
||||
@@ -211,7 +204,7 @@ jobs:
|
||||
merge-reports:
|
||||
name: Merge reports
|
||||
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
needs: [changes, check-label, e2e, e2e-embed, e2e-embed-react, e2e-app-store, e2e-atoms]
|
||||
needs: [changes, check-label, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
uses: ./.github/workflows/merge-reports.yml
|
||||
secrets: inherit
|
||||
|
||||
@@ -255,8 +248,7 @@ jobs:
|
||||
e2e-api-v2,
|
||||
e2e-embed,
|
||||
e2e-embed-react,
|
||||
e2e-app-store,
|
||||
e2e-atoms,
|
||||
e2e-app-store
|
||||
]
|
||||
if: always()
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
|
||||
@@ -16,5 +16,4 @@ public
|
||||
packages/prisma/zod
|
||||
packages/prisma/enums
|
||||
apps/web/public/embed
|
||||
apps/api/v2/swagger/documentation.json
|
||||
packages/ui/components/icon/dynamicIconImports.tsx
|
||||
|
||||
Vendored
@@ -13,5 +13,8 @@
|
||||
"connect": "^3.7.0",
|
||||
"http": "^0.0.1-security",
|
||||
"http-proxy-middleware": "^2.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/eslint-config": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { nextJsConfig } from "@calcom/eslint-config/next-js";
|
||||
|
||||
export default nextJsConfig;
|
||||
+11
-10
@@ -10,7 +10,7 @@
|
||||
"build": "next build",
|
||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
|
||||
"dev": "PORT=3003 next dev",
|
||||
"lint": "eslint . --ignore-path .gitignore",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||
"start": "PORT=3003 next start",
|
||||
"docker-start-api": "PORT=80 next start",
|
||||
@@ -18,18 +18,19 @@
|
||||
"type-check:ci": "tsc-absolute --pretty --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/tsconfig": "*",
|
||||
"@calcom/types": "*",
|
||||
"@calcom/eslint-config": "workspace:*",
|
||||
"@calcom/tsconfig": "workspace:*",
|
||||
"@calcom/types": "workspace:*",
|
||||
"node-mocks-http": "^1.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/app-store": "*",
|
||||
"@calcom/dayjs": "*",
|
||||
"@calcom/emails": "*",
|
||||
"@calcom/features": "*",
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/prisma": "*",
|
||||
"@calcom/trpc": "*",
|
||||
"@calcom/app-store": "workspace:*",
|
||||
"@calcom/dayjs": "workspace:*",
|
||||
"@calcom/emails": "workspace:*",
|
||||
"@calcom/features": "workspace:*",
|
||||
"@calcom/lib": "workspace:*",
|
||||
"@calcom/prisma": "workspace:*",
|
||||
"@calcom/trpc": "workspace:*",
|
||||
"@prisma/nextjs-monorepo-workaround-plugin": "^6.16.1",
|
||||
"@sentry/nextjs": "^9.15.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
|
||||
@@ -59,7 +59,7 @@ export async function getHandler(req: NextApiRequest) {
|
||||
};
|
||||
}
|
||||
|
||||
const [total, data] = await prisma.$transaction([
|
||||
const [total, data] = await Promise.all([
|
||||
prisma.user.count({ where }),
|
||||
prisma.user.findMany({ where, take, skip }),
|
||||
]);
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
module.exports = {
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: "tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: ["@typescript-eslint/eslint-plugin"],
|
||||
extends: ["plugin:@typescript-eslint/recommended"],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: [".eslintrc.js", "next-i18next.config.js"],
|
||||
rules: {
|
||||
"@typescript-eslint/interface-name-prefix": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["./src/**/*.controller.ts"],
|
||||
excludedFiles: "*.spec.js",
|
||||
rules: {
|
||||
"@typescript-eslint/explicit-function-return-type": "error",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -16,7 +16,7 @@
|
||||
"_dev:build:watch:enums": "yarn workspace @calcom/platform-enums build:watch",
|
||||
"_dev:build:watch:utils": "yarn workspace @calcom/platform-utils build:watch",
|
||||
"_dev:build:watch:types": "yarn workspace @calcom/platform-types build:watch",
|
||||
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build",
|
||||
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build && yarn workspace @calcom/platform-libraries build",
|
||||
"dev": "yarn dev:build && ts-node scripts/docker-start.ts && yarn copy-swagger-module && yarn start --watch",
|
||||
"dev:no-docker": "yarn dev:build && yarn copy-swagger-module && yarn start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
@@ -30,18 +30,19 @@
|
||||
"test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.ts --watch",
|
||||
"prisma": "yarn workspace @calcom/prisma prisma",
|
||||
"generate-schemas": "yarn prisma generate && yarn prisma format",
|
||||
"copy-swagger-module": "ts-node -r tsconfig-paths/register swagger/copy-swagger-module.ts",
|
||||
"copy-swagger-module": "ts-node -r tsconfig-paths/register src/swagger/copy-swagger-module.ts",
|
||||
"generate-swagger": "yarn copy-swagger-module && yarn build && node ./dist/apps/api/v2/src/swagger/generate-swagger-script.js",
|
||||
"prepare": "yarn run snyk-protect",
|
||||
"snyk-protect": "snyk-protect"
|
||||
},
|
||||
"dependencies": {
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "*",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
"@calcom/platform-constants": "workspace:*",
|
||||
"@calcom/platform-enums": "workspace:*",
|
||||
"@calcom/platform-libraries": "workspace:*",
|
||||
"@calcom/platform-types": "workspace:*",
|
||||
"@calcom/platform-utils": "workspace:*",
|
||||
"@calcom/prisma": "workspace:*",
|
||||
"@golevelup/ts-jest": "^0.4.0",
|
||||
"@microsoft/microsoft-graph-types-beta": "^0.42.0-preview",
|
||||
"@nest-lab/throttler-storage-redis": "1.0.0",
|
||||
@@ -88,6 +89,7 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/eslint-config": "workspace:*",
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { ReassignBookingOutput_2024_08_13 as ReassignBookingOutputData_2024_08_13 } from "@calcom/platform-types";
|
||||
|
||||
@ApiExtraModels(ReassignBookingOutputData_2024_08_13)
|
||||
export class ReassignBookingOutput_2024_08_13 {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
|
||||
@@ -7,8 +7,6 @@ import {
|
||||
BookingOutput_2024_08_13,
|
||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
||||
CreateSeatedBookingOutput_2024_08_13,
|
||||
GetRecurringSeatedBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
|
||||
+12
-108
@@ -1,39 +1,29 @@
|
||||
import type { AppConfig } from "@/config/type";
|
||||
import { getEnv } from "@/env";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import type { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
|
||||
import {
|
||||
PathItemObject,
|
||||
PathsObject,
|
||||
OperationObject,
|
||||
TagObject,
|
||||
} from "@nestjs/swagger/dist/interfaces/open-api-spec.interface";
|
||||
import "dotenv/config";
|
||||
import * as fs from "fs";
|
||||
import { Server } from "http";
|
||||
import { WinstonModule } from "nest-winston";
|
||||
|
||||
import { bootstrap } from "./app";
|
||||
import { AppModule } from "./app.module";
|
||||
import { loggerConfig } from "./lib/logger";
|
||||
import { generateSwaggerForApp } from "./swagger/generate-swagger";
|
||||
|
||||
const HttpMethods: (keyof PathItemObject)[] = ["get", "post", "put", "delete", "patch", "options", "head"];
|
||||
|
||||
const run = async () => {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
||||
logger: WinstonModule.createLogger(loggerConfig()),
|
||||
bodyParser: false,
|
||||
});
|
||||
run().catch((error: Error) => {
|
||||
console.error("Failed to start Cal Platform API", { error: error.stack });
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
async function run() {
|
||||
const app = await createNestApp();
|
||||
const logger = new Logger("App");
|
||||
|
||||
try {
|
||||
bootstrap(app);
|
||||
const port = app.get(ConfigService<AppConfig, true>).get("api.port", { infer: true });
|
||||
void generateSwagger(app);
|
||||
generateSwaggerForApp(app);
|
||||
await app.listen(port);
|
||||
logger.log(`Application started on port: ${port}`);
|
||||
} catch (error) {
|
||||
@@ -42,97 +32,11 @@ const run = async () => {
|
||||
error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function customTagSort(a: string, b: string): number {
|
||||
const platformPrefix = "Platform";
|
||||
const orgsPrefix = "Orgs";
|
||||
|
||||
if (a.startsWith(platformPrefix) && !b.startsWith(platformPrefix)) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.startsWith(platformPrefix) && b.startsWith(platformPrefix)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.startsWith(orgsPrefix) && !b.startsWith(orgsPrefix)) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.startsWith(orgsPrefix) && b.startsWith(orgsPrefix)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
|
||||
function isOperationObject(obj: any): obj is OperationObject {
|
||||
return obj && typeof obj === "object" && "tags" in obj;
|
||||
}
|
||||
|
||||
function groupAndSortPathsByFirstTag(paths: PathsObject): PathsObject {
|
||||
const groupedPaths: { [key: string]: PathsObject } = {};
|
||||
|
||||
Object.keys(paths).forEach((pathKey) => {
|
||||
const pathItem = paths[pathKey];
|
||||
|
||||
HttpMethods.forEach((method) => {
|
||||
const operation = pathItem[method];
|
||||
|
||||
if (isOperationObject(operation) && operation.tags && operation.tags.length > 0) {
|
||||
const firstTag = operation.tags[0];
|
||||
|
||||
if (!groupedPaths[firstTag]) {
|
||||
groupedPaths[firstTag] = {};
|
||||
}
|
||||
|
||||
groupedPaths[firstTag][pathKey] = pathItem;
|
||||
}
|
||||
});
|
||||
export async function createNestApp() {
|
||||
return NestFactory.create<NestExpressApplication>(AppModule, {
|
||||
logger: WinstonModule.createLogger(loggerConfig()),
|
||||
bodyParser: false,
|
||||
});
|
||||
|
||||
const sortedTags = Object.keys(groupedPaths).sort(customTagSort);
|
||||
const sortedPaths: PathsObject = {};
|
||||
|
||||
sortedTags.forEach((tag) => {
|
||||
Object.assign(sortedPaths, groupedPaths[tag]);
|
||||
});
|
||||
|
||||
return sortedPaths;
|
||||
}
|
||||
|
||||
async function generateSwagger(app: NestExpressApplication<Server>) {
|
||||
const logger = new Logger("App");
|
||||
logger.log(`Generating Swagger documentation...\n`);
|
||||
|
||||
const config = new DocumentBuilder().setTitle("Cal.com API v2").build();
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
document.paths = groupAndSortPathsByFirstTag(document.paths);
|
||||
|
||||
const swaggerOutputFile = "./swagger/documentation.json";
|
||||
const docsOutputFile = "../../../docs/api-reference/v2/openapi.json";
|
||||
const stringifiedContents = JSON.stringify(document, null, 2);
|
||||
|
||||
if (fs.existsSync(swaggerOutputFile)) {
|
||||
fs.unlinkSync(swaggerOutputFile);
|
||||
}
|
||||
|
||||
fs.writeFileSync(swaggerOutputFile, stringifiedContents, { encoding: "utf8" });
|
||||
|
||||
if (fs.existsSync(docsOutputFile) && getEnv("NODE_ENV") === "development") {
|
||||
fs.unlinkSync(docsOutputFile);
|
||||
fs.writeFileSync(docsOutputFile, stringifiedContents, { encoding: "utf8" });
|
||||
}
|
||||
|
||||
if (!process.env.DOCS_URL) {
|
||||
SwaggerModule.setup("docs", app, document, {
|
||||
customCss: ".swagger-ui .topbar { display: none }",
|
||||
});
|
||||
|
||||
logger.log(`Swagger documentation available in the "/docs" endpoint\n`);
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((error: Error) => {
|
||||
console.error("Failed to start Cal Platform API", { error: error.stack });
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings
|
||||
import { BillingProcessor } from "@/modules/billing/billing.processor";
|
||||
import { BillingRepository } from "@/modules/billing/billing.repository";
|
||||
import { BillingController } from "@/modules/billing/controllers/billing.controller";
|
||||
import { BillingServiceCachingProxy } from "@/modules/billing/services/billing-service-caching-proxy";
|
||||
import { BillingConfigService } from "@/modules/billing/services/billing.config.service";
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { ManagedOrganizationsBillingService } from "@/modules/billing/services/managed-organizations.billing.service";
|
||||
@@ -9,6 +10,7 @@ import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { BullModule } from "@nestjs/bull";
|
||||
@@ -18,6 +20,7 @@ import { Module } from "@nestjs/common";
|
||||
imports: [
|
||||
PrismaModule,
|
||||
StripeModule,
|
||||
RedisModule,
|
||||
MembershipsModule,
|
||||
OrganizationsModule,
|
||||
BullModule.registerQueue({
|
||||
@@ -32,6 +35,10 @@ import { Module } from "@nestjs/common";
|
||||
providers: [
|
||||
BillingConfigService,
|
||||
BillingService,
|
||||
{
|
||||
provide: "IBillingService",
|
||||
useClass: BillingServiceCachingProxy,
|
||||
},
|
||||
BillingRepository,
|
||||
BillingProcessor,
|
||||
ManagedOrganizationsBillingService,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CheckPlatformBillingResponseDto } from "@/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
@@ -16,7 +17,7 @@ import { OrganizationRepositoryFixture } from "test/fixtures/repository/organiza
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
import { withNextAuth } from "test/utils/withNextAuth";
|
||||
|
||||
import type { Team, PlatformBilling } from "@calcom/prisma/client";
|
||||
|
||||
@@ -31,9 +32,8 @@ describe("Platform Billing Controller (e2e)", () => {
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture;
|
||||
let organization: Team;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
const moduleRef = await withNextAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
@@ -44,8 +44,10 @@ describe("Platform Billing Controller (e2e)", () => {
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef);
|
||||
|
||||
organization = await organizationsRepositoryFixture.create({
|
||||
name: `billing-organization-${randomString()}`,
|
||||
isPlatform: true,
|
||||
});
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
@@ -87,6 +89,17 @@ describe("Platform Billing Controller (e2e)", () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("/billing/webhook (GET) should not get billing plan for org since it's not set yet", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/billing/${organization.id}/check`)
|
||||
|
||||
.expect(200)
|
||||
.then(async (res) => {
|
||||
const data = res.body.data as CheckPlatformBillingResponseDto;
|
||||
expect(data?.plan).toEqual("FREE");
|
||||
});
|
||||
});
|
||||
|
||||
it("/billing/webhook (POST) should set billing free plan for org", () => {
|
||||
jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(
|
||||
() =>
|
||||
@@ -119,6 +132,17 @@ describe("Platform Billing Controller (e2e)", () => {
|
||||
expect(billing?.plan).toEqual("FREE");
|
||||
});
|
||||
});
|
||||
|
||||
it("/billing/webhook (GET) should get billing plan for org", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/billing/${organization.id}/check`)
|
||||
.expect(200)
|
||||
.then(async (res) => {
|
||||
const data = res.body.data as CheckPlatformBillingResponseDto;
|
||||
expect(data?.plan).toEqual("FREE");
|
||||
});
|
||||
});
|
||||
|
||||
it("/billing/webhook (POST) failed payment should set billing free plan to overdue", () => {
|
||||
jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(
|
||||
() =>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles
|
||||
import { SubscribeToPlanInput } from "@/modules/billing/controllers/inputs/subscribe-to-plan.input";
|
||||
import { CheckPlatformBillingResponseDto } from "@/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto";
|
||||
import { SubscribeTeamToBillingResponseDto } from "@/modules/billing/controllers/outputs/SubscribeTeamToBillingResponse.dto";
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { IBillingService } from "@/modules/billing/interfaces/billing-service.interface";
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import {
|
||||
Body,
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
Headers,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Inject,
|
||||
Logger,
|
||||
Delete,
|
||||
ParseIntPipe,
|
||||
@@ -40,7 +41,7 @@ export class BillingController {
|
||||
private logger = new Logger("Billing Controller");
|
||||
|
||||
constructor(
|
||||
private readonly billingService: BillingService,
|
||||
@Inject("IBillingService") private readonly billingService: IBillingService,
|
||||
public readonly stripeService: StripeService,
|
||||
private readonly configService: ConfigService<AppConfig>
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { PlatformPlan } from "@/modules/billing/types";
|
||||
import type { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import Stripe from "stripe";
|
||||
import { PlatformBilling, Team } from "@calcom/prisma/client";
|
||||
|
||||
export type BillingData = {
|
||||
team: (Team & { platformBilling: PlatformBilling | null }) | null;
|
||||
status: "valid" | "no_subscription" | "no_billing";
|
||||
plan: string;
|
||||
};
|
||||
|
||||
export interface IBillingService {
|
||||
getBillingData(teamId: number): Promise<BillingData>;
|
||||
createTeamBilling(teamId: number): Promise<string>;
|
||||
redirectToSubscribeCheckout(teamId: number, plan: PlatformPlan, customerId?: string): Promise<string>;
|
||||
updateSubscriptionForTeam(teamId: number, plan: PlatformPlan): Promise<string>;
|
||||
cancelTeamSubscription(teamId: number): Promise<void>;
|
||||
handleStripeSubscriptionDeleted(event: Stripe.Event): Promise<void>;
|
||||
handleStripePaymentSuccess(event: Stripe.Event): Promise<void>;
|
||||
handleStripePaymentFailed(event: Stripe.Event): Promise<void>;
|
||||
handleStripePaymentPastDue(event: Stripe.Event): Promise<void>;
|
||||
handleStripeCheckoutEvents(event: Stripe.Event): Promise<void>;
|
||||
handleStripeSubscriptionForActiveManagedUsers(event: Stripe.Event): Promise<void>;
|
||||
getSubscriptionIdFromInvoice(invoice: Stripe.Invoice): string | null;
|
||||
getCustomerIdFromInvoice(invoice: Stripe.Invoice): string | null;
|
||||
stripeService: StripeService;
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { IBillingService, BillingData } from "@/modules/billing/interfaces/billing-service.interface";
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { PlatformPlan } from "@/modules/billing/types";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import Stripe from "stripe";
|
||||
|
||||
export const REDIS_BILLING_CACHE_KEY = (teamId: number) => `apiv2:team:${teamId}:billing`;
|
||||
export const BILLING_CACHE_TTL_MS = 3_600_000; // 1 hour
|
||||
|
||||
@Injectable()
|
||||
export class BillingServiceCachingProxy implements IBillingService {
|
||||
constructor(private readonly billingService: BillingService, private readonly redisService: RedisService) {}
|
||||
|
||||
async getBillingData(teamId: number) {
|
||||
const cachedBillingData = await this.getBillingCache(teamId);
|
||||
if (cachedBillingData) {
|
||||
return cachedBillingData;
|
||||
}
|
||||
|
||||
const billingData = await this.billingService.getBillingData(teamId);
|
||||
await this.setBillingCache(teamId, billingData);
|
||||
return billingData;
|
||||
}
|
||||
|
||||
private async deleteBillingCache(teamId: number) {
|
||||
await this.redisService.del(REDIS_BILLING_CACHE_KEY(teamId));
|
||||
}
|
||||
|
||||
private async getBillingCache(teamId: number) {
|
||||
const cachedResult = await this.redisService.get<BillingData>(REDIS_BILLING_CACHE_KEY(teamId));
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
private async setBillingCache(teamId: number, billingData: BillingData): Promise<void> {
|
||||
await this.redisService.set<BillingData>(REDIS_BILLING_CACHE_KEY(teamId), billingData, {
|
||||
ttl: BILLING_CACHE_TTL_MS,
|
||||
});
|
||||
}
|
||||
|
||||
async createTeamBilling(teamId: number): Promise<string> {
|
||||
return this.billingService.createTeamBilling(teamId);
|
||||
}
|
||||
|
||||
async redirectToSubscribeCheckout(
|
||||
teamId: number,
|
||||
plan: PlatformPlan,
|
||||
customerId?: string
|
||||
): Promise<string> {
|
||||
return this.billingService.redirectToSubscribeCheckout(teamId, plan, customerId);
|
||||
}
|
||||
|
||||
async updateSubscriptionForTeam(teamId: number, plan: PlatformPlan): Promise<string> {
|
||||
return this.billingService.updateSubscriptionForTeam(teamId, plan);
|
||||
}
|
||||
|
||||
async cancelTeamSubscription(teamId: number): Promise<void> {
|
||||
await this.billingService.cancelTeamSubscription(teamId);
|
||||
await this.deleteBillingCache(teamId);
|
||||
}
|
||||
|
||||
async handleStripeSubscriptionDeleted(event: Stripe.Event): Promise<void> {
|
||||
await this.billingService.handleStripeSubscriptionDeleted(event);
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
const teamId = subscription?.metadata?.teamId;
|
||||
if (teamId) {
|
||||
await this.deleteBillingCache(Number.parseInt(teamId));
|
||||
}
|
||||
}
|
||||
|
||||
async handleStripePaymentSuccess(event: Stripe.Event): Promise<void> {
|
||||
await this.billingService.handleStripePaymentSuccess(event);
|
||||
const invoice = event.data.object as Stripe.Invoice;
|
||||
const subscriptionId = this.getSubscriptionIdFromInvoice(invoice);
|
||||
if (subscriptionId) {
|
||||
const teamBilling = await this.billingService.billingRepository.getBillingForTeamBySubscriptionId(
|
||||
subscriptionId
|
||||
);
|
||||
if (teamBilling?.id) {
|
||||
await this.deleteBillingCache(teamBilling.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleStripePaymentFailed(event: Stripe.Event): Promise<void> {
|
||||
await this.billingService.handleStripePaymentFailed(event);
|
||||
const invoice = event.data.object as Stripe.Invoice;
|
||||
const subscriptionId = this.getSubscriptionIdFromInvoice(invoice);
|
||||
if (subscriptionId) {
|
||||
const teamBilling = await this.billingService.billingRepository.getBillingForTeamBySubscriptionId(
|
||||
subscriptionId
|
||||
);
|
||||
if (teamBilling?.id) {
|
||||
await this.deleteBillingCache(teamBilling.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleStripePaymentPastDue(event: Stripe.Event): Promise<void> {
|
||||
await this.billingService.handleStripePaymentPastDue(event);
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
const subscriptionId = subscription.id;
|
||||
if (subscriptionId) {
|
||||
const teamBilling = await this.billingService.billingRepository.getBillingForTeamBySubscriptionId(
|
||||
subscriptionId
|
||||
);
|
||||
if (teamBilling?.id) {
|
||||
await this.deleteBillingCache(teamBilling.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleStripeCheckoutEvents(event: Stripe.Event): Promise<void> {
|
||||
await this.billingService.handleStripeCheckoutEvents(event);
|
||||
const checkoutSession = event.data.object as Stripe.Checkout.Session;
|
||||
const teamId = checkoutSession.metadata?.teamId;
|
||||
if (teamId) {
|
||||
await this.deleteBillingCache(Number.parseInt(teamId));
|
||||
}
|
||||
}
|
||||
|
||||
async handleStripeSubscriptionForActiveManagedUsers(event: Stripe.Event): Promise<void> {
|
||||
return this.billingService.handleStripeSubscriptionForActiveManagedUsers(event);
|
||||
}
|
||||
|
||||
getSubscriptionIdFromInvoice(invoice: Stripe.Invoice): string | null {
|
||||
return this.billingService.getSubscriptionIdFromInvoice(invoice);
|
||||
}
|
||||
|
||||
getCustomerIdFromInvoice(invoice: Stripe.Invoice): string | null {
|
||||
return this.billingService.getCustomerIdFromInvoice(invoice);
|
||||
}
|
||||
|
||||
get stripeService() {
|
||||
return this.billingService.stripeService;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { AppConfig } from "@/config/type";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import { BILLING_QUEUE, INCREMENT_JOB, IncrementJobDataType } from "@/modules/billing/billing.processor";
|
||||
import { BillingRepository } from "@/modules/billing/billing.repository";
|
||||
import { IBillingService } from "@/modules/billing/interfaces/billing-service.interface";
|
||||
import { BillingConfigService } from "@/modules/billing/services/billing.config.service";
|
||||
import { PlatformPlan } from "@/modules/billing/types";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
@@ -23,14 +24,14 @@ import { DateTime } from "luxon";
|
||||
import Stripe from "stripe";
|
||||
|
||||
@Injectable()
|
||||
export class BillingService implements OnModuleDestroy {
|
||||
export class BillingService implements IBillingService, OnModuleDestroy {
|
||||
private logger = new Logger("BillingService");
|
||||
private readonly webAppUrl: string;
|
||||
|
||||
constructor(
|
||||
private readonly teamsRepository: OrganizationsRepository,
|
||||
public readonly stripeService: StripeService,
|
||||
private readonly billingRepository: BillingRepository,
|
||||
public readonly billingRepository: BillingRepository,
|
||||
private readonly configService: ConfigService<AppConfig>,
|
||||
private readonly billingConfigService: BillingConfigService,
|
||||
private readonly usersRepository: UsersRepository,
|
||||
@@ -43,18 +44,23 @@ export class BillingService implements OnModuleDestroy {
|
||||
|
||||
async getBillingData(teamId: number) {
|
||||
const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
|
||||
|
||||
if (teamWithBilling?.platformBilling) {
|
||||
if (!teamWithBilling?.platformBilling.subscriptionId) {
|
||||
return { team: teamWithBilling, status: "no_subscription", plan: "none" };
|
||||
return { team: teamWithBilling, status: "no_subscription" as const, plan: "none" };
|
||||
} else {
|
||||
return {
|
||||
team: teamWithBilling,
|
||||
status: "valid" as const,
|
||||
plan: teamWithBilling.platformBilling.plan,
|
||||
};
|
||||
}
|
||||
|
||||
return { team: teamWithBilling, status: "valid", plan: teamWithBilling.platformBilling.plan };
|
||||
} else {
|
||||
return { team: teamWithBilling, status: "no_billing", plan: "none" };
|
||||
return { team: teamWithBilling, status: "no_billing" as const, plan: "none" };
|
||||
}
|
||||
}
|
||||
|
||||
async createTeamBilling(teamId: number) {
|
||||
async createTeamBilling(teamId: number): Promise<string> {
|
||||
const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
|
||||
let customerId = teamWithBilling?.platformBilling?.customerId;
|
||||
|
||||
@@ -67,7 +73,7 @@ export class BillingService implements OnModuleDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
return customerId;
|
||||
return customerId!;
|
||||
}
|
||||
|
||||
async redirectToSubscribeCheckout(teamId: number, plan: PlatformPlan, customerId?: string) {
|
||||
|
||||
+7
-1
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
||||
import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsISO8601, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
@@ -297,6 +297,12 @@ export class DateTimeWithZone {
|
||||
timeZone!: string;
|
||||
}
|
||||
|
||||
@ApiExtraModels(
|
||||
CalendarEventVideoLocation,
|
||||
CalendarEventPhoneLocation,
|
||||
CalendarEventSipLocation,
|
||||
CalendarEventMoreLocation
|
||||
)
|
||||
export class UnifiedCalendarEventOutput {
|
||||
@ValidateNested()
|
||||
@Type(() => DateTimeWithZone)
|
||||
|
||||
+3
-1
@@ -4,9 +4,11 @@ import {
|
||||
MicrosoftServiceAccountKeyInput,
|
||||
} from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input";
|
||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { ApiExtraModels } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsString, IsNotEmpty, ValidateNested, Validate } from "class-validator";
|
||||
import { IsString, IsNotEmpty, Validate } from "class-validator";
|
||||
|
||||
@ApiExtraModels(GoogleServiceAccountKeyInput, MicrosoftServiceAccountKeyInput)
|
||||
export class CreateDelegationCredentialInput {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
|
||||
+4
-3
@@ -3,10 +3,11 @@ import {
|
||||
MicrosoftServiceAccountKeyInput,
|
||||
ServiceAccountKeyValidator,
|
||||
} from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input";
|
||||
import { ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Expose, plainToClass, Transform, Type } from "class-transformer";
|
||||
import { IsBoolean, IsOptional, Validate, ValidateNested } from "class-validator";
|
||||
import { ApiExtraModels, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsBoolean, IsOptional, Validate } from "class-validator";
|
||||
|
||||
@ApiExtraModels(GoogleServiceAccountKeyInput, MicrosoftServiceAccountKeyInput)
|
||||
export class UpdateDelegationCredentialInput {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
|
||||
@@ -174,4 +174,14 @@ export class OrganizationsRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findTeamByPlatformBillingId(billingId: number) {
|
||||
return this.dbRead.prisma.team.findFirst({
|
||||
where: {
|
||||
platformBilling: {
|
||||
id: billingId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import { OrganizationsTeamsController } from "@/modules/organizations/teams/inde
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
|
||||
import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service";
|
||||
import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.controller";
|
||||
import { OrganizationsTeamsInviteController } from "@/modules/organizations/teams/invite/organizations-teams-invite.controller";
|
||||
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
|
||||
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service";
|
||||
import { OrganizationsTeamsRoutingFormsModule } from "@/modules/organizations/teams/routing-forms/organizations-teams-routing-forms.module";
|
||||
@@ -183,6 +184,7 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsMembershipsController,
|
||||
OrganizationsEventTypesController,
|
||||
OrganizationsTeamsMembershipsController,
|
||||
OrganizationsTeamsInviteController,
|
||||
OrganizationsAttributesController,
|
||||
OrganizationsAttributesOptionsController,
|
||||
OrganizationsWebhooksController,
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ export class ManagedOrganizationsRepository {
|
||||
managedOrganization: managedOrganizationFilter,
|
||||
};
|
||||
|
||||
const [totalItems, linkRows] = await this.dbRead.prisma.$transaction([
|
||||
const [totalItems, linkRows] = await Promise.all([
|
||||
this.dbRead.prisma.managedOrganization.count({ where }),
|
||||
this.dbRead.prisma.managedOrganization.findMany({
|
||||
where,
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
||||
import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsArray, IsBoolean, IsInt, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
@@ -75,6 +75,7 @@ class Routing {
|
||||
crmOwnerRecordType?: string;
|
||||
}
|
||||
|
||||
@ApiExtraModels(SlotsOutput_2024_09_04, RangeSlotsOutput_2024_09_04)
|
||||
export class CreateRoutingFormResponseOutputData {
|
||||
@ApiPropertyOptional({
|
||||
type: Number,
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { Team, User } from "@calcom/prisma/client";
|
||||
|
||||
describe("Organizations Teams Invite Endpoints", () => {
|
||||
describe("User Authentication - User is Org Team Admin", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
let org: Team;
|
||||
let orgTeam: Team;
|
||||
let nonOrgTeam: Team;
|
||||
|
||||
const userEmail = `organizations-teams-invite-admin-${randomString()}@api.com`;
|
||||
|
||||
let user: User;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
})
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
});
|
||||
|
||||
org = await organizationsRepositoryFixture.create({
|
||||
name: `organizations-teams-invite-organization-${randomString()}`,
|
||||
isOrganization: true,
|
||||
});
|
||||
|
||||
orgTeam = await teamsRepositoryFixture.create({
|
||||
name: `organizations-teams-invite-team-${randomString()}`,
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
nonOrgTeam = await teamsRepositoryFixture.create({
|
||||
name: `organizations-teams-invite-non-org-team-${randomString()}`,
|
||||
isOrganization: false,
|
||||
});
|
||||
|
||||
// Admin of the org team
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "ADMIN",
|
||||
user: { connect: { id: user.id } },
|
||||
team: { connect: { id: orgTeam.id } },
|
||||
});
|
||||
|
||||
// Also a member of the organization
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "ADMIN",
|
||||
user: { connect: { id: user.id } },
|
||||
team: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("should create a team invite", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/invite`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
expect(response.body.data.token.length).toBeGreaterThan(0);
|
||||
expect(response.body.data.inviteLink).toEqual(expect.any(String));
|
||||
expect(response.body.data.inviteLink).toContain(response.body.data.token);
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a new invite on each request", async () => {
|
||||
const first = await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/invite`)
|
||||
.expect(200);
|
||||
const firstToken = first.body.data.token as string;
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/invite`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
expect(response.body.data.token).not.toEqual(firstToken);
|
||||
expect(response.body.data.inviteLink).toEqual(expect.any(String));
|
||||
expect(response.body.data.inviteLink).toContain(response.body.data.token);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail for team not in organization", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/teams/${nonOrgTeam.id}/invite`)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await organizationsRepositoryFixture.delete(org.id);
|
||||
await teamsRepositoryFixture.delete(nonOrgTeam.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { OPTIONAL_API_KEY_HEADER, OPTIONAL_X_CAL_CLIENT_ID_HEADER, OPTIONAL_X_CAL_SECRET_KEY_HEADER } from "@/lib/docs/headers";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||
import { Controller, UseGuards, Post, Param, ParseIntPipe, HttpCode, HttpStatus } from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
|
||||
|
||||
import { CreateInviteOutputDto } from "./outputs/invite.output";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/organizations/:orgId/teams/:teamId",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@DocsTags("Orgs / Teams / Invite")
|
||||
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
|
||||
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||
@ApiHeader(OPTIONAL_API_KEY_HEADER)
|
||||
export class OrganizationsTeamsInviteController {
|
||||
@Post("/invite")
|
||||
@Roles("TEAM_ADMIN")
|
||||
@ApiOperation({ summary: "Create team invite link" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async createInvite(
|
||||
@Param("orgId", ParseIntPipe) _orgId: number,
|
||||
@Param("teamId", ParseIntPipe) teamId: number
|
||||
): Promise<CreateInviteOutputDto> {
|
||||
const result = await TeamService.createInvite(teamId);
|
||||
return { status: SUCCESS_STATUS, data: result };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class InviteDataDto {
|
||||
@ApiProperty({
|
||||
description:
|
||||
"Unique invitation token for this team. Share this token with prospective members to allow them to join the team/organization.",
|
||||
example: "f6a5c8b1d2e34c7f90a1b2c3d4e5f6a5b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2",
|
||||
})
|
||||
token!: string;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
"Complete invitation URL that can be shared with prospective members. Opens the signup page with the token and redirects to getting started after signup.",
|
||||
example: "http://app.cal.com/signup?token=f6a5c8b1d2e34c7f90a1b2c3d4e5f6a5b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2&callbackUrl=/getting-started",
|
||||
})
|
||||
inviteLink!: string;
|
||||
}
|
||||
|
||||
export class CreateInviteOutputDto {
|
||||
@ApiProperty({ example: "success" })
|
||||
status!: string;
|
||||
|
||||
@ApiProperty({ type: InviteDataDto })
|
||||
data!: InviteDataDto;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
|
||||
import { ApiResponseWithoutData, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
||||
|
||||
@ApiExtraModels(TeamEventTypeOutput_2024_06_14)
|
||||
export class CreateTeamEventTypeOutput extends ApiResponseWithoutData {
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
|
||||
import { ApiResponseWithoutData, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
||||
|
||||
@ApiExtraModels(TeamEventTypeOutput_2024_06_14)
|
||||
export class UpdateTeamEventTypeOutput extends ApiResponseWithoutData {
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsEnum, ValidateNested, IsString, IsUrl } from "class-validator";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { TeamOutputDto } from "@calcom/platform-types";
|
||||
|
||||
class Output {
|
||||
class CreateTeamOutputData {
|
||||
@Expose()
|
||||
@IsString()
|
||||
message!: string;
|
||||
@@ -19,16 +19,17 @@ class Output {
|
||||
pendingTeam!: TeamOutputDto;
|
||||
}
|
||||
|
||||
@ApiExtraModels(TeamOutputDto, CreateTeamOutputData)
|
||||
export class CreateTeamOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
oneOf: [{ $ref: getSchemaPath(Output) }, { $ref: getSchemaPath(TeamOutputDto) }],
|
||||
oneOf: [{ $ref: getSchemaPath(CreateTeamOutputData) }, { $ref: getSchemaPath(TeamOutputDto) }],
|
||||
description: "Either an Output object or a TeamOutputDto.",
|
||||
})
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
data!: Output | TeamOutputDto;
|
||||
data!: CreateTeamOutputData | TeamOutputDto;
|
||||
}
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@ import * as path from "path";
|
||||
// "nest-cli" with the "nest-cli.json" file, and for nest cli to be loaded with plugins correctly the "@nestjs/swagger"
|
||||
// should reside in the project's node_modules already before the "nest start" command is executed.
|
||||
async function copyNestSwagger() {
|
||||
const monorepoRoot = path.resolve(__dirname, "../../../../");
|
||||
const nodeModulesNestjs = path.resolve(__dirname, "../node_modules/@nestjs");
|
||||
const monorepoRoot = path.resolve(__dirname, "../../../../../");
|
||||
const nodeModulesNestjs = path.resolve(__dirname, "../../node_modules/@nestjs");
|
||||
const swaggerModulePath = "@nestjs/swagger";
|
||||
|
||||
const sourceDir = path.join(monorepoRoot, "node_modules", swaggerModulePath);
|
||||
@@ -0,0 +1,29 @@
|
||||
import "dotenv/config";
|
||||
|
||||
import { bootstrap } from "../app";
|
||||
import { createNestApp } from "../main";
|
||||
import { generateSwaggerForApp } from "../swagger/generate-swagger";
|
||||
|
||||
generateSwagger()
|
||||
.then(() => {
|
||||
console.log("✅ Swagger generation completed successfully");
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
console.error("❌ Failed to generate swagger", { error: error.stack });
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
async function generateSwagger() {
|
||||
const app = await createNestApp();
|
||||
|
||||
try {
|
||||
bootstrap(app);
|
||||
await generateSwaggerForApp(app);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { getEnv } from "@/env";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import type { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
|
||||
import {
|
||||
PathItemObject,
|
||||
PathsObject,
|
||||
OperationObject,
|
||||
} from "@nestjs/swagger/dist/interfaces/open-api-spec.interface";
|
||||
import "dotenv/config";
|
||||
import * as fs from "fs";
|
||||
import { Server } from "http";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const HttpMethods: (keyof PathItemObject)[] = ["get", "post", "put", "delete", "patch", "options", "head"];
|
||||
|
||||
export async function generateSwaggerForApp(app: NestExpressApplication<Server>) {
|
||||
const logger = new Logger("App");
|
||||
logger.log(`Generating Swagger documentation...\n`);
|
||||
|
||||
const config = new DocumentBuilder().setTitle("Cal.com API v2").build();
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
document.paths = groupAndSortPathsByFirstTag(document.paths);
|
||||
|
||||
const docsOutputFile = "../../../docs/api-reference/v2/openapi.json";
|
||||
const stringifiedContents = JSON.stringify(document, null, 2);
|
||||
|
||||
if (fs.existsSync(docsOutputFile) && getEnv("NODE_ENV") === "development") {
|
||||
fs.unlinkSync(docsOutputFile);
|
||||
fs.writeFileSync(docsOutputFile, stringifiedContents, { encoding: "utf8" });
|
||||
spawnSync("npx", ["prettier", docsOutputFile, "--write"], { stdio: "inherit" });
|
||||
}
|
||||
|
||||
if (!process.env.DOCS_URL) {
|
||||
SwaggerModule.setup("docs", app, document, {
|
||||
customCss: ".swagger-ui .topbar { display: none }",
|
||||
});
|
||||
|
||||
logger.log(`Swagger documentation available in the "/docs" endpoint\n`);
|
||||
}
|
||||
}
|
||||
|
||||
function groupAndSortPathsByFirstTag(paths: PathsObject): PathsObject {
|
||||
const groupedPaths: { [key: string]: PathsObject } = {};
|
||||
|
||||
Object.keys(paths).forEach((pathKey) => {
|
||||
const pathItem = paths[pathKey];
|
||||
|
||||
HttpMethods.forEach((method) => {
|
||||
const operation = pathItem[method];
|
||||
|
||||
if (isOperationObject(operation) && operation.tags && operation.tags.length > 0) {
|
||||
const firstTag = operation.tags[0];
|
||||
|
||||
if (!groupedPaths[firstTag]) {
|
||||
groupedPaths[firstTag] = {};
|
||||
}
|
||||
|
||||
groupedPaths[firstTag][pathKey] = pathItem;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const sortedTags = Object.keys(groupedPaths).sort(customTagSort);
|
||||
const sortedPaths: PathsObject = {};
|
||||
|
||||
sortedTags.forEach((tag) => {
|
||||
Object.assign(sortedPaths, groupedPaths[tag]);
|
||||
});
|
||||
|
||||
return sortedPaths;
|
||||
}
|
||||
|
||||
function customTagSort(a: string, b: string): number {
|
||||
const platformPrefix = "Platform";
|
||||
const orgsPrefix = "Orgs";
|
||||
|
||||
if (a.startsWith(platformPrefix) && !b.startsWith(platformPrefix)) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.startsWith(platformPrefix) && b.startsWith(platformPrefix)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.startsWith(orgsPrefix) && !b.startsWith(orgsPrefix)) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.startsWith(orgsPrefix) && b.startsWith(orgsPrefix)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
|
||||
function isOperationObject(obj: any): obj is OperationObject {
|
||||
return obj && typeof obj === "object" && "tags" in obj;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ export class PlatformBillingRepositoryFixture {
|
||||
id: orgId,
|
||||
customerId: `cus_123_${randomString}`,
|
||||
subscriptionId: `sub_123_${randomString}`,
|
||||
plan: plan || "STARTER",
|
||||
plan: plan || "FREE",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { nextJsConfig } from "@calcom/eslint-config/next-js";
|
||||
|
||||
export default nextJsConfig;
|
||||
@@ -6,12 +6,12 @@
|
||||
"dev": "next dev --turbopack --port=1337",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/config": "*",
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/ui": "*",
|
||||
"@calcom/config": "workspace:*",
|
||||
"@calcom/lib": "workspace:*",
|
||||
"@calcom/ui": "workspace:*",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"airtable": "^0.12.2",
|
||||
"fumadocs-core": "^14.5.4",
|
||||
@@ -24,14 +24,12 @@
|
||||
"react-hot-toast": "^2.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/tsconfig": "*",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@calcom/eslint-config": "workspace:*",
|
||||
"@calcom/tsconfig": "workspace:*",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8.34.0",
|
||||
"eslint-config-next": "15.1.6",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.9.0-beta"
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
public/embed
|
||||
@@ -6,6 +6,8 @@ import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
@@ -33,9 +35,30 @@ const Page = async ({ params }: PageProps) => {
|
||||
const t = await getTranslate();
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
|
||||
let canReadOthersBookings = false;
|
||||
if (session?.user?.id) {
|
||||
const permissionService = new PermissionCheckService();
|
||||
const userId = session.user.id;
|
||||
|
||||
const teamIdsWithPermission = await permissionService.getTeamIdsWithPermission({
|
||||
userId,
|
||||
permission: "booking.read",
|
||||
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
});
|
||||
// We check if teamIdsWithPermission.length > 0.
|
||||
// While this may not be entirely accurate, it's acceptable
|
||||
// because we perform a thorough validation on the server side for the actual filter values.
|
||||
// This variable is primarily for UI purposes.
|
||||
canReadOthersBookings = teamIdsWithPermission.length > 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<ShellMainAppDir heading={t("bookings")} subtitle={t("bookings_description")}>
|
||||
<BookingsList status={parsed.data.status} userId={session?.user?.id} />
|
||||
<BookingsList
|
||||
status={parsed.data.status}
|
||||
userId={session?.user?.id}
|
||||
permissions={{ canReadOthersBookings }}
|
||||
/>
|
||||
</ShellMainAppDir>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ export const TeamsCTA = () => {
|
||||
data-testid="new-team-btn"
|
||||
variant="fab"
|
||||
StartIcon="plus"
|
||||
size="sm"
|
||||
type="button"
|
||||
href={`${WEBAPP_URL}/settings/teams/new?returnTo=${WEBAPP_URL}/teams`}>
|
||||
{t("new")}
|
||||
|
||||
+8
@@ -438,6 +438,7 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record<number, T
|
||||
name={t("profile")}
|
||||
href={`/settings/teams/${team.id}/profile`}
|
||||
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||
className="me-5 h-7 w-auto !px-2"
|
||||
disableChevron
|
||||
/>
|
||||
)}
|
||||
@@ -445,6 +446,7 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record<number, T
|
||||
name={t("members")}
|
||||
href={`/settings/teams/${team.id}/members`}
|
||||
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||
className="me-5 h-7 w-auto !px-2"
|
||||
disableChevron
|
||||
/>
|
||||
{/* Show roles only for sub-teams with PBAC-enabled parent */}
|
||||
@@ -465,6 +467,7 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record<number, T
|
||||
name={t("appearance")}
|
||||
href={`/settings/teams/${team.id}/appearance`}
|
||||
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||
className="me-5 h-7 w-auto !px-2"
|
||||
disableChevron
|
||||
/>
|
||||
{/* Hide if there is a parent ID */}
|
||||
@@ -474,6 +477,7 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record<number, T
|
||||
name={t("billing")}
|
||||
href={`/settings/teams/${team.id}/billing`}
|
||||
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||
className="me-5 h-7 w-auto !px-2"
|
||||
disableChevron
|
||||
/>
|
||||
</>
|
||||
@@ -482,6 +486,7 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record<number, T
|
||||
name={t("settings")}
|
||||
href={`/settings/teams/${team.id}/settings`}
|
||||
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||
className="me-5 h-7 w-auto !px-2"
|
||||
disableChevron
|
||||
/>
|
||||
</>
|
||||
@@ -643,6 +648,7 @@ const SettingsSidebarContainer = ({
|
||||
name={t("add_a_team")}
|
||||
href={`${WEBAPP_URL}/settings/teams/new`}
|
||||
textClassNames="px-3 items-center mt-2 text-emphasis font-medium text-sm"
|
||||
className="me-5 h-7 w-auto !px-2"
|
||||
icon="plus"
|
||||
disableChevron
|
||||
/>
|
||||
@@ -744,12 +750,14 @@ const SettingsSidebarContainer = ({
|
||||
name={t("profile")}
|
||||
href={`/settings/organizations/teams/other/${otherTeam.id}/profile`}
|
||||
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||
className="me-5 h-7 w-auto !px-2"
|
||||
disableChevron
|
||||
/>
|
||||
<VerticalTabItem
|
||||
name={t("members")}
|
||||
href={`/settings/organizations/teams/other/${otherTeam.id}/members`}
|
||||
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||
className="me-5 h-7 w-auto !px-2"
|
||||
disableChevron
|
||||
/>
|
||||
<>
|
||||
|
||||
@@ -21,7 +21,7 @@ const Page = async () => {
|
||||
<SettingsHeader
|
||||
title={t("billing")}
|
||||
description={t("manage_billing_description")}
|
||||
borderInShellHeader={true}>
|
||||
borderInShellHeader={false}>
|
||||
<BillingView />
|
||||
</SettingsHeader>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import type { PageProps as ServerPageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import CreateTeamEventType, { LayoutWrapper } from "~/settings/teams/[id]/event-types-view";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { CreateTeamEventType, LayoutWrapper } from "~/settings/teams/[id]/event-types-view";
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
|
||||
await _generateMetadata(
|
||||
@@ -11,10 +21,34 @@ export const generateMetadata = async ({ params }: { params: Promise<{ id: strin
|
||||
`/settings/teams/${(await params).id}/event-type`
|
||||
);
|
||||
|
||||
const ServerPage = async () => {
|
||||
const querySchema = z.object({
|
||||
id: z.coerce.number().refine((val) => !Number.isNaN(val), {
|
||||
message: "id must be a string that can be cast to a number",
|
||||
}),
|
||||
});
|
||||
|
||||
const ServerPage = async ({ params }: ServerPageProps) => {
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
if (!session?.user?.id) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
const parsed = querySchema.safeParse(await params);
|
||||
if (!parsed.success) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const permissionService = new PermissionCheckService();
|
||||
const canCreateEventType = await permissionService.checkPermission({
|
||||
userId: session.user.id,
|
||||
teamId: parsed.data.id,
|
||||
permission: "eventType.create",
|
||||
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
|
||||
});
|
||||
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<CreateTeamEventType />
|
||||
<CreateTeamEventType permissions={{ canCreateEventType }} />
|
||||
</LayoutWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,13 +3,15 @@ import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { BillingPlanService } from "@calcom/features/ee/billing/domain/billing-plans";
|
||||
import type { Contact } from "@calcom/features/ee/support/lib/intercom/intercom";
|
||||
import { intercom } from "@calcom/features/ee/support/lib/intercom/intercom";
|
||||
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { MembershipRepository } from "@calcom/lib/server/repository/membership";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
@@ -43,6 +45,11 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
const { user } = session;
|
||||
|
||||
const membershipRepository = new MembershipRepository(prisma);
|
||||
const memberships = await membershipRepository.findAllMembershipsByUserIdForBilling({ userId: user.id });
|
||||
const billingPlanService = new BillingPlanService();
|
||||
const plan = await billingPlanService.getUserPlanByMemberships(memberships);
|
||||
|
||||
if (!existingContact.data) {
|
||||
const additionalUserInfo = await new UserRepository(prisma).getUserStats({ userId: session.user.id });
|
||||
const sumOfTeamEventTypes = additionalUserInfo?.teams.reduce(
|
||||
@@ -72,6 +79,7 @@ export async function POST(req: NextRequest) {
|
||||
sum_of_teams: additionalUserInfo?._count?.teams,
|
||||
sum_of_event_types: additionalUserInfo?._count?.eventTypes,
|
||||
sum_of_team_event_types: sumOfTeamEventTypes,
|
||||
Plan: plan,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -213,7 +213,6 @@ const ConfigureStepCardContent: FC<ConfigureStepCardProps> = (props) => {
|
||||
);
|
||||
|
||||
const [submit, setSubmit] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const allUpdated = updatedEventTypesStatus.every((item) => item.every((iitem) => iitem.updated));
|
||||
|
||||
useEffect(() => {
|
||||
@@ -223,11 +222,7 @@ const ConfigureStepCardContent: FC<ConfigureStepCardProps> = (props) => {
|
||||
}
|
||||
}, [submit, allUpdated, mainForSubmitRef]);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!formPortalRef?.current || !mounted) {
|
||||
if (!formPortalRef?.current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ describe("EditLocationDialog", () => {
|
||||
},
|
||||
{
|
||||
value: "integrations:daily",
|
||||
label: "Cal Video (Global)",
|
||||
label: "Cal Video (Default)",
|
||||
disabled: false,
|
||||
icon: "/app-store/dailyvideo/icon.svg",
|
||||
slug: "daily-video",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { nextJsConfig } from "@calcom/eslint-config/next-js";
|
||||
|
||||
export default [...nextJsConfig];
|
||||
@@ -310,7 +310,6 @@ const OnboardingPage = ({
|
||||
/>
|
||||
)}
|
||||
{currentStep === AppOnboardingSteps.CONFIGURE_STEP &&
|
||||
formPortalRef.current &&
|
||||
eventTypeGroups && (
|
||||
<ConfigureStepCard
|
||||
slug={appMetadata.slug}
|
||||
|
||||
@@ -57,6 +57,9 @@ const descriptionByStatus: Record<BookingListingStatus, string> = {
|
||||
type BookingsProps = {
|
||||
status: (typeof validStatuses)[number];
|
||||
userId?: number;
|
||||
permissions: {
|
||||
canReadOthersBookings: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
function useSystemSegments(userId?: number) {
|
||||
@@ -111,7 +114,7 @@ type RowData =
|
||||
type: "today" | "next";
|
||||
};
|
||||
|
||||
function BookingsContent({ status }: BookingsProps) {
|
||||
function BookingsContent({ status, permissions }: BookingsProps) {
|
||||
const { t } = useLocale();
|
||||
const user = useMeQuery().data;
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -154,7 +157,7 @@ function BookingsContent({ status }: BookingsProps) {
|
||||
href: queryString ? `${tabConfig.path}?${queryString}` : tabConfig.path,
|
||||
"data-testid": tabConfig["data-testid"],
|
||||
}));
|
||||
}, [searchParams?.toString()]);
|
||||
}, [searchParams]);
|
||||
|
||||
const eventTypeIds = useFilterValue("eventTypeId", ZMultiSelectFilterValue)?.data as number[] | undefined;
|
||||
const teamIds = useFilterValue("teamId", ZMultiSelectFilterValue)?.data as number[] | undefined;
|
||||
@@ -215,7 +218,7 @@ function BookingsContent({ status }: BookingsProps) {
|
||||
columnHelper.accessor((row) => row.type === "data" && row.booking.user?.id, {
|
||||
id: "userId",
|
||||
header: t("member"),
|
||||
enableColumnFilter: user?.isTeamAdminOrOwner ?? false,
|
||||
enableColumnFilter: permissions.canReadOthersBookings,
|
||||
enableSorting: false,
|
||||
cell: () => null,
|
||||
meta: {
|
||||
@@ -314,7 +317,7 @@ function BookingsContent({ status }: BookingsProps) {
|
||||
},
|
||||
}),
|
||||
];
|
||||
}, [user, status, t]);
|
||||
}, [user, status, t, permissions.canReadOthersBookings]);
|
||||
|
||||
const isEmpty = useMemo(() => !query.data?.bookings.length, [query.data]);
|
||||
|
||||
@@ -352,7 +355,7 @@ function BookingsContent({ status }: BookingsProps) {
|
||||
isToday: false,
|
||||
})) || []
|
||||
);
|
||||
}, [query.data]);
|
||||
}, [query.data, status, user?.timeZone]);
|
||||
|
||||
const bookingsToday = useMemo<RowData[]>(() => {
|
||||
return (
|
||||
@@ -371,7 +374,7 @@ function BookingsContent({ status }: BookingsProps) {
|
||||
isToday: true,
|
||||
})) ?? []
|
||||
);
|
||||
}, [query.data]);
|
||||
}, [query.data, user?.timeZone]);
|
||||
|
||||
const finalData = useMemo<RowData[]>(() => {
|
||||
if (status !== "upcoming") {
|
||||
|
||||
@@ -556,7 +556,9 @@ export default function Success(props: PageProps) {
|
||||
<div className="font-medium">
|
||||
{isCancelled ? t("reason") : t("reschedule_reason")}
|
||||
</div>
|
||||
<div className="col-span-2 mb-6 last:mb-0">{cancellationReason}</div>
|
||||
<div className="col-span-2 mb-6 last:mb-0">
|
||||
<p className="break-words">{cancellationReason}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isCancelled && bookingInfo?.cancelledBy && (
|
||||
|
||||
@@ -12,7 +12,10 @@ import { useOrgBranding } from "@calcom/features/ee/organizations/context/provid
|
||||
import { CreateButton } from "@calcom/features/ee/teams/components/createButton/CreateButton";
|
||||
import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/embed/EventTypeEmbed";
|
||||
import { EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||
import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog";
|
||||
import {
|
||||
CreateEventTypeDialog,
|
||||
type ProfileOption,
|
||||
} from "@calcom/features/eventtypes/components/CreateEventTypeDialog";
|
||||
import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog";
|
||||
import { InfiniteSkeletonLoader } from "@calcom/features/eventtypes/components/SkeletonLoader";
|
||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
@@ -846,17 +849,7 @@ const CreateFirstEventTypeView = ({ slug, searchTerm }: { slug: string; searchTe
|
||||
);
|
||||
};
|
||||
|
||||
const CTA = ({
|
||||
profileOptions,
|
||||
}: {
|
||||
profileOptions: {
|
||||
teamId: number | null | undefined;
|
||||
label: string | null;
|
||||
image: string;
|
||||
membershipRole: MembershipRole | null | undefined;
|
||||
slug: string | null;
|
||||
}[];
|
||||
}) => {
|
||||
const CTA = ({ profileOptions }: { profileOptions: ProfileOption[] }) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
if (!profileOptions.length) return null;
|
||||
@@ -951,7 +944,7 @@ type Props = {
|
||||
|
||||
export const EventTypesCTA = ({ userEventGroupsData }: Omit<Props, "user">) => {
|
||||
const profileOptions =
|
||||
userEventGroupsData?.profiles
|
||||
userEventGroupsData.profiles
|
||||
?.filter((profile) => !profile.readOnly)
|
||||
?.filter((profile) => !profile.eventTypesLockedByOrg)
|
||||
?.filter((profile) => {
|
||||
@@ -973,12 +966,20 @@ export const EventTypesCTA = ({ userEventGroupsData }: Omit<Props, "user">) => {
|
||||
);
|
||||
})
|
||||
?.map((profile) => {
|
||||
const permissions = profile.teamId
|
||||
? userEventGroupsData.teamPermissions[profile.teamId]
|
||||
: {
|
||||
// always can create eventType on personal level
|
||||
canCreateEventType: true,
|
||||
};
|
||||
|
||||
return {
|
||||
teamId: profile.teamId,
|
||||
label: profile.name || profile.slug,
|
||||
image: profile.image,
|
||||
membershipRole: profile.membershipRole,
|
||||
slug: profile.slug,
|
||||
permissions,
|
||||
};
|
||||
}) ?? [];
|
||||
|
||||
|
||||
@@ -30,16 +30,18 @@ declare global {
|
||||
export default function PlatformBillingUpgrade() {
|
||||
const pathname = usePathname();
|
||||
const { t } = useLocale();
|
||||
const { isUserLoading, isUserBillingDataLoading, isPlatformUser, userBillingData, isPaidUser, userOrgId } =
|
||||
useGetUserAttributes();
|
||||
|
||||
const returnTo = pathname;
|
||||
const billingHref = `/api/integrations/stripepayment/portal?returnTo=${WEBAPP_URL}${returnTo}`;
|
||||
const teamId = `teamId=${userOrgId}`;
|
||||
const billingHref = `/api/integrations/stripepayment/portal?returnTo=${WEBAPP_URL}${returnTo}&${teamId}`;
|
||||
|
||||
const onContactSupportClick = async () => {
|
||||
if (window.Support) {
|
||||
window.Support.open();
|
||||
}
|
||||
};
|
||||
const { isUserLoading, isUserBillingDataLoading, isPlatformUser, userBillingData, isPaidUser, userOrgId } =
|
||||
useGetUserAttributes();
|
||||
|
||||
const { mutateAsync: removeTeamSubscription, isPending: isRemoveTeamSubscriptionLoading } =
|
||||
useUnsubscribeTeamToStripe({
|
||||
|
||||
@@ -8,11 +8,15 @@ import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useCreateEventType } from "@calcom/lib/hooks/useCreateEventType";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { WizardLayout } from "@calcom/ui/components/layout";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
|
||||
export const CreateTeamEventType = () => {
|
||||
type CreateTeamEventTypeProps = {
|
||||
permissions: { canCreateEventType: boolean };
|
||||
};
|
||||
|
||||
export const CreateTeamEventType = ({ permissions }: CreateTeamEventTypeProps) => {
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
@@ -53,7 +57,7 @@ export const CreateTeamEventType = () => {
|
||||
<TeamEventTypeForm
|
||||
teamSlug={team?.slug}
|
||||
teamId={teamId}
|
||||
isTeamAdminOrOwner={true}
|
||||
permissions={permissions}
|
||||
urlPrefix={urlPrefix}
|
||||
isPending={createMutation.isPending}
|
||||
form={form}
|
||||
@@ -82,5 +86,3 @@ export const LayoutWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
</WizardLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateTeamEventType;
|
||||
|
||||
@@ -426,6 +426,7 @@ export default function Signup({
|
||||
id="signup-email"
|
||||
{...register("email")}
|
||||
label={t("email")}
|
||||
placeholder="john@doe.com"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
disabled={prepopulateFormValues?.email}
|
||||
@@ -522,8 +523,8 @@ export default function Signup({
|
||||
usernameTaken
|
||||
}>
|
||||
{premiumUsername && !usernameTaken
|
||||
? `${t("create_account")} (${getPremiumPlanPriceValue()})`
|
||||
: t("create_account")}
|
||||
? `${t("get_started")} (${getPremiumPlanPriceValue()})`
|
||||
: t("get_started")}
|
||||
</Button>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import { MemberInvitationModalWithoutMembers } from "@calcom/features/ee/teams/components/MemberInvitationModal";
|
||||
import MemberList from "@calcom/features/ee/teams/components/MemberList";
|
||||
@@ -23,18 +22,23 @@ interface TeamMembersViewProps {
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
attributes?: any[];
|
||||
attributes?: {
|
||||
id: string;
|
||||
name: string;
|
||||
options: {
|
||||
value: string;
|
||||
}[];
|
||||
}[];
|
||||
permissions: MemberPermissions;
|
||||
}
|
||||
|
||||
export const TeamMembersView = ({ team, facetedTeamValues, permissions }: TeamMembersViewProps) => {
|
||||
const { t } = useLocale();
|
||||
const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(false);
|
||||
const [showInviteLinkSettingsModal, setShowInviteLinkSettingsModal] = useState(false);
|
||||
const [_showInviteLinkSettingsModal, setShowInviteLinkSettingsModal] = useState(false);
|
||||
|
||||
// Use PBAC permissions if available, otherwise fall back to role-based check
|
||||
const isTeamAdminOrOwner = checkAdminOrOwner(team.membership.role);
|
||||
const canLoggedInUserSeeMembers = permissions?.canListMembers ?? (!team.isPrivate || isTeamAdminOrOwner);
|
||||
// Use PBAC permissions - server-side permission check should be done in parent component
|
||||
const canLoggedInUserSeeMembers = permissions?.canListMembers ?? false;
|
||||
|
||||
return (
|
||||
<LicenseRequired>
|
||||
|
||||
+17
-16
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "5.7.4",
|
||||
"version": "5.7.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -20,8 +20,8 @@
|
||||
"copy-app-store-static": "node scripts/copy-app-store-static.js",
|
||||
"build": "turbo run copy-app-store-static && next build && yarn sentry:release",
|
||||
"start": "next start",
|
||||
"lint": "eslint . --ignore-path .gitignore",
|
||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"lint:report": "eslint . --format json --output-file ../../lint-results/web.json",
|
||||
"check-changed-files": "ts-node scripts/ts-check-changed-files.ts",
|
||||
"translate-locales": "ts-node scripts/check-missing-translations.ts",
|
||||
@@ -32,20 +32,20 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@boxyhq/saml-jackson": "1.52.2",
|
||||
"@calcom/app-store": "*",
|
||||
"@calcom/app-store-cli": "*",
|
||||
"@calcom/dayjs": "*",
|
||||
"@calcom/app-store": "workspace:*",
|
||||
"@calcom/app-store-cli": "workspace:*",
|
||||
"@calcom/dayjs": "workspace:*",
|
||||
"@calcom/embed-core": "workspace:*",
|
||||
"@calcom/embed-react": "workspace:*",
|
||||
"@calcom/embed-snippet": "workspace:*",
|
||||
"@calcom/features": "*",
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/prisma": "*",
|
||||
"@calcom/trpc": "*",
|
||||
"@calcom/tsconfig": "*",
|
||||
"@calcom/ui": "*",
|
||||
"@calcom/features": "workspace:*",
|
||||
"@calcom/lib": "workspace:*",
|
||||
"@calcom/platform-enums": "workspace:*",
|
||||
"@calcom/platform-types": "workspace:*",
|
||||
"@calcom/prisma": "workspace:*",
|
||||
"@calcom/trpc": "workspace:*",
|
||||
"@calcom/tsconfig": "workspace:*",
|
||||
"@calcom/ui": "workspace:*",
|
||||
"@daily-co/daily-js": "^0.83.1",
|
||||
"@daily-co/daily-react": "^0.23.2",
|
||||
"@dub/analytics": "^0.0.27",
|
||||
@@ -158,8 +158,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.6",
|
||||
"@calcom/config": "*",
|
||||
"@calcom/types": "*",
|
||||
"@calcom/config": "workspace:*",
|
||||
"@calcom/eslint-config": "workspace:*",
|
||||
"@calcom/types": "workspace:*",
|
||||
"@microsoft/microsoft-graph-types-beta": "0.15.0-preview",
|
||||
"@playwright/test": "^1.45.3",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
|
||||
test.describe("Booking Filters", () => {
|
||||
test("Member role should not see the member filter", async ({ page, users }) => {
|
||||
const teamMateName = "team mate 1";
|
||||
await users.create(undefined, {
|
||||
hasTeam: true,
|
||||
isOrg: true,
|
||||
teammates: [{ name: teamMateName }],
|
||||
});
|
||||
|
||||
const allUsers = await users.get();
|
||||
const memberUser = allUsers.find((user) => user.name === teamMateName);
|
||||
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
if (!memberUser) {
|
||||
throw new Error("user should exist");
|
||||
}
|
||||
|
||||
await memberUser.apiLogin();
|
||||
const bookingsGetResponse = page.waitForResponse((response) =>
|
||||
/\/api\/trpc\/bookings\/get.*/.test(response.url())
|
||||
);
|
||||
await page.goto(`/bookings/upcoming`, { waitUntil: "domcontentloaded" });
|
||||
await bookingsGetResponse;
|
||||
await page.locator('[data-testid="add-filter-button"]').click();
|
||||
await expect(page.locator('[data-testid="add-filter-item-userId"]')).toBeHidden();
|
||||
});
|
||||
|
||||
test("Admin role should see the member filter", async ({ page, users }) => {
|
||||
const owner = await users.create(undefined, {
|
||||
hasTeam: true,
|
||||
isOrg: true,
|
||||
});
|
||||
|
||||
await owner.apiLogin();
|
||||
const bookingsGetResponse = page.waitForResponse((response) =>
|
||||
/\/api\/trpc\/bookings\/get.*/.test(response.url())
|
||||
);
|
||||
await page.goto(`/bookings/upcoming`, { waitUntil: "domcontentloaded" });
|
||||
await bookingsGetResponse;
|
||||
await page.locator('[data-testid="add-filter-button"]').click();
|
||||
await expect(page.locator('[data-testid="add-filter-item-userId"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -60,7 +60,6 @@ test.describe("Embed Code Generator Tests", () => {
|
||||
});
|
||||
|
||||
// To prevent early timeouts
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1000);
|
||||
await expectToContainValidPreviewIframe(page, {
|
||||
embedType: "inline",
|
||||
@@ -98,7 +97,6 @@ test.describe("Embed Code Generator Tests", () => {
|
||||
});
|
||||
|
||||
// To prevent early timeouts
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1000);
|
||||
await expectToContainValidPreviewIframe(page, {
|
||||
embedType: "floating-popup",
|
||||
@@ -136,7 +134,6 @@ test.describe("Embed Code Generator Tests", () => {
|
||||
});
|
||||
|
||||
// To prevent early timeouts
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1000);
|
||||
await expectToContainValidPreviewIframe(page, {
|
||||
embedType: "element-click",
|
||||
@@ -176,7 +173,6 @@ test.describe("Embed Code Generator Tests", () => {
|
||||
});
|
||||
|
||||
// To prevent early timeouts
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1000);
|
||||
await expectToContainValidPreviewIframe(page, {
|
||||
embedType: "inline",
|
||||
@@ -234,7 +230,6 @@ test.describe("Embed Code Generator Tests", () => {
|
||||
});
|
||||
|
||||
// To prevent early timeouts
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1000);
|
||||
await expectToContainValidPreviewIframe(page, {
|
||||
embedType: "inline",
|
||||
@@ -275,7 +270,6 @@ test.describe("Embed Code Generator Tests", () => {
|
||||
});
|
||||
|
||||
// To prevent early timeouts
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1000);
|
||||
await expectToContainValidPreviewIframe(page, {
|
||||
embedType: "floating-popup",
|
||||
@@ -315,7 +309,6 @@ test.describe("Embed Code Generator Tests", () => {
|
||||
});
|
||||
|
||||
// To prevent early timeouts
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1000);
|
||||
await expectToContainValidPreviewIframe(page, {
|
||||
embedType: "element-click",
|
||||
@@ -333,8 +326,7 @@ function chooseEmbedType(page: Page, embedType: EmbedType) {
|
||||
}
|
||||
|
||||
async function goToReactCodeTab(page: Page) {
|
||||
// To prevent early timeouts
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
// To prevent early timeo
|
||||
await page.waitForTimeout(1000);
|
||||
await page.locator("[data-testid=horizontal-tab-react]").click();
|
||||
}
|
||||
@@ -343,7 +335,7 @@ async function clickEmbedButton(page: Page) {
|
||||
const embedButton = page.locator("[data-testid=embed]");
|
||||
const embedUrl = await embedButton.getAttribute("data-test-embed-url");
|
||||
embedButton.click();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
|
||||
return embedUrl!;
|
||||
}
|
||||
|
||||
@@ -439,18 +431,24 @@ async function expectValidHtmlEmbedSnippet(
|
||||
}
|
||||
|
||||
function assertThatCodeIsValidVanillaJsCode(code: string) {
|
||||
const lintResult = linter.verify(code, {
|
||||
env: {
|
||||
browser: true,
|
||||
const lintResult = linter.verify(code, [
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 2021,
|
||||
sourceType: "module",
|
||||
parserOptions: { ecmaFeatures: { jsx: true } },
|
||||
globals: {
|
||||
window: "readonly",
|
||||
document: "readonly",
|
||||
navigator: "readonly",
|
||||
Cal: "readonly",
|
||||
console: "readonly",
|
||||
},
|
||||
},
|
||||
rules: eslintRules,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2021,
|
||||
},
|
||||
globals: {
|
||||
Cal: "readonly",
|
||||
},
|
||||
rules: eslintRules,
|
||||
});
|
||||
]);
|
||||
|
||||
if (lintResult.length) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
@@ -459,23 +457,35 @@ function assertThatCodeIsValidVanillaJsCode(code: string) {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
expect(lintResult.length).toBe(0);
|
||||
}
|
||||
|
||||
function assertThatCodeIsValidReactCode(code: string) {
|
||||
const lintResult = linter.verify(code, {
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2021,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
const lintResult = linter.verify(code, [
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 2021,
|
||||
sourceType: "module",
|
||||
parserOptions: {
|
||||
ecmaFeatures: { jsx: true },
|
||||
},
|
||||
globals: {
|
||||
window: "readonly",
|
||||
document: "readonly",
|
||||
navigator: "readonly",
|
||||
console: "readonly",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...eslintRules,
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"no-undef": "off",
|
||||
semi: "off",
|
||||
},
|
||||
sourceType: "module",
|
||||
},
|
||||
rules: eslintRules,
|
||||
});
|
||||
]);
|
||||
|
||||
if (lintResult.length) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
@@ -484,6 +494,7 @@ function assertThatCodeIsValidReactCode(code: string) {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
expect(lintResult.length).toBe(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@ test.describe("Event Types tests", () => {
|
||||
await gotoFirstEventType(page);
|
||||
|
||||
await page.getByTestId("location-select").click();
|
||||
await page.locator(`text="Cal Video (Global)"`).click();
|
||||
await page.locator(`text="Cal Video (Default)"`).click();
|
||||
|
||||
await saveEventType(page);
|
||||
await gotoBookingPage(page);
|
||||
@@ -282,7 +282,7 @@ test.describe("Event Types tests", () => {
|
||||
await selectAttendeePhoneNumber(page);
|
||||
|
||||
// Add Cal Video location
|
||||
await addAnotherLocation(page, "Cal Video (Global)");
|
||||
await addAnotherLocation(page, "Cal Video (Default)");
|
||||
|
||||
await saveEventType(page);
|
||||
|
||||
@@ -496,6 +496,9 @@ test.describe("Event Types tests", () => {
|
||||
});
|
||||
await test.step("should open first eventType and check Interface Language", async () => {
|
||||
await gotoFirstEventType(page);
|
||||
// Go to Advanced tab
|
||||
await page.click("[data-testid=vertical-tab-event_advanced_tab_title]");
|
||||
await page.click("[data-testid=event-interface-language-toggle]");
|
||||
const interfaceLanguageValue = page
|
||||
.getByTestId("event-interface-language")
|
||||
.locator('div[class$="-singleValue"]');
|
||||
@@ -518,6 +521,9 @@ test.describe("Event Types tests", () => {
|
||||
|
||||
await test.step("should open first eventType and change Interface Language to Deutsche", async () => {
|
||||
await gotoFirstEventType(page);
|
||||
// Go to Advanced tab and enable offerSeats
|
||||
await page.click("[data-testid=vertical-tab-event_advanced_tab_title]");
|
||||
await page.click("[data-testid=event-interface-language-toggle]");
|
||||
await page.getByTestId("event-interface-language").click();
|
||||
await page.locator(`text="Deutsch"`).click();
|
||||
await saveEventType(page);
|
||||
@@ -574,6 +580,9 @@ test.describe("Event Types tests", () => {
|
||||
await page.goto("/event-types");
|
||||
await page.waitForSelector('[data-testid="event-types"]');
|
||||
await gotoFirstEventType(page);
|
||||
// Go to Advanced tab and enable offerSeats
|
||||
await page.click("[data-testid=vertical-tab-event_advanced_tab_title]");
|
||||
await page.click("[data-testid=event-interface-language-toggle]");
|
||||
await page.getByTestId("event-interface-language").click();
|
||||
await page.getByTestId("select-option-es").click();
|
||||
await saveEventType(page);
|
||||
|
||||
@@ -198,7 +198,7 @@ export async function expectSegmentCleared(page: Page) {
|
||||
// Check that no segment is selected (button shows default text)
|
||||
const segmentSelect = page.getByTestId("filter-segment-select");
|
||||
const buttonText = await segmentSelect.textContent();
|
||||
expect(buttonText?.trim()).toEqual("Segment");
|
||||
expect(buttonText?.trim()).toEqual("Saved filters");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -199,6 +199,7 @@ export function createBookingPageFixture(page: Page) {
|
||||
await expect(eventTypePage.getByTestId("time")).toHaveCount(count);
|
||||
},
|
||||
getAITranslationToggleDisabled: async () => {
|
||||
await page.click("[data-testid=vertical-tab-event_advanced_tab_title]");
|
||||
await page.waitForSelector('[data-testid="ai_translation_toggle"]', {
|
||||
timeout: 5000,
|
||||
state: "attached",
|
||||
@@ -207,6 +208,7 @@ export function createBookingPageFixture(page: Page) {
|
||||
return (await toggle.getAttribute("disabled")) !== null;
|
||||
},
|
||||
toggleAITranslation: async () => {
|
||||
await page.click("[data-testid=vertical-tab-event_advanced_tab_title]");
|
||||
await page.getByTestId("ai_translation_toggle").click();
|
||||
},
|
||||
updateEventTypeDescription: async (description: string) => {
|
||||
|
||||
@@ -81,10 +81,13 @@ test.describe("Managed Event Types", () => {
|
||||
await page.goto(`/event-types/${managedEvent.id}?tabName=setup`);
|
||||
await expect(page.getByTestId("vertical-tab-basics")).toHaveAttribute("aria-current", "page"); // fix the race condition
|
||||
await expect(page.getByTestId("vertical-tab-basics")).toContainText("Basics"); //fix the race condition
|
||||
|
||||
await page.locator("#location-select").click();
|
||||
const optionText = await getByKey(page, "organizer_default_conferencing_app");
|
||||
await expect(optionText).toBeVisible();
|
||||
await optionText.click();
|
||||
const targetText = (await localize("en"))("organizer_default_conferencing_app");
|
||||
const options = page.locator('[data-testid="location-select-item-conferencing"]');
|
||||
const targetOption = options.locator(`text=${targetText}`);
|
||||
await expect(targetOption).toBeVisible();
|
||||
await targetOption.click();
|
||||
await saveAndWaitForResponse(page);
|
||||
|
||||
await page.getByTestId("vertical-tab-assignment").click();
|
||||
@@ -190,7 +193,7 @@ test.describe("Managed Event Types", () => {
|
||||
});
|
||||
|
||||
const MANAGED_EVENT_TABS: { slug: string; locator: (page: Page) => Locator | Promise<Locator> }[] = [
|
||||
{ slug: "setup", locator: (page) => getByKey(page, "translate_description_button") },
|
||||
{ slug: "setup", locator: (page) => getByKey(page, "allow_multiple_durations") },
|
||||
{
|
||||
slug: "team",
|
||||
locator: (page) => getByKey(page, "automatically_add_all_team_members"),
|
||||
|
||||
@@ -209,7 +209,7 @@ test.describe("Email Signup Flow Test", async () => {
|
||||
data: {
|
||||
identifier: userToCreate.email,
|
||||
token,
|
||||
expires: new Date(new Date().setHours(168)), // +1 week
|
||||
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // +1 week
|
||||
team: {
|
||||
create: {
|
||||
name: "Rick's Team",
|
||||
|
||||
@@ -321,6 +321,7 @@
|
||||
"secret_meeting": "Secret Meeting",
|
||||
"login_instead": "Login instead",
|
||||
"create_account": "Create Account",
|
||||
"get_started": "Get started",
|
||||
"confirm_password": "Confirm password",
|
||||
"reset_your_password": "Set your new password with the instructions sent to your email address.",
|
||||
"org_banner_instructions": "Please upload image of {{width}} width and {{height}} height.",
|
||||
@@ -958,7 +959,7 @@
|
||||
"light_event_type_color": "Event Type Color (Light Theme)",
|
||||
"dark_event_type_color": "Event Type Color (Dark Theme)",
|
||||
"file_not_named": "File is not named [idOrSlug]/[user]",
|
||||
"create_team": "Create Team",
|
||||
"create_team": "Create team",
|
||||
"name": "Name",
|
||||
"nameless_team": "Nameless Team",
|
||||
"oauth_clients": "OAuth Clients",
|
||||
@@ -1019,6 +1020,7 @@
|
||||
"attendees": "Attendees",
|
||||
"add_attendees": "Add attendees",
|
||||
"show_advanced_settings": "Show advanced settings",
|
||||
"hide_advanced_settings": "Hide advanced settings",
|
||||
"event_name": "Event Name",
|
||||
"event_name_in_calendar": "Calendar event name",
|
||||
"event_name_tooltip": "The name that will appear in calendars",
|
||||
@@ -1231,7 +1233,7 @@
|
||||
"contact_sales": "Contact Sales",
|
||||
"error_404": "Error 404",
|
||||
"default": "Default",
|
||||
"set_to_default": "Set to Default",
|
||||
"set_to_default": "Set as Default",
|
||||
"new_schedule_btn": "New schedule",
|
||||
"add_new_schedule": "Add a new schedule",
|
||||
"add_new_calendar": "Add a new calendar",
|
||||
@@ -1329,7 +1331,7 @@
|
||||
"make_team_private_description": "Your team members won't be able to see other team members when this is turned on.",
|
||||
"you_cannot_see_team_members": "You cannot see all the team members of a private team.",
|
||||
"you_cannot_see_teams_of_org": "You cannot see teams of a private organization.",
|
||||
"allow_booker_to_select_duration": "Allow booker to select from multiple durations",
|
||||
"allow_multiple_durations": "Allow multiple durations",
|
||||
"impersonate_user_tip": "All uses of this feature is audited.",
|
||||
"impersonating_user_warning": "Impersonating username \"{{user}}\".",
|
||||
"impersonating_stop_instructions": "Click here to stop",
|
||||
@@ -2689,7 +2691,7 @@
|
||||
"enterprise": "Enterprise",
|
||||
"enterprise_description": "Upgrade to Enterprise to create your Organization",
|
||||
"create_your_org": "Create your Organization",
|
||||
"create_org": "Create Organization",
|
||||
"create_org": "Create organization",
|
||||
"create_your_org_description": "Upgrade to Organizations and receive a subdomain, unified billing, Insights, extensive whitelabeling and more",
|
||||
"create_your_enterprise_description": "Upgrade to Enterprise and get access to Active Directory Sync, SCIM Automatic User provisioning, Cal.ai Voice Agents, Admin APIs and more!",
|
||||
"other_payment_app_enabled": "You can only enable one payment app per event type",
|
||||
@@ -2923,6 +2925,7 @@
|
||||
"proceeding_agreement": "By proceeding, you agree to our",
|
||||
"terms": "Terms",
|
||||
"always_show_x_days": "Always {{x}} days available",
|
||||
"always_show_x_days_description": "Automatically moves the {{x}}-day booking window forward each day, ensuring your calendar always shows the next {{x}} days of available slots.",
|
||||
"team_subscription_cancelled_successfully": "Team subscription cancelled successfully",
|
||||
"team_subscription_cancellation_error": "Error while cancelling the team subscription",
|
||||
"unable_to_subscribe_to_the_platform": "An error occurred while trying to subscribe to the platform plan, please try again later",
|
||||
@@ -3144,7 +3147,8 @@
|
||||
"filter_operator_ends_with": "Ends with",
|
||||
"filter_operator_is_empty": "Is empty",
|
||||
"filter_operator_not_empty": "Not empty",
|
||||
"translate_description_button": "Translate title/description to the visitor's browser language using AI",
|
||||
"translate_description_button": "Auto translate title and description",
|
||||
"translate_description_button_description": "Automatically translate titles and descriptions to the visitor’s browser language using AI.",
|
||||
"rr_distribution_method": "Distribution",
|
||||
"rr_distribution_method_description": "Allows for optimising distribution for maximum availability or to aim for a more balanced assignment.",
|
||||
"rr_distribution_method_availability_title": "Maximize availability",
|
||||
@@ -3310,6 +3314,7 @@
|
||||
"seats_doesnt_support_recurring": "Seats feature doesn't support recurring event. Disable seats feature to make it recurring.",
|
||||
"recurring_event_seats_error": "Recurring event doesn't support seats feature. Disable seats feature or make the event non-recurring.",
|
||||
"segment": "Segment",
|
||||
"saved_filters": "Saved filters",
|
||||
"rename": "Rename",
|
||||
"rename_segment": "Rename Segment",
|
||||
"delete_segment": "Delete Segment",
|
||||
@@ -3339,6 +3344,7 @@
|
||||
"picklist": "Picklist",
|
||||
"most_cancelled_bookings": "Most Cancelled",
|
||||
"interface_language": "Interface Language",
|
||||
"interface_language_description": "Set your preferred language for the booking interface",
|
||||
"visitors_browser_language": "Visitor's browser language",
|
||||
"name_or_email": "Name or Email",
|
||||
"salesforce_round_robin_skip_fallback_to_lead_owner": "If no contact is found, fallback to lead owner if it exists",
|
||||
@@ -3510,10 +3516,20 @@
|
||||
"pbac_desc_update_workflows": "Edit and modify workflow settings",
|
||||
"pbac_desc_delete_workflows": "Remove workflows from the system",
|
||||
"pbac_resource_webhook": "Webhook",
|
||||
"pbac_resource_availability": "Availability",
|
||||
"pbac_resource_out_of_office": "Out of Office",
|
||||
"pbac_desc_create_webhooks": "Create webhooks",
|
||||
"pbac_desc_view_webhooks": "View webhooks",
|
||||
"pbac_desc_update_webhooks": "Update webhooks",
|
||||
"pbac_desc_delete_webhooks": "Delete webhooks",
|
||||
"pbac_desc_create_availability": "Create availability",
|
||||
"pbac_desc_view_availability": "View availability",
|
||||
"pbac_desc_update_availability": "Update availability",
|
||||
"pbac_desc_delete_availability": "Delete availability",
|
||||
"pbac_desc_create_out_of_office": "Create out of office",
|
||||
"pbac_desc_view_out_of_office": "View out of office",
|
||||
"pbac_desc_update_out_of_office": "Update out of office",
|
||||
"pbac_desc_delete_out_of_office": "Delete out of office",
|
||||
"pbac_desc_manage_workflows": "Full management access to all workflows",
|
||||
"pbac_desc_create_event_types": "Create event types",
|
||||
"pbac_desc_view_event_types": "View event types",
|
||||
@@ -3561,8 +3577,8 @@
|
||||
"rr_load_balancing_disabled_with_groups": "Load Balancing is not available with Round Robin Groups",
|
||||
"round_robin_settings_description": "Customize the default round robin settings for this team",
|
||||
"round_robin_settings_updated_successfully": "Round Robin settings updated successfully",
|
||||
"booking_creation_time": "Booking Creation Time",
|
||||
"meeting_start_time": "Meeting Start Time",
|
||||
"booking_creation_time": "Booking creation time",
|
||||
"meeting_start_time": "Meeting start time",
|
||||
"members_affected_by_disabling_delegation_credential": "Affected members",
|
||||
"no_members_affected_by_disabling_delegation_credential": "No members affected by disabling delegation credential",
|
||||
"download_expense_log": "Download Expense Log",
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import prismaMock from "../../../../tests/libs/__mocks__/prismaMock";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import {
|
||||
@@ -9,6 +7,13 @@ import {
|
||||
} from "@calcom/lib/intervalLimits/server/checkDurationLimits";
|
||||
import { validateIntervalLimitOrder } from "@calcom/lib/intervalLimits/validateIntervalLimitOrder";
|
||||
|
||||
const mockGetTotalBookingDuration = vi.fn();
|
||||
vi.mock("@calcom/lib/server/repository/booking", () => ({
|
||||
BookingRepository: vi.fn().mockImplementation(() => ({
|
||||
getTotalBookingDuration: mockGetTotalBookingDuration,
|
||||
})),
|
||||
}));
|
||||
|
||||
type MockData = {
|
||||
id: number;
|
||||
startDate: Date;
|
||||
@@ -22,19 +27,19 @@ const MOCK_DATA: MockData = {
|
||||
// Path: apps/web/test/lib/checkDurationLimits.ts
|
||||
describe("Check Duration Limits Tests", () => {
|
||||
it("Should return no errors if limit is not reached", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 0 }]);
|
||||
mockGetTotalBookingDuration.mockResolvedValue(0);
|
||||
await expect(
|
||||
checkDurationLimits({ PER_DAY: 60 }, MOCK_DATA.startDate, MOCK_DATA.id)
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
it("Should throw an error if limit is reached", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
|
||||
mockGetTotalBookingDuration.mockResolvedValue(60);
|
||||
await expect(
|
||||
checkDurationLimits({ PER_DAY: 60 }, MOCK_DATA.startDate, MOCK_DATA.id)
|
||||
).rejects.toThrowError();
|
||||
});
|
||||
it("Should pass with multiple duration limits", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 30 }]);
|
||||
mockGetTotalBookingDuration.mockResolvedValue(30);
|
||||
await expect(
|
||||
checkDurationLimits(
|
||||
{
|
||||
@@ -47,7 +52,7 @@ describe("Check Duration Limits Tests", () => {
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
it("Should pass with multiple duration limits with one undefined", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 30 }]);
|
||||
mockGetTotalBookingDuration.mockResolvedValue(30);
|
||||
await expect(
|
||||
checkDurationLimits(
|
||||
{
|
||||
@@ -60,7 +65,7 @@ describe("Check Duration Limits Tests", () => {
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
it("Should return no errors if limit is not reached with multiple bookings", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
|
||||
mockGetTotalBookingDuration.mockResolvedValue(60);
|
||||
await expect(
|
||||
checkDurationLimits(
|
||||
{
|
||||
@@ -73,7 +78,7 @@ describe("Check Duration Limits Tests", () => {
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
it("Should throw an error if one of the limit is reached with multiple bookings", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 90 }]);
|
||||
mockGetTotalBookingDuration.mockResolvedValue(90);
|
||||
await expect(
|
||||
checkDurationLimits(
|
||||
{
|
||||
@@ -90,7 +95,7 @@ describe("Check Duration Limits Tests", () => {
|
||||
// Path: apps/web/test/lib/checkDurationLimits.ts
|
||||
describe("Check Duration Limit Tests", () => {
|
||||
it("Should return no busyTimes and no error if limit is not reached", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
|
||||
mockGetTotalBookingDuration.mockResolvedValue(60);
|
||||
await expect(
|
||||
checkDurationLimit({
|
||||
key: "PER_DAY",
|
||||
|
||||
@@ -143,9 +143,10 @@ describe("handleChildrenEventTypes", () => {
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
const { createdAt, updatedAt, ...expectedEvType } = evType;
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...evType,
|
||||
...expectedEvType,
|
||||
parentId: 1,
|
||||
users: { connect: [{ id: 4 }] },
|
||||
lockTimeZoneToggleOnBookingPage: false,
|
||||
@@ -206,7 +207,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
bookingLimits: undefined,
|
||||
},
|
||||
});
|
||||
const { profileId, autoTranslateDescriptionEnabled, ...rest } = evType;
|
||||
const { profileId, autoTranslateDescriptionEnabled, createdAt, updatedAt, ...rest } = evType;
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...rest,
|
||||
@@ -317,9 +318,10 @@ describe("handleChildrenEventTypes", () => {
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
const { createdAt, updatedAt, ...expectedEvType } = evType;
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...evType,
|
||||
...expectedEvType,
|
||||
parentId: 1,
|
||||
users: { connect: [{ id: 4 }] },
|
||||
bookingLimits: undefined,
|
||||
@@ -383,7 +385,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
length: 30,
|
||||
},
|
||||
});
|
||||
const { profileId, autoTranslateDescriptionEnabled, ...rest } = evType;
|
||||
const { profileId, autoTranslateDescriptionEnabled, createdAt, updatedAt, ...rest } = evType;
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...rest,
|
||||
@@ -464,7 +466,11 @@ describe("handleChildrenEventTypes", () => {
|
||||
...evType,
|
||||
};
|
||||
|
||||
prismaMock.eventType.update.mockResolvedValue(mockUpdatedEventType);
|
||||
prismaMock.eventType.update.mockResolvedValue({
|
||||
...mockUpdatedEventType,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await updateChildrenEventTypes({
|
||||
eventTypeId: 1,
|
||||
@@ -480,9 +486,10 @@ describe("handleChildrenEventTypes", () => {
|
||||
updatedValues: {},
|
||||
});
|
||||
|
||||
const { createdAt, updatedAt, ...expectedEvType } = evType;
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...evType,
|
||||
...expectedEvType,
|
||||
bookingLimits: undefined,
|
||||
durationLimits: undefined,
|
||||
recurringEvent: undefined,
|
||||
@@ -507,7 +514,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
allowReschedulingCancelledBookings: false,
|
||||
},
|
||||
});
|
||||
const { profileId, rrSegmentQueryValue, ...rest } = evType;
|
||||
const { profileId, rrSegmentQueryValue, createdAt: _, updatedAt: __, ...rest } = evType;
|
||||
if ("workflows" in rest) delete rest.workflows;
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
|
||||
+393
-468
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -213,7 +213,8 @@
|
||||
"platform/atoms/event-type",
|
||||
"platform/atoms/calendar-settings",
|
||||
"platform/atoms/payment-form",
|
||||
"platform/atoms/conferencing-apps"
|
||||
"platform/atoms/conferencing-apps",
|
||||
"platform/atoms/calendar-view"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: "Calendar view"
|
||||
---
|
||||
|
||||
The calendar view atom is basically a weekly calendar view wherein users can see entire week at a glance, clearly showing both available and unavailable time slots. This view is designed to provide a comprehensive and easy-to-read schedule, helping users quickly identify open slots for booking or planning.
|
||||
|
||||
## Individual event type
|
||||
|
||||
Below code snippet can be used to render the calendar view atom for an individual event
|
||||
|
||||
```js
|
||||
import { CalendarView } from "@calcom/atoms";
|
||||
|
||||
export default function Booker( props : BookerProps ) {
|
||||
return (
|
||||
<div>
|
||||
<CalendarView username={props.calUsername} eventSlug={props.eventSlug} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Team event type
|
||||
|
||||
Below code snippet can be used to render the calendar view atom for a team event
|
||||
|
||||
```js
|
||||
import { CalendarView } from "@calcom/atoms";
|
||||
|
||||
export default function Booker( props : BookerProps ) {
|
||||
return (
|
||||
<div>
|
||||
<CalendarView isTeamEvent={true} teamId={props.teamId} eventSlug={props.eventSlug} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
For a demonstration of the calendar view atom, please refer to the video below.
|
||||
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
height="315"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/40ebab9efaa149019ea487e35ff8f656?sid=c6a748c3-0564-4400-ac7c-a17a404c9af6"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen="true"
|
||||
></iframe>
|
||||
|
||||
<p></p>
|
||||
|
||||
Below is a list of props that can be passed to the create event type atom
|
||||
|
||||
| Name | Required | Description |
|
||||
|:----------------------------|:----------|:-------------------------------------------------------------------------------------------------------|
|
||||
| username | Yes | Username of the person whose schedule is to be displayed |
|
||||
| eventSlug | Yes | Unique slug created for a particular event | |
|
||||
| isTeamEvent | No | Boolean indicating if it is a team event, to be passed only for team events |
|
||||
| teamId | No | The id of the team for which the event is created, to be passed only for team events |
|
||||
@@ -332,7 +332,7 @@ Below video shows a demonstration of the all the options you get in the advanced
|
||||
<iframe
|
||||
height="315"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/e0121b51920544eea92251dba99245fa?sid=ac627de7-5673-41b4-aa78-c6c9e439bdb5"
|
||||
src="https://www.loom.com/embed/7a03c4a453ca401ab787b2db98728f31?sid=8cdc6398-0017-41c8-a0a1-8fa31f80616c"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen="true"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { config } from "@calcom/eslint-config/base";
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { nextJsConfig } from "@calcom/eslint-config/next-js";
|
||||
|
||||
export default nextJsConfig;
|
||||
@@ -6,10 +6,11 @@
|
||||
"dev": "PORT=5100 next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/atoms": "*",
|
||||
"@calcom/atoms": "workspace:*",
|
||||
"@calcom/eslint-config": "workspace:*",
|
||||
"@prisma/client": "6.7.0",
|
||||
"next": "14.0.4",
|
||||
"prisma": "^6.7.0",
|
||||
@@ -22,8 +23,6 @@
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.34.0",
|
||||
"eslint-config-next": "14.0.4",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"typescript": "^4.9.4"
|
||||
|
||||
+10
-9
@@ -8,6 +8,7 @@
|
||||
"packages/*",
|
||||
"packages/embeds/*",
|
||||
"packages/features/*",
|
||||
"packages/app-store",
|
||||
"packages/app-store/*",
|
||||
"packages/platform/*",
|
||||
"packages/platform/examples/base",
|
||||
@@ -53,9 +54,9 @@
|
||||
"env-check:common": "dotenv-checker --schema .env.example --env .env",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||
"heroku-postbuild": "turbo run @calcom/web#build",
|
||||
"lint:fix": "turbo run lint:fix",
|
||||
"lint:report": "turbo run lint:report",
|
||||
"lint": "turbo run lint",
|
||||
"lint:fix": "turbo lint:fix",
|
||||
"lint:report": "turbo lint:report",
|
||||
"lint": "turbo lint",
|
||||
"postinstall": "husky install && turbo run post-install",
|
||||
"pre-commit": "lint-staged",
|
||||
"predev": "echo 'Checking env files'",
|
||||
@@ -71,8 +72,8 @@
|
||||
"test-e2e:embed": "yarn db-seed && yarn e2e:embed",
|
||||
"test-e2e:embed-react": "yarn db-seed && yarn e2e:embed-react",
|
||||
"test-playwright": "yarn playwright test --config=playwright.config.ts",
|
||||
"test": "vitest run",
|
||||
"test:ui": "vitest --ui",
|
||||
"test": "TZ=UTC vitest run",
|
||||
"test:ui": "TZ=UTC vitest --ui",
|
||||
"type-check": "turbo run type-check",
|
||||
"type-check:ci": "turbo run type-check:ci --log-prefix=none",
|
||||
"web": "yarn workspace @calcom/web",
|
||||
@@ -84,6 +85,7 @@
|
||||
"changesets-release": "NODE_OPTIONS='--max_old_space_size=12288' turbo run build --filter=@calcom/atoms && yarn changeset publish"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/eslint-config": "workspace:*",
|
||||
"@changesets/changelog-github": "^0.5.1",
|
||||
"@changesets/cli": "2.29.4",
|
||||
"@faker-js/faker": "9.2.0",
|
||||
@@ -96,7 +98,7 @@
|
||||
"c8": "^7.13.0",
|
||||
"checkly": "latest",
|
||||
"dotenv-checker": "^1.1.5",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint": "9.36.0",
|
||||
"husky": "^8.0.0",
|
||||
"i18n-unused": "^0.13.0",
|
||||
"jest-diff": "^29.5.0",
|
||||
@@ -110,6 +112,7 @@
|
||||
"prismock": "1.35.3",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"tsc-absolute": "^1.0.0",
|
||||
"turbo": "^2.5.5",
|
||||
"typescript": "5.9.0-beta",
|
||||
"vitest": "^2.1.9",
|
||||
"vitest-fetch-mock": "^0.3.0",
|
||||
@@ -122,9 +125,7 @@
|
||||
"@vercel/functions": "^1.4.0",
|
||||
"city-timezones": "^1.2.1",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"eslint": "^8.34.0",
|
||||
"p-limit": "^6.2.0",
|
||||
"turbo": "^2.5.5"
|
||||
"p-limit": "^6.2.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"types-ramda": "0.29.4",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"dist/cli.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/lib": "workspace:*",
|
||||
"ink": "^3.2.0",
|
||||
"ink-select-input": "^4.2.1",
|
||||
"ink-text-input": "^4.0.3",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/lib": "workspace:*",
|
||||
"@getalby/lightning-tools": "^5.1.2",
|
||||
"@getalby/sdk": "^4.0.0",
|
||||
"@webbtc/webln-types": "^3.0.0",
|
||||
@@ -13,7 +13,7 @@
|
||||
"svix": "^0.85.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "Your Bitcoin & Nostr companion for the web. Use Alby to charge Satoshi for your Cal.com meetings.\r"
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
"main": "./index.ts",
|
||||
"description": "The joyful productivity app\r\r",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
"main": "./index.ts",
|
||||
"description": "Apple calendar runs both the macOS and iOS mobile operating systems. Offering online cloud backup of calendars using Apple’s iCloud service, it can sync with Google Calendar and Microsoft Exchange Server. Users can schedule events in their day that include time, location, duration, and extra notes.",
|
||||
"dependencies": {
|
||||
"@calcom/prisma": "*",
|
||||
"@calcom/prisma": "workspace:*",
|
||||
"react-hook-form": "^7.43.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "Attio is the AI-native CRM that builds, scales and grows your company to the next level."
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "You simply paste a Cal.com link of someone, select a frequency and we will take care of the scheduling for you.\r\r"
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "Request a signed Business Associate Agreement for your HIPAA compliance records"
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "Basecamp puts everything you need to get work done in one place. It's the calm, organized way to manage projects, work with clients, and communicate company-wide."
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.1",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "24x7 AI receptionists to answer all inbound calls"
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "BTCPay Server is a self-hosted open source Bitcoin payment processor. Start receiving bitcoin payments for your events and bookings."
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
"main": "./index.ts",
|
||||
"description": "Caldav is a protocol that allows different clients/servers to access scheduling information on remote servers as well as schedule meetings with other users on the same server or other servers. It extends WebDAV specification and uses iCalendar format for the data.",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/prisma": "*",
|
||||
"@calcom/ui": "*",
|
||||
"@calcom/lib": "workspace:*",
|
||||
"@calcom/prisma": "workspace:*",
|
||||
"@calcom/ui": "workspace:*",
|
||||
"react-hook-form": "^7.43.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
"main": "./index.ts",
|
||||
"description": "Feel connected with your remote team. Team events, new hire onboardings, coffee chats, all on Campfire. No more awkward Zoom calls.\r\r",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "Add Cal.com to your Chatbase AI Chatbot and accept bookings right inside chat."
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.1.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "Create, List and Interact with Your Cal.com links and connections easily."
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/prisma": "*"
|
||||
"@calcom/lib": "workspace:*",
|
||||
"@calcom/prisma": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "All of your commitments, now in one place."
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"main": "./index.ts",
|
||||
"description": "Cal Video is the in-house web-based video conferencing platform powered by Daily.co, which is minimalistic and lightweight, but has most of the features you need.",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/prisma": "*"
|
||||
"@calcom/lib": "workspace:*",
|
||||
"@calcom/prisma": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/lib": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "workspace:*"
|
||||
},
|
||||
"description": "Integrate Deel with your Calendar and get automatic updates on key employee dates like PTO, start days, public holidays, and birthdays. No manual entry. No errors. Just a calendar that's always in sync with your team."
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user