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
|
/**/package.json @calcom/Foundation
|
||||||
/apps/api/**/* @calcom/Platform @calcom/Foundation
|
/apps/api/**/* @calcom/API
|
||||||
/apps/ui-playground/**/* @calcom/Consumer @calcom/Foundation
|
/apps/ui-playground/**/* @calcom/Consumer @calcom/Foundation
|
||||||
/apps/web/**/layout.tsx @calcom/Consumer @calcom/Foundation
|
/apps/web/**/layout.tsx @calcom/Consumer @calcom/Foundation
|
||||||
/apps/web/lib/csp.ts @calcom/Foundation
|
/apps/web/lib/csp.ts @calcom/Foundation
|
||||||
|
|||||||
@@ -74,14 +74,8 @@ jobs:
|
|||||||
uses: ./.github/workflows/e2e-embed-react.yml
|
uses: ./.github/workflows/e2e-embed-react.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
e2e-atoms:
|
|
||||||
name: Tests
|
|
||||||
needs: [lint, build, build-atoms, build-api-v2]
|
|
||||||
uses: ./.github/workflows/e2e-atoms.yml
|
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
required:
|
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()
|
if: always()
|
||||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: E2E
|
name: Check breaking changes and run E2E
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
env:
|
env:
|
||||||
@@ -23,6 +23,10 @@ env:
|
|||||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||||
SLOTS_CACHE_TTL: ${{ secrets.CI_SLOTS_CACHE_TTL }}
|
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:
|
jobs:
|
||||||
e2e:
|
e2e:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
@@ -68,6 +72,18 @@ jobs:
|
|||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
- uses: ./.github/actions/cache-db
|
- 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
|
- name: Run Tests
|
||||||
working-directory: apps/api/v2
|
working-directory: apps/api/v2
|
||||||
run: |
|
run: |
|
||||||
@@ -75,6 +91,7 @@ jobs:
|
|||||||
EXIT_CODE=$?
|
EXIT_CODE=$?
|
||||||
echo "yarn workspace @calcom/platform-libraries build && yarn test:e2e command exit code: $EXIT_CODE"
|
echo "yarn workspace @calcom/platform-libraries build && yarn test:e2e command exit code: $EXIT_CODE"
|
||||||
exit $EXIT_CODE
|
exit $EXIT_CODE
|
||||||
|
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
||||||
organization-name: calcom
|
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:
|
apply-labels-from-issue:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
|||||||
@@ -195,13 +195,6 @@ jobs:
|
|||||||
uses: ./.github/workflows/e2e-embed-react.yml
|
uses: ./.github/workflows/e2e-embed-react.yml
|
||||||
secrets: inherit
|
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:
|
analyze:
|
||||||
name: Analyze Build
|
name: Analyze Build
|
||||||
needs: [build]
|
needs: [build]
|
||||||
@@ -211,7 +204,7 @@ jobs:
|
|||||||
merge-reports:
|
merge-reports:
|
||||||
name: Merge reports
|
name: Merge reports
|
||||||
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
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
|
uses: ./.github/workflows/merge-reports.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
@@ -255,8 +248,7 @@ jobs:
|
|||||||
e2e-api-v2,
|
e2e-api-v2,
|
||||||
e2e-embed,
|
e2e-embed,
|
||||||
e2e-embed-react,
|
e2e-embed-react,
|
||||||
e2e-app-store,
|
e2e-app-store
|
||||||
e2e-atoms,
|
|
||||||
]
|
]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||||
|
|||||||
@@ -16,5 +16,4 @@ public
|
|||||||
packages/prisma/zod
|
packages/prisma/zod
|
||||||
packages/prisma/enums
|
packages/prisma/enums
|
||||||
apps/web/public/embed
|
apps/web/public/embed
|
||||||
apps/api/v2/swagger/documentation.json
|
|
||||||
packages/ui/components/icon/dynamicIconImports.tsx
|
packages/ui/components/icon/dynamicIconImports.tsx
|
||||||
|
|||||||
Vendored
@@ -13,5 +13,8 @@
|
|||||||
"connect": "^3.7.0",
|
"connect": "^3.7.0",
|
||||||
"http": "^0.0.1-security",
|
"http": "^0.0.1-security",
|
||||||
"http-proxy-middleware": "^2.0.6"
|
"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",
|
"build": "next build",
|
||||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
|
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
|
||||||
"dev": "PORT=3003 next dev",
|
"dev": "PORT=3003 next dev",
|
||||||
"lint": "eslint . --ignore-path .gitignore",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||||
"start": "PORT=3003 next start",
|
"start": "PORT=3003 next start",
|
||||||
"docker-start-api": "PORT=80 next start",
|
"docker-start-api": "PORT=80 next start",
|
||||||
@@ -18,18 +18,19 @@
|
|||||||
"type-check:ci": "tsc-absolute --pretty --noEmit"
|
"type-check:ci": "tsc-absolute --pretty --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/tsconfig": "*",
|
"@calcom/eslint-config": "workspace:*",
|
||||||
"@calcom/types": "*",
|
"@calcom/tsconfig": "workspace:*",
|
||||||
|
"@calcom/types": "workspace:*",
|
||||||
"node-mocks-http": "^1.11.0"
|
"node-mocks-http": "^1.11.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/app-store": "*",
|
"@calcom/app-store": "workspace:*",
|
||||||
"@calcom/dayjs": "*",
|
"@calcom/dayjs": "workspace:*",
|
||||||
"@calcom/emails": "*",
|
"@calcom/emails": "workspace:*",
|
||||||
"@calcom/features": "*",
|
"@calcom/features": "workspace:*",
|
||||||
"@calcom/lib": "*",
|
"@calcom/lib": "workspace:*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "workspace:*",
|
||||||
"@calcom/trpc": "*",
|
"@calcom/trpc": "workspace:*",
|
||||||
"@prisma/nextjs-monorepo-workaround-plugin": "^6.16.1",
|
"@prisma/nextjs-monorepo-workaround-plugin": "^6.16.1",
|
||||||
"@sentry/nextjs": "^9.15.0",
|
"@sentry/nextjs": "^9.15.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"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.count({ where }),
|
||||||
prisma.user.findMany({ where, take, skip }),
|
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:enums": "yarn workspace @calcom/platform-enums build:watch",
|
||||||
"_dev:build:watch:utils": "yarn workspace @calcom/platform-utils 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: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": "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",
|
"dev:no-docker": "yarn dev:build && yarn copy-swagger-module && yarn start --watch",
|
||||||
"start:debug": "nest start --debug --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",
|
"test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.ts --watch",
|
||||||
"prisma": "yarn workspace @calcom/prisma prisma",
|
"prisma": "yarn workspace @calcom/prisma prisma",
|
||||||
"generate-schemas": "yarn prisma generate && yarn prisma format",
|
"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",
|
"prepare": "yarn run snyk-protect",
|
||||||
"snyk-protect": "snyk-protect"
|
"snyk-protect": "snyk-protect"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@axiomhq/winston": "^1.2.0",
|
"@axiomhq/winston": "^1.2.0",
|
||||||
"@calcom/platform-constants": "*",
|
"@calcom/platform-constants": "workspace:*",
|
||||||
"@calcom/platform-enums": "*",
|
"@calcom/platform-enums": "workspace:*",
|
||||||
"@calcom/platform-libraries": "*",
|
"@calcom/platform-libraries": "workspace:*",
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "workspace:*",
|
||||||
"@calcom/platform-utils": "*",
|
"@calcom/platform-utils": "workspace:*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "workspace:*",
|
||||||
"@golevelup/ts-jest": "^0.4.0",
|
"@golevelup/ts-jest": "^0.4.0",
|
||||||
"@microsoft/microsoft-graph-types-beta": "^0.42.0-preview",
|
"@microsoft/microsoft-graph-types-beta": "^0.42.0-preview",
|
||||||
"@nest-lab/throttler-storage-redis": "1.0.0",
|
"@nest-lab/throttler-storage-redis": "1.0.0",
|
||||||
@@ -88,6 +89,7 @@
|
|||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@calcom/eslint-config": "workspace:*",
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.0.0",
|
||||||
"@nestjs/testing": "^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 { Type } from "class-transformer";
|
||||||
import { IsEnum, ValidateNested } from "class-validator";
|
import { IsEnum, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
import { ReassignBookingOutput_2024_08_13 as ReassignBookingOutputData_2024_08_13 } from "@calcom/platform-types";
|
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 {
|
export class ReassignBookingOutput_2024_08_13 {
|
||||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import {
|
|||||||
BookingOutput_2024_08_13,
|
BookingOutput_2024_08_13,
|
||||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
CreateRecurringSeatedBookingOutput_2024_08_13,
|
||||||
CreateSeatedBookingOutput_2024_08_13,
|
CreateSeatedBookingOutput_2024_08_13,
|
||||||
GetRecurringSeatedBookingOutput_2024_08_13,
|
|
||||||
GetSeatedBookingOutput_2024_08_13,
|
|
||||||
RecurringBookingOutput_2024_08_13,
|
RecurringBookingOutput_2024_08_13,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
|
|
||||||
|
|||||||
+12
-108
@@ -1,39 +1,29 @@
|
|||||||
import type { AppConfig } from "@/config/type";
|
import type { AppConfig } from "@/config/type";
|
||||||
import { getEnv } from "@/env";
|
|
||||||
import { Logger } from "@nestjs/common";
|
import { Logger } from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { NestFactory } from "@nestjs/core";
|
import { NestFactory } from "@nestjs/core";
|
||||||
import type { NestExpressApplication } from "@nestjs/platform-express";
|
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 "dotenv/config";
|
||||||
import * as fs from "fs";
|
|
||||||
import { Server } from "http";
|
|
||||||
import { WinstonModule } from "nest-winston";
|
import { WinstonModule } from "nest-winston";
|
||||||
|
|
||||||
import { bootstrap } from "./app";
|
import { bootstrap } from "./app";
|
||||||
import { AppModule } from "./app.module";
|
import { AppModule } from "./app.module";
|
||||||
import { loggerConfig } from "./lib/logger";
|
import { loggerConfig } from "./lib/logger";
|
||||||
|
import { generateSwaggerForApp } from "./swagger/generate-swagger";
|
||||||
|
|
||||||
const HttpMethods: (keyof PathItemObject)[] = ["get", "post", "put", "delete", "patch", "options", "head"];
|
run().catch((error: Error) => {
|
||||||
|
console.error("Failed to start Cal Platform API", { error: error.stack });
|
||||||
const run = async () => {
|
process.exit(1);
|
||||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
});
|
||||||
logger: WinstonModule.createLogger(loggerConfig()),
|
|
||||||
bodyParser: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const app = await createNestApp();
|
||||||
const logger = new Logger("App");
|
const logger = new Logger("App");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
bootstrap(app);
|
bootstrap(app);
|
||||||
const port = app.get(ConfigService<AppConfig, true>).get("api.port", { infer: true });
|
const port = app.get(ConfigService<AppConfig, true>).get("api.port", { infer: true });
|
||||||
void generateSwagger(app);
|
generateSwaggerForApp(app);
|
||||||
await app.listen(port);
|
await app.listen(port);
|
||||||
logger.log(`Application started on port: ${port}`);
|
logger.log(`Application started on port: ${port}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -42,97 +32,11 @@ const run = async () => {
|
|||||||
error,
|
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 {
|
export async function createNestApp() {
|
||||||
return obj && typeof obj === "object" && "tags" in obj;
|
return NestFactory.create<NestExpressApplication>(AppModule, {
|
||||||
}
|
logger: WinstonModule.createLogger(loggerConfig()),
|
||||||
|
bodyParser: false,
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 { BillingProcessor } from "@/modules/billing/billing.processor";
|
||||||
import { BillingRepository } from "@/modules/billing/billing.repository";
|
import { BillingRepository } from "@/modules/billing/billing.repository";
|
||||||
import { BillingController } from "@/modules/billing/controllers/billing.controller";
|
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 { BillingConfigService } from "@/modules/billing/services/billing.config.service";
|
||||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||||
import { ManagedOrganizationsBillingService } from "@/modules/billing/services/managed-organizations.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 { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { RedisModule } from "@/modules/redis/redis.module";
|
||||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||||
import { UsersModule } from "@/modules/users/users.module";
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
import { BullModule } from "@nestjs/bull";
|
import { BullModule } from "@nestjs/bull";
|
||||||
@@ -18,6 +20,7 @@ import { Module } from "@nestjs/common";
|
|||||||
imports: [
|
imports: [
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
StripeModule,
|
StripeModule,
|
||||||
|
RedisModule,
|
||||||
MembershipsModule,
|
MembershipsModule,
|
||||||
OrganizationsModule,
|
OrganizationsModule,
|
||||||
BullModule.registerQueue({
|
BullModule.registerQueue({
|
||||||
@@ -32,6 +35,10 @@ import { Module } from "@nestjs/common";
|
|||||||
providers: [
|
providers: [
|
||||||
BillingConfigService,
|
BillingConfigService,
|
||||||
BillingService,
|
BillingService,
|
||||||
|
{
|
||||||
|
provide: "IBillingService",
|
||||||
|
useClass: BillingServiceCachingProxy,
|
||||||
|
},
|
||||||
BillingRepository,
|
BillingRepository,
|
||||||
BillingProcessor,
|
BillingProcessor,
|
||||||
ManagedOrganizationsBillingService,
|
ManagedOrganizationsBillingService,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { bootstrap } from "@/app";
|
import { bootstrap } from "@/app";
|
||||||
import { AppModule } from "@/app.module";
|
import { AppModule } from "@/app.module";
|
||||||
|
import { CheckPlatformBillingResponseDto } from "@/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
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 { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { randomString } from "test/utils/randomString";
|
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";
|
import type { Team, PlatformBilling } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@@ -31,9 +32,8 @@ describe("Platform Billing Controller (e2e)", () => {
|
|||||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||||
let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture;
|
let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture;
|
||||||
let organization: Team;
|
let organization: Team;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withApiAuth(
|
const moduleRef = await withNextAuth(
|
||||||
userEmail,
|
userEmail,
|
||||||
Test.createTestingModule({
|
Test.createTestingModule({
|
||||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||||
@@ -44,8 +44,10 @@ describe("Platform Billing Controller (e2e)", () => {
|
|||||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef);
|
platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
organization = await organizationsRepositoryFixture.create({
|
organization = await organizationsRepositoryFixture.create({
|
||||||
name: `billing-organization-${randomString()}`,
|
name: `billing-organization-${randomString()}`,
|
||||||
|
isPlatform: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
user = await userRepositoryFixture.create({
|
user = await userRepositoryFixture.create({
|
||||||
@@ -87,6 +89,17 @@ describe("Platform Billing Controller (e2e)", () => {
|
|||||||
await app.close();
|
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", () => {
|
it("/billing/webhook (POST) should set billing free plan for org", () => {
|
||||||
jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(
|
jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(
|
||||||
() =>
|
() =>
|
||||||
@@ -119,6 +132,17 @@ describe("Platform Billing Controller (e2e)", () => {
|
|||||||
expect(billing?.plan).toEqual("FREE");
|
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", () => {
|
it("/billing/webhook (POST) failed payment should set billing free plan to overdue", () => {
|
||||||
jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(
|
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 { SubscribeToPlanInput } from "@/modules/billing/controllers/inputs/subscribe-to-plan.input";
|
||||||
import { CheckPlatformBillingResponseDto } from "@/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto";
|
import { CheckPlatformBillingResponseDto } from "@/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto";
|
||||||
import { SubscribeTeamToBillingResponseDto } from "@/modules/billing/controllers/outputs/SubscribeTeamToBillingResponse.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 { StripeService } from "@/modules/stripe/stripe.service";
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
Headers,
|
Headers,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
|
Inject,
|
||||||
Logger,
|
Logger,
|
||||||
Delete,
|
Delete,
|
||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
@@ -40,7 +41,7 @@ export class BillingController {
|
|||||||
private logger = new Logger("Billing Controller");
|
private logger = new Logger("Billing Controller");
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly billingService: BillingService,
|
@Inject("IBillingService") private readonly billingService: IBillingService,
|
||||||
public readonly stripeService: StripeService,
|
public readonly stripeService: StripeService,
|
||||||
private readonly configService: ConfigService<AppConfig>
|
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 { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||||
import { BILLING_QUEUE, INCREMENT_JOB, IncrementJobDataType } from "@/modules/billing/billing.processor";
|
import { BILLING_QUEUE, INCREMENT_JOB, IncrementJobDataType } from "@/modules/billing/billing.processor";
|
||||||
import { BillingRepository } from "@/modules/billing/billing.repository";
|
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 { BillingConfigService } from "@/modules/billing/services/billing.config.service";
|
||||||
import { PlatformPlan } from "@/modules/billing/types";
|
import { PlatformPlan } from "@/modules/billing/types";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
@@ -23,14 +24,14 @@ import { DateTime } from "luxon";
|
|||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BillingService implements OnModuleDestroy {
|
export class BillingService implements IBillingService, OnModuleDestroy {
|
||||||
private logger = new Logger("BillingService");
|
private logger = new Logger("BillingService");
|
||||||
private readonly webAppUrl: string;
|
private readonly webAppUrl: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly teamsRepository: OrganizationsRepository,
|
private readonly teamsRepository: OrganizationsRepository,
|
||||||
public readonly stripeService: StripeService,
|
public readonly stripeService: StripeService,
|
||||||
private readonly billingRepository: BillingRepository,
|
public readonly billingRepository: BillingRepository,
|
||||||
private readonly configService: ConfigService<AppConfig>,
|
private readonly configService: ConfigService<AppConfig>,
|
||||||
private readonly billingConfigService: BillingConfigService,
|
private readonly billingConfigService: BillingConfigService,
|
||||||
private readonly usersRepository: UsersRepository,
|
private readonly usersRepository: UsersRepository,
|
||||||
@@ -43,18 +44,23 @@ export class BillingService implements OnModuleDestroy {
|
|||||||
|
|
||||||
async getBillingData(teamId: number) {
|
async getBillingData(teamId: number) {
|
||||||
const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
|
const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
|
||||||
|
|
||||||
if (teamWithBilling?.platformBilling) {
|
if (teamWithBilling?.platformBilling) {
|
||||||
if (!teamWithBilling?.platformBilling.subscriptionId) {
|
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 {
|
} 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);
|
const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
|
||||||
let customerId = teamWithBilling?.platformBilling?.customerId;
|
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) {
|
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 { Type } from "class-transformer";
|
||||||
import { IsEnum, IsISO8601, IsOptional, IsString, ValidateNested } from "class-validator";
|
import { IsEnum, IsISO8601, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
@@ -297,6 +297,12 @@ export class DateTimeWithZone {
|
|||||||
timeZone!: string;
|
timeZone!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiExtraModels(
|
||||||
|
CalendarEventVideoLocation,
|
||||||
|
CalendarEventPhoneLocation,
|
||||||
|
CalendarEventSipLocation,
|
||||||
|
CalendarEventMoreLocation
|
||||||
|
)
|
||||||
export class UnifiedCalendarEventOutput {
|
export class UnifiedCalendarEventOutput {
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => DateTimeWithZone)
|
@Type(() => DateTimeWithZone)
|
||||||
|
|||||||
+3
-1
@@ -4,9 +4,11 @@ import {
|
|||||||
MicrosoftServiceAccountKeyInput,
|
MicrosoftServiceAccountKeyInput,
|
||||||
} from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input";
|
} from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input";
|
||||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||||
|
import { ApiExtraModels } from "@nestjs/swagger";
|
||||||
import { Expose, Type } from "class-transformer";
|
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 {
|
export class CreateDelegationCredentialInput {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
+4
-3
@@ -3,10 +3,11 @@ import {
|
|||||||
MicrosoftServiceAccountKeyInput,
|
MicrosoftServiceAccountKeyInput,
|
||||||
ServiceAccountKeyValidator,
|
ServiceAccountKeyValidator,
|
||||||
} from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input";
|
} from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input";
|
||||||
import { ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
import { ApiExtraModels, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
||||||
import { Expose, plainToClass, Transform, Type } from "class-transformer";
|
import { Expose, Type } from "class-transformer";
|
||||||
import { IsBoolean, IsOptional, Validate, ValidateNested } from "class-validator";
|
import { IsBoolean, IsOptional, Validate } from "class-validator";
|
||||||
|
|
||||||
|
@ApiExtraModels(GoogleServiceAccountKeyInput, MicrosoftServiceAccountKeyInput)
|
||||||
export class UpdateDelegationCredentialInput {
|
export class UpdateDelegationCredentialInput {
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
@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 { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
|
||||||
import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service";
|
import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service";
|
||||||
import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.controller";
|
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 { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
|
||||||
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service";
|
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";
|
import { OrganizationsTeamsRoutingFormsModule } from "@/modules/organizations/teams/routing-forms/organizations-teams-routing-forms.module";
|
||||||
@@ -183,6 +184,7 @@ import { Module } from "@nestjs/common";
|
|||||||
OrganizationsMembershipsController,
|
OrganizationsMembershipsController,
|
||||||
OrganizationsEventTypesController,
|
OrganizationsEventTypesController,
|
||||||
OrganizationsTeamsMembershipsController,
|
OrganizationsTeamsMembershipsController,
|
||||||
|
OrganizationsTeamsInviteController,
|
||||||
OrganizationsAttributesController,
|
OrganizationsAttributesController,
|
||||||
OrganizationsAttributesOptionsController,
|
OrganizationsAttributesOptionsController,
|
||||||
OrganizationsWebhooksController,
|
OrganizationsWebhooksController,
|
||||||
|
|||||||
+1
-1
@@ -68,7 +68,7 @@ export class ManagedOrganizationsRepository {
|
|||||||
managedOrganization: managedOrganizationFilter,
|
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.count({ where }),
|
||||||
this.dbRead.prisma.managedOrganization.findMany({
|
this.dbRead.prisma.managedOrganization.findMany({
|
||||||
where,
|
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 { Type } from "class-transformer";
|
||||||
import { IsArray, IsBoolean, IsInt, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator";
|
import { IsArray, IsBoolean, IsInt, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
@@ -75,6 +75,7 @@ class Routing {
|
|||||||
crmOwnerRecordType?: string;
|
crmOwnerRecordType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiExtraModels(SlotsOutput_2024_09_04, RangeSlotsOutput_2024_09_04)
|
||||||
export class CreateRoutingFormResponseOutputData {
|
export class CreateRoutingFormResponseOutputData {
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
type: Number,
|
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 { Type } from "class-transformer";
|
||||||
import { IsNotEmptyObject, ValidateNested } from "class-validator";
|
import { IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
import { ApiResponseWithoutData, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
import { ApiResponseWithoutData, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@ApiExtraModels(TeamEventTypeOutput_2024_06_14)
|
||||||
export class CreateTeamEventTypeOutput extends ApiResponseWithoutData {
|
export class CreateTeamEventTypeOutput extends ApiResponseWithoutData {
|
||||||
@IsNotEmptyObject()
|
@IsNotEmptyObject()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsNotEmptyObject, ValidateNested } from "class-validator";
|
import { IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
import { ApiResponseWithoutData, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
import { ApiResponseWithoutData, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@ApiExtraModels(TeamEventTypeOutput_2024_06_14)
|
||||||
export class UpdateTeamEventTypeOutput extends ApiResponseWithoutData {
|
export class UpdateTeamEventTypeOutput extends ApiResponseWithoutData {
|
||||||
@IsNotEmptyObject()
|
@IsNotEmptyObject()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||||
import { Expose } from "class-transformer";
|
import { Expose } from "class-transformer";
|
||||||
import { IsEnum, ValidateNested, IsString, IsUrl } from "class-validator";
|
import { IsEnum, ValidateNested, IsString, IsUrl } from "class-validator";
|
||||||
|
|
||||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { TeamOutputDto } from "@calcom/platform-types";
|
import { TeamOutputDto } from "@calcom/platform-types";
|
||||||
|
|
||||||
class Output {
|
class CreateTeamOutputData {
|
||||||
@Expose()
|
@Expose()
|
||||||
@IsString()
|
@IsString()
|
||||||
message!: string;
|
message!: string;
|
||||||
@@ -19,16 +19,17 @@ class Output {
|
|||||||
pendingTeam!: TeamOutputDto;
|
pendingTeam!: TeamOutputDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiExtraModels(TeamOutputDto, CreateTeamOutputData)
|
||||||
export class CreateTeamOutput {
|
export class CreateTeamOutput {
|
||||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
oneOf: [{ $ref: getSchemaPath(Output) }, { $ref: getSchemaPath(TeamOutputDto) }],
|
oneOf: [{ $ref: getSchemaPath(CreateTeamOutputData) }, { $ref: getSchemaPath(TeamOutputDto) }],
|
||||||
description: "Either an Output object or a TeamOutputDto.",
|
description: "Either an Output object or a TeamOutputDto.",
|
||||||
})
|
})
|
||||||
@Expose()
|
@Expose()
|
||||||
@ValidateNested()
|
@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"
|
// "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.
|
// should reside in the project's node_modules already before the "nest start" command is executed.
|
||||||
async function copyNestSwagger() {
|
async function copyNestSwagger() {
|
||||||
const monorepoRoot = path.resolve(__dirname, "../../../../");
|
const monorepoRoot = path.resolve(__dirname, "../../../../../");
|
||||||
const nodeModulesNestjs = path.resolve(__dirname, "../node_modules/@nestjs");
|
const nodeModulesNestjs = path.resolve(__dirname, "../../node_modules/@nestjs");
|
||||||
const swaggerModulePath = "@nestjs/swagger";
|
const swaggerModulePath = "@nestjs/swagger";
|
||||||
|
|
||||||
const sourceDir = path.join(monorepoRoot, "node_modules", swaggerModulePath);
|
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,
|
id: orgId,
|
||||||
customerId: `cus_123_${randomString}`,
|
customerId: `cus_123_${randomString}`,
|
||||||
subscriptionId: `sub_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",
|
"dev": "next dev --turbopack --port=1337",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/config": "*",
|
"@calcom/config": "workspace:*",
|
||||||
"@calcom/lib": "*",
|
"@calcom/lib": "workspace:*",
|
||||||
"@calcom/ui": "*",
|
"@calcom/ui": "workspace:*",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
"airtable": "^0.12.2",
|
"airtable": "^0.12.2",
|
||||||
"fumadocs-core": "^14.5.4",
|
"fumadocs-core": "^14.5.4",
|
||||||
@@ -24,14 +24,12 @@
|
|||||||
"react-hot-toast": "^2.5.2"
|
"react-hot-toast": "^2.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/tsconfig": "*",
|
"@calcom/eslint-config": "workspace:*",
|
||||||
"@eslint/eslintrc": "^3",
|
"@calcom/tsconfig": "workspace:*",
|
||||||
"@types/mdx": "^2.0.13",
|
"@types/mdx": "^2.0.13",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"eslint": "^8.34.0",
|
|
||||||
"eslint-config-next": "15.1.6",
|
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5.9.0-beta"
|
"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 { z } from "zod";
|
||||||
|
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
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 { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||||
|
|
||||||
@@ -33,9 +35,30 @@ const Page = async ({ params }: PageProps) => {
|
|||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
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 (
|
return (
|
||||||
<ShellMainAppDir heading={t("bookings")} subtitle={t("bookings_description")}>
|
<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>
|
</ShellMainAppDir>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const TeamsCTA = () => {
|
|||||||
data-testid="new-team-btn"
|
data-testid="new-team-btn"
|
||||||
variant="fab"
|
variant="fab"
|
||||||
StartIcon="plus"
|
StartIcon="plus"
|
||||||
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
href={`${WEBAPP_URL}/settings/teams/new?returnTo=${WEBAPP_URL}/teams`}>
|
href={`${WEBAPP_URL}/settings/teams/new?returnTo=${WEBAPP_URL}/teams`}>
|
||||||
{t("new")}
|
{t("new")}
|
||||||
|
|||||||
+8
@@ -438,6 +438,7 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record<number, T
|
|||||||
name={t("profile")}
|
name={t("profile")}
|
||||||
href={`/settings/teams/${team.id}/profile`}
|
href={`/settings/teams/${team.id}/profile`}
|
||||||
textClassNames="px-3 text-emphasis font-medium text-sm"
|
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||||
|
className="me-5 h-7 w-auto !px-2"
|
||||||
disableChevron
|
disableChevron
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -445,6 +446,7 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record<number, T
|
|||||||
name={t("members")}
|
name={t("members")}
|
||||||
href={`/settings/teams/${team.id}/members`}
|
href={`/settings/teams/${team.id}/members`}
|
||||||
textClassNames="px-3 text-emphasis font-medium text-sm"
|
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||||
|
className="me-5 h-7 w-auto !px-2"
|
||||||
disableChevron
|
disableChevron
|
||||||
/>
|
/>
|
||||||
{/* Show roles only for sub-teams with PBAC-enabled parent */}
|
{/* Show roles only for sub-teams with PBAC-enabled parent */}
|
||||||
@@ -465,6 +467,7 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record<number, T
|
|||||||
name={t("appearance")}
|
name={t("appearance")}
|
||||||
href={`/settings/teams/${team.id}/appearance`}
|
href={`/settings/teams/${team.id}/appearance`}
|
||||||
textClassNames="px-3 text-emphasis font-medium text-sm"
|
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||||
|
className="me-5 h-7 w-auto !px-2"
|
||||||
disableChevron
|
disableChevron
|
||||||
/>
|
/>
|
||||||
{/* Hide if there is a parent ID */}
|
{/* Hide if there is a parent ID */}
|
||||||
@@ -474,6 +477,7 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record<number, T
|
|||||||
name={t("billing")}
|
name={t("billing")}
|
||||||
href={`/settings/teams/${team.id}/billing`}
|
href={`/settings/teams/${team.id}/billing`}
|
||||||
textClassNames="px-3 text-emphasis font-medium text-sm"
|
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||||
|
className="me-5 h-7 w-auto !px-2"
|
||||||
disableChevron
|
disableChevron
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -482,6 +486,7 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record<number, T
|
|||||||
name={t("settings")}
|
name={t("settings")}
|
||||||
href={`/settings/teams/${team.id}/settings`}
|
href={`/settings/teams/${team.id}/settings`}
|
||||||
textClassNames="px-3 text-emphasis font-medium text-sm"
|
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||||
|
className="me-5 h-7 w-auto !px-2"
|
||||||
disableChevron
|
disableChevron
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -643,6 +648,7 @@ const SettingsSidebarContainer = ({
|
|||||||
name={t("add_a_team")}
|
name={t("add_a_team")}
|
||||||
href={`${WEBAPP_URL}/settings/teams/new`}
|
href={`${WEBAPP_URL}/settings/teams/new`}
|
||||||
textClassNames="px-3 items-center mt-2 text-emphasis font-medium text-sm"
|
textClassNames="px-3 items-center mt-2 text-emphasis font-medium text-sm"
|
||||||
|
className="me-5 h-7 w-auto !px-2"
|
||||||
icon="plus"
|
icon="plus"
|
||||||
disableChevron
|
disableChevron
|
||||||
/>
|
/>
|
||||||
@@ -744,12 +750,14 @@ const SettingsSidebarContainer = ({
|
|||||||
name={t("profile")}
|
name={t("profile")}
|
||||||
href={`/settings/organizations/teams/other/${otherTeam.id}/profile`}
|
href={`/settings/organizations/teams/other/${otherTeam.id}/profile`}
|
||||||
textClassNames="px-3 text-emphasis font-medium text-sm"
|
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||||
|
className="me-5 h-7 w-auto !px-2"
|
||||||
disableChevron
|
disableChevron
|
||||||
/>
|
/>
|
||||||
<VerticalTabItem
|
<VerticalTabItem
|
||||||
name={t("members")}
|
name={t("members")}
|
||||||
href={`/settings/organizations/teams/other/${otherTeam.id}/members`}
|
href={`/settings/organizations/teams/other/${otherTeam.id}/members`}
|
||||||
textClassNames="px-3 text-emphasis font-medium text-sm"
|
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||||
|
className="me-5 h-7 w-auto !px-2"
|
||||||
disableChevron
|
disableChevron
|
||||||
/>
|
/>
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const Page = async () => {
|
|||||||
<SettingsHeader
|
<SettingsHeader
|
||||||
title={t("billing")}
|
title={t("billing")}
|
||||||
description={t("manage_billing_description")}
|
description={t("manage_billing_description")}
|
||||||
borderInShellHeader={true}>
|
borderInShellHeader={false}>
|
||||||
<BillingView />
|
<BillingView />
|
||||||
</SettingsHeader>
|
</SettingsHeader>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
|
import type { PageProps as ServerPageProps } from "app/_types";
|
||||||
import { _generateMetadata } from "app/_utils";
|
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 }> }) =>
|
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
|
||||||
await _generateMetadata(
|
await _generateMetadata(
|
||||||
@@ -11,10 +21,34 @@ export const generateMetadata = async ({ params }: { params: Promise<{ id: strin
|
|||||||
`/settings/teams/${(await params).id}/event-type`
|
`/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 (
|
return (
|
||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<CreateTeamEventType />
|
<CreateTeamEventType permissions={{ canCreateEventType }} />
|
||||||
</LayoutWrapper>
|
</LayoutWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ import type { NextRequest } from "next/server";
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
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 type { Contact } from "@calcom/features/ee/support/lib/intercom/intercom";
|
||||||
import { intercom } 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 { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||||
|
import { MembershipRepository } from "@calcom/lib/server/repository/membership";
|
||||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||||
import prisma from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
|
|
||||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||||
|
|
||||||
@@ -43,6 +45,11 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
const { user } = session;
|
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) {
|
if (!existingContact.data) {
|
||||||
const additionalUserInfo = await new UserRepository(prisma).getUserStats({ userId: session.user.id });
|
const additionalUserInfo = await new UserRepository(prisma).getUserStats({ userId: session.user.id });
|
||||||
const sumOfTeamEventTypes = additionalUserInfo?.teams.reduce(
|
const sumOfTeamEventTypes = additionalUserInfo?.teams.reduce(
|
||||||
@@ -72,6 +79,7 @@ export async function POST(req: NextRequest) {
|
|||||||
sum_of_teams: additionalUserInfo?._count?.teams,
|
sum_of_teams: additionalUserInfo?._count?.teams,
|
||||||
sum_of_event_types: additionalUserInfo?._count?.eventTypes,
|
sum_of_event_types: additionalUserInfo?._count?.eventTypes,
|
||||||
sum_of_team_event_types: sumOfTeamEventTypes,
|
sum_of_team_event_types: sumOfTeamEventTypes,
|
||||||
|
Plan: plan,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -213,7 +213,6 @@ const ConfigureStepCardContent: FC<ConfigureStepCardProps> = (props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [submit, setSubmit] = useState(false);
|
const [submit, setSubmit] = useState(false);
|
||||||
const [mounted, setMounted] = useState(false);
|
|
||||||
const allUpdated = updatedEventTypesStatus.every((item) => item.every((iitem) => iitem.updated));
|
const allUpdated = updatedEventTypesStatus.every((item) => item.every((iitem) => iitem.updated));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -223,11 +222,7 @@ const ConfigureStepCardContent: FC<ConfigureStepCardProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}, [submit, allUpdated, mainForSubmitRef]);
|
}, [submit, allUpdated, mainForSubmitRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
if (!formPortalRef?.current) {
|
||||||
setMounted(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!formPortalRef?.current || !mounted) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ describe("EditLocationDialog", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "integrations:daily",
|
value: "integrations:daily",
|
||||||
label: "Cal Video (Global)",
|
label: "Cal Video (Default)",
|
||||||
disabled: false,
|
disabled: false,
|
||||||
icon: "/app-store/dailyvideo/icon.svg",
|
icon: "/app-store/dailyvideo/icon.svg",
|
||||||
slug: "daily-video",
|
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 &&
|
{currentStep === AppOnboardingSteps.CONFIGURE_STEP &&
|
||||||
formPortalRef.current &&
|
|
||||||
eventTypeGroups && (
|
eventTypeGroups && (
|
||||||
<ConfigureStepCard
|
<ConfigureStepCard
|
||||||
slug={appMetadata.slug}
|
slug={appMetadata.slug}
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ const descriptionByStatus: Record<BookingListingStatus, string> = {
|
|||||||
type BookingsProps = {
|
type BookingsProps = {
|
||||||
status: (typeof validStatuses)[number];
|
status: (typeof validStatuses)[number];
|
||||||
userId?: number;
|
userId?: number;
|
||||||
|
permissions: {
|
||||||
|
canReadOthersBookings: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
function useSystemSegments(userId?: number) {
|
function useSystemSegments(userId?: number) {
|
||||||
@@ -111,7 +114,7 @@ type RowData =
|
|||||||
type: "today" | "next";
|
type: "today" | "next";
|
||||||
};
|
};
|
||||||
|
|
||||||
function BookingsContent({ status }: BookingsProps) {
|
function BookingsContent({ status, permissions }: BookingsProps) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const user = useMeQuery().data;
|
const user = useMeQuery().data;
|
||||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -154,7 +157,7 @@ function BookingsContent({ status }: BookingsProps) {
|
|||||||
href: queryString ? `${tabConfig.path}?${queryString}` : tabConfig.path,
|
href: queryString ? `${tabConfig.path}?${queryString}` : tabConfig.path,
|
||||||
"data-testid": tabConfig["data-testid"],
|
"data-testid": tabConfig["data-testid"],
|
||||||
}));
|
}));
|
||||||
}, [searchParams?.toString()]);
|
}, [searchParams]);
|
||||||
|
|
||||||
const eventTypeIds = useFilterValue("eventTypeId", ZMultiSelectFilterValue)?.data as number[] | undefined;
|
const eventTypeIds = useFilterValue("eventTypeId", ZMultiSelectFilterValue)?.data as number[] | undefined;
|
||||||
const teamIds = useFilterValue("teamId", 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, {
|
columnHelper.accessor((row) => row.type === "data" && row.booking.user?.id, {
|
||||||
id: "userId",
|
id: "userId",
|
||||||
header: t("member"),
|
header: t("member"),
|
||||||
enableColumnFilter: user?.isTeamAdminOrOwner ?? false,
|
enableColumnFilter: permissions.canReadOthersBookings,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
cell: () => null,
|
cell: () => null,
|
||||||
meta: {
|
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]);
|
const isEmpty = useMemo(() => !query.data?.bookings.length, [query.data]);
|
||||||
|
|
||||||
@@ -352,7 +355,7 @@ function BookingsContent({ status }: BookingsProps) {
|
|||||||
isToday: false,
|
isToday: false,
|
||||||
})) || []
|
})) || []
|
||||||
);
|
);
|
||||||
}, [query.data]);
|
}, [query.data, status, user?.timeZone]);
|
||||||
|
|
||||||
const bookingsToday = useMemo<RowData[]>(() => {
|
const bookingsToday = useMemo<RowData[]>(() => {
|
||||||
return (
|
return (
|
||||||
@@ -371,7 +374,7 @@ function BookingsContent({ status }: BookingsProps) {
|
|||||||
isToday: true,
|
isToday: true,
|
||||||
})) ?? []
|
})) ?? []
|
||||||
);
|
);
|
||||||
}, [query.data]);
|
}, [query.data, user?.timeZone]);
|
||||||
|
|
||||||
const finalData = useMemo<RowData[]>(() => {
|
const finalData = useMemo<RowData[]>(() => {
|
||||||
if (status !== "upcoming") {
|
if (status !== "upcoming") {
|
||||||
|
|||||||
@@ -556,7 +556,9 @@ export default function Success(props: PageProps) {
|
|||||||
<div className="font-medium">
|
<div className="font-medium">
|
||||||
{isCancelled ? t("reason") : t("reschedule_reason")}
|
{isCancelled ? t("reason") : t("reschedule_reason")}
|
||||||
</div>
|
</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 && (
|
{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 { CreateButton } from "@calcom/features/ee/teams/components/createButton/CreateButton";
|
||||||
import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/embed/EventTypeEmbed";
|
import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/embed/EventTypeEmbed";
|
||||||
import { EventTypeDescription } from "@calcom/features/eventtypes/components";
|
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 { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog";
|
||||||
import { InfiniteSkeletonLoader } from "@calcom/features/eventtypes/components/SkeletonLoader";
|
import { InfiniteSkeletonLoader } from "@calcom/features/eventtypes/components/SkeletonLoader";
|
||||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
@@ -846,17 +849,7 @@ const CreateFirstEventTypeView = ({ slug, searchTerm }: { slug: string; searchTe
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CTA = ({
|
const CTA = ({ profileOptions }: { profileOptions: ProfileOption[] }) => {
|
||||||
profileOptions,
|
|
||||||
}: {
|
|
||||||
profileOptions: {
|
|
||||||
teamId: number | null | undefined;
|
|
||||||
label: string | null;
|
|
||||||
image: string;
|
|
||||||
membershipRole: MembershipRole | null | undefined;
|
|
||||||
slug: string | null;
|
|
||||||
}[];
|
|
||||||
}) => {
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
if (!profileOptions.length) return null;
|
if (!profileOptions.length) return null;
|
||||||
@@ -951,7 +944,7 @@ type Props = {
|
|||||||
|
|
||||||
export const EventTypesCTA = ({ userEventGroupsData }: Omit<Props, "user">) => {
|
export const EventTypesCTA = ({ userEventGroupsData }: Omit<Props, "user">) => {
|
||||||
const profileOptions =
|
const profileOptions =
|
||||||
userEventGroupsData?.profiles
|
userEventGroupsData.profiles
|
||||||
?.filter((profile) => !profile.readOnly)
|
?.filter((profile) => !profile.readOnly)
|
||||||
?.filter((profile) => !profile.eventTypesLockedByOrg)
|
?.filter((profile) => !profile.eventTypesLockedByOrg)
|
||||||
?.filter((profile) => {
|
?.filter((profile) => {
|
||||||
@@ -973,12 +966,20 @@ export const EventTypesCTA = ({ userEventGroupsData }: Omit<Props, "user">) => {
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
?.map((profile) => {
|
?.map((profile) => {
|
||||||
|
const permissions = profile.teamId
|
||||||
|
? userEventGroupsData.teamPermissions[profile.teamId]
|
||||||
|
: {
|
||||||
|
// always can create eventType on personal level
|
||||||
|
canCreateEventType: true,
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
teamId: profile.teamId,
|
teamId: profile.teamId,
|
||||||
label: profile.name || profile.slug,
|
label: profile.name || profile.slug,
|
||||||
image: profile.image,
|
image: profile.image,
|
||||||
membershipRole: profile.membershipRole,
|
membershipRole: profile.membershipRole,
|
||||||
slug: profile.slug,
|
slug: profile.slug,
|
||||||
|
permissions,
|
||||||
};
|
};
|
||||||
}) ?? [];
|
}) ?? [];
|
||||||
|
|
||||||
|
|||||||
@@ -30,16 +30,18 @@ declare global {
|
|||||||
export default function PlatformBillingUpgrade() {
|
export default function PlatformBillingUpgrade() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
const { isUserLoading, isUserBillingDataLoading, isPlatformUser, userBillingData, isPaidUser, userOrgId } =
|
||||||
|
useGetUserAttributes();
|
||||||
|
|
||||||
const returnTo = pathname;
|
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 () => {
|
const onContactSupportClick = async () => {
|
||||||
if (window.Support) {
|
if (window.Support) {
|
||||||
window.Support.open();
|
window.Support.open();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const { isUserLoading, isUserBillingDataLoading, isPlatformUser, userBillingData, isPaidUser, userOrgId } =
|
|
||||||
useGetUserAttributes();
|
|
||||||
|
|
||||||
const { mutateAsync: removeTeamSubscription, isPending: isRemoveTeamSubscriptionLoading } =
|
const { mutateAsync: removeTeamSubscription, isPending: isRemoveTeamSubscriptionLoading } =
|
||||||
useUnsubscribeTeamToStripe({
|
useUnsubscribeTeamToStripe({
|
||||||
|
|||||||
@@ -8,11 +8,15 @@ import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
|||||||
import { useCreateEventType } from "@calcom/lib/hooks/useCreateEventType";
|
import { useCreateEventType } from "@calcom/lib/hooks/useCreateEventType";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import { Button } from "@calcom/ui/components/button";
|
||||||
import { WizardLayout } from "@calcom/ui/components/layout";
|
import { WizardLayout } from "@calcom/ui/components/layout";
|
||||||
import { showToast } from "@calcom/ui/components/toast";
|
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 searchParams = useCompatSearchParams();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -53,7 +57,7 @@ export const CreateTeamEventType = () => {
|
|||||||
<TeamEventTypeForm
|
<TeamEventTypeForm
|
||||||
teamSlug={team?.slug}
|
teamSlug={team?.slug}
|
||||||
teamId={teamId}
|
teamId={teamId}
|
||||||
isTeamAdminOrOwner={true}
|
permissions={permissions}
|
||||||
urlPrefix={urlPrefix}
|
urlPrefix={urlPrefix}
|
||||||
isPending={createMutation.isPending}
|
isPending={createMutation.isPending}
|
||||||
form={form}
|
form={form}
|
||||||
@@ -82,5 +86,3 @@ export const LayoutWrapper = ({ children }: { children: React.ReactNode }) => {
|
|||||||
</WizardLayout>
|
</WizardLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CreateTeamEventType;
|
|
||||||
|
|||||||
@@ -426,6 +426,7 @@ export default function Signup({
|
|||||||
id="signup-email"
|
id="signup-email"
|
||||||
{...register("email")}
|
{...register("email")}
|
||||||
label={t("email")}
|
label={t("email")}
|
||||||
|
placeholder="john@doe.com"
|
||||||
type="email"
|
type="email"
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
disabled={prepopulateFormValues?.email}
|
disabled={prepopulateFormValues?.email}
|
||||||
@@ -522,8 +523,8 @@ export default function Signup({
|
|||||||
usernameTaken
|
usernameTaken
|
||||||
}>
|
}>
|
||||||
{premiumUsername && !usernameTaken
|
{premiumUsername && !usernameTaken
|
||||||
? `${t("create_account")} (${getPremiumPlanPriceValue()})`
|
? `${t("get_started")} (${getPremiumPlanPriceValue()})`
|
||||||
: t("create_account")}
|
: t("get_started")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
|
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
import { MemberInvitationModalWithoutMembers } from "@calcom/features/ee/teams/components/MemberInvitationModal";
|
import { MemberInvitationModalWithoutMembers } from "@calcom/features/ee/teams/components/MemberInvitationModal";
|
||||||
import MemberList from "@calcom/features/ee/teams/components/MemberList";
|
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;
|
permissions: MemberPermissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TeamMembersView = ({ team, facetedTeamValues, permissions }: TeamMembersViewProps) => {
|
export const TeamMembersView = ({ team, facetedTeamValues, permissions }: TeamMembersViewProps) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(false);
|
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
|
// Use PBAC permissions - server-side permission check should be done in parent component
|
||||||
const isTeamAdminOrOwner = checkAdminOrOwner(team.membership.role);
|
const canLoggedInUserSeeMembers = permissions?.canListMembers ?? false;
|
||||||
const canLoggedInUserSeeMembers = permissions?.canListMembers ?? (!team.isPrivate || isTeamAdminOrOwner);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LicenseRequired>
|
<LicenseRequired>
|
||||||
|
|||||||
+17
-16
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "5.7.4",
|
"version": "5.7.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -20,8 +20,8 @@
|
|||||||
"copy-app-store-static": "node scripts/copy-app-store-static.js",
|
"copy-app-store-static": "node scripts/copy-app-store-static.js",
|
||||||
"build": "turbo run copy-app-store-static && next build && yarn sentry:release",
|
"build": "turbo run copy-app-store-static && next build && yarn sentry:release",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint . --ignore-path .gitignore",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"lint:report": "eslint . --format json --output-file ../../lint-results/web.json",
|
"lint:report": "eslint . --format json --output-file ../../lint-results/web.json",
|
||||||
"check-changed-files": "ts-node scripts/ts-check-changed-files.ts",
|
"check-changed-files": "ts-node scripts/ts-check-changed-files.ts",
|
||||||
"translate-locales": "ts-node scripts/check-missing-translations.ts",
|
"translate-locales": "ts-node scripts/check-missing-translations.ts",
|
||||||
@@ -32,20 +32,20 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@boxyhq/saml-jackson": "1.52.2",
|
"@boxyhq/saml-jackson": "1.52.2",
|
||||||
"@calcom/app-store": "*",
|
"@calcom/app-store": "workspace:*",
|
||||||
"@calcom/app-store-cli": "*",
|
"@calcom/app-store-cli": "workspace:*",
|
||||||
"@calcom/dayjs": "*",
|
"@calcom/dayjs": "workspace:*",
|
||||||
"@calcom/embed-core": "workspace:*",
|
"@calcom/embed-core": "workspace:*",
|
||||||
"@calcom/embed-react": "workspace:*",
|
"@calcom/embed-react": "workspace:*",
|
||||||
"@calcom/embed-snippet": "workspace:*",
|
"@calcom/embed-snippet": "workspace:*",
|
||||||
"@calcom/features": "*",
|
"@calcom/features": "workspace:*",
|
||||||
"@calcom/lib": "*",
|
"@calcom/lib": "workspace:*",
|
||||||
"@calcom/platform-enums": "*",
|
"@calcom/platform-enums": "workspace:*",
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "workspace:*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "workspace:*",
|
||||||
"@calcom/trpc": "*",
|
"@calcom/trpc": "workspace:*",
|
||||||
"@calcom/tsconfig": "*",
|
"@calcom/tsconfig": "workspace:*",
|
||||||
"@calcom/ui": "*",
|
"@calcom/ui": "workspace:*",
|
||||||
"@daily-co/daily-js": "^0.83.1",
|
"@daily-co/daily-js": "^0.83.1",
|
||||||
"@daily-co/daily-react": "^0.23.2",
|
"@daily-co/daily-react": "^0.23.2",
|
||||||
"@dub/analytics": "^0.0.27",
|
"@dub/analytics": "^0.0.27",
|
||||||
@@ -158,8 +158,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.19.6",
|
"@babel/core": "^7.19.6",
|
||||||
"@calcom/config": "*",
|
"@calcom/config": "workspace:*",
|
||||||
"@calcom/types": "*",
|
"@calcom/eslint-config": "workspace:*",
|
||||||
|
"@calcom/types": "workspace:*",
|
||||||
"@microsoft/microsoft-graph-types-beta": "0.15.0-preview",
|
"@microsoft/microsoft-graph-types-beta": "0.15.0-preview",
|
||||||
"@playwright/test": "^1.45.3",
|
"@playwright/test": "^1.45.3",
|
||||||
"@testing-library/react": "^13.3.0",
|
"@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
|
// To prevent early timeouts
|
||||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
await expectToContainValidPreviewIframe(page, {
|
await expectToContainValidPreviewIframe(page, {
|
||||||
embedType: "inline",
|
embedType: "inline",
|
||||||
@@ -98,7 +97,6 @@ test.describe("Embed Code Generator Tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// To prevent early timeouts
|
// To prevent early timeouts
|
||||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
await expectToContainValidPreviewIframe(page, {
|
await expectToContainValidPreviewIframe(page, {
|
||||||
embedType: "floating-popup",
|
embedType: "floating-popup",
|
||||||
@@ -136,7 +134,6 @@ test.describe("Embed Code Generator Tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// To prevent early timeouts
|
// To prevent early timeouts
|
||||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
await expectToContainValidPreviewIframe(page, {
|
await expectToContainValidPreviewIframe(page, {
|
||||||
embedType: "element-click",
|
embedType: "element-click",
|
||||||
@@ -176,7 +173,6 @@ test.describe("Embed Code Generator Tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// To prevent early timeouts
|
// To prevent early timeouts
|
||||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
await expectToContainValidPreviewIframe(page, {
|
await expectToContainValidPreviewIframe(page, {
|
||||||
embedType: "inline",
|
embedType: "inline",
|
||||||
@@ -234,7 +230,6 @@ test.describe("Embed Code Generator Tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// To prevent early timeouts
|
// To prevent early timeouts
|
||||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
await expectToContainValidPreviewIframe(page, {
|
await expectToContainValidPreviewIframe(page, {
|
||||||
embedType: "inline",
|
embedType: "inline",
|
||||||
@@ -275,7 +270,6 @@ test.describe("Embed Code Generator Tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// To prevent early timeouts
|
// To prevent early timeouts
|
||||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
await expectToContainValidPreviewIframe(page, {
|
await expectToContainValidPreviewIframe(page, {
|
||||||
embedType: "floating-popup",
|
embedType: "floating-popup",
|
||||||
@@ -315,7 +309,6 @@ test.describe("Embed Code Generator Tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// To prevent early timeouts
|
// To prevent early timeouts
|
||||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
await expectToContainValidPreviewIframe(page, {
|
await expectToContainValidPreviewIframe(page, {
|
||||||
embedType: "element-click",
|
embedType: "element-click",
|
||||||
@@ -333,8 +326,7 @@ function chooseEmbedType(page: Page, embedType: EmbedType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function goToReactCodeTab(page: Page) {
|
async function goToReactCodeTab(page: Page) {
|
||||||
// To prevent early timeouts
|
// To prevent early timeo
|
||||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
await page.locator("[data-testid=horizontal-tab-react]").click();
|
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 embedButton = page.locator("[data-testid=embed]");
|
||||||
const embedUrl = await embedButton.getAttribute("data-test-embed-url");
|
const embedUrl = await embedButton.getAttribute("data-test-embed-url");
|
||||||
embedButton.click();
|
embedButton.click();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
return embedUrl!;
|
return embedUrl!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,18 +431,24 @@ async function expectValidHtmlEmbedSnippet(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function assertThatCodeIsValidVanillaJsCode(code: string) {
|
function assertThatCodeIsValidVanillaJsCode(code: string) {
|
||||||
const lintResult = linter.verify(code, {
|
const lintResult = linter.verify(code, [
|
||||||
env: {
|
{
|
||||||
browser: true,
|
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) {
|
if (lintResult.length) {
|
||||||
console.log(
|
console.log(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -459,23 +457,35 @@ function assertThatCodeIsValidVanillaJsCode(code: string) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(lintResult.length).toBe(0);
|
expect(lintResult.length).toBe(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertThatCodeIsValidReactCode(code: string) {
|
function assertThatCodeIsValidReactCode(code: string) {
|
||||||
const lintResult = linter.verify(code, {
|
const lintResult = linter.verify(code, [
|
||||||
env: {
|
{
|
||||||
browser: true,
|
languageOptions: {
|
||||||
},
|
ecmaVersion: 2021,
|
||||||
parserOptions: {
|
sourceType: "module",
|
||||||
ecmaVersion: 2021,
|
parserOptions: {
|
||||||
ecmaFeatures: {
|
ecmaFeatures: { jsx: true },
|
||||||
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) {
|
if (lintResult.length) {
|
||||||
console.log(
|
console.log(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -484,6 +494,7 @@ function assertThatCodeIsValidReactCode(code: string) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(lintResult.length).toBe(0);
|
expect(lintResult.length).toBe(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ test.describe("Event Types tests", () => {
|
|||||||
await gotoFirstEventType(page);
|
await gotoFirstEventType(page);
|
||||||
|
|
||||||
await page.getByTestId("location-select").click();
|
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 saveEventType(page);
|
||||||
await gotoBookingPage(page);
|
await gotoBookingPage(page);
|
||||||
@@ -282,7 +282,7 @@ test.describe("Event Types tests", () => {
|
|||||||
await selectAttendeePhoneNumber(page);
|
await selectAttendeePhoneNumber(page);
|
||||||
|
|
||||||
// Add Cal Video location
|
// Add Cal Video location
|
||||||
await addAnotherLocation(page, "Cal Video (Global)");
|
await addAnotherLocation(page, "Cal Video (Default)");
|
||||||
|
|
||||||
await saveEventType(page);
|
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 test.step("should open first eventType and check Interface Language", async () => {
|
||||||
await gotoFirstEventType(page);
|
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
|
const interfaceLanguageValue = page
|
||||||
.getByTestId("event-interface-language")
|
.getByTestId("event-interface-language")
|
||||||
.locator('div[class$="-singleValue"]');
|
.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 test.step("should open first eventType and change Interface Language to Deutsche", async () => {
|
||||||
await gotoFirstEventType(page);
|
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("event-interface-language").click();
|
||||||
await page.locator(`text="Deutsch"`).click();
|
await page.locator(`text="Deutsch"`).click();
|
||||||
await saveEventType(page);
|
await saveEventType(page);
|
||||||
@@ -574,6 +580,9 @@ test.describe("Event Types tests", () => {
|
|||||||
await page.goto("/event-types");
|
await page.goto("/event-types");
|
||||||
await page.waitForSelector('[data-testid="event-types"]');
|
await page.waitForSelector('[data-testid="event-types"]');
|
||||||
await gotoFirstEventType(page);
|
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("event-interface-language").click();
|
||||||
await page.getByTestId("select-option-es").click();
|
await page.getByTestId("select-option-es").click();
|
||||||
await saveEventType(page);
|
await saveEventType(page);
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ export async function expectSegmentCleared(page: Page) {
|
|||||||
// Check that no segment is selected (button shows default text)
|
// Check that no segment is selected (button shows default text)
|
||||||
const segmentSelect = page.getByTestId("filter-segment-select");
|
const segmentSelect = page.getByTestId("filter-segment-select");
|
||||||
const buttonText = await segmentSelect.textContent();
|
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);
|
await expect(eventTypePage.getByTestId("time")).toHaveCount(count);
|
||||||
},
|
},
|
||||||
getAITranslationToggleDisabled: async () => {
|
getAITranslationToggleDisabled: async () => {
|
||||||
|
await page.click("[data-testid=vertical-tab-event_advanced_tab_title]");
|
||||||
await page.waitForSelector('[data-testid="ai_translation_toggle"]', {
|
await page.waitForSelector('[data-testid="ai_translation_toggle"]', {
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
state: "attached",
|
state: "attached",
|
||||||
@@ -207,6 +208,7 @@ export function createBookingPageFixture(page: Page) {
|
|||||||
return (await toggle.getAttribute("disabled")) !== null;
|
return (await toggle.getAttribute("disabled")) !== null;
|
||||||
},
|
},
|
||||||
toggleAITranslation: async () => {
|
toggleAITranslation: async () => {
|
||||||
|
await page.click("[data-testid=vertical-tab-event_advanced_tab_title]");
|
||||||
await page.getByTestId("ai_translation_toggle").click();
|
await page.getByTestId("ai_translation_toggle").click();
|
||||||
},
|
},
|
||||||
updateEventTypeDescription: async (description: string) => {
|
updateEventTypeDescription: async (description: string) => {
|
||||||
|
|||||||
@@ -81,10 +81,13 @@ test.describe("Managed Event Types", () => {
|
|||||||
await page.goto(`/event-types/${managedEvent.id}?tabName=setup`);
|
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")).toHaveAttribute("aria-current", "page"); // fix the race condition
|
||||||
await expect(page.getByTestId("vertical-tab-basics")).toContainText("Basics"); //fix the race condition
|
await expect(page.getByTestId("vertical-tab-basics")).toContainText("Basics"); //fix the race condition
|
||||||
|
|
||||||
await page.locator("#location-select").click();
|
await page.locator("#location-select").click();
|
||||||
const optionText = await getByKey(page, "organizer_default_conferencing_app");
|
const targetText = (await localize("en"))("organizer_default_conferencing_app");
|
||||||
await expect(optionText).toBeVisible();
|
const options = page.locator('[data-testid="location-select-item-conferencing"]');
|
||||||
await optionText.click();
|
const targetOption = options.locator(`text=${targetText}`);
|
||||||
|
await expect(targetOption).toBeVisible();
|
||||||
|
await targetOption.click();
|
||||||
await saveAndWaitForResponse(page);
|
await saveAndWaitForResponse(page);
|
||||||
|
|
||||||
await page.getByTestId("vertical-tab-assignment").click();
|
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> }[] = [
|
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",
|
slug: "team",
|
||||||
locator: (page) => getByKey(page, "automatically_add_all_team_members"),
|
locator: (page) => getByKey(page, "automatically_add_all_team_members"),
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ test.describe("Email Signup Flow Test", async () => {
|
|||||||
data: {
|
data: {
|
||||||
identifier: userToCreate.email,
|
identifier: userToCreate.email,
|
||||||
token,
|
token,
|
||||||
expires: new Date(new Date().setHours(168)), // +1 week
|
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // +1 week
|
||||||
team: {
|
team: {
|
||||||
create: {
|
create: {
|
||||||
name: "Rick's Team",
|
name: "Rick's Team",
|
||||||
|
|||||||
@@ -321,6 +321,7 @@
|
|||||||
"secret_meeting": "Secret Meeting",
|
"secret_meeting": "Secret Meeting",
|
||||||
"login_instead": "Login instead",
|
"login_instead": "Login instead",
|
||||||
"create_account": "Create Account",
|
"create_account": "Create Account",
|
||||||
|
"get_started": "Get started",
|
||||||
"confirm_password": "Confirm password",
|
"confirm_password": "Confirm password",
|
||||||
"reset_your_password": "Set your new password with the instructions sent to your email address.",
|
"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.",
|
"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)",
|
"light_event_type_color": "Event Type Color (Light Theme)",
|
||||||
"dark_event_type_color": "Event Type Color (Dark Theme)",
|
"dark_event_type_color": "Event Type Color (Dark Theme)",
|
||||||
"file_not_named": "File is not named [idOrSlug]/[user]",
|
"file_not_named": "File is not named [idOrSlug]/[user]",
|
||||||
"create_team": "Create Team",
|
"create_team": "Create team",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"nameless_team": "Nameless Team",
|
"nameless_team": "Nameless Team",
|
||||||
"oauth_clients": "OAuth Clients",
|
"oauth_clients": "OAuth Clients",
|
||||||
@@ -1019,6 +1020,7 @@
|
|||||||
"attendees": "Attendees",
|
"attendees": "Attendees",
|
||||||
"add_attendees": "Add attendees",
|
"add_attendees": "Add attendees",
|
||||||
"show_advanced_settings": "Show advanced settings",
|
"show_advanced_settings": "Show advanced settings",
|
||||||
|
"hide_advanced_settings": "Hide advanced settings",
|
||||||
"event_name": "Event Name",
|
"event_name": "Event Name",
|
||||||
"event_name_in_calendar": "Calendar event name",
|
"event_name_in_calendar": "Calendar event name",
|
||||||
"event_name_tooltip": "The name that will appear in calendars",
|
"event_name_tooltip": "The name that will appear in calendars",
|
||||||
@@ -1231,7 +1233,7 @@
|
|||||||
"contact_sales": "Contact Sales",
|
"contact_sales": "Contact Sales",
|
||||||
"error_404": "Error 404",
|
"error_404": "Error 404",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"set_to_default": "Set to Default",
|
"set_to_default": "Set as Default",
|
||||||
"new_schedule_btn": "New schedule",
|
"new_schedule_btn": "New schedule",
|
||||||
"add_new_schedule": "Add a new schedule",
|
"add_new_schedule": "Add a new schedule",
|
||||||
"add_new_calendar": "Add a new calendar",
|
"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.",
|
"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_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.",
|
"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.",
|
"impersonate_user_tip": "All uses of this feature is audited.",
|
||||||
"impersonating_user_warning": "Impersonating username \"{{user}}\".",
|
"impersonating_user_warning": "Impersonating username \"{{user}}\".",
|
||||||
"impersonating_stop_instructions": "Click here to stop",
|
"impersonating_stop_instructions": "Click here to stop",
|
||||||
@@ -2689,7 +2691,7 @@
|
|||||||
"enterprise": "Enterprise",
|
"enterprise": "Enterprise",
|
||||||
"enterprise_description": "Upgrade to Enterprise to create your Organization",
|
"enterprise_description": "Upgrade to Enterprise to create your Organization",
|
||||||
"create_your_org": "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_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!",
|
"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",
|
"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",
|
"proceeding_agreement": "By proceeding, you agree to our",
|
||||||
"terms": "Terms",
|
"terms": "Terms",
|
||||||
"always_show_x_days": "Always {{x}} days available",
|
"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_cancelled_successfully": "Team subscription cancelled successfully",
|
||||||
"team_subscription_cancellation_error": "Error while cancelling the team subscription",
|
"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",
|
"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_ends_with": "Ends with",
|
||||||
"filter_operator_is_empty": "Is empty",
|
"filter_operator_is_empty": "Is empty",
|
||||||
"filter_operator_not_empty": "Not 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": "Distribution",
|
||||||
"rr_distribution_method_description": "Allows for optimising distribution for maximum availability or to aim for a more balanced assignment.",
|
"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",
|
"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.",
|
"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.",
|
"recurring_event_seats_error": "Recurring event doesn't support seats feature. Disable seats feature or make the event non-recurring.",
|
||||||
"segment": "Segment",
|
"segment": "Segment",
|
||||||
|
"saved_filters": "Saved filters",
|
||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"rename_segment": "Rename Segment",
|
"rename_segment": "Rename Segment",
|
||||||
"delete_segment": "Delete Segment",
|
"delete_segment": "Delete Segment",
|
||||||
@@ -3339,6 +3344,7 @@
|
|||||||
"picklist": "Picklist",
|
"picklist": "Picklist",
|
||||||
"most_cancelled_bookings": "Most Cancelled",
|
"most_cancelled_bookings": "Most Cancelled",
|
||||||
"interface_language": "Interface Language",
|
"interface_language": "Interface Language",
|
||||||
|
"interface_language_description": "Set your preferred language for the booking interface",
|
||||||
"visitors_browser_language": "Visitor's browser language",
|
"visitors_browser_language": "Visitor's browser language",
|
||||||
"name_or_email": "Name or Email",
|
"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",
|
"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_update_workflows": "Edit and modify workflow settings",
|
||||||
"pbac_desc_delete_workflows": "Remove workflows from the system",
|
"pbac_desc_delete_workflows": "Remove workflows from the system",
|
||||||
"pbac_resource_webhook": "Webhook",
|
"pbac_resource_webhook": "Webhook",
|
||||||
|
"pbac_resource_availability": "Availability",
|
||||||
|
"pbac_resource_out_of_office": "Out of Office",
|
||||||
"pbac_desc_create_webhooks": "Create webhooks",
|
"pbac_desc_create_webhooks": "Create webhooks",
|
||||||
"pbac_desc_view_webhooks": "View webhooks",
|
"pbac_desc_view_webhooks": "View webhooks",
|
||||||
"pbac_desc_update_webhooks": "Update webhooks",
|
"pbac_desc_update_webhooks": "Update webhooks",
|
||||||
"pbac_desc_delete_webhooks": "Delete 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_manage_workflows": "Full management access to all workflows",
|
||||||
"pbac_desc_create_event_types": "Create event types",
|
"pbac_desc_create_event_types": "Create event types",
|
||||||
"pbac_desc_view_event_types": "View 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",
|
"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_description": "Customize the default round robin settings for this team",
|
||||||
"round_robin_settings_updated_successfully": "Round Robin settings updated successfully",
|
"round_robin_settings_updated_successfully": "Round Robin settings updated successfully",
|
||||||
"booking_creation_time": "Booking Creation Time",
|
"booking_creation_time": "Booking creation time",
|
||||||
"meeting_start_time": "Meeting Start Time",
|
"meeting_start_time": "Meeting start time",
|
||||||
"members_affected_by_disabling_delegation_credential": "Affected members",
|
"members_affected_by_disabling_delegation_credential": "Affected members",
|
||||||
"no_members_affected_by_disabling_delegation_credential": "No members affected by disabling delegation credential",
|
"no_members_affected_by_disabling_delegation_credential": "No members affected by disabling delegation credential",
|
||||||
"download_expense_log": "Download Expense Log",
|
"download_expense_log": "Download Expense Log",
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import prismaMock from "../../../../tests/libs/__mocks__/prismaMock";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import {
|
import {
|
||||||
@@ -9,6 +7,13 @@ import {
|
|||||||
} from "@calcom/lib/intervalLimits/server/checkDurationLimits";
|
} from "@calcom/lib/intervalLimits/server/checkDurationLimits";
|
||||||
import { validateIntervalLimitOrder } from "@calcom/lib/intervalLimits/validateIntervalLimitOrder";
|
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 = {
|
type MockData = {
|
||||||
id: number;
|
id: number;
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
@@ -22,19 +27,19 @@ const MOCK_DATA: MockData = {
|
|||||||
// Path: apps/web/test/lib/checkDurationLimits.ts
|
// Path: apps/web/test/lib/checkDurationLimits.ts
|
||||||
describe("Check Duration Limits Tests", () => {
|
describe("Check Duration Limits Tests", () => {
|
||||||
it("Should return no errors if limit is not reached", async () => {
|
it("Should return no errors if limit is not reached", async () => {
|
||||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 0 }]);
|
mockGetTotalBookingDuration.mockResolvedValue(0);
|
||||||
await expect(
|
await expect(
|
||||||
checkDurationLimits({ PER_DAY: 60 }, MOCK_DATA.startDate, MOCK_DATA.id)
|
checkDurationLimits({ PER_DAY: 60 }, MOCK_DATA.startDate, MOCK_DATA.id)
|
||||||
).resolves.toBeTruthy();
|
).resolves.toBeTruthy();
|
||||||
});
|
});
|
||||||
it("Should throw an error if limit is reached", async () => {
|
it("Should throw an error if limit is reached", async () => {
|
||||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
|
mockGetTotalBookingDuration.mockResolvedValue(60);
|
||||||
await expect(
|
await expect(
|
||||||
checkDurationLimits({ PER_DAY: 60 }, MOCK_DATA.startDate, MOCK_DATA.id)
|
checkDurationLimits({ PER_DAY: 60 }, MOCK_DATA.startDate, MOCK_DATA.id)
|
||||||
).rejects.toThrowError();
|
).rejects.toThrowError();
|
||||||
});
|
});
|
||||||
it("Should pass with multiple duration limits", async () => {
|
it("Should pass with multiple duration limits", async () => {
|
||||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 30 }]);
|
mockGetTotalBookingDuration.mockResolvedValue(30);
|
||||||
await expect(
|
await expect(
|
||||||
checkDurationLimits(
|
checkDurationLimits(
|
||||||
{
|
{
|
||||||
@@ -47,7 +52,7 @@ describe("Check Duration Limits Tests", () => {
|
|||||||
).resolves.toBeTruthy();
|
).resolves.toBeTruthy();
|
||||||
});
|
});
|
||||||
it("Should pass with multiple duration limits with one undefined", async () => {
|
it("Should pass with multiple duration limits with one undefined", async () => {
|
||||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 30 }]);
|
mockGetTotalBookingDuration.mockResolvedValue(30);
|
||||||
await expect(
|
await expect(
|
||||||
checkDurationLimits(
|
checkDurationLimits(
|
||||||
{
|
{
|
||||||
@@ -60,7 +65,7 @@ describe("Check Duration Limits Tests", () => {
|
|||||||
).resolves.toBeTruthy();
|
).resolves.toBeTruthy();
|
||||||
});
|
});
|
||||||
it("Should return no errors if limit is not reached with multiple bookings", async () => {
|
it("Should return no errors if limit is not reached with multiple bookings", async () => {
|
||||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
|
mockGetTotalBookingDuration.mockResolvedValue(60);
|
||||||
await expect(
|
await expect(
|
||||||
checkDurationLimits(
|
checkDurationLimits(
|
||||||
{
|
{
|
||||||
@@ -73,7 +78,7 @@ describe("Check Duration Limits Tests", () => {
|
|||||||
).resolves.toBeTruthy();
|
).resolves.toBeTruthy();
|
||||||
});
|
});
|
||||||
it("Should throw an error if one of the limit is reached with multiple bookings", async () => {
|
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(
|
await expect(
|
||||||
checkDurationLimits(
|
checkDurationLimits(
|
||||||
{
|
{
|
||||||
@@ -90,7 +95,7 @@ describe("Check Duration Limits Tests", () => {
|
|||||||
// Path: apps/web/test/lib/checkDurationLimits.ts
|
// Path: apps/web/test/lib/checkDurationLimits.ts
|
||||||
describe("Check Duration Limit Tests", () => {
|
describe("Check Duration Limit Tests", () => {
|
||||||
it("Should return no busyTimes and no error if limit is not reached", async () => {
|
it("Should return no busyTimes and no error if limit is not reached", async () => {
|
||||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
|
mockGetTotalBookingDuration.mockResolvedValue(60);
|
||||||
await expect(
|
await expect(
|
||||||
checkDurationLimit({
|
checkDurationLimit({
|
||||||
key: "PER_DAY",
|
key: "PER_DAY",
|
||||||
|
|||||||
@@ -143,9 +143,10 @@ describe("handleChildrenEventTypes", () => {
|
|||||||
profileId: null,
|
profileId: null,
|
||||||
updatedValues: {},
|
updatedValues: {},
|
||||||
});
|
});
|
||||||
|
const { createdAt, updatedAt, ...expectedEvType } = evType;
|
||||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||||
data: {
|
data: {
|
||||||
...evType,
|
...expectedEvType,
|
||||||
parentId: 1,
|
parentId: 1,
|
||||||
users: { connect: [{ id: 4 }] },
|
users: { connect: [{ id: 4 }] },
|
||||||
lockTimeZoneToggleOnBookingPage: false,
|
lockTimeZoneToggleOnBookingPage: false,
|
||||||
@@ -206,7 +207,7 @@ describe("handleChildrenEventTypes", () => {
|
|||||||
bookingLimits: undefined,
|
bookingLimits: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { profileId, autoTranslateDescriptionEnabled, ...rest } = evType;
|
const { profileId, autoTranslateDescriptionEnabled, createdAt, updatedAt, ...rest } = evType;
|
||||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||||
data: {
|
data: {
|
||||||
...rest,
|
...rest,
|
||||||
@@ -317,9 +318,10 @@ describe("handleChildrenEventTypes", () => {
|
|||||||
profileId: null,
|
profileId: null,
|
||||||
updatedValues: {},
|
updatedValues: {},
|
||||||
});
|
});
|
||||||
|
const { createdAt, updatedAt, ...expectedEvType } = evType;
|
||||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||||
data: {
|
data: {
|
||||||
...evType,
|
...expectedEvType,
|
||||||
parentId: 1,
|
parentId: 1,
|
||||||
users: { connect: [{ id: 4 }] },
|
users: { connect: [{ id: 4 }] },
|
||||||
bookingLimits: undefined,
|
bookingLimits: undefined,
|
||||||
@@ -383,7 +385,7 @@ describe("handleChildrenEventTypes", () => {
|
|||||||
length: 30,
|
length: 30,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { profileId, autoTranslateDescriptionEnabled, ...rest } = evType;
|
const { profileId, autoTranslateDescriptionEnabled, createdAt, updatedAt, ...rest } = evType;
|
||||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||||
data: {
|
data: {
|
||||||
...rest,
|
...rest,
|
||||||
@@ -464,7 +466,11 @@ describe("handleChildrenEventTypes", () => {
|
|||||||
...evType,
|
...evType,
|
||||||
};
|
};
|
||||||
|
|
||||||
prismaMock.eventType.update.mockResolvedValue(mockUpdatedEventType);
|
prismaMock.eventType.update.mockResolvedValue({
|
||||||
|
...mockUpdatedEventType,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
await updateChildrenEventTypes({
|
await updateChildrenEventTypes({
|
||||||
eventTypeId: 1,
|
eventTypeId: 1,
|
||||||
@@ -480,9 +486,10 @@ describe("handleChildrenEventTypes", () => {
|
|||||||
updatedValues: {},
|
updatedValues: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { createdAt, updatedAt, ...expectedEvType } = evType;
|
||||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||||
data: {
|
data: {
|
||||||
...evType,
|
...expectedEvType,
|
||||||
bookingLimits: undefined,
|
bookingLimits: undefined,
|
||||||
durationLimits: undefined,
|
durationLimits: undefined,
|
||||||
recurringEvent: undefined,
|
recurringEvent: undefined,
|
||||||
@@ -507,7 +514,7 @@ describe("handleChildrenEventTypes", () => {
|
|||||||
allowReschedulingCancelledBookings: false,
|
allowReschedulingCancelledBookings: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { profileId, rrSegmentQueryValue, ...rest } = evType;
|
const { profileId, rrSegmentQueryValue, createdAt: _, updatedAt: __, ...rest } = evType;
|
||||||
if ("workflows" in rest) delete rest.workflows;
|
if ("workflows" in rest) delete rest.workflows;
|
||||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||||
data: {
|
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/event-type",
|
||||||
"platform/atoms/calendar-settings",
|
"platform/atoms/calendar-settings",
|
||||||
"platform/atoms/payment-form",
|
"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
|
<iframe
|
||||||
height="315"
|
height="315"
|
||||||
style={{ width: "100%", maxWidth: "560px" }}
|
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"
|
frameborder="0"
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
allowfullscreen="true"
|
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",
|
"dev": "PORT=5100 next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/atoms": "*",
|
"@calcom/atoms": "workspace:*",
|
||||||
|
"@calcom/eslint-config": "workspace:*",
|
||||||
"@prisma/client": "6.7.0",
|
"@prisma/client": "6.7.0",
|
||||||
"next": "14.0.4",
|
"next": "14.0.4",
|
||||||
"prisma": "^6.7.0",
|
"prisma": "^6.7.0",
|
||||||
@@ -22,8 +23,6 @@
|
|||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"autoprefixer": "^10.0.1",
|
"autoprefixer": "^10.0.1",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"eslint": "^8.34.0",
|
|
||||||
"eslint-config-next": "14.0.4",
|
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.3.0",
|
"tailwindcss": "^3.3.0",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4"
|
||||||
|
|||||||
+10
-9
@@ -8,6 +8,7 @@
|
|||||||
"packages/*",
|
"packages/*",
|
||||||
"packages/embeds/*",
|
"packages/embeds/*",
|
||||||
"packages/features/*",
|
"packages/features/*",
|
||||||
|
"packages/app-store",
|
||||||
"packages/app-store/*",
|
"packages/app-store/*",
|
||||||
"packages/platform/*",
|
"packages/platform/*",
|
||||||
"packages/platform/examples/base",
|
"packages/platform/examples/base",
|
||||||
@@ -53,9 +54,9 @@
|
|||||||
"env-check:common": "dotenv-checker --schema .env.example --env .env",
|
"env-check:common": "dotenv-checker --schema .env.example --env .env",
|
||||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||||
"heroku-postbuild": "turbo run @calcom/web#build",
|
"heroku-postbuild": "turbo run @calcom/web#build",
|
||||||
"lint:fix": "turbo run lint:fix",
|
"lint:fix": "turbo lint:fix",
|
||||||
"lint:report": "turbo run lint:report",
|
"lint:report": "turbo lint:report",
|
||||||
"lint": "turbo run lint",
|
"lint": "turbo lint",
|
||||||
"postinstall": "husky install && turbo run post-install",
|
"postinstall": "husky install && turbo run post-install",
|
||||||
"pre-commit": "lint-staged",
|
"pre-commit": "lint-staged",
|
||||||
"predev": "echo 'Checking env files'",
|
"predev": "echo 'Checking env files'",
|
||||||
@@ -71,8 +72,8 @@
|
|||||||
"test-e2e:embed": "yarn db-seed && yarn e2e:embed",
|
"test-e2e:embed": "yarn db-seed && yarn e2e:embed",
|
||||||
"test-e2e:embed-react": "yarn db-seed && yarn e2e:embed-react",
|
"test-e2e:embed-react": "yarn db-seed && yarn e2e:embed-react",
|
||||||
"test-playwright": "yarn playwright test --config=playwright.config.ts",
|
"test-playwright": "yarn playwright test --config=playwright.config.ts",
|
||||||
"test": "vitest run",
|
"test": "TZ=UTC vitest run",
|
||||||
"test:ui": "vitest --ui",
|
"test:ui": "TZ=UTC vitest --ui",
|
||||||
"type-check": "turbo run type-check",
|
"type-check": "turbo run type-check",
|
||||||
"type-check:ci": "turbo run type-check:ci --log-prefix=none",
|
"type-check:ci": "turbo run type-check:ci --log-prefix=none",
|
||||||
"web": "yarn workspace @calcom/web",
|
"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"
|
"changesets-release": "NODE_OPTIONS='--max_old_space_size=12288' turbo run build --filter=@calcom/atoms && yarn changeset publish"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@calcom/eslint-config": "workspace:*",
|
||||||
"@changesets/changelog-github": "^0.5.1",
|
"@changesets/changelog-github": "^0.5.1",
|
||||||
"@changesets/cli": "2.29.4",
|
"@changesets/cli": "2.29.4",
|
||||||
"@faker-js/faker": "9.2.0",
|
"@faker-js/faker": "9.2.0",
|
||||||
@@ -96,7 +98,7 @@
|
|||||||
"c8": "^7.13.0",
|
"c8": "^7.13.0",
|
||||||
"checkly": "latest",
|
"checkly": "latest",
|
||||||
"dotenv-checker": "^1.1.5",
|
"dotenv-checker": "^1.1.5",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint": "9.36.0",
|
||||||
"husky": "^8.0.0",
|
"husky": "^8.0.0",
|
||||||
"i18n-unused": "^0.13.0",
|
"i18n-unused": "^0.13.0",
|
||||||
"jest-diff": "^29.5.0",
|
"jest-diff": "^29.5.0",
|
||||||
@@ -110,6 +112,7 @@
|
|||||||
"prismock": "1.35.3",
|
"prismock": "1.35.3",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"tsc-absolute": "^1.0.0",
|
"tsc-absolute": "^1.0.0",
|
||||||
|
"turbo": "^2.5.5",
|
||||||
"typescript": "5.9.0-beta",
|
"typescript": "5.9.0-beta",
|
||||||
"vitest": "^2.1.9",
|
"vitest": "^2.1.9",
|
||||||
"vitest-fetch-mock": "^0.3.0",
|
"vitest-fetch-mock": "^0.3.0",
|
||||||
@@ -122,9 +125,7 @@
|
|||||||
"@vercel/functions": "^1.4.0",
|
"@vercel/functions": "^1.4.0",
|
||||||
"city-timezones": "^1.2.1",
|
"city-timezones": "^1.2.1",
|
||||||
"date-fns-tz": "^3.2.0",
|
"date-fns-tz": "^3.2.0",
|
||||||
"eslint": "^8.34.0",
|
"p-limit": "^6.2.0"
|
||||||
"p-limit": "^6.2.0",
|
|
||||||
"turbo": "^2.5.5"
|
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"types-ramda": "0.29.4",
|
"types-ramda": "0.29.4",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"dist/cli.js"
|
"dist/cli.js"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/lib": "*",
|
"@calcom/lib": "workspace:*",
|
||||||
"ink": "^3.2.0",
|
"ink": "^3.2.0",
|
||||||
"ink-select-input": "^4.2.1",
|
"ink-select-input": "^4.2.1",
|
||||||
"ink-text-input": "^4.0.3",
|
"ink-text-input": "^4.0.3",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/lib": "*",
|
"@calcom/lib": "workspace:*",
|
||||||
"@getalby/lightning-tools": "^5.1.2",
|
"@getalby/lightning-tools": "^5.1.2",
|
||||||
"@getalby/sdk": "^4.0.0",
|
"@getalby/sdk": "^4.0.0",
|
||||||
"@webbtc/webln-types": "^3.0.0",
|
"@webbtc/webln-types": "^3.0.0",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"svix": "^0.85.1"
|
"svix": "^0.85.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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"
|
"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",
|
"main": "./index.ts",
|
||||||
"description": "The joyful productivity app\r\r",
|
"description": "The joyful productivity app\r\r",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/lib": "*"
|
"@calcom/lib": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/types": "*"
|
"@calcom/types": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
"main": "./index.ts",
|
"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.",
|
"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": {
|
"dependencies": {
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "workspace:*",
|
||||||
"react-hook-form": "^7.43.3"
|
"react-hook-form": "^7.43.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/types": "*"
|
"@calcom/types": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/lib": "*"
|
"@calcom/lib": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/types": "*"
|
"@calcom/types": "workspace:*"
|
||||||
},
|
},
|
||||||
"description": "Attio is the AI-native CRM that builds, scales and grows your company to the next level."
|
"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",
|
"version": "0.0.0",
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/lib": "*"
|
"@calcom/lib": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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"
|
"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",
|
"version": "0.0.0",
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/lib": "*"
|
"@calcom/lib": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/types": "*"
|
"@calcom/types": "workspace:*"
|
||||||
},
|
},
|
||||||
"description": "Request a signed Business Associate Agreement for your HIPAA compliance records"
|
"description": "Request a signed Business Associate Agreement for your HIPAA compliance records"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/lib": "*"
|
"@calcom/lib": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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."
|
"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",
|
"version": "0.0.1",
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/lib": "*"
|
"@calcom/lib": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/types": "*"
|
"@calcom/types": "workspace:*"
|
||||||
},
|
},
|
||||||
"description": "24x7 AI receptionists to answer all inbound calls"
|
"description": "24x7 AI receptionists to answer all inbound calls"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/lib": "*"
|
"@calcom/lib": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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."
|
"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",
|
"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.",
|
"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": {
|
"dependencies": {
|
||||||
"@calcom/lib": "*",
|
"@calcom/lib": "workspace:*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "workspace:*",
|
||||||
"@calcom/ui": "*",
|
"@calcom/ui": "workspace:*",
|
||||||
"react-hook-form": "^7.43.3"
|
"react-hook-form": "^7.43.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/types": "*"
|
"@calcom/types": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
"main": "./index.ts",
|
"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",
|
"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": {
|
"dependencies": {
|
||||||
"@calcom/lib": "*"
|
"@calcom/lib": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/types": "*"
|
"@calcom/types": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/lib": "*"
|
"@calcom/lib": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/types": "*"
|
"@calcom/types": "workspace:*"
|
||||||
},
|
},
|
||||||
"description": "Add Cal.com to your Chatbase AI Chatbot and accept bookings right inside chat."
|
"description": "Add Cal.com to your Chatbase AI Chatbot and accept bookings right inside chat."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/lib": "*"
|
"@calcom/lib": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/types": "*"
|
"@calcom/types": "workspace:*"
|
||||||
},
|
},
|
||||||
"description": "Create, List and Interact with Your Cal.com links and connections easily."
|
"description": "Create, List and Interact with Your Cal.com links and connections easily."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
"test:coverage": "vitest run --coverage"
|
"test:coverage": "vitest run --coverage"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/lib": "*",
|
"@calcom/lib": "workspace:*",
|
||||||
"@calcom/prisma": "*"
|
"@calcom/prisma": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/types": "*"
|
"@calcom/types": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/lib": "*"
|
"@calcom/lib": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/types": "*"
|
"@calcom/types": "workspace:*"
|
||||||
},
|
},
|
||||||
"description": "All of your commitments, now in one place."
|
"description": "All of your commitments, now in one place."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
"main": "./index.ts",
|
"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.",
|
"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": {
|
"dependencies": {
|
||||||
"@calcom/lib": "*",
|
"@calcom/lib": "workspace:*",
|
||||||
"@calcom/prisma": "*"
|
"@calcom/prisma": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/types": "*"
|
"@calcom/types": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/lib": "*"
|
"@calcom/lib": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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."
|
"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