Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2701b4de79 | ||
|
|
a9f83ac5e2 | ||
|
|
ff1533d344 | ||
|
|
d9cddd85ff | ||
|
|
1ab4b9dfab | ||
|
|
f3d0475840 | ||
|
|
061283bab9 | ||
|
|
4e0798577a | ||
|
|
c95b0833c1 | ||
|
|
2a7c6590a3 | ||
|
|
7d2a06ddb6 | ||
|
|
d34ad2b0f6 | ||
|
|
e06249d2df | ||
|
|
a09dfe37a1 | ||
|
|
71e7f16b29 | ||
|
|
d6546c3107 | ||
|
|
625c260e40 | ||
|
|
81224f324a | ||
|
|
700d8e7f30 | ||
|
|
301376e33b | ||
|
|
57476ea392 | ||
|
|
9c2d7f99c2 | ||
|
|
ef3281f06f | ||
|
|
d03f45dd0f | ||
|
|
2b2bf36703 | ||
|
|
272d97c0a3 | ||
|
|
f2b2b6e491 | ||
|
|
15ef547e41 | ||
|
|
251d29f65b | ||
|
|
8b652613dd | ||
|
|
8e1a0999d2 | ||
|
|
e29aa2097e | ||
|
|
1578dee622 | ||
|
|
e5ebf7feb4 | ||
|
|
06348982a3 | ||
|
|
a7d87fc5be | ||
|
|
0d77fc8474 | ||
|
|
0af77be127 | ||
|
|
afbfa11ab5 | ||
|
|
a7aabc8b4f | ||
|
|
c941192979 | ||
|
|
44311f51ba | ||
|
|
0af6354954 | ||
|
|
dd7c553cc3 | ||
|
|
5cb1d4aad0 | ||
|
|
701490a493 | ||
|
|
6499243a42 | ||
|
|
3c8215add3 | ||
|
|
2bd7549f20 | ||
|
|
629371a5f1 | ||
|
|
dc3df122e2 | ||
|
|
176fb777fc | ||
|
|
ae7fd0cae2 | ||
|
|
d1184f65e2 | ||
|
|
342e5baca2 | ||
|
|
fbfa27735b | ||
|
|
37c26793f5 | ||
|
|
956f81fab3 | ||
|
|
1dcd54aab7 | ||
|
|
68d3d9d964 | ||
|
|
717a26f223 | ||
|
|
6818806c5e | ||
|
|
dd7f108f08 | ||
|
|
7e4d9e26c6 | ||
|
|
a7352cf587 | ||
|
|
5f8db0557c | ||
|
|
88be45457f | ||
|
|
90a42c37fd | ||
|
|
cf7430d531 | ||
|
|
105a82f97d | ||
|
|
fdd3d11111 | ||
|
|
e7ea1884b7 | ||
|
|
c04402e7ee | ||
|
|
075d209b6f | ||
|
|
26e8582302 | ||
|
|
9f277c5179 | ||
|
|
21411b8f93 | ||
|
|
0c160de404 | ||
|
|
a0eca13c18 | ||
|
|
5cda123a4c | ||
|
|
6169b92e98 | ||
|
|
cc1efe1298 | ||
|
|
7635d338c0 | ||
|
|
03ebabd6b4 | ||
|
|
0ab1bd0b6c | ||
|
|
908e5e05f2 | ||
|
|
3490272815 | ||
|
|
02dab51543 | ||
|
|
358b873aa7 | ||
|
|
44aa26292d | ||
|
|
b8530e2d78 | ||
|
|
7f1b94ad19 | ||
|
|
801ba50221 | ||
|
|
74af23136c | ||
|
|
da9ee5c186 | ||
|
|
037cb8ee2a | ||
|
|
f0e10129a1 | ||
|
|
7c5731e893 | ||
|
|
1012f2f153 | ||
|
|
d6f6be7d50 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@calcom/atoms": minor
|
||||
---
|
||||
|
||||
Add defaultPhoneCountry prop to BookerPlatformWrapper with ISO 3166-1 alpha-2 type safety
|
||||
+8
-1
@@ -214,6 +214,8 @@ STRIPE_TEAM_PRODUCT_ID=
|
||||
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
|
||||
STRIPE_ORG_MONTHLY_PRICE_ID=
|
||||
STRIPE_ORG_PRODUCT_ID=
|
||||
# Used to pass trial days for orgs during the Stripe checkout session
|
||||
STRIPE_ORG_TRIAL_DAYS=
|
||||
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_WEBHOOK_SECRET_APPS=
|
||||
@@ -423,7 +425,8 @@ VAPID_PRIVATE_KEY=
|
||||
|
||||
# Mintlify chat api
|
||||
# Power AI chat in for docs
|
||||
NEXT_PUBLIC_MINTLIFY_CHAT_API_KEY=
|
||||
MINTLIFY_CHAT_API_KEY=
|
||||
NEXT_PUBLIC_ENABLE_MINTLIFY_CHAT=
|
||||
NEXT_PUBLIC_CHAT_API_URL=
|
||||
NEXT_PUBLIC_DOCS_URL=
|
||||
|
||||
@@ -489,3 +492,7 @@ SALESFORCE_GRAPHQL_MAX_RETRIES=3
|
||||
|
||||
# Force Node.js to use UTC as process timezone
|
||||
TZ=UTC
|
||||
|
||||
# test oauth client for `packages/platform/examples/base` (optional)
|
||||
SEED_PLATFORM_OAUTH_CLIENT_ID=
|
||||
SEED_PLATFORM_OAUTH_CLIENT_SECRET=
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/**/package.json @calcom/Foundation
|
||||
/apps/api/**/* @calcom/API
|
||||
/apps/ui-playground/**/* @calcom/Consumer @calcom/Foundation
|
||||
/apps/web/**/layout.tsx @calcom/Consumer @calcom/Foundation
|
||||
/apps/web/lib/csp.ts @calcom/Foundation
|
||||
/apps/web/lib/buildNonce.ts @calcom/Foundation
|
||||
|
||||
@@ -26,7 +26,7 @@ runs:
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}
|
||||
- run: USE_POOL=true yarn db-seed
|
||||
- run: yarn db-seed
|
||||
if: steps.cache-db.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
- name: Postgres Dump Backup
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
- name: Checkout GitHub Pages Branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: calcom/test-results
|
||||
repository: calcom/test-results-2
|
||||
ref: gh-pages
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
- name: Checkout GitHub Pages Branch
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: calcom/test-results
|
||||
repository: calcom/test-results-2
|
||||
ref: gh-pages
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
- name: Set Git User
|
||||
|
||||
@@ -5,6 +5,7 @@ permissions:
|
||||
contents: read
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
INTEGRATION_TESTS: "true"
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Checkout GitHub Pages Branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: calcom/test-results
|
||||
repository: calcom/test-results-2
|
||||
ref: gh-pages
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
- name: Set Git User
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
- name: Output Report URL as Workflow Annotation
|
||||
id: url
|
||||
run: |
|
||||
FULL_HTML_REPORT_URL=https://calcom.github.io/test-results/$HTML_REPORT_URL_PATH
|
||||
FULL_HTML_REPORT_URL=https://calcom.github.io/test-results-2/$HTML_REPORT_URL_PATH
|
||||
echo "::notice title=📋 Published Playwright Test Report::$FULL_HTML_REPORT_URL"
|
||||
echo "link=$FULL_HTML_REPORT_URL" >> $GITHUB_OUTPUT
|
||||
- name: Find Comment
|
||||
|
||||
Vendored
-1
@@ -4,7 +4,6 @@
|
||||
"yzhang.markdown-all-in-one", // nicer markdown support
|
||||
"esbenp.prettier-vscode", // prettier plugin
|
||||
"dbaeumer.vscode-eslint", // eslint plugin
|
||||
"bradlc.vscode-tailwindcss", // hinting / autocompletion for tailwind
|
||||
"ban.spellright", // Spell check for docs
|
||||
"stripe.vscode-stripe", // stripe VSCode extension
|
||||
"Prisma.prisma", // syntax|format|completion for prisma
|
||||
|
||||
Vendored
+1
-1
@@ -8,5 +8,5 @@
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"spellright.language": ["en"],
|
||||
"spellright.documentTypes": ["markdown", "typescript", "typescriptreact"],
|
||||
"tailwindCSS.experimental.classRegex": [["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]]
|
||||
"tailwindCSS.classFunctions": ["cva"]
|
||||
}
|
||||
|
||||
Vendored
Vendored
Vendored
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
Vendored
Vendored
-15
@@ -1,15 +0,0 @@
|
||||
Possible issue:
|
||||
- We won't be able to use already built calendar-cache when Delegation Credential is enabled because existing CalendarCache entries don't have userId set.
|
||||
|
||||
## Approach of always using SelectedCalendar.credentialId for CalendarCache even for DelegationCredentials
|
||||
- Delete Credentials when DelegationCredential is disabled
|
||||
|
||||
|
||||
TO Test
|
||||
- New members beyond the batch size are processed
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -16,15 +16,15 @@ export const isAdminGuard = async (req: NextApiRequest) => {
|
||||
where: {
|
||||
userId: userId,
|
||||
accepted: true,
|
||||
role: {
|
||||
in: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
},
|
||||
team: {
|
||||
isOrganization: true,
|
||||
organizationSettings: {
|
||||
is: {
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
},
|
||||
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
|
||||
},
|
||||
select: {
|
||||
team: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, beforeAll } from "vitest";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -11,6 +11,30 @@ type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
describe("PATCH /api/bookings", () => {
|
||||
beforeAll(async () => {
|
||||
const acmeOrg = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: "acme",
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (acmeOrg) {
|
||||
await prisma.organizationSettings.upsert({
|
||||
where: {
|
||||
organizationId: acmeOrg.id,
|
||||
},
|
||||
update: {
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
create: {
|
||||
organizationId: acmeOrg.id,
|
||||
orgAutoAcceptEmail: "acme.com",
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
it("Returns 403 when user has no permission to the booking", async () => {
|
||||
const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } });
|
||||
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
||||
|
||||
@@ -16,14 +16,33 @@ const DefaultPagination = {
|
||||
skip: 0,
|
||||
};
|
||||
|
||||
describe("GET /api/bookings", () => {
|
||||
let proUser: Awaited<ReturnType<typeof prisma.user.findFirstOrThrow>>;
|
||||
let proUserBooking: Awaited<ReturnType<typeof prisma.booking.findFirstOrThrow>>;
|
||||
|
||||
describe("GET /api/bookings", async () => {
|
||||
beforeAll(async () => {
|
||||
proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
||||
proUserBooking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
|
||||
const acmeOrg = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: "acme",
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (acmeOrg) {
|
||||
await prisma.organizationSettings.upsert({
|
||||
where: {
|
||||
organizationId: acmeOrg.id,
|
||||
},
|
||||
update: {
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
create: {
|
||||
organizationId: acmeOrg.id,
|
||||
orgAutoAcceptEmail: "acme.com",
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
||||
const proUserBooking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
|
||||
|
||||
it("Does not return bookings of other users when user has no permission", async () => {
|
||||
const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, beforeAll } from "vitest";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -12,6 +12,53 @@ type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
describe("isAdmin guard", () => {
|
||||
beforeAll(async () => {
|
||||
const acmeOrg = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: "acme",
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (acmeOrg) {
|
||||
await prisma.organizationSettings.upsert({
|
||||
where: {
|
||||
organizationId: acmeOrg.id,
|
||||
},
|
||||
update: {
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
create: {
|
||||
organizationId: acmeOrg.id,
|
||||
orgAutoAcceptEmail: "acme.com",
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const dunderOrg = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: "dunder-mifflin",
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (dunderOrg) {
|
||||
await prisma.organizationSettings.upsert({
|
||||
where: {
|
||||
organizationId: dunderOrg.id,
|
||||
},
|
||||
update: {
|
||||
isAdminAPIEnabled: false,
|
||||
},
|
||||
create: {
|
||||
organizationId: dunderOrg.id,
|
||||
orgAutoAcceptEmail: "dunder-mifflin.com",
|
||||
isAdminAPIEnabled: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
it("Returns false when user does not exist in the system", async () => {
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, beforeAll } from "vitest";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -8,6 +8,30 @@ import {
|
||||
} from "../../../lib/utils/retrieveScopedAccessibleUsers";
|
||||
|
||||
describe("retrieveScopedAccessibleUsers tests", () => {
|
||||
beforeAll(async () => {
|
||||
const acmeOrg = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: "acme",
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (acmeOrg) {
|
||||
await prisma.organizationSettings.upsert({
|
||||
where: {
|
||||
organizationId: acmeOrg.id,
|
||||
},
|
||||
update: {
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
create: {
|
||||
organizationId: acmeOrg.id,
|
||||
orgAutoAcceptEmail: "acme.com",
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
describe("getAccessibleUsers", () => {
|
||||
it("Does not return members when only admin user ID is supplied", async () => {
|
||||
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
|
||||
|
||||
@@ -189,7 +189,7 @@ export class BookingsController_2024_08_13 {
|
||||
2. uid of one of the recurring booking recurrences
|
||||
|
||||
3. uid of recurring booking which will return an array of all recurring booking recurrences (stored as recurringBookingUid on one of the individual recurrences).
|
||||
|
||||
|
||||
If you are fetching a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
||||
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
|
||||
|
||||
@@ -213,7 +213,7 @@ export class BookingsController_2024_08_13 {
|
||||
@ApiOperation({
|
||||
summary: "Get all the recordings for the booking",
|
||||
description: `Fetches all the recordings for the booking \`:bookingUid\`
|
||||
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
@@ -229,9 +229,9 @@ export class BookingsController_2024_08_13 {
|
||||
@Get("/:bookingUid/transcripts")
|
||||
@UseGuards(BookingUidGuard)
|
||||
@ApiOperation({
|
||||
summary: "Get all the transcripts download links for the booking",
|
||||
summary: "Get Cal Video real time transcript download links for the booking",
|
||||
description: `Fetches all the transcripts download links for the booking \`:bookingUid\`
|
||||
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
@@ -279,7 +279,7 @@ export class BookingsController_2024_08_13 {
|
||||
@ApiOperation({
|
||||
summary: "Reschedule a booking",
|
||||
description: `Reschedule a booking or seated booking
|
||||
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
@@ -321,7 +321,7 @@ export class BookingsController_2024_08_13 {
|
||||
@ApiOperation({
|
||||
summary: "Cancel a booking",
|
||||
description: `:bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.
|
||||
|
||||
|
||||
\nCancelling normal bookings:
|
||||
If the booking is not seated and not recurring, simply pass :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and optionally cancellationReason in the request body \`{"cancellationReason": "Will travel"}\`.
|
||||
|
||||
@@ -329,11 +329,11 @@ export class BookingsController_2024_08_13 {
|
||||
It is possible to cancel specific seat within a booking as an attendee or all of the seats as the host.
|
||||
\n1. As an attendee - provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and seatUid in the request body \`{"seatUid": "123-123-123"}\` . This will remove this particular attendance from the booking.
|
||||
\n2. As the host or org admin of host - host can cancel booking for all attendees aka for every seat, this also applies to org admins. Provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and cancellationReason in the request body \`{"cancellationReason": "Will travel"}\` and \`Authorization: Bearer token\` request header where token is event type owner (host) credential. This will cancel the booking for all attendees.
|
||||
|
||||
|
||||
\nCancelling recurring seated bookings:
|
||||
For recurring seated bookings it is not possible to cancel all of them with 1 call
|
||||
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.
|
||||
|
||||
|
||||
If you are cancelling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
||||
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
|
||||
|
||||
@@ -374,7 +374,7 @@ export class BookingsController_2024_08_13 {
|
||||
@ApiOperation({
|
||||
summary: "Mark a booking absence",
|
||||
description: `The provided authorization header refers to the owner of the booking.
|
||||
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
@@ -399,7 +399,7 @@ export class BookingsController_2024_08_13 {
|
||||
@ApiOperation({
|
||||
summary: "Reassign a booking to auto-selected host",
|
||||
description: `Currently only supports reassigning host for round robin bookings. The provided authorization header refers to the owner of the booking.
|
||||
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
@@ -423,7 +423,7 @@ export class BookingsController_2024_08_13 {
|
||||
@ApiOperation({
|
||||
summary: "Reassign a booking to a specific host",
|
||||
description: `Currently only supports reassigning host for round robin bookings. The provided authorization header refers to the owner of the booking.
|
||||
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
@@ -454,7 +454,7 @@ export class BookingsController_2024_08_13 {
|
||||
@ApiOperation({
|
||||
summary: "Confirm a booking",
|
||||
description: `The provided authorization header refers to the owner of the booking.
|
||||
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
@@ -478,8 +478,8 @@ export class BookingsController_2024_08_13 {
|
||||
@ApiOperation({
|
||||
summary: "Decline a booking",
|
||||
description: `The provided authorization header refers to the owner of the booking.
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
async declineBooking(
|
||||
@@ -502,8 +502,8 @@ export class BookingsController_2024_08_13 {
|
||||
@ApiOperation({
|
||||
summary: "Get 'Add to Calendar' links for a booking",
|
||||
description: `Retrieve calendar links for a booking that can be used to add the event to various calendar services. Returns links for Google Calendar, Microsoft Office, Microsoft Outlook, and a downloadable ICS file.
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
@HttpCode(HttpStatus.OK)
|
||||
|
||||
@@ -20,11 +20,7 @@ import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
GetBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { CreateBookingInput_2024_08_13, GetBookingOutput_2024_08_13, GetSeatedBookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import { BookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import type { Booking, PlatformOAuthClient, Team, User, EventType } from "@calcom/prisma/client";
|
||||
|
||||
@@ -854,7 +850,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
describe("Booking Field Type Validation", () => {
|
||||
const basePayload = {
|
||||
start: "2025-06-19T11:00:00.000Z",
|
||||
start: new Date(Date.UTC(2030, 5, 19, 11, 0, 0)).toISOString(),
|
||||
attendee: {
|
||||
name: "Charlie TypeTest",
|
||||
email: "charlie.typetest@example.com",
|
||||
@@ -1162,6 +1158,31 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
`One or more invalid options for booking field '${fieldName}'. Allowed options are: blue, red.`
|
||||
);
|
||||
});
|
||||
|
||||
it("should transform null values to empty strings in bookingFieldsResponses", async () => {
|
||||
const payload = {
|
||||
...basePayload,
|
||||
eventTypeId: eventTypeWithBookingFields.id,
|
||||
bookingFieldsResponses: {
|
||||
"favorite-movie": "The Matrix",
|
||||
rescheduleReason: null,
|
||||
notes: null,
|
||||
},
|
||||
};
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings`)
|
||||
.send(payload)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
if (responseDataIsBooking(response.body.data)) {
|
||||
const data: BookingOutput_2024_08_13 = response.body.data;
|
||||
expect(data.bookingFieldsResponses.notes).toBe("");
|
||||
expect(data.bookingFieldsResponses.rescheduleReason).toBe("");
|
||||
expect(data.bookingFieldsResponses["favorite-movie"]).toBe("The Matrix");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -1176,4 +1197,4 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
|
||||
}
|
||||
});
|
||||
});
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.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 { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import type {
|
||||
CreateBookingInput_2024_08_13,
|
||||
BookingOutput_2024_08_13,
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
GetBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import type { EventType, User } from "@calcom/prisma/client";
|
||||
|
||||
describe("Reschedule bookings 2024-08-13", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
|
||||
let normalBookingUser: User;
|
||||
let normalBookingEventType: EventType;
|
||||
let normalBookingEventTypeId: number;
|
||||
const normalBookingUserEmail = `user-bookings-normal-user-${randomString(5)}@api.com`;
|
||||
const normalBookingEventTypeSlug = `user-bookings-normal-event-type-${randomString(5)}`;
|
||||
let oldNormalBooking: BookingOutput_2024_08_13;
|
||||
let newNormalBooking: BookingOutput_2024_08_13;
|
||||
|
||||
const RESCHEDULE_REASON = "Flying to venus that day";
|
||||
const RESCHEDULED_BY = `user-venus-bookings-rescheduler-${randomString(5)}@api.com`;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
normalBookingUserEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
// initialize normal booking user, schedule and event type
|
||||
normalBookingUser = await userRepositoryFixture.create({
|
||||
email: normalBookingUserEmail,
|
||||
username: normalBookingUserEmail,
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: `user-bookings-normal-schedule-${randomString()}`,
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(normalBookingUser.id, userSchedule);
|
||||
|
||||
normalBookingEventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `user-bookings-normal-event-type-${randomString()}`,
|
||||
slug: normalBookingEventTypeSlug,
|
||||
length: 60,
|
||||
},
|
||||
normalBookingUser.id
|
||||
);
|
||||
normalBookingEventTypeId = normalBookingEventType.id;
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
await app.init();
|
||||
});
|
||||
|
||||
describe("reschedule normal booking", () => {
|
||||
it("should create a booking to be rescheduled", async () => {
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 1, 9, 10, 0, 0)).toISOString(),
|
||||
eventTypeId: normalBookingEventTypeId,
|
||||
attendee: {
|
||||
name: `user-venus-bookings-attendee-${randomString(10)}`,
|
||||
email: `user-venus-bookings-attendee-${randomString(10)}@gmail.com`,
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/oj12u83128u9312ou3",
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
expect(bookingResponseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(bookingResponseBody.data)).toBe(true);
|
||||
if (responseDataIsBooking(bookingResponseBody.data)) {
|
||||
const created: BookingOutput_2024_08_13 = bookingResponseBody.data;
|
||||
oldNormalBooking = created;
|
||||
expect(oldNormalBooking).toBeDefined();
|
||||
expect(oldNormalBooking.uid).toBeDefined();
|
||||
} else {
|
||||
throw new Error("should create a booking to be rescheduled - Invalid response data");
|
||||
}
|
||||
});
|
||||
|
||||
it("should reschedule", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 1, 9, 12, 0, 0)).toISOString(),
|
||||
reschedulingReason: RESCHEDULE_REASON,
|
||||
rescheduledBy: RESCHEDULED_BY,
|
||||
};
|
||||
|
||||
const beforeCreate = new Date();
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${oldNormalBooking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const afterCreate = new Date();
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data as BookingOutput_2024_08_13;
|
||||
expect(data.reschedulingReason).toEqual(RESCHEDULE_REASON);
|
||||
expect(data.rescheduledFromUid).toEqual(oldNormalBooking.uid);
|
||||
expect(data.rescheduledByEmail).toEqual(RESCHEDULED_BY);
|
||||
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2040, 1, 9, 13, 0, 0)).toISOString());
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(normalBookingUser.id);
|
||||
expect(data.hosts[0].username).toEqual(normalBookingUser.username);
|
||||
expect(data.hosts[0].email).toEqual(normalBookingUser.email);
|
||||
expect(data.status).toEqual(oldNormalBooking.status);
|
||||
expect(data.duration).toEqual(oldNormalBooking.duration);
|
||||
expect(data.eventTypeId).toEqual(oldNormalBooking.eventTypeId);
|
||||
expect(data.attendees[0]).toEqual(oldNormalBooking.attendees[0]);
|
||||
expect(data.location).toEqual(oldNormalBooking.location);
|
||||
expect(data.absentHost).toEqual(oldNormalBooking.absentHost);
|
||||
expect(data.metadata).toEqual(oldNormalBooking.metadata);
|
||||
|
||||
newNormalBooking = data;
|
||||
expect(newNormalBooking).toBeDefined();
|
||||
|
||||
const createdAtDate = new Date(data.createdAt);
|
||||
expect(createdAtDate.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
|
||||
expect(createdAtDate.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch old booking and verify rescheduledToUid and rescheduledByEmail and status cancelled", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${oldNormalBooking.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.rescheduledToUid).toEqual(newNormalBooking.uid);
|
||||
expect(data.rescheduledByEmail).toEqual(RESCHEDULED_BY);
|
||||
expect(data.status).toEqual("cancelled");
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch new booking and verify rescheduledFromUid, rescheduledByEmail and reschedulingReason", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${newNormalBooking.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.rescheduledFromUid).toEqual(oldNormalBooking.uid);
|
||||
expect(data.rescheduledByEmail).toEqual(RESCHEDULED_BY);
|
||||
expect(data.reschedulingReason).toEqual(RESCHEDULE_REASON);
|
||||
expect(data.status).toEqual("accepted");
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(normalBookingUser.email);
|
||||
});
|
||||
|
||||
function responseDataIsBooking(data: unknown): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data !== null && data && "id" in data;
|
||||
}
|
||||
});
|
||||
+254
-356
@@ -79,7 +79,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const eventTypeRequiresConfirmationSlug = `user-bookings-event-type-requires-confirmation-${randomString()}`;
|
||||
|
||||
let createdBooking: BookingOutput_2024_08_13;
|
||||
let rescheduledBooking: BookingOutput_2024_08_13;
|
||||
let createdRecurringBooking: RecurringBookingOutput_2024_08_13[];
|
||||
|
||||
let bookingInThePast: Booking;
|
||||
@@ -1360,139 +1359,217 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("reschedule bookings", () => {
|
||||
it("should reschedule normal booking", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
const beforeCreate = new Date();
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${createdBooking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const afterCreate = new Date();
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.reschedulingReason).toEqual(body.reschedulingReason);
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 8, 15, 0, 0)).toISOString());
|
||||
expect(data.rescheduledFromUid).toEqual(createdBooking.uid);
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.hosts[0].username).toEqual(user.username);
|
||||
expect(data.hosts[0].email).toEqual(user.email);
|
||||
expect(data.status).toEqual(createdBooking.status);
|
||||
expect(data.duration).toEqual(createdBooking.duration);
|
||||
expect(data.eventTypeId).toEqual(createdBooking.eventTypeId);
|
||||
expect(data.attendees[0]).toEqual(createdBooking.attendees[0]);
|
||||
expect(data.location).toEqual(createdBooking.location);
|
||||
expect(data.absentHost).toEqual(createdBooking.absentHost);
|
||||
expect(data.metadata).toEqual(createdBooking.metadata);
|
||||
|
||||
// When a booking is rescheduled, a new booking is created and the old booking is cancelled.
|
||||
// We want to make sure the createdAt date of the new booking is between the beforeCreate and afterCreate dates.
|
||||
const createdAtDate = new Date(data.createdAt);
|
||||
expect(createdAtDate.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
|
||||
expect(createdAtDate.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
|
||||
|
||||
rescheduledBooking = data;
|
||||
describe("rescheduling bookings", () => {
|
||||
describe("cant't reschedule cancelled or rescheduled booking booking", () => {
|
||||
it("should not be able to reschedule cancelled booking", async () => {
|
||||
const cancelledBooking = await bookingsRepositoryFixture.create({
|
||||
status: "CANCELLED",
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
|
||||
title: "peer coding lets goo",
|
||||
uid: `cancelled-booking-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
location: "integrations:daily",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "Oldie",
|
||||
email: "oldie@gmail.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "oldie@gmail.com",
|
||||
name: "Oldie",
|
||||
locale: "lv",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${cancelledBooking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error.message).toEqual(
|
||||
`Can't reschedule booking with uid=${cancelledBooking.uid} because it has been cancelled. Please provide uid of a booking that is not cancelled.`
|
||||
);
|
||||
await bookingsRepositoryFixture.deleteById(cancelledBooking.id);
|
||||
});
|
||||
|
||||
it("should not be able to reschedule rescheduled booking", async () => {
|
||||
const rescheduledBooking = await bookingsRepositoryFixture.create({
|
||||
status: "CANCELLED",
|
||||
rescheduled: true,
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
|
||||
title: "peer coding lets goo",
|
||||
uid: `cancelled-booking-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
location: "integrations:daily",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "Oldie",
|
||||
email: "oldie@gmail.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "oldie@gmail.com",
|
||||
name: "Oldie",
|
||||
locale: "lv",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const newBooking = await bookingsRepositoryFixture.create({
|
||||
status: "ACCEPTED",
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
fromReschedule: rescheduledBooking.uid,
|
||||
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
|
||||
title: "peer coding lets goo",
|
||||
uid: `new-booking-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
location: "integrations:daily",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "Oldie",
|
||||
email: "oldie@gmail.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "oldie@gmail.com",
|
||||
name: "Oldie",
|
||||
locale: "lv",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${rescheduledBooking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error.message).toEqual(
|
||||
`Can't reschedule booking with uid=${rescheduledBooking.uid} because it has been cancelled and rescheduled already to booking with uid=${newBooking.uid}. You probably want to reschedule ${newBooking.uid} instead by passing it within the request URL.`
|
||||
);
|
||||
await bookingsRepositoryFixture.deleteById(rescheduledBooking.id);
|
||||
await bookingsRepositoryFixture.deleteById(newBooking.id);
|
||||
});
|
||||
});
|
||||
|
||||
it("should set rescheduled booking status to cancelled", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${createdBooking.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.status).toEqual("cancelled");
|
||||
expect(data.rescheduledToUid).toEqual(rescheduledBooking.uid);
|
||||
});
|
||||
});
|
||||
describe("recurring booking", () => {
|
||||
it("should reschedule recurrence of a recurring booking", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2035, 0, 9, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars again",
|
||||
};
|
||||
|
||||
it("should reschedule recurrence of a recurring booking", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2035, 0, 9, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars again",
|
||||
};
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${createdRecurringBooking[0].uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const data: RecurringBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.hosts[0].username).toEqual(user.username);
|
||||
expect(data.hosts[0].email).toEqual(user.email);
|
||||
expect(data.status).toEqual(createdRecurringBooking[0].status);
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 9, 15, 0, 0)).toISOString());
|
||||
expect(data.duration).toEqual(createdRecurringBooking[0].duration);
|
||||
expect(data.recurringBookingUid).toEqual(createdRecurringBooking[0].recurringBookingUid);
|
||||
expect(data.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId);
|
||||
expect(data.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]);
|
||||
expect(data.location).toEqual(createdRecurringBooking[0].location);
|
||||
expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost);
|
||||
expect(data.metadata).toEqual(createdRecurringBooking[0].metadata);
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${createdRecurringBooking[0].uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const data: RecurringBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.hosts[0].username).toEqual(user.username);
|
||||
expect(data.hosts[0].email).toEqual(user.email);
|
||||
expect(data.status).toEqual(createdRecurringBooking[0].status);
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 9, 15, 0, 0)).toISOString());
|
||||
expect(data.duration).toEqual(createdRecurringBooking[0].duration);
|
||||
expect(data.recurringBookingUid).toEqual(createdRecurringBooking[0].recurringBookingUid);
|
||||
expect(data.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId);
|
||||
expect(data.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]);
|
||||
expect(data.location).toEqual(createdRecurringBooking[0].location);
|
||||
expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost);
|
||||
expect(data.metadata).toEqual(createdRecurringBooking[0].metadata);
|
||||
const oldBooking = await bookingsRepositoryFixture.getByUid(createdRecurringBooking[0].uid);
|
||||
expect(oldBooking).toBeDefined();
|
||||
expect(oldBooking?.status).toEqual("CANCELLED");
|
||||
});
|
||||
});
|
||||
|
||||
const oldBooking = await bookingsRepositoryFixture.getByUid(createdRecurringBooking[0].uid);
|
||||
expect(oldBooking).toBeDefined();
|
||||
expect(oldBooking?.status).toEqual("CANCELLED");
|
||||
});
|
||||
});
|
||||
it("should get recurring booking recurrences after rescheduling one", async () => {
|
||||
const recurringBookingUid = createdRecurringBooking[0].recurringBookingUid;
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${recurringBookingUid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true);
|
||||
|
||||
it("should get recurring booking recurrences after rescheduling one", async () => {
|
||||
const recurringBookingUid = createdRecurringBooking[0].recurringBookingUid;
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${recurringBookingUid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsRecurringBooking(responseBody.data)) {
|
||||
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
|
||||
expect(data.length).toEqual(4);
|
||||
const cancelled = data.find((booking) => booking.status === "cancelled");
|
||||
expect(cancelled).toBeDefined();
|
||||
const rescheduledNew = data.find(
|
||||
(booking) => booking.start === new Date(Date.UTC(2035, 0, 9, 14, 0, 0)).toISOString()
|
||||
);
|
||||
expect(rescheduledNew).toBeDefined();
|
||||
createdRecurringBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected recurring booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
if (responseDataIsRecurringBooking(responseBody.data)) {
|
||||
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
|
||||
expect(data.length).toEqual(4);
|
||||
const cancelled = data.find((booking) => booking.status === "cancelled");
|
||||
expect(cancelled).toBeDefined();
|
||||
const rescheduledNew = data.find(
|
||||
(booking) => booking.start === new Date(Date.UTC(2035, 0, 9, 14, 0, 0)).toISOString()
|
||||
);
|
||||
expect(rescheduledNew).toBeDefined();
|
||||
createdRecurringBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected recurring booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2385,95 +2462,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("rescheduling booking with invalid booking responses", () => {
|
||||
it("should reschedule", async () => {
|
||||
const email = `user-bookings-attendee-${randomString(10)}@gmail.com`;
|
||||
const username = `user-bookings-attendee-${randomString(10)}`;
|
||||
const locale = "it";
|
||||
const timeZone = "Europe/Rome";
|
||||
const attendee = await userRepositoryFixture.create({
|
||||
email,
|
||||
username,
|
||||
locale,
|
||||
timeZone,
|
||||
});
|
||||
|
||||
const booking = await bookingsRepositoryFixture.create({
|
||||
uid: `booking-uid-${randomString(10)}`,
|
||||
title: "booking title",
|
||||
startTime: "2060-09-05T11:00:00.000Z",
|
||||
endTime: "2060-09-05T12:00:00.000Z",
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
status: "ACCEPTED",
|
||||
metadata: {},
|
||||
responses: "null",
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: email,
|
||||
name: username,
|
||||
timeZone,
|
||||
locale,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2060, 0, 9, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${booking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.reschedulingReason).toEqual(body.reschedulingReason);
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2060, 0, 9, 15, 0, 0)).toISOString());
|
||||
expect(data.rescheduledFromUid).toEqual(booking.uid);
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.hosts[0].username).toEqual(user.username);
|
||||
expect(data.hosts[0].email).toEqual(user.email);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.eventTypeId).toEqual(booking.eventTypeId);
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: attendee.username,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
absent: false,
|
||||
});
|
||||
expect(data.bookingFieldsResponses).toEqual({
|
||||
name: attendee.username,
|
||||
email: attendee.email,
|
||||
rescheduledReason: body.reschedulingReason,
|
||||
guests: [],
|
||||
});
|
||||
await bookingsRepositoryFixture.deleteById(data.id);
|
||||
await userRepositoryFixture.delete(attendee.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("create booking with attendee sms reminder", () => {
|
||||
it("should not be able create a booking if attendee sms reminder is missing", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
@@ -2559,146 +2547,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("cant't reschedule cancelled booking", () => {
|
||||
it("should not be able to reschedule cancelled booking", async () => {
|
||||
const cancelledBooking = await bookingsRepositoryFixture.create({
|
||||
status: "CANCELLED",
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
|
||||
title: "peer coding lets goo",
|
||||
uid: `cancelled-booking-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
location: "integrations:daily",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "Oldie",
|
||||
email: "oldie@gmail.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "oldie@gmail.com",
|
||||
name: "Oldie",
|
||||
locale: "lv",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${cancelledBooking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error.message).toEqual(
|
||||
`Can't reschedule booking with uid=${cancelledBooking.uid} because it has been cancelled. Please provide uid of a booking that is not cancelled.`
|
||||
);
|
||||
await bookingsRepositoryFixture.deleteById(cancelledBooking.id);
|
||||
});
|
||||
|
||||
it("should not be able to reschedule rescheduled booking", async () => {
|
||||
const rescheduledBooking = await bookingsRepositoryFixture.create({
|
||||
status: "CANCELLED",
|
||||
rescheduled: true,
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
|
||||
title: "peer coding lets goo",
|
||||
uid: `cancelled-booking-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
location: "integrations:daily",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "Oldie",
|
||||
email: "oldie@gmail.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "oldie@gmail.com",
|
||||
name: "Oldie",
|
||||
locale: "lv",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const newBooking = await bookingsRepositoryFixture.create({
|
||||
status: "ACCEPTED",
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
fromReschedule: rescheduledBooking.uid,
|
||||
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
|
||||
title: "peer coding lets goo",
|
||||
uid: `new-booking-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
location: "integrations:daily",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "Oldie",
|
||||
email: "oldie@gmail.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "oldie@gmail.com",
|
||||
name: "Oldie",
|
||||
locale: "lv",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${rescheduledBooking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error.message).toEqual(
|
||||
`Can't reschedule booking with uid=${rescheduledBooking.uid} because it has been cancelled and rescheduled already to booking with uid=${newBooking.uid}. You probably want to reschedule ${newBooking.uid} instead by passing it within the request URL.`
|
||||
);
|
||||
await bookingsRepositoryFixture.deleteById(rescheduledBooking.id);
|
||||
await bookingsRepositoryFixture.deleteById(newBooking.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cant't cancel already cancelled booking", () => {
|
||||
it("should not be able to cancel alraedy cancelled booking", async () => {
|
||||
const cancelledBooking = await bookingsRepositoryFixture.create({
|
||||
@@ -2752,6 +2600,56 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancellation reason and cancelled by returned if null in database", () => {
|
||||
it("should return cancellation reason and cancelled by if null in database", async () => {
|
||||
const dbBooking = await bookingsRepositoryFixture.create({
|
||||
uid: randomString(10),
|
||||
title: "test",
|
||||
startTime: new Date(Date.UTC(2040, 0, 13, 16, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2040, 0, 13, 17, 0, 0)),
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "tester",
|
||||
email: "tester@example.com",
|
||||
guests: [],
|
||||
},
|
||||
cancellationReason: null,
|
||||
cancelledBy: null,
|
||||
});
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${dbBooking.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.cancellationReason).toBe("");
|
||||
expect(data.cancelledByEmail).toBe("");
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("calendar events", () => {
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
|
||||
@@ -544,12 +544,18 @@ export class InputBookingsService_2024_08_13 {
|
||||
Object.assign(newRequest, { userId, ...oAuthClientParams, platformBookingLocation: location });
|
||||
newRequest.body = {
|
||||
...bodyTransformed,
|
||||
rescheduledBy: request.body.rescheduledBy,
|
||||
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
|
||||
creationSource: CreationSource.API_V2,
|
||||
};
|
||||
} else {
|
||||
Object.assign(newRequest, { userId, platformBookingLocation: location });
|
||||
newRequest.body = { ...bodyTransformed, noEmail: false, creationSource: CreationSource.API_V2 };
|
||||
newRequest.body = {
|
||||
...bodyTransformed,
|
||||
noEmail: false,
|
||||
creationSource: CreationSource.API_V2,
|
||||
rescheduledBy: request.body.rescheduledBy,
|
||||
};
|
||||
}
|
||||
|
||||
return newRequest as unknown as BookingRequest;
|
||||
|
||||
@@ -25,7 +25,6 @@ import type {
|
||||
ReassignBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import type { Booking, BookingSeat } from "@calcom/prisma/client";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
export const bookingResponsesSchema = z
|
||||
.object({
|
||||
@@ -105,14 +104,8 @@ export class OutputBookingsService_2024_08_13 {
|
||||
);
|
||||
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||
const rescheduledToInfo = databaseBooking.rescheduled
|
||||
? await this.getRescheduledToInfo(databaseBooking.uid)
|
||||
: undefined;
|
||||
|
||||
const rescheduledToUid = rescheduledToInfo?.uid;
|
||||
const rescheduledByEmail = databaseBooking.rescheduled
|
||||
? rescheduledToInfo?.rescheduledBy
|
||||
: databaseBooking.rescheduledBy;
|
||||
const rescheduledToUid = await this.getRescheduledToUid(databaseBooking);
|
||||
const rescheduledByEmail = await this.getRescheduledByEmail(databaseBooking);
|
||||
|
||||
const booking = {
|
||||
id: databaseBooking.id,
|
||||
@@ -121,9 +114,8 @@ export class OutputBookingsService_2024_08_13 {
|
||||
description: databaseBooking.description,
|
||||
hosts: [this.getHost(databaseBooking.user)],
|
||||
status: databaseBooking.status.toLowerCase(),
|
||||
cancellationReason:
|
||||
databaseBooking.status === "CANCELLED" ? databaseBooking.cancellationReason : undefined,
|
||||
cancelledByEmail: databaseBooking.status === "CANCELLED" ? databaseBooking.cancelledBy : undefined,
|
||||
cancellationReason: databaseBooking.cancellationReason || "",
|
||||
cancelledByEmail: databaseBooking.cancelledBy || "",
|
||||
reschedulingReason: bookingResponses?.rescheduledReason,
|
||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||
start: databaseBooking.startTime,
|
||||
@@ -168,6 +160,23 @@ export class OutputBookingsService_2024_08_13 {
|
||||
};
|
||||
}
|
||||
|
||||
private async getRescheduledToUid(databaseBooking: DatabaseBooking) {
|
||||
if (!databaseBooking.rescheduled) return undefined;
|
||||
const rescheduledTo = await this.bookingsRepository.getByFromReschedule(databaseBooking.uid);
|
||||
return rescheduledTo?.uid;
|
||||
}
|
||||
|
||||
private async getRescheduledByEmail(databaseBooking: DatabaseBooking) {
|
||||
if (databaseBooking.rescheduled) {
|
||||
return databaseBooking.rescheduledBy;
|
||||
}
|
||||
if (databaseBooking.fromReschedule) {
|
||||
const previousBooking = await this.bookingsRepository.getByUid(databaseBooking.fromReschedule);
|
||||
return previousBooking?.rescheduledBy ?? databaseBooking.rescheduledBy;
|
||||
}
|
||||
return databaseBooking.rescheduledBy;
|
||||
}
|
||||
|
||||
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
|
||||
if (databaseMetadata === null) return {};
|
||||
|
||||
@@ -227,10 +236,8 @@ export class OutputBookingsService_2024_08_13 {
|
||||
description: databaseBooking.description,
|
||||
hosts: [this.getHost(databaseBooking.user)],
|
||||
status: databaseBooking.status.toLowerCase(),
|
||||
cancellationReason:
|
||||
databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancellationReason : undefined,
|
||||
cancelledByEmail:
|
||||
databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancelledBy : undefined,
|
||||
cancellationReason: databaseBooking.cancellationReason || "",
|
||||
cancelledByEmail: databaseBooking.cancelledBy || "",
|
||||
reschedulingReason: bookingResponses?.rescheduledReason,
|
||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||
start: databaseBooking.startTime,
|
||||
@@ -284,14 +291,8 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||
const rescheduledToInfo = databaseBooking.rescheduled
|
||||
? await this.getRescheduledToInfo(databaseBooking.uid)
|
||||
: undefined;
|
||||
|
||||
const rescheduledToUid = rescheduledToInfo?.uid;
|
||||
const rescheduledByEmail = databaseBooking.rescheduled
|
||||
? rescheduledToInfo?.rescheduledBy
|
||||
: databaseBooking.rescheduledBy;
|
||||
const rescheduledToUid = await this.getRescheduledToUid(databaseBooking);
|
||||
const rescheduledByEmail = await this.getRescheduledByEmail(databaseBooking);
|
||||
|
||||
const booking = {
|
||||
id: databaseBooking.id,
|
||||
@@ -300,8 +301,7 @@ export class OutputBookingsService_2024_08_13 {
|
||||
description: databaseBooking.description,
|
||||
hosts: [this.getHost(databaseBooking.user)],
|
||||
status: databaseBooking.status.toLowerCase(),
|
||||
cancellationReason:
|
||||
databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancellationReason : undefined,
|
||||
cancellationReason: databaseBooking.cancellationReason || "",
|
||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||
start: databaseBooking.startTime,
|
||||
end: databaseBooking.endTime,
|
||||
@@ -428,8 +428,7 @@ export class OutputBookingsService_2024_08_13 {
|
||||
description: databaseBooking.description,
|
||||
hosts: [this.getHost(databaseBooking.user)],
|
||||
status: databaseBooking.status.toLowerCase(),
|
||||
cancellationReason:
|
||||
databaseBooking.status === BookingStatus.CANCELLED ? databaseBooking.cancellationReason : undefined,
|
||||
cancellationReason: databaseBooking.cancellationReason || "",
|
||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||
start: databaseBooking.startTime,
|
||||
end: databaseBooking.endTime,
|
||||
|
||||
@@ -263,6 +263,10 @@ export class EventTypesService_2024_06_14 {
|
||||
return [dynamicEventType];
|
||||
}
|
||||
|
||||
if (authUser?.id) {
|
||||
return await this.getUserEventTypes(authUser.id);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,8 @@ describe("Me Endpoints", () => {
|
||||
expect(responseBody.data.id).toEqual(user.id);
|
||||
expect(responseBody.data.email).toEqual(user.email);
|
||||
expect(responseBody.data.name).toEqual(user.name);
|
||||
expect(responseBody.data.avatarUrl).toEqual(user.avatarUrl);
|
||||
expect(responseBody.data.bio).toEqual(user.bio);
|
||||
expect(responseBody.data.timeFormat).toEqual(user.timeFormat);
|
||||
expect(responseBody.data.defaultScheduleId).toEqual(user.defaultScheduleId);
|
||||
expect(responseBody.data.weekStart).toEqual(user.weekStart);
|
||||
@@ -117,6 +119,8 @@ describe("Me Endpoints", () => {
|
||||
|
||||
expect(responseBody.data.id).toEqual(user.id);
|
||||
expect(responseBody.data.email).toEqual(user.email);
|
||||
expect(responseBody.data.avatarUrl).toEqual(user.avatarUrl);
|
||||
expect(responseBody.data.bio).toEqual(user.bio);
|
||||
expect(responseBody.data.timeFormat).toEqual(user.timeFormat);
|
||||
expect(responseBody.data.defaultScheduleId).toEqual(user.defaultScheduleId);
|
||||
expect(responseBody.data.weekStart).toEqual(user.weekStart);
|
||||
@@ -151,13 +155,13 @@ describe("Me Endpoints", () => {
|
||||
});
|
||||
|
||||
it("should not update user associated with access token given invalid time format", async () => {
|
||||
const bodyWithIncorrectTimeFormat: UpdateManagedUserInput = { timeFormat: 100 as any };
|
||||
const bodyWithIncorrectTimeFormat = { timeFormat: 100 };
|
||||
|
||||
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectTimeFormat).expect(400);
|
||||
});
|
||||
|
||||
it("should not update user associated with access token given invalid week start", async () => {
|
||||
const bodyWithIncorrectWeekStart: UpdateManagedUserInput = { weekStart: "waba luba dub dub" as any };
|
||||
const bodyWithIncorrectWeekStart = { weekStart: "waba luba dub dub" };
|
||||
|
||||
return request(app.getHttpServer()).patch("/v2/me").send(bodyWithIncorrectWeekStart).expect(400);
|
||||
});
|
||||
|
||||
@@ -27,6 +27,14 @@ export class MeOutput {
|
||||
@DocsProperty({ type: String, nullable: true })
|
||||
name!: string | null;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ type: String, nullable: true })
|
||||
avatarUrl!: string | null;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ type: String, nullable: true })
|
||||
bio!: string | null;
|
||||
|
||||
@IsInt()
|
||||
@DocsProperty()
|
||||
timeFormat!: number;
|
||||
|
||||
@@ -10,7 +10,6 @@ import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository"
|
||||
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||
import { AvailableSlotsService } from "@/lib/services/available-slots.service";
|
||||
import { BusyTimesService } from "@/lib/services/busy-times.service";
|
||||
import { CacheService } from "@/lib/services/cache.service";
|
||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||
import { FilterHostsService } from "@/lib/services/filter-hosts.service";
|
||||
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
|
||||
@@ -35,7 +34,6 @@ import { Module } from "@nestjs/common";
|
||||
PrismaFeaturesRepository,
|
||||
PrismaMembershipRepository,
|
||||
CheckBookingLimitsService,
|
||||
CacheService,
|
||||
AvailableSlotsService,
|
||||
UserAvailabilityService,
|
||||
BusyTimesService,
|
||||
|
||||
@@ -4,7 +4,6 @@ import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.rep
|
||||
import { PrismaHostRepository } from "@/lib/repositories/prisma-host.repository";
|
||||
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
|
||||
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||
import { CacheService } from "@/lib/services/cache.service";
|
||||
import { CheckBookingAndDurationLimitsService } from "@/lib/services/check-booking-and-duration-limits.service";
|
||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||
import { HashedLinkService } from "@/lib/services/hashed-link.service";
|
||||
@@ -22,7 +21,6 @@ import { Module } from "@nestjs/common";
|
||||
PrismaHostRepository,
|
||||
PrismaOOORepository,
|
||||
PrismaUserRepository,
|
||||
CacheService,
|
||||
CheckBookingAndDurationLimitsService,
|
||||
CheckBookingLimitsService,
|
||||
HashedLinkService,
|
||||
|
||||
@@ -8,7 +8,6 @@ import { PrismaSelectedSlotRepository } from "@/lib/repositories/prisma-selected
|
||||
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
|
||||
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||
import { BusyTimesService } from "@/lib/services/busy-times.service";
|
||||
import { CacheService } from "@/lib/services/cache.service";
|
||||
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
|
||||
import { NoSlotsNotificationService } from "@/lib/services/no-slots-notification.service";
|
||||
import { QualifiedHostsService } from "@/lib/services/qualified-hosts.service";
|
||||
@@ -34,7 +33,6 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
||||
featuresRepository: PrismaFeaturesRepository,
|
||||
qualifiedHostsService: QualifiedHostsService,
|
||||
checkBookingLimitsService: CheckBookingLimitsService,
|
||||
cacheService: CacheService,
|
||||
userAvailabilityService: UserAvailabilityService,
|
||||
busyTimesService: BusyTimesService,
|
||||
noSlotsNotificationService: NoSlotsNotificationService
|
||||
@@ -50,7 +48,6 @@ export class AvailableSlotsService extends BaseAvailableSlotsService {
|
||||
userRepo: userRepository,
|
||||
redisClient: redisService,
|
||||
checkBookingLimitsService,
|
||||
cacheService,
|
||||
userAvailabilityService,
|
||||
busyTimesService,
|
||||
qualifiedHostsService,
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { CacheService as BaseCacheService } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class CacheService extends BaseCacheService {
|
||||
constructor(featuresRepository: PrismaFeaturesRepository) {
|
||||
super({ featuresRepository });
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
|
||||
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
|
||||
import { CacheService } from "@/lib/services/cache.service";
|
||||
import { CheckBookingAndDurationLimitsService } from "@/lib/services/check-booking-and-duration-limits.service";
|
||||
import { HashedLinkService } from "@/lib/services/hashed-link.service";
|
||||
import { LuckyUserService } from "@/lib/services/lucky-user.service";
|
||||
@@ -13,7 +12,6 @@ import type { PrismaClient } from "@calcom/prisma";
|
||||
@Injectable()
|
||||
export class RegularBookingService extends BaseRegularBookingService {
|
||||
constructor(
|
||||
cacheService: CacheService,
|
||||
checkBookingAndDurationLimitsService: CheckBookingAndDurationLimitsService,
|
||||
prismaWriteService: PrismaWriteService,
|
||||
bookingRepository: PrismaBookingRepository,
|
||||
@@ -22,7 +20,6 @@ export class RegularBookingService extends BaseRegularBookingService {
|
||||
userRepository: PrismaUserRepository
|
||||
) {
|
||||
super({
|
||||
cacheService,
|
||||
checkBookingAndDurationLimitsService,
|
||||
prismaClient: prismaWriteService.prisma as unknown as PrismaClient,
|
||||
bookingRepository,
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# Source maps
|
||||
*.source/*
|
||||
@@ -1,36 +0,0 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
@@ -1,9 +0,0 @@
|
||||
import { HomeLayout } from "fumadocs-ui/layouts/home";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { baseOptions } from "../layout.config";
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
// @ts-expect-error weird type check
|
||||
return <HomeLayout {...baseOptions}>{children}</HomeLayout>;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="font-cal -mt-16 flex min-h-screen w-screen items-center justify-center border pt-16 text-center ">
|
||||
<div>
|
||||
<h1 className="text-7xl leading-none md:text-8xl">
|
||||
Open UI for absolutely
|
||||
<br /> everyone
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { componentSource } from "@/app/source";
|
||||
import { createSearchAPI } from "fumadocs-core/search/server";
|
||||
|
||||
const indexes = [componentSource].flatMap((src) =>
|
||||
src.getPages().map((page) => ({
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
structuredData: page.data.structuredData,
|
||||
id: page.url,
|
||||
url: page.url,
|
||||
}))
|
||||
);
|
||||
|
||||
export const { GET } = createSearchAPI("advanced", {
|
||||
indexes,
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
};
|
||||
export const Icon: React.FC<PropsWithChildren<Props>> = (props) => {
|
||||
return (
|
||||
<div className="text-gray-12 flex flex-col items-center justify-center gap-4">
|
||||
<div className="size-12 border-gray-5 bg-gray-3 flex aspect-square items-center justify-center rounded-lg border ">
|
||||
{props.children}
|
||||
</div>
|
||||
<span className="font-mono text-sm">
|
||||
{"<"}
|
||||
{props.name}
|
||||
{"/>"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Icon.displayName = "Icon";
|
||||
@@ -1,57 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { type PropsWithChildren, useState } from "react";
|
||||
|
||||
import className from "@calcom/ui/classNames";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
|
||||
type Props = {
|
||||
customCodeSnippet?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const RenderComponentWithSnippet: React.FC<PropsWithChildren<Props>> = (props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
// const snippet =
|
||||
// props.customCodeSnippet ??
|
||||
// reactElementToJSXString(props.children, {
|
||||
// showFunctions: true,
|
||||
// useBooleanShorthandSyntax: true,
|
||||
|
||||
// displayName: (node) => {
|
||||
// // @ts-ignore
|
||||
// return node?.type.displayName ?? node?.type ?? "Unknown";
|
||||
// },
|
||||
// });
|
||||
return (
|
||||
<div className="border-gray-6 bg-default rounded-lg border">
|
||||
<div className={className("not-prose p-8 xl:p-12", props.className)}>{props.children}</div>
|
||||
|
||||
<div className="bg-gray-3 border-gray-6 flex items-center justify-start border-b border-t p-2">
|
||||
<Button
|
||||
color="minimal"
|
||||
onClick={() => {
|
||||
setOpen(!open);
|
||||
}}>
|
||||
Code (WIP)
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={className(
|
||||
"bg-gray-2 max-h-96 w-full overflow-y-scroll transition-all",
|
||||
open ? "block" : "hidden"
|
||||
)}>
|
||||
<div className="flex items-start">
|
||||
{/* <pre className="py-0 text-gray-8 text-right">
|
||||
{snippet
|
||||
.split("\n")
|
||||
.map((_line, i) => i + 1)
|
||||
.join("\n")}
|
||||
</pre>
|
||||
<pre className="py-0 text-gray-12 w-full">{snippet}</pre> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
export const Row: React.FC<PropsWithChildren> = (props) => {
|
||||
return <div className="flex w-full items-center justify-between gap-8">{props.children}</div>;
|
||||
};
|
||||
|
||||
Row.displayName = "Row";
|
||||
@@ -1,56 +0,0 @@
|
||||
import { componentSource } from "@/app/source";
|
||||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
import { createTypeTable } from "fumadocs-typescript/ui";
|
||||
import defaultMdxComponents from "fumadocs-ui/mdx";
|
||||
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/page";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
export default async function Page(props: { params: Promise<{ slug?: string[] }> }) {
|
||||
const params = await props.params;
|
||||
|
||||
const page = componentSource.getPage(params.slug);
|
||||
|
||||
if (!page) {
|
||||
notFound();
|
||||
}
|
||||
const { AutoTypeTable } = createTypeTable();
|
||||
|
||||
const MDX = page.data.body;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* @ts-expect-error idk why fumadocs doersnt like this */}
|
||||
<DocsPage toc={page.data.toc} full={page.data.full}>
|
||||
<Toaster />
|
||||
<DocsTitle>{page.data.title}</DocsTitle>
|
||||
|
||||
<DocsDescription>{page.data.description}</DocsDescription>
|
||||
|
||||
<DocsBody>
|
||||
<TooltipProvider>
|
||||
{/* @ts-expect-error idk why fumadocs doersnt like this */}
|
||||
<MDX components={{ ...defaultMdxComponents, AutoTypeTable }} />
|
||||
</TooltipProvider>
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return componentSource.generateParams();
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ slug?: string[] }> }) {
|
||||
const page = componentSource.getPage((await params).slug);
|
||||
if (!page) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
} satisfies Metadata;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { componentSource } from "@/app/source";
|
||||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
import { DocsLayout } from "fumadocs-ui/layouts/notebook";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { IconSprites } from "@calcom/ui/components/icon";
|
||||
|
||||
import { baseOptions } from "../layout.config";
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<DocsLayout tree={componentSource.pageTree} {...baseOptions}>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
<IconSprites />
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,26 +0,0 @@
|
||||
import type { HomeLayoutProps } from "fumadocs-ui/layouts/home";
|
||||
|
||||
/**
|
||||
* Shared layout configurations
|
||||
*
|
||||
* you can configure layouts individually from:
|
||||
* Home Layout: app/(home)/layout.tsx
|
||||
* Docs Layout: app/docs/layout.tsx
|
||||
*/
|
||||
export const baseOptions: HomeLayoutProps = {
|
||||
nav: {
|
||||
title: "Cal.com",
|
||||
},
|
||||
githubUrl: "https://github.com/calcom/cal.com",
|
||||
links: [
|
||||
{
|
||||
text: "Design",
|
||||
url: "/design",
|
||||
active: "nested-url",
|
||||
},
|
||||
{
|
||||
text: "GitHub",
|
||||
url: "https://github.com/calcom/cal.com",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import { RootProvider } from "fumadocs-ui/provider";
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
|
||||
import "./global.css";
|
||||
|
||||
const inter = Inter({
|
||||
variable: "--font-inter",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<style>{`
|
||||
:root {
|
||||
--font-inter: ${inter.style.fontFamily.replace(/\'/g, "")};
|
||||
}
|
||||
`}</style>
|
||||
</head>
|
||||
<body className={`${inter.variable} antialiased`}>
|
||||
<RootProvider>{children}</RootProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { components, docs, meta } from "@/.source";
|
||||
import { loader } from "fumadocs-core/source";
|
||||
import { createMDXSource } from "fumadocs-mdx";
|
||||
|
||||
export const source = loader({
|
||||
baseUrl: "/docs",
|
||||
source: createMDXSource(docs, meta),
|
||||
});
|
||||
|
||||
export const componentSource = loader({
|
||||
baseUrl: "/design",
|
||||
source: createMDXSource(components, []),
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { Status } from "./Status";
|
||||
|
||||
export function MDXLayout({ children, frontmatter }: { children: React.ReactNode; frontmatter: any }) {
|
||||
return (
|
||||
<div className="prose max-w-none">
|
||||
<Toaster />
|
||||
{frontmatter.airtableId && (
|
||||
<Status
|
||||
airtableId={frontmatter.airtableId}
|
||||
designStatus={frontmatter.designStatus}
|
||||
devStatus={frontmatter.devStatus}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
interface BorderRadiusToken {
|
||||
name: string;
|
||||
value: string;
|
||||
className: string;
|
||||
}
|
||||
|
||||
interface BorderRadiusTableProps {
|
||||
tokens: BorderRadiusToken[];
|
||||
}
|
||||
|
||||
const tokens: BorderRadiusToken[] = [
|
||||
{ name: "None", value: "0px", className: "rounded-none" },
|
||||
{ name: "Small", value: "0.125rem", className: "rounded-sm" },
|
||||
{ name: "Default", value: "0.25rem", className: "rounded" },
|
||||
{ name: "Medium", value: "0.375rem", className: "rounded-md" },
|
||||
{ name: "Large", value: "0.5rem", className: "rounded-lg" },
|
||||
{ name: "XLarge", value: "0.75rem", className: "rounded-xl" },
|
||||
{ name: "2XLarge", value: "1rem", className: "rounded-2xl" },
|
||||
{ name: "3XLarge", value: "1.5rem", className: "rounded-3xl" },
|
||||
{ name: "Full", value: "9999px", className: "rounded-full" },
|
||||
];
|
||||
|
||||
export const BorderRadiusTable: React.FC<BorderRadiusTableProps> = () => {
|
||||
const handleCopy = async (value: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
showToast(`Copied ${value}`, "success");
|
||||
} catch (error) {
|
||||
console.error("Failed to copy:", error);
|
||||
showToast("Failed to copy", "error");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toaster position="bottom-right" />
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{tokens.map((token) => (
|
||||
<div
|
||||
key={token.name}
|
||||
onClick={() => handleCopy(token.className)}
|
||||
className="border-subtle bg-default hover:bg-subtle group relative cursor-pointer overflow-hidden rounded-lg border p-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-emphasis text-sm font-medium">{token.name}</p>
|
||||
<p className="text-subtle mt-1 text-xs">{token.className}</p>
|
||||
</div>
|
||||
<p className="text-subtle text-sm">{token.value}</p>
|
||||
</div>
|
||||
<div className={`bg-emphasis h-16 w-full ${token.className}`} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,372 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useMemo } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
type IconName =
|
||||
| "activity"
|
||||
| "arrow-down"
|
||||
| "arrow-left"
|
||||
| "arrow-right"
|
||||
| "arrow-up-right"
|
||||
| "arrow-up"
|
||||
| "asterisk"
|
||||
| "at-sign"
|
||||
| "atom"
|
||||
| "badge-check"
|
||||
| "ban"
|
||||
| "bell"
|
||||
| "binary"
|
||||
| "blocks"
|
||||
| "bold"
|
||||
| "book"
|
||||
| "book-open-check"
|
||||
| "book-open"
|
||||
| "book-user"
|
||||
| "building"
|
||||
| "calendar-check-2"
|
||||
| "calendar-days"
|
||||
| "calendar-heart"
|
||||
| "calendar-range"
|
||||
| "calendar-search"
|
||||
| "calendar-x-2"
|
||||
| "calendar"
|
||||
| "chart-bar"
|
||||
| "chart-line"
|
||||
| "check-check"
|
||||
| "check"
|
||||
| "chevron-down"
|
||||
| "chevron-left"
|
||||
| "chevron-right"
|
||||
| "chevron-up"
|
||||
| "chevrons-down-up"
|
||||
| "chevrons-left"
|
||||
| "chevrons-right"
|
||||
| "chevrons-up-down"
|
||||
| "circle-alert"
|
||||
| "circle-arrow-up"
|
||||
| "circle-check-big"
|
||||
| "circle-check"
|
||||
| "circle-help"
|
||||
| "circle-x"
|
||||
| "circle"
|
||||
| "clipboard-check"
|
||||
| "clipboard"
|
||||
| "clock"
|
||||
| "code"
|
||||
| "columns-3"
|
||||
| "command"
|
||||
| "contact"
|
||||
| "copy"
|
||||
| "corner-down-left"
|
||||
| "corner-down-right"
|
||||
| "credit-card"
|
||||
| "disc"
|
||||
| "dot"
|
||||
| "download"
|
||||
| "ellipsis-vertical"
|
||||
| "ellipsis"
|
||||
| "external-link"
|
||||
| "eye-off"
|
||||
| "eye"
|
||||
| "file-down"
|
||||
| "file-text"
|
||||
| "file"
|
||||
| "filter"
|
||||
| "fingerprint"
|
||||
| "flag"
|
||||
| "folder"
|
||||
| "gift"
|
||||
| "git-merge"
|
||||
| "github"
|
||||
| "globe"
|
||||
| "grid-3x3"
|
||||
| "handshake"
|
||||
| "info"
|
||||
| "italic"
|
||||
| "key"
|
||||
| "layers"
|
||||
| "layout-dashboard"
|
||||
| "link-2"
|
||||
| "link"
|
||||
| "loader"
|
||||
| "lock-open"
|
||||
| "lock"
|
||||
| "log-out"
|
||||
| "mail-open"
|
||||
| "mail"
|
||||
| "map-pin"
|
||||
| "map"
|
||||
| "menu"
|
||||
| "message-circle"
|
||||
| "messages-square"
|
||||
| "moon"
|
||||
| "paintbrush"
|
||||
| "paperclip"
|
||||
| "pencil"
|
||||
| "phone-call"
|
||||
| "phone"
|
||||
| "plus"
|
||||
| "refresh-ccw"
|
||||
| "refresh-cw"
|
||||
| "repeat"
|
||||
| "rocket"
|
||||
| "rotate-ccw"
|
||||
| "rotate-cw"
|
||||
| "search"
|
||||
| "send"
|
||||
| "settings"
|
||||
| "share-2"
|
||||
| "shield-check"
|
||||
| "shield"
|
||||
| "shuffle"
|
||||
| "sliders-horizontal"
|
||||
| "sliders-vertical"
|
||||
| "smartphone"
|
||||
| "sparkles"
|
||||
| "square-check"
|
||||
| "star"
|
||||
| "sun"
|
||||
| "sunrise"
|
||||
| "sunset"
|
||||
| "tags"
|
||||
| "terminal"
|
||||
| "trash-2"
|
||||
| "trash"
|
||||
| "trello"
|
||||
| "triangle-alert"
|
||||
| "upload"
|
||||
| "user-check"
|
||||
| "user-plus"
|
||||
| "user-x"
|
||||
| "user"
|
||||
| "users"
|
||||
| "venetian-mask"
|
||||
| "video"
|
||||
| "waypoints"
|
||||
| "webhook"
|
||||
| "x"
|
||||
| "zap";
|
||||
|
||||
interface IconGridProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface CopyMenuProps {
|
||||
name: IconName;
|
||||
onCopy: (format: string) => void;
|
||||
}
|
||||
|
||||
const CopyMenu: React.FC<CopyMenuProps> = ({ name, onCopy }) => {
|
||||
return (
|
||||
<div className="absolute inset-2 flex items-center justify-center bg-black/10 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<div className="bg-default space-y-1 rounded-md p-2 shadow-lg">
|
||||
<button
|
||||
onClick={() => onCopy(`<Icon name="${name}" className="h-4 w-4" />`)}
|
||||
className="hover:bg-subtle w-full rounded px-3 py-1 text-left text-sm">
|
||||
Copy Component
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const IconGrid: React.FC<IconGridProps> = ({ className }) => {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const handleCopy = async (value: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
showToast(`Copied ${value}`, "success");
|
||||
} catch (error) {
|
||||
console.error("Failed to copy:", error);
|
||||
showToast("Failed to copy", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const icons: IconName[] = [
|
||||
"activity",
|
||||
"arrow-down",
|
||||
"arrow-left",
|
||||
"arrow-right",
|
||||
"arrow-up-right",
|
||||
"arrow-up",
|
||||
"asterisk",
|
||||
"at-sign",
|
||||
"atom",
|
||||
"badge-check",
|
||||
"ban",
|
||||
"bell",
|
||||
"binary",
|
||||
"blocks",
|
||||
"bold",
|
||||
"book-open-check",
|
||||
"book-open",
|
||||
"book-user",
|
||||
"building",
|
||||
"calendar-check-2",
|
||||
"calendar-days",
|
||||
"calendar-heart",
|
||||
"calendar-range",
|
||||
"calendar-search",
|
||||
"calendar-x-2",
|
||||
"calendar",
|
||||
"chart-bar",
|
||||
"chart-line",
|
||||
"check-check",
|
||||
"check",
|
||||
"chevron-down",
|
||||
"chevron-left",
|
||||
"chevron-right",
|
||||
"chevron-up",
|
||||
"chevrons-down-up",
|
||||
"chevrons-left",
|
||||
"chevrons-right",
|
||||
"chevrons-up-down",
|
||||
"circle-alert",
|
||||
"circle-arrow-up",
|
||||
"circle-check-big",
|
||||
"circle-check",
|
||||
"circle-help",
|
||||
"circle-x",
|
||||
"circle",
|
||||
"clipboard-check",
|
||||
"clipboard",
|
||||
"clock",
|
||||
"code",
|
||||
"columns-3",
|
||||
"command",
|
||||
"contact",
|
||||
"copy",
|
||||
"corner-down-left",
|
||||
"corner-down-right",
|
||||
"credit-card",
|
||||
"disc",
|
||||
"dot",
|
||||
"download",
|
||||
"ellipsis-vertical",
|
||||
"ellipsis",
|
||||
"external-link",
|
||||
"eye-off",
|
||||
"eye",
|
||||
"file-down",
|
||||
"file-text",
|
||||
"file",
|
||||
"filter",
|
||||
"fingerprint",
|
||||
"flag",
|
||||
"folder",
|
||||
"gift",
|
||||
"git-merge",
|
||||
"github",
|
||||
"globe",
|
||||
"grid-3x3",
|
||||
"handshake",
|
||||
"info",
|
||||
"italic",
|
||||
"key",
|
||||
"layers",
|
||||
"layout-dashboard",
|
||||
"link-2",
|
||||
"link",
|
||||
"loader",
|
||||
"lock-open",
|
||||
"lock",
|
||||
"log-out",
|
||||
"mail-open",
|
||||
"mail",
|
||||
"map-pin",
|
||||
"map",
|
||||
"menu",
|
||||
"message-circle",
|
||||
"messages-square",
|
||||
"moon",
|
||||
"paintbrush",
|
||||
"paperclip",
|
||||
"pencil",
|
||||
"phone-call",
|
||||
"phone",
|
||||
"plus",
|
||||
"refresh-ccw",
|
||||
"refresh-cw",
|
||||
"repeat",
|
||||
"rocket",
|
||||
"rotate-ccw",
|
||||
"rotate-cw",
|
||||
"search",
|
||||
"send",
|
||||
"settings",
|
||||
"share-2",
|
||||
"shield-check",
|
||||
"shield",
|
||||
"shuffle",
|
||||
"sliders-horizontal",
|
||||
"sliders-vertical",
|
||||
"smartphone",
|
||||
"sparkles",
|
||||
"square-check",
|
||||
"star",
|
||||
"sun",
|
||||
"sunrise",
|
||||
"sunset",
|
||||
"tags",
|
||||
"terminal",
|
||||
"trash-2",
|
||||
"trash",
|
||||
"trello",
|
||||
"triangle-alert",
|
||||
"upload",
|
||||
"user-check",
|
||||
"user-plus",
|
||||
"user-x",
|
||||
"user",
|
||||
"users",
|
||||
"venetian-mask",
|
||||
"video",
|
||||
"waypoints",
|
||||
"webhook",
|
||||
"x",
|
||||
"zap",
|
||||
];
|
||||
|
||||
const filteredIcons = useMemo(() => {
|
||||
if (!searchQuery) return icons;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return icons.filter((icon) => icon.toLowerCase().includes(query));
|
||||
}, [searchQuery]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toaster position="bottom-right" />
|
||||
<div className={className}>
|
||||
<div className="mb-8">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search icons..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="border-subtle bg-default text-emphasis focus:ring-emphasis placeholder:text-subtle w-full rounded-md border px-4 py-2 focus:outline-none focus:ring-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6">
|
||||
{filteredIcons.map((iconName) => (
|
||||
<div
|
||||
key={iconName}
|
||||
className="border-subtle bg-default group relative overflow-hidden rounded-lg border p-4">
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<div className="bg-subtle flex h-12 w-12 items-center justify-center rounded-md">
|
||||
<Icon name={iconName} className="text-emphasis h-6 w-6" />
|
||||
</div>
|
||||
<p className="text-emphasis text-center text-sm">{iconName}</p>
|
||||
</div>
|
||||
<CopyMenu name={iconName} onCopy={handleCopy} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,126 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
interface ShadowToken {
|
||||
name: string;
|
||||
className: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface ShadowTableProps {
|
||||
tokens: ShadowToken[];
|
||||
}
|
||||
|
||||
const tokens: ShadowToken[] = [
|
||||
{
|
||||
name: "Dropdown",
|
||||
className: "shadow-dropdown",
|
||||
description: "Used for dropdown menus and popovers",
|
||||
},
|
||||
{
|
||||
name: "Solid Gray Rested",
|
||||
className: "shadow-solid-gray-rested",
|
||||
description: "Default state for solid gray buttons",
|
||||
},
|
||||
{
|
||||
name: "Solid Gray Hover",
|
||||
className: "shadow-solid-gray-hover",
|
||||
description: "Hover state for solid gray buttons",
|
||||
},
|
||||
{
|
||||
name: "Solid Gray Active",
|
||||
className: "shadow-solid-gray-active",
|
||||
description: "Active/pressed state for solid gray buttons",
|
||||
},
|
||||
{
|
||||
name: "Outline Gray Rested",
|
||||
className: "shadow-outline-gray-rested",
|
||||
description: "Default state for outline gray buttons",
|
||||
},
|
||||
{
|
||||
name: "Outline Gray Hover",
|
||||
className: "shadow-outline-gray-hover",
|
||||
description: "Hover state for outline gray buttons",
|
||||
},
|
||||
{
|
||||
name: "Outline Gray Active",
|
||||
className: "shadow-outline-gray-active",
|
||||
description: "Active/pressed state for outline gray buttons",
|
||||
},
|
||||
{
|
||||
name: "Outline Red Rested",
|
||||
className: "shadow-outline-red-rested",
|
||||
description: "Default state for outline red buttons",
|
||||
},
|
||||
{
|
||||
name: "Outline Red Hover",
|
||||
className: "shadow-outline-red-hover",
|
||||
description: "Hover state for outline red buttons",
|
||||
},
|
||||
{
|
||||
name: "Outline Red Active",
|
||||
className: "shadow-outline-red-active",
|
||||
description: "Active/pressed state for outline red buttons",
|
||||
},
|
||||
{
|
||||
name: "Elevation Low",
|
||||
className: "shadow-elevation-low",
|
||||
description: "Subtle elevation for UI elements",
|
||||
},
|
||||
{
|
||||
name: "Button Solid Brand Default",
|
||||
className: "shadow-button-solid-brand-default",
|
||||
description: "Default state for solid brand buttons",
|
||||
},
|
||||
{
|
||||
name: "Button Solid Brand Hover",
|
||||
className: "shadow-button-solid-brand-hover",
|
||||
description: "Hover state for solid brand buttons",
|
||||
},
|
||||
{
|
||||
name: "Button Solid Brand Active",
|
||||
className: "shadow-button-solid-brand-active",
|
||||
description: "Active/pressed state for solid brand buttons",
|
||||
},
|
||||
];
|
||||
|
||||
export const ShadowTable: React.FC<ShadowTableProps> = () => {
|
||||
const handleCopy = async (value: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
showToast(`Copied ${value}`, "success");
|
||||
} catch (error) {
|
||||
console.error("Failed to copy:", error);
|
||||
showToast("Failed to copy", "error");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toaster position="bottom-right" />
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
{tokens.map((token) => (
|
||||
<div
|
||||
key={token.name}
|
||||
onClick={() => handleCopy(token.className)}
|
||||
className="group relative cursor-pointer">
|
||||
<div
|
||||
className={`border-subtle bg-default hover:bg-subtle relative overflow-hidden rounded-lg border p-4 transition-colors ${token.className}`}>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<p className="text-emphasis text-sm font-medium">{token.name}</p>
|
||||
<p className="text-subtle mt-1 text-xs">{token.className}</p>
|
||||
</div>
|
||||
<p className="text-subtle text-sm">{token.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
interface SpacingToken {
|
||||
name: string;
|
||||
value: string;
|
||||
pixels: string;
|
||||
}
|
||||
|
||||
interface SpacingTableProps {
|
||||
tokens: SpacingToken[];
|
||||
}
|
||||
|
||||
export const SpacingTable: React.FC<SpacingTableProps> = ({ tokens }) => {
|
||||
return (
|
||||
<>
|
||||
<Toaster position="bottom-right" />
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{tokens.map((token, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border-subtle bg-default group relative overflow-hidden rounded-lg border">
|
||||
<div className="flex items-center gap-4 p-4">
|
||||
<div className="w-[100px] space-y-1">
|
||||
<p className="text-emphasis text-sm font-medium">
|
||||
{token.name === "px" ? "px" : `${token.name} (${token.pixels})`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-subtle relative h-8 rounded">
|
||||
<div
|
||||
className="absolute left-0 top-1/2 h-2 -translate-y-1/2 bg-red-500"
|
||||
style={{ width: token.pixels }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,103 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { getColorFormats, copyToClipboard } from "@/lib/colorUtils";
|
||||
import React from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
interface ColorSwatch {
|
||||
name: string;
|
||||
value: string;
|
||||
className: string;
|
||||
}
|
||||
|
||||
interface ColorSection {
|
||||
title: string;
|
||||
description?: string;
|
||||
colors: ColorSwatch[];
|
||||
}
|
||||
|
||||
interface TypeTableProps {
|
||||
sections: ColorSection[];
|
||||
}
|
||||
|
||||
interface CopyMenuProps {
|
||||
color: string;
|
||||
className: string;
|
||||
onCopy: (format: string) => void;
|
||||
}
|
||||
|
||||
const CopyMenu: React.FC<CopyMenuProps> = ({ color, className, onCopy }) => {
|
||||
const formats = getColorFormats(color, className);
|
||||
if (!formats) return null;
|
||||
|
||||
return (
|
||||
<div className="absolute inset-2 flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<div className="bg-default space-y-1 rounded-md p-2 shadow-lg">
|
||||
<button
|
||||
onClick={() => onCopy(formats.hex)}
|
||||
className="hover:bg-subtle w-full rounded px-3 py-1 text-left text-sm">
|
||||
Copy as HEX
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onCopy(formats.rgb)}
|
||||
className="hover:bg-subtle w-full rounded px-3 py-1 text-left text-sm">
|
||||
Copy as RGB
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onCopy(formats.hsl)}
|
||||
className="hover:bg-subtle w-full rounded px-3 py-1 text-left text-sm">
|
||||
Copy as HSL
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onCopy(formats.tailwind)}
|
||||
className="hover:bg-subtle w-full rounded px-3 py-1 text-left text-sm">
|
||||
Copy Tailwind Class
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const TypeTable: React.FC<TypeTableProps> = ({ sections }) => {
|
||||
const handleCopy = async (colorValue: string) => {
|
||||
try {
|
||||
await copyToClipboard(colorValue);
|
||||
showToast(`Copied ${colorValue}`, "success");
|
||||
} catch (error) {
|
||||
console.error("Failed to copy color:", error);
|
||||
showToast("Failed to copy color", "error");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toaster position="bottom-right" />
|
||||
<div className="space-y-8">
|
||||
{sections.map((section, sectionIndex) => (
|
||||
<div key={sectionIndex} className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-emphasis text-lg font-semibold">{section.title}</h3>
|
||||
{section.description && <p className="text-subtle text-sm">{section.description}</p>}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{section.colors.map((color, colorIndex) => (
|
||||
<div key={colorIndex} className="border-subtle bg-default overflow-hidden rounded-lg border">
|
||||
<div className="group relative">
|
||||
<div className={`h-36 w-full ${color.className}`} title={color.value} />
|
||||
<CopyMenu color={color.value} className={color.className} onCopy={handleCopy} />
|
||||
</div>
|
||||
<div className="space-y-1 p-2">
|
||||
<p className="text-emphasis text-sm font-medium">{color.name}</p>
|
||||
<p className="text-subtle text-xs">{color.value}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,109 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
interface TypographyStyle {
|
||||
name: string;
|
||||
className: string;
|
||||
specs: {
|
||||
fontFamily: string;
|
||||
fontSize: string;
|
||||
lineHeight: string;
|
||||
weight: string;
|
||||
};
|
||||
category?: string;
|
||||
}
|
||||
|
||||
interface TypographySection {
|
||||
title: string;
|
||||
description?: string;
|
||||
styles: TypographyStyle[];
|
||||
}
|
||||
|
||||
interface TypographyTableProps {
|
||||
sections: TypographySection[];
|
||||
}
|
||||
|
||||
interface CopyMenuProps {
|
||||
style: TypographyStyle;
|
||||
onCopy: (format: string) => void;
|
||||
}
|
||||
|
||||
const CopyMenu: React.FC<CopyMenuProps> = ({ style, onCopy }) => {
|
||||
return (
|
||||
<div className="absolute inset-2 flex items-center justify-center bg-black/10 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<div className="bg-default space-y-1 rounded-md p-2 shadow-lg">
|
||||
<button
|
||||
onClick={() => onCopy(style.className)}
|
||||
className="hover:bg-subtle w-full rounded px-3 py-1 text-left text-sm">
|
||||
Copy Tailwind Class
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
onCopy(`font-family: ${style.specs.fontFamily};
|
||||
font-size: ${style.specs.fontSize}px;
|
||||
line-height: ${style.specs.lineHeight};
|
||||
font-weight: ${style.specs.weight};`)
|
||||
}
|
||||
className="hover:bg-subtle w-full rounded px-3 py-1 text-left text-sm">
|
||||
Copy CSS
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const TypographyTable: React.FC<TypographyTableProps> = ({ sections }) => {
|
||||
const handleCopy = async (value: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
showToast(`Copied to clipboard`, "success");
|
||||
} catch (error) {
|
||||
console.error("Failed to copy:", error);
|
||||
showToast("Failed to copy", "error");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toaster position="bottom-right" />
|
||||
<div className="space-y-12">
|
||||
{sections.map((section, sectionIndex) => (
|
||||
<div key={sectionIndex} className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-emphasis text-2xl font-semibold">{section.title}</h3>
|
||||
{section.description && <p className="text-subtle mt-2 text-base">{section.description}</p>}
|
||||
</div>
|
||||
<div className="grid gap-6">
|
||||
{section.styles.map((style, styleIndex) => (
|
||||
<div
|
||||
key={styleIndex}
|
||||
className="border-subtle bg-default group relative overflow-hidden rounded-lg border p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-emphasis text-sm font-medium">{style.name}</p>
|
||||
<p className="text-subtle mt-1 text-xs">{style.className}</p>
|
||||
</div>
|
||||
<div className={style.className}>The quick brown fox jumps over the lazy dog</div>
|
||||
</div>
|
||||
<div className="text-subtle space-y-2 text-sm">
|
||||
<p>Font: {style.specs.fontFamily}</p>
|
||||
<p>Size: {style.specs.fontSize}px</p>
|
||||
<p>Line Height: {style.specs.lineHeight}</p>
|
||||
<p>Weight: {style.specs.weight}</p>
|
||||
</div>
|
||||
</div>
|
||||
<CopyMenu style={style} onCopy={handleCopy} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
title: Border Radius
|
||||
description: Cal.com's border radius system
|
||||
---
|
||||
|
||||
import { BorderRadiusTable } from '@/components/ui/BorderRadiusTable';
|
||||
|
||||
# Border Radius
|
||||
|
||||
Our design system uses a consistent set of border radius values to create a cohesive and modern interface. The border radius scale ranges from none (sharp corners) to full (completely rounded).
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
// No border radius
|
||||
<div className="rounded-none">Sharp corners</div>
|
||||
|
||||
// Default radius
|
||||
<div className="rounded">Default rounded corners</div>
|
||||
|
||||
// Larger radius
|
||||
<div className="rounded-lg">Larger rounded corners</div>
|
||||
|
||||
// Fully rounded (useful for pills, badges)
|
||||
<div className="rounded-full">Completely rounded</div>
|
||||
```
|
||||
|
||||
## Border Radius Scale
|
||||
|
||||
Click on any radius to copy its Tailwind class name. Each preview shows how the radius appears when applied.
|
||||
|
||||
<BorderRadiusTable />
|
||||
@@ -1,144 +0,0 @@
|
||||
---
|
||||
title: Colors
|
||||
description: Cal.com Color System Documentation
|
||||
---
|
||||
|
||||
import { TypeTable } from '@/components/ui/TypeTable';
|
||||
|
||||
export const backgroundTokens = {
|
||||
title: 'Background Tokens',
|
||||
colors: [
|
||||
{ name: 'emphasis', value: 'var(--cal-bg-emphasis)', className: 'bg-emphasis' },
|
||||
{ name: 'default', value: 'var(--cal-bg, white)', className: 'bg-default' },
|
||||
{ name: 'subtle', value: 'var(--cal-bg-subtle)', className: 'bg-subtle' },
|
||||
{ name: 'muted', value: 'var(--cal-bg-muted)', className: 'bg-muted' },
|
||||
{ name: 'inverted', value: 'var(--cal-bg-inverted)', className: 'bg-inverted' },
|
||||
],
|
||||
};
|
||||
|
||||
export const primaryBackgroundTokens = {
|
||||
title: 'Primary Background Tokens',
|
||||
colors: [
|
||||
{ name: 'primary.default', value: 'var(--cal-bg-primary)', className: 'bg-primary-default' },
|
||||
{ name: 'primary.emphasis', value: 'var(--cal-bg-primary-emphasis)', className: 'bg-primary-emphasis' },
|
||||
{ name: 'primary.muted', value: 'var(--cal-bg-primary-muted)', className: 'bg-primary-muted' },
|
||||
],
|
||||
};
|
||||
|
||||
export const brandTokens = {
|
||||
title: 'Brand Colors',
|
||||
colors: [
|
||||
{ name: 'brand.default', value: 'var(--cal-brand,#111827)', className: 'bg-brand-default' },
|
||||
{ name: 'brand.emphasis', value: 'var(--cal-brand-emphasis,#101010)', className: 'bg-brand-emphasis' },
|
||||
{ name: 'brand.subtle', value: 'var(--cal-brand-subtle,#9CA3AF)', className: 'bg-brand-subtle' },
|
||||
{ name: 'brand.accent', value: 'var(--cal-brand-accent,white)', className: 'bg-brand-accent' },
|
||||
],
|
||||
};
|
||||
|
||||
export const semanticBackgroundTokens = {
|
||||
title: 'Semantic Background Tokens',
|
||||
colors: [
|
||||
{ name: 'semantic.info.subtle', value: 'var(--cal-bg-semantic-info-subtle)', className: 'bg-semantic-info-subtle' },
|
||||
{ name: 'semantic.info.emphasis', value: 'var(--cal-bg-semantic-info-emphasis)', className: 'bg-semantic-info-emphasis' },
|
||||
{ name: 'semantic.success.subtle', value: 'var(--cal-bg-semantic-success-subtle)', className: 'bg-semantic-success-subtle' },
|
||||
{ name: 'semantic.success.emphasis', value: 'var(--cal-bg-semantic-success-emphasis)', className: 'bg-semantic-success-emphasis' },
|
||||
{ name: 'semantic.attention.subtle', value: 'var(--cal-bg-semantic-attention-subtle)', className: 'bg-semantic-attention-subtle' },
|
||||
{ name: 'semantic.attention.emphasis', value: 'var(--cal-bg-semantic-attention-emphasis)', className: 'bg-semantic-attention-emphasis' },
|
||||
{ name: 'semantic.error.subtle', value: 'var(--cal-bg-semantic-error-subtle)', className: 'bg-semantic-error-subtle' },
|
||||
{ name: 'semantic.error.emphasis', value: 'var(--cal-bg-semantic-error-emphasis)', className: 'bg-semantic-error-emphasis' },
|
||||
],
|
||||
};
|
||||
|
||||
export const borderTokens = {
|
||||
title: 'Border Tokens',
|
||||
colors: [
|
||||
{ name: 'border.emphasis', value: 'var(--cal-border-emphasis)', className: 'border-emphasis' },
|
||||
{ name: 'border.default', value: 'var(--cal-border)', className: 'border-default' },
|
||||
{ name: 'border.subtle', value: 'var(--cal-border-subtle)', className: 'border-subtle' },
|
||||
{ name: 'border.muted', value: 'var(--cal-border-muted)', className: 'border-muted' },
|
||||
{ name: 'border.semantic.error', value: 'var(--cal-border-semantic-error)', className: 'border-semantic-error' },
|
||||
],
|
||||
};
|
||||
|
||||
export const textTokens = {
|
||||
title: 'Text Tokens',
|
||||
description: 'Semantic text colors for various purposes',
|
||||
colors: [
|
||||
{ name: 'text.emphasis', value: 'var(--cal-text-emphasis)', className: 'text-emphasis' },
|
||||
{ name: 'text.default', value: 'var(--cal-text)', className: 'text-default' },
|
||||
{ name: 'text.subtle', value: 'var(--cal-text-subtle)', className: 'text-subtle' },
|
||||
{ name: 'text.muted', value: 'var(--cal-text-muted)', className: 'text-muted' },
|
||||
{ name: 'text.inverted', value: 'var(--cal-text-inverted)', className: 'text-inverted' },
|
||||
],
|
||||
};
|
||||
|
||||
# Color System
|
||||
|
||||
Our color system is built on semantic tokens that provide consistent theming across light and dark modes. The system is organized into five main categories:
|
||||
|
||||
1. Background Colors
|
||||
2. Primary Colors
|
||||
3. Brand Colors
|
||||
4. Semantic Colors
|
||||
5. Text & Border Colors
|
||||
|
||||
## Usage Guidelines
|
||||
|
||||
1. **Use Semantic Tokens**: Always prefer semantic tokens over direct color values to maintain consistency and theme support.
|
||||
2. **Dark Mode Support**: Our semantic tokens automatically handle dark mode transitions.
|
||||
3. **Component Design**: When creating new components, follow the semantic token system:
|
||||
- Use `bg-*` tokens for backgrounds
|
||||
- Use `text-*` tokens for text content
|
||||
- Use `border-*` tokens for borders
|
||||
4. **Semantic States**: Use semantic color variations to convey meaning:
|
||||
- Info: For informational UI elements
|
||||
- Success: For successful actions and positive states
|
||||
- Attention: For warnings and important notices
|
||||
- Error: For error states and destructive actions
|
||||
5. **Brand Colors**: Use brand colors sparingly and primarily for:
|
||||
- Primary CTAs
|
||||
- Brand identity elements
|
||||
- Key interactive components
|
||||
|
||||
|
||||
## Using Semantic Tokens
|
||||
|
||||
Our color tokens are CSS variables that automatically handle light and dark mode transitions. Here's how to use them:
|
||||
|
||||
### Background Colors
|
||||
```tsx
|
||||
// Basic backgrounds
|
||||
<div className="bg-default">Default background</div>
|
||||
<div className="bg-subtle">Subtle background</div>
|
||||
<div className="bg-emphasis">Emphasis background</div>
|
||||
|
||||
// Primary backgrounds
|
||||
<div className="bg-primary-default">Primary background</div>
|
||||
<div className="bg-primary-emphasis">Primary emphasis</div>
|
||||
```
|
||||
|
||||
### Semantic Colors
|
||||
```tsx
|
||||
// Info states
|
||||
<div className="bg-semantic-info-subtle">Info subtle</div>
|
||||
<div className="bg-semantic-info-emphasis">Info emphasis</div>
|
||||
|
||||
// Success states
|
||||
<div className="bg-semantic-success-subtle">Success subtle</div>
|
||||
<div className="bg-semantic-success-emphasis">Success emphasis</div>
|
||||
|
||||
// Error states
|
||||
<div className="bg-semantic-error-subtle">Error subtle</div>
|
||||
<div className="bg-semantic-error-emphasis">Error emphasis</div>
|
||||
```
|
||||
|
||||
### Text Colors
|
||||
```tsx
|
||||
<p className="text-emphasis">High emphasis text</p>
|
||||
<p className="text-default">Default text</p>
|
||||
<p className="text-subtle">Subtle text</p>
|
||||
<p className="text-muted">Muted text</p>
|
||||
```
|
||||
|
||||
<TypeTable sections={[backgroundTokens, primaryBackgroundTokens, brandTokens, semanticBackgroundTokens, borderTokens, textTokens]} />
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
---
|
||||
title: MultiOptionInput
|
||||
---
|
||||
import { MultiOptionInput } from "@calcom/ui/components/form"
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render"
|
||||
import { Row } from "@/app/components/row"
|
||||
import { StatesExample } from "./multiInputField.states"
|
||||
import { PasteExample } from "./multiInputField.paste"
|
||||
import { CustomizationExample } from "./multiInputField.customization"
|
||||
|
||||
## Overview
|
||||
|
||||
The MultiOptionInput component is a flexible form input that allows users to manage a list of options with features like:
|
||||
- Adding and removing options
|
||||
- Reordering options with move up/down buttons
|
||||
- Pasting multiple options with configurable delimiters
|
||||
- Minimum options requirement
|
||||
- Customizable placeholders and labels
|
||||
- Key-value pair mode for environment variables, configuration settings, etc.
|
||||
|
||||
## States
|
||||
|
||||
MultiOptionInput in different states: default, disabled, and with minimum options.
|
||||
|
||||
<StatesExample/>
|
||||
|
||||
## Paste Support
|
||||
|
||||
Demonstrates the paste functionality with different delimiters. Users can paste comma-separated values, newline-separated values, or use custom delimiters.
|
||||
|
||||
In key-value mode, the component can also parse key-value pairs from pasted text. For example:
|
||||
- `KEY=value`
|
||||
- `NODE_ENV:production`
|
||||
|
||||
<PasteExample/>
|
||||
|
||||
## Customization
|
||||
|
||||
Examples of different customization options like placeholders, move buttons, remove buttons, and key-value pairs.
|
||||
|
||||
<CustomizationExample/>
|
||||
|
||||
## Usage
|
||||
|
||||
### Standard Options
|
||||
|
||||
```tsx
|
||||
import { MultiOptionInput } from "@calcom/ui/components/form/inputs/MultiOptionInput";
|
||||
import { useForm, FormProvider } from "react-hook-form";
|
||||
|
||||
type FormValues = {
|
||||
options: Array<{ label: string; id: string }>;
|
||||
};
|
||||
|
||||
const MyForm = () => {
|
||||
const methods = useForm<FormValues>();
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form>
|
||||
<MultiOptionInput<FormValues>
|
||||
fieldArrayName="options"
|
||||
optionPlaceholders={["Option 1", "Option 2"]}
|
||||
defaultNumberOfOptions={2}
|
||||
pasteDelimiters={["\n", ","]} // Optional
|
||||
showMoveButtons={true} // Optional
|
||||
minOptions={1} // Optional
|
||||
addOptionLabel="Add another option" // Optional
|
||||
/>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Key-Value Pairs
|
||||
|
||||
```tsx
|
||||
import { MultiOptionInput } from "@calcom/ui/components/form/inputs/MultiOptionInput";
|
||||
import { useForm, FormProvider } from "react-hook-form";
|
||||
|
||||
type FormValues = {
|
||||
envVars: Array<{ label: string; value: string; id: string }>;
|
||||
};
|
||||
|
||||
const EnvVarsForm = () => {
|
||||
const methods = useForm<FormValues>();
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form>
|
||||
<MultiOptionInput<FormValues>
|
||||
fieldArrayName="envVars"
|
||||
keyValueMode={true}
|
||||
keyLabel="Environment Variable"
|
||||
valueLabel="Value"
|
||||
optionPlaceholders={["NODE_ENV", "PORT"]}
|
||||
valuePlaceholders={["production", "3000"]}
|
||||
keyValueDelimiters={[":", "="]} // Optional, defaults to [":", "="]
|
||||
defaultNumberOfOptions={2}
|
||||
/>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
```
|
||||
@@ -1,28 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { Alert } from "@calcom/ui/components/alert";
|
||||
|
||||
const severities = ["neutral", "info", "warning", "error"] as const;
|
||||
|
||||
export const ActionsExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="no-prose space-y-4">
|
||||
{severities.map((severity) => (
|
||||
<Alert
|
||||
key={severity}
|
||||
severity={severity}
|
||||
title={`${severity.charAt(0).toUpperCase() + severity.slice(1)} Alert with Actions`}
|
||||
message={`This is a ${severity} alert with action buttons.`}
|
||||
actions={
|
||||
<div className="flex space-x-2">
|
||||
<button className="rounded-md px-3 py-2 text-sm font-medium hover:opacity-90">Dismiss</button>
|
||||
<button className="rounded-md px-3 py-2 text-sm font-medium hover:opacity-90">View</button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,22 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { Alert } from "@calcom/ui/components/alert";
|
||||
|
||||
const severities = ["neutral", "info", "warning", "error"] as const;
|
||||
|
||||
export const BasicExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="not-prose space-y-4">
|
||||
{severities.map((severity) => (
|
||||
<Alert
|
||||
key={severity}
|
||||
severity={severity}
|
||||
title={`${severity.charAt(0).toUpperCase() + severity.slice(1)} Alert`}
|
||||
message={`This is a ${severity} alert message to show important information to users.`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,24 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { Alert } from "@calcom/ui/components/alert";
|
||||
|
||||
const severities = ["neutral", "info", "warning", "error"] as const;
|
||||
|
||||
export const CustomIconColorExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="not-prose space-y-4">
|
||||
{severities.map((severity) => (
|
||||
<Alert
|
||||
key={severity}
|
||||
severity={severity}
|
||||
CustomIcon="bell"
|
||||
customIconColor="text-emphasis"
|
||||
title={`${severity.charAt(0).toUpperCase() + severity.slice(1)} Alert with Custom Icon Color`}
|
||||
message="This alert uses a custom icon color."
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,25 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { Alert } from "@calcom/ui/components/alert";
|
||||
|
||||
const severities = ["neutral", "info", "warning", "error"] as const;
|
||||
const customIcons = ["info", "alert-triangle", "circle-x", "circle-check", "bell"] as const;
|
||||
|
||||
export const CustomIconsExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="not-prose space-y-4">
|
||||
{severities.map((severity, index) => (
|
||||
<Alert
|
||||
key={severity}
|
||||
severity={severity}
|
||||
// @ts-expect-error Didnt type this as IconName for CustomIcon
|
||||
CustomIcon={customIcons[index % customIcons.length]}
|
||||
title={`${severity.charAt(0).toUpperCase() + severity.slice(1)} Alert with Custom Icon`}
|
||||
message="This alert uses a custom icon instead of the default one."
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
title: Alert
|
||||
airtableId: recnvwaXqOucqLo9Y
|
||||
---
|
||||
import { Alert } from "@calcom/ui/components/alert"
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render"
|
||||
import { Row } from "@/app/components/row"
|
||||
import { BasicExample } from "./alert.basic"
|
||||
import { ActionsExample } from "./alert.actions"
|
||||
import { CustomIconsExample } from "./alert.customIcons"
|
||||
import { CustomIconColorExample } from "./alert.customIconColor"
|
||||
import { NoTitleExample } from "./alert.noTitle"
|
||||
|
||||
## Basic Alerts
|
||||
|
||||
Alerts come in different severities to convey different levels of importance.
|
||||
|
||||
<BasicExample/>
|
||||
|
||||
## Alerts with Actions
|
||||
|
||||
Alerts can include action buttons to provide users with immediate options.
|
||||
|
||||
<ActionsExample/>
|
||||
|
||||
## Custom Icons
|
||||
|
||||
You can customize the alert icon to better match your message context.
|
||||
|
||||
<CustomIconsExample/>
|
||||
|
||||
## Custom Icon Colors
|
||||
|
||||
The icon color can be customized independently of the alert severity.
|
||||
|
||||
<CustomIconColorExample/>
|
||||
|
||||
## Without Title
|
||||
|
||||
For simpler messages, alerts can be displayed without a title.
|
||||
|
||||
<NoTitleExample/>
|
||||
@@ -1,21 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { Alert } from "@calcom/ui/components/alert";
|
||||
|
||||
const severities = ["neutral", "info", "warning", "error"] as const;
|
||||
|
||||
export const NoTitleExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="not-prose space-y-4">
|
||||
{severities.map((severity) => (
|
||||
<Alert
|
||||
key={severity}
|
||||
severity={severity}
|
||||
message={`This is a ${severity} alert without a title to show a more compact version.`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,22 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { Avatar } from "@calcom/ui/components/avatar";
|
||||
|
||||
const sampleImage = "https://cal.com/stakeholder/peer.jpg";
|
||||
|
||||
export const ImageExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Avatar size="md" alt="With image" imageSrc={sampleImage} />
|
||||
<span className="text-subtle text-xs">With Image</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Avatar size="md" alt="Without image" imageSrc="https://cal.com/avatar.svg" />
|
||||
<span className="text-subtle text-xs">Without Image</span>
|
||||
</div>
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,25 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { Avatar } from "@calcom/ui/components/avatar";
|
||||
|
||||
const sampleImage = "https://cal.com/stakeholder/peer.jpg";
|
||||
|
||||
export const IndicatorExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Avatar
|
||||
size="md"
|
||||
alt="With indicator"
|
||||
imageSrc={sampleImage}
|
||||
indicator={
|
||||
<div className="absolute bottom-0 right-0 h-2.5 w-2.5 rounded-full bg-green-500 ring-2 ring-white" />
|
||||
}
|
||||
/>
|
||||
<span className="text-subtle text-xs">With Status Indicator</span>
|
||||
</div>
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,18 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { Avatar } from "@calcom/ui/components/avatar";
|
||||
|
||||
const sampleImage = "https://cal.com/stakeholder/peer.jpg";
|
||||
|
||||
export const LinkExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Avatar size="md" alt="With link" imageSrc={sampleImage} href="https://cal.com" />
|
||||
<span className="text-subtle text-xs">Clickable</span>
|
||||
</div>
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,103 +0,0 @@
|
||||
---
|
||||
title: Avatar
|
||||
airtableId: reciflFrJo5cVgGrS
|
||||
---
|
||||
import { Avatar } from "@calcom/ui/components/avatar"
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render"
|
||||
import { Row } from "@/app/components/row"
|
||||
import { SizesExample } from "./avatar.sizes"
|
||||
import { ImageExample } from "./avatar.image"
|
||||
import { TooltipExample } from "./avatar.tooltip"
|
||||
import { LinkExample } from "./avatar.link"
|
||||
import { IndicatorExample } from "./avatar.indicator"
|
||||
import { ShapeExample } from "./avatar.shape"
|
||||
|
||||
## Size Variations
|
||||
|
||||
Avatars come in different sizes to suit various use cases.
|
||||
|
||||
<SizesExample/>
|
||||
|
||||
## Shape Variations
|
||||
|
||||
Avatars can be displayed in either circular or square shapes.
|
||||
|
||||
<ShapeExample/>
|
||||
|
||||
## With and Without Image
|
||||
|
||||
Avatars can display either an image or a fallback placeholder.
|
||||
|
||||
<ImageExample/>
|
||||
|
||||
## With Tooltip
|
||||
|
||||
Avatars can show additional information on hover using tooltips.
|
||||
|
||||
<TooltipExample/>
|
||||
|
||||
## With Link
|
||||
|
||||
Avatars can be made clickable by providing an href.
|
||||
|
||||
<LinkExample/>
|
||||
|
||||
## With Indicator
|
||||
|
||||
Avatars can include status indicators or badges.
|
||||
|
||||
<IndicatorExample/>
|
||||
|
||||
## Usage Example
|
||||
|
||||
Here's a complete example showing how to use the Avatar component with all its props:
|
||||
|
||||
```tsx
|
||||
import { Avatar } from "@calcom/ui/components/avatar";
|
||||
|
||||
function Example() {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Basic Avatar */}
|
||||
<Avatar
|
||||
alt="User Name"
|
||||
title="User Name"
|
||||
size="md"
|
||||
shape="circle"
|
||||
imageSrc="https://example.com/avatar.jpg"
|
||||
/>
|
||||
|
||||
{/* Avatar with Link */}
|
||||
<Avatar
|
||||
alt="User Name"
|
||||
title="User Name"
|
||||
size="lg"
|
||||
shape="square"
|
||||
imageSrc="https://example.com/avatar.jpg"
|
||||
href="/profile"
|
||||
/>
|
||||
|
||||
{/* Avatar with Tooltip and Indicator */}
|
||||
<Avatar
|
||||
alt="User Name"
|
||||
title="User Name"
|
||||
size="md"
|
||||
shape="circle"
|
||||
imageSrc="https://example.com/avatar.jpg"
|
||||
indicator={<div className="absolute bottom-0 right-0 h-3 w-3 rounded-full bg-green-500" />}
|
||||
/>
|
||||
|
||||
{/* Avatar with Fallback */}
|
||||
<Avatar
|
||||
alt="User Name"
|
||||
title="User Name"
|
||||
size="md"
|
||||
shape="circle"
|
||||
fallback={<span>UN</span>}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Avatar } from "@calcom/ui/components/avatar";
|
||||
|
||||
export function ShapeExample() {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar
|
||||
alt="Circle Avatar"
|
||||
title="Circle Avatar"
|
||||
size="lg"
|
||||
shape="circle"
|
||||
imageSrc="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
/>
|
||||
<Avatar
|
||||
alt="Square Avatar"
|
||||
title="Square Avatar"
|
||||
size="lg"
|
||||
shape="square"
|
||||
imageSrc="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { Avatar } from "@calcom/ui/components/avatar";
|
||||
|
||||
const sizes = ["xs", "xsm", "sm", "md", "mdLg", "lg", "xl"] as const;
|
||||
const sampleImage = "https://cal.com/stakeholder/peer.jpg";
|
||||
|
||||
export const SizesExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="flex items-center gap-4">
|
||||
{sizes.map((size) => (
|
||||
<div key={size} className="flex flex-col items-center gap-2">
|
||||
<Avatar size={size} alt={`${size} avatar`} imageSrc={sampleImage} />
|
||||
<span className="text-subtle text-xs">{size}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-4">
|
||||
{sizes.map((size) => (
|
||||
<div key={size} className="flex flex-col items-center gap-2">
|
||||
<Avatar size={size} alt={`${size} avatar`} imageSrc={sampleImage} shape="square" />
|
||||
<span className="text-subtle text-xs">{size}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,18 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { Avatar } from "@calcom/ui/components/avatar";
|
||||
|
||||
const sampleImage = "https://cal.com/stakeholder/peer.jpg";
|
||||
|
||||
export const TooltipExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Avatar size="md" alt="With tooltip" imageSrc={sampleImage} title="Hover me!" />
|
||||
<span className="text-subtle text-xs">Hover to see tooltip</span>
|
||||
</div>
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,33 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
|
||||
const variants = [
|
||||
"default",
|
||||
"warning",
|
||||
"orange",
|
||||
"success",
|
||||
"green",
|
||||
"gray",
|
||||
"blue",
|
||||
"red",
|
||||
"error",
|
||||
"grayWithoutHover",
|
||||
] as const;
|
||||
|
||||
export const DotsExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{variants.map((variant) => (
|
||||
<div key={variant} className="flex flex-col items-center gap-2">
|
||||
<Badge variant={variant} withDot>
|
||||
{variant}
|
||||
</Badge>
|
||||
<span className="text-subtle text-xs">Dot</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,33 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
|
||||
const variants = [
|
||||
"default",
|
||||
"warning",
|
||||
"orange",
|
||||
"success",
|
||||
"green",
|
||||
"gray",
|
||||
"blue",
|
||||
"red",
|
||||
"error",
|
||||
"grayWithoutHover",
|
||||
] as const;
|
||||
|
||||
export const IconsExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{variants.map((variant) => (
|
||||
<div key={variant} className="flex flex-col items-center gap-2">
|
||||
<Badge variant={variant} startIcon="bell">
|
||||
{variant}
|
||||
</Badge>
|
||||
<span className="text-subtle text-xs">Icon</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,33 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
|
||||
const variants = [
|
||||
"default",
|
||||
"warning",
|
||||
"orange",
|
||||
"success",
|
||||
"green",
|
||||
"gray",
|
||||
"blue",
|
||||
"red",
|
||||
"error",
|
||||
"grayWithoutHover",
|
||||
] as const;
|
||||
|
||||
export const InteractiveExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{variants.map((variant) => (
|
||||
<div key={variant} className="flex flex-col items-center gap-2">
|
||||
<Badge variant={variant} onClick={() => alert(`${variant} badge clicked!`)}>
|
||||
Click me
|
||||
</Badge>
|
||||
<span className="text-subtle text-xs">Clickable</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
title: Badge
|
||||
airtableId: recso7QMfD2xwI04K
|
||||
---
|
||||
import { Badge } from "@calcom/ui/components/badge"
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render"
|
||||
import { Row } from "@/app/components/row"
|
||||
import { VariantsExample } from "./badge.variants"
|
||||
import { IconsExample } from "./badge.icons"
|
||||
import { DotsExample } from "./badge.dots"
|
||||
import { InteractiveExample } from "./badge.interactive"
|
||||
import { RoundedExample } from "./badge.rounded"
|
||||
|
||||
## Variants and Sizes
|
||||
|
||||
Badges come in different variants and sizes to suit various use cases.
|
||||
|
||||
<VariantsExample/>
|
||||
|
||||
## With Icons
|
||||
|
||||
Badges can include icons to provide additional visual context.
|
||||
|
||||
<IconsExample/>
|
||||
|
||||
## With Dots
|
||||
|
||||
Badges can include dots, useful for showing status or notifications.
|
||||
|
||||
<DotsExample/>
|
||||
|
||||
## Interactive
|
||||
|
||||
Badges can be made interactive with click handlers.
|
||||
|
||||
<InteractiveExample/>
|
||||
|
||||
## Rounded
|
||||
|
||||
Badges can be fully rounded, useful for numerical indicators.
|
||||
|
||||
<RoundedExample/>
|
||||
@@ -1,33 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
|
||||
const variants = [
|
||||
"default",
|
||||
"warning",
|
||||
"orange",
|
||||
"success",
|
||||
"green",
|
||||
"gray",
|
||||
"blue",
|
||||
"red",
|
||||
"error",
|
||||
"grayWithoutHover",
|
||||
] as const;
|
||||
|
||||
export const RoundedExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{variants.map((variant) => (
|
||||
<div key={variant} className="flex flex-col items-center gap-2">
|
||||
<Badge variant={variant} rounded>
|
||||
1
|
||||
</Badge>
|
||||
<span className="text-subtle text-xs">{variant}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,42 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
|
||||
const variants = [
|
||||
"default",
|
||||
"warning",
|
||||
"orange",
|
||||
"success",
|
||||
"green",
|
||||
"gray",
|
||||
"blue",
|
||||
"red",
|
||||
"error",
|
||||
"grayWithoutHover",
|
||||
] as const;
|
||||
|
||||
const sizes = ["sm", "md", "lg"] as const;
|
||||
|
||||
export const VariantsExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="space-y-8">
|
||||
{variants.map((variant) => (
|
||||
<div key={variant} className="space-y-2">
|
||||
<h4 className="text-default text-sm capitalize">{variant}</h4>
|
||||
<div className="flex items-center gap-4">
|
||||
{sizes.map((size) => (
|
||||
<div key={size} className="flex flex-col items-center gap-2">
|
||||
<Badge variant={variant} size={size}>
|
||||
{variant}
|
||||
</Badge>
|
||||
<span className="text-subtle text-xs">{size}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,63 +0,0 @@
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
|
||||
|
||||
export const ButtonGroupExample = () => {
|
||||
return (
|
||||
<RenderComponentWithSnippet className="flex flex-col space-y-6">
|
||||
{/* Basic Button Group */}
|
||||
<ButtonGroup>
|
||||
<Button variant="icon" StartIcon="ellipsis" color="secondary" />
|
||||
<Button variant="icon" StartIcon="ellipsis" color="secondary" />
|
||||
<Button variant="icon" StartIcon="ellipsis" color="secondary" />
|
||||
</ButtonGroup>
|
||||
|
||||
{/* Combined Button Group */}
|
||||
<ButtonGroup combined>
|
||||
<Button variant="icon" StartIcon="ellipsis" color="secondary" />
|
||||
<Button variant="icon" StartIcon="ellipsis" color="secondary" />
|
||||
<Button variant="icon" StartIcon="ellipsis" color="secondary" />
|
||||
</ButtonGroup>
|
||||
|
||||
{/* Button Group with different variants */}
|
||||
<ButtonGroup combined>
|
||||
<Button variant="icon" color="secondary" StartIcon="ellipsis" />
|
||||
<Button variant="icon" color="secondary" StartIcon="ellipsis" />
|
||||
<Button variant="icon" color="destructive" StartIcon="trash" />
|
||||
</ButtonGroup>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
};
|
||||
|
||||
export const ButtonGroupSizes = () => {
|
||||
const sizes = ["xs", "sm", "base", "lg"] as const;
|
||||
|
||||
return (
|
||||
<RenderComponentWithSnippet className="flex flex-col space-y-6">
|
||||
{sizes.map((size) => (
|
||||
<ButtonGroup key={size}>
|
||||
<Button variant="icon" color="secondary" StartIcon="ellipsis" size={size} />
|
||||
<Button variant="icon" color="secondary" StartIcon="ellipsis" size={size} />
|
||||
<Button variant="icon" color="secondary" StartIcon="ellipsis" size={size} />
|
||||
</ButtonGroup>
|
||||
))}
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
};
|
||||
|
||||
export const ButtonGroupSizesCombined = () => {
|
||||
const sizes = ["xs", "sm", "base", "lg"] as const;
|
||||
|
||||
return (
|
||||
<RenderComponentWithSnippet className="flex flex-col space-y-6">
|
||||
{sizes.map((size) => (
|
||||
<ButtonGroup key={size} combined>
|
||||
<Button variant="icon" color="secondary" StartIcon="ellipsis" size={size} />
|
||||
<Button variant="icon" color="secondary" StartIcon="ellipsis" size={size} />
|
||||
<Button variant="icon" color="secondary" StartIcon="ellipsis" size={size} />
|
||||
</ButtonGroup>
|
||||
))}
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
import { Row } from "@/app/components/row";
|
||||
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
|
||||
const colors = ["primary", "secondary", "minimal", "destructive"] as const;
|
||||
|
||||
export const DisabledExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h3 className="text-default mb-4 text-sm">Disabled State</h3>
|
||||
<Row>
|
||||
{colors.map((color) => (
|
||||
<div key={color} className="flex flex-col items-center gap-2">
|
||||
<Button color={color} disabled>
|
||||
{color}
|
||||
</Button>
|
||||
<span className="text-subtle text-xs">Disabled</span>
|
||||
</div>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-default mb-4 text-sm">Disabled with Icons</h3>
|
||||
<Row>
|
||||
{colors.map((color) => (
|
||||
<div key={color} className="flex flex-col items-center gap-2">
|
||||
<Button color={color} disabled StartIcon="calendar">
|
||||
{color}
|
||||
</Button>
|
||||
<span className="text-subtle text-xs">With Icon</span>
|
||||
</div>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,42 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
import { Row } from "@/app/components/row";
|
||||
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
|
||||
const colors = ["primary", "secondary", "minimal", "destructive"] as const;
|
||||
|
||||
export const IconsExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h3 className="text-default mb-4 text-sm">Start Icon</h3>
|
||||
<Row>
|
||||
{colors.map((color) => (
|
||||
<div key={color} className="flex flex-col items-center gap-2">
|
||||
<Button color={color} StartIcon="calendar">
|
||||
{color}
|
||||
</Button>
|
||||
<span className="text-subtle text-xs">Start Icon</span>
|
||||
</div>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-default mb-4 text-sm">End Icon</h3>
|
||||
<Row>
|
||||
{colors.map((color) => (
|
||||
<div key={color} className="flex flex-col items-center gap-2">
|
||||
<Button color={color} EndIcon="arrow-right">
|
||||
{color}
|
||||
</Button>
|
||||
<span className="text-subtle text-xs">End Icon</span>
|
||||
</div>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,44 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
import { Row } from "@/app/components/row";
|
||||
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
|
||||
const colors = ["primary", "secondary", "minimal", "destructive"] as const;
|
||||
|
||||
export const LoadingExample: React.FC = () => {
|
||||
return (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h3 className="text-default mb-4 text-sm">Loading State</h3>
|
||||
<Row>
|
||||
{colors.map((color) => (
|
||||
<div key={color} className="flex flex-col items-center gap-2">
|
||||
<Button color={color} loading>
|
||||
{color}
|
||||
</Button>
|
||||
<span className="text-subtle text-xs">Loading</span>
|
||||
</div>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-default mb-4 text-sm">Loading with Icons</h3>
|
||||
<Row>
|
||||
{colors.map((color) => (
|
||||
<div key={color} className="flex flex-col items-center gap-2">
|
||||
<Button color={color} loading StartIcon="calendar">
|
||||
{color}
|
||||
</Button>
|
||||
<span className="text-subtle text-xs">With Icon</span>
|
||||
</div>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
};
|
||||
@@ -1,98 +0,0 @@
|
||||
---
|
||||
title: Button
|
||||
airtableId: recEJyzUeMMXCjyTB
|
||||
---
|
||||
import { Button } from "@calcom/ui/components/button"
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render"
|
||||
import { Row } from "@/app/components/row"
|
||||
import { OnClickExample } from "./button.onClick"
|
||||
import { VariantExample } from "./button.variant"
|
||||
import { IconsExample } from "./button.icons"
|
||||
import { LoadingExample } from "./button.loading"
|
||||
import { DisabledExample } from "./button.disabled"
|
||||
import { ButtonGroupExample,ButtonGroupSizes,ButtonGroupSizesCombined} from "./button.buttongroup"
|
||||
|
||||
## Variants and Colors
|
||||
|
||||
Buttons come in different variants and colors to suit different purposes. Each variant can be combined with any color and size.
|
||||
|
||||
<VariantExample/>
|
||||
|
||||
## Icons
|
||||
|
||||
Buttons can include icons at the start or end. The icon color and size are automatically handled by the button component.
|
||||
|
||||
<IconsExample/>
|
||||
|
||||
## Loading State
|
||||
|
||||
Buttons can show a loading state to indicate an ongoing action. The loading spinner replaces the button text while maintaining the button's width.
|
||||
|
||||
<LoadingExample/>
|
||||
|
||||
## Disabled State
|
||||
|
||||
Buttons can be disabled to indicate that they are not interactive. The disabled state is visually distinct and prevents any click events.
|
||||
|
||||
<DisabledExample/>
|
||||
|
||||
## onClick Events
|
||||
|
||||
Buttons handle click events like any other button component. You can provide an onClick handler to respond to user interactions.
|
||||
|
||||
<OnClickExample/>
|
||||
|
||||
Providing a loading state will add a spinner on the left side of the button.
|
||||
|
||||
<LoadingExample/>
|
||||
|
||||
## Disabled
|
||||
|
||||
<DisabledExample/>
|
||||
|
||||
## Button Groups
|
||||
|
||||
Buttons can be grouped together using the ButtonGroup component. The ButtonGroup component can be used to create a group of related buttons with consistent spacing and optional combined borders.
|
||||
|
||||
<ButtonGroupExample/>
|
||||
|
||||
### Button Group Sizes
|
||||
|
||||
#### Separated
|
||||
|
||||
<ButtonGroupSizes/>
|
||||
|
||||
#### Combined
|
||||
|
||||
<ButtonGroupSizesCombined/>
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
// Primary button (default)
|
||||
<Button>Primary Button</Button>
|
||||
|
||||
// Secondary button
|
||||
<Button color="secondary">Secondary Button</Button>
|
||||
|
||||
// Minimal button
|
||||
<Button color="minimal">Minimal Button</Button>
|
||||
|
||||
// Destructive button
|
||||
<Button color="destructive">Destructive Button</Button>
|
||||
|
||||
// With start icon
|
||||
<Button StartIcon="plus">Add Item</Button>
|
||||
|
||||
// With end icon
|
||||
<Button EndIcon="chevronRight">View More</Button>
|
||||
|
||||
// With link
|
||||
<Button href="/some-path">Navigate</Button>
|
||||
|
||||
// Different sizes
|
||||
<Button size="xs">Extra Small</Button>
|
||||
<Button size="sm">Small</Button>
|
||||
<Button size="base">Base (Default)</Button>
|
||||
<Button size="lg">Large</Button>
|
||||
```
|
||||
@@ -1,14 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
import { Row } from "@/app/components/row";
|
||||
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
|
||||
export const OnClickExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<Row>
|
||||
<Button onClick={() => alert("hello")}>Trigger Alert</Button>
|
||||
</Row>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,43 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
import { Row } from "@/app/components/row";
|
||||
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
|
||||
const variants = ["button", "icon"] as const;
|
||||
const colors = ["primary", "secondary", "minimal", "destructive"] as const;
|
||||
const sizes = ["xs", "sm", "base", "lg"] as const;
|
||||
|
||||
export const VariantExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="space-y-8">
|
||||
{variants.map((variant) => (
|
||||
<div key={variant} className="space-y-6">
|
||||
<h3 className="text-default text-sm capitalize">{variant === "button" ? "Default" : variant}</h3>
|
||||
<div className="space-y-6">
|
||||
{colors.map((color) => (
|
||||
<div key={color} className="space-y-2">
|
||||
<h4 className="text-default text-sm capitalize">{color}</h4>
|
||||
<Row>
|
||||
{sizes.map((size) => (
|
||||
<div key={size} className="flex flex-col items-center gap-2">
|
||||
<Button
|
||||
variant={variant}
|
||||
color={color}
|
||||
size={size}
|
||||
StartIcon={variant === "icon" ? "plus" : undefined}>
|
||||
{variant !== "icon" && `Label`}
|
||||
</Button>
|
||||
<span className="text-subtle text-xs">{size}</span>
|
||||
</div>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,31 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { Checkbox } from "@calcom/ui/components/form";
|
||||
|
||||
export const BasicExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<Checkbox id="basic-unchecked" />
|
||||
<span className="text-subtle text-xs">Unchecked</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<Checkbox id="basic-checked" defaultChecked />
|
||||
<span className="text-subtle text-xs">Checked</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<Checkbox id="basic-disabled" disabled />
|
||||
<span className="text-subtle text-xs">Disabled</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<Checkbox id="basic-disabled-checked" disabled defaultChecked />
|
||||
<span className="text-subtle text-xs">Disabled Checked</span>
|
||||
</div>
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,15 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { CheckboxField } from "@calcom/ui/components/form";
|
||||
|
||||
export const DescriptionExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="space-y-4">
|
||||
<CheckboxField description="This is a regular description" id="desc-normal" />
|
||||
<CheckboxField description="This description acts as the label" descriptionAsLabel id="desc-as-label" />
|
||||
<CheckboxField id="desc-long" label="Label and description" description="Label and description" />
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,16 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { CheckboxField } from "@calcom/ui/components/form";
|
||||
|
||||
export const InfoIconExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<CheckboxField
|
||||
description="Checkbox with additional information"
|
||||
id="info-icon"
|
||||
label="Information Icon"
|
||||
informationIconText="This is additional information that appears in a tooltip"
|
||||
/>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,24 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { CheckboxField } from "@calcom/ui/components/form";
|
||||
|
||||
export const LabelPositionExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="space-y-4">
|
||||
<CheckboxField
|
||||
description="Description with label above (default on mobile)"
|
||||
id="label-above"
|
||||
label="Label Above"
|
||||
/>
|
||||
<div className="sm:min-w-[400px]">
|
||||
<CheckboxField
|
||||
description="Description with label to the side (on larger screens)"
|
||||
id="label-side"
|
||||
label="Label to the Side"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
title: Checkbox
|
||||
airtableId: recXGPNAfDJVm8NHo
|
||||
---
|
||||
import { Checkbox } from "@calcom/ui/components/form"
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render"
|
||||
import { Row } from "@/app/components/row"
|
||||
import { BasicExample } from "./checkbox.basic"
|
||||
import { DescriptionExample } from "./checkbox.description"
|
||||
import { LabelPositionExample } from "./checkbox.labelPosition"
|
||||
import { InfoIconExample } from "./checkbox.infoIcon"
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Checkboxes in their various states - unchecked, checked, disabled, and disabled checked.
|
||||
|
||||
<BasicExample/>
|
||||
|
||||
## With Description
|
||||
|
||||
Checkboxes can include descriptions to provide additional context.
|
||||
|
||||
<DescriptionExample/>
|
||||
|
||||
## Label Positions
|
||||
|
||||
Labels can be positioned above or to the side of the checkbox.
|
||||
|
||||
<LabelPositionExample/>
|
||||
|
||||
## With Information Icon
|
||||
|
||||
Checkboxes can include an information icon with a tooltip for additional details.
|
||||
|
||||
<InfoIconExample/>
|
||||
@@ -1,33 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Dialog, DialogClose, DialogContent, DialogFooter } from "@calcom/ui/components/dialog";
|
||||
|
||||
export const BasicExample: React.FC = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="space-y-2">
|
||||
<Button onClick={() => setOpen(true)}>Open Basic Dialog</Button>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent
|
||||
title="Basic Dialog"
|
||||
description="This is a basic dialog with a title and description.">
|
||||
<p className="text-default text-sm">
|
||||
Dialogs are used to display content that requires user attention or interaction. They appear
|
||||
above the page content and must be closed before interacting with the page again.
|
||||
</p>
|
||||
<DialogFooter>
|
||||
<DialogClose />
|
||||
<Button>Action</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Dialog, DialogClose, DialogContent, DialogFooter } from "@calcom/ui/components/dialog";
|
||||
|
||||
export const ConfirmationExample: React.FC = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="space-y-2">
|
||||
<Button color="destructive" onClick={() => setOpen(true)}>
|
||||
Delete Item
|
||||
</Button>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent
|
||||
title="Confirm Deletion"
|
||||
description="Are you sure you want to delete this item? This action cannot be undone.">
|
||||
<DialogFooter>
|
||||
<DialogClose />
|
||||
<Button
|
||||
color="destructive"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Dialog, DialogClose, DialogContent, DialogFooter } from "@calcom/ui/components/dialog";
|
||||
import { TextAreaField } from "@calcom/ui/components/form";
|
||||
|
||||
export const FormExample: React.FC = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [formInput, setFormInput] = useState("");
|
||||
|
||||
return (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="space-y-2">
|
||||
<Button color="secondary" onClick={() => setOpen(true)}>
|
||||
Open Form Dialog
|
||||
</Button>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent title="Feedback Form" description="Please provide your feedback below">
|
||||
<div>
|
||||
<TextAreaField
|
||||
name="feedback"
|
||||
label={
|
||||
<>
|
||||
Your Feedback
|
||||
<span className="text-subtle font-normal"> (Optional)</span>
|
||||
</>
|
||||
}
|
||||
value={formInput}
|
||||
onChange={(e) => setFormInput(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose />
|
||||
<Button
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
setFormInput("");
|
||||
}}>
|
||||
Submit Feedback
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div className="mt-6">
|
||||
<h4 className="text-emphasis text-sm font-medium">Current Form Input:</h4>
|
||||
<pre className="text-default bg-subtle mt-2 rounded-md p-4 text-sm">
|
||||
{formInput || "No input yet"}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Dialog, DialogClose, DialogContent, DialogFooter } from "@calcom/ui/components/dialog";
|
||||
|
||||
export const LargeContentExample: React.FC = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="space-y-2">
|
||||
<Button color="secondary" onClick={() => setOpen(true)}>
|
||||
Open Large Content Dialog
|
||||
</Button>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent
|
||||
title="Terms and Conditions"
|
||||
description="Please review our terms and conditions carefully"
|
||||
enableOverflow>
|
||||
<div className="space-y-6">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="space-y-4">
|
||||
<h3 className="text-emphasis font-semibold">Section {i + 1}</h3>
|
||||
<p className="text-default text-sm">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam in dui mauris. Vivamus
|
||||
hendrerit arcu sed erat molestie vehicula. Sed auctor neque eu tellus rhoncus ut eleifend
|
||||
nibh porttitor. Ut in nulla enim. Phasellus molestie magna non est bibendum non venenatis
|
||||
nisl tempor.
|
||||
</p>
|
||||
<p className="text-default text-sm">
|
||||
Suspendisse in orci enim. Integer vel sapien at risus ultrices ornare nec sit amet nibh.
|
||||
Duis blandit lectus ac odio rhoncus non congue diam bibendum. Aliquam erat volutpat.
|
||||
</p>
|
||||
{i === 2 && (
|
||||
<div className="border-subtle rounded-lg border p-4 shadow-sm">
|
||||
<h4 className="text-emphasis mb-2 font-medium">Important Notice</h4>
|
||||
<p className="text-default text-sm">
|
||||
This is a highlighted section within the content to demonstrate how different UI
|
||||
elements appear within a scrollable dialog.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose>Decline</DialogClose>
|
||||
<Button>Accept Terms</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
title: Dialog
|
||||
airtableId: rechCoaoEMZLnLChR
|
||||
---
|
||||
import { Dialog } from "@calcom/ui/components/dialog"
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render"
|
||||
import { Row } from "@/app/components/row"
|
||||
import { BasicExample } from "./dialog.basic"
|
||||
import { LargeContentExample } from "./dialog.largeContent"
|
||||
import { FormExample } from "./dialog.form"
|
||||
import { ConfirmationExample } from "./dialog.confirmation"
|
||||
|
||||
## Basic Dialog
|
||||
|
||||
A simple dialog with a title, description, and action buttons.
|
||||
|
||||
<BasicExample/>
|
||||
|
||||
## Large Content Dialog
|
||||
|
||||
A dialog that can handle scrollable content, such as terms and conditions.
|
||||
|
||||
<LargeContentExample/>
|
||||
|
||||
## Form Dialog
|
||||
|
||||
A dialog containing a form for user input.
|
||||
|
||||
<FormExample/>
|
||||
|
||||
## Confirmation Dialog
|
||||
|
||||
A dialog for confirming destructive actions.
|
||||
|
||||
<ConfirmationExample/>
|
||||
@@ -1,38 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { Avatar } from "@calcom/ui/components/avatar";
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
} from "@calcom/ui/components/dropdown";
|
||||
|
||||
export const AvatarTriggerExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="flex flex-wrap items-center gap-8">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="cursor-pointer">
|
||||
<Avatar size="sm" imageSrc="https://cal.com/stakeholder/peer.jpg" alt="Avatar" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>Profile</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownItem>View Profile</DropdownItem>
|
||||
<DropdownItem>Settings</DropdownItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownItem className="text-error">Sign out</DropdownItem>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
<span className="text-subtle text-xs">Avatar Menu</span>
|
||||
</div>
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,84 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import type { IconName } from "@calcom/ui";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownItem,
|
||||
DropdownMenuLabel,
|
||||
} from "@calcom/ui/components/dropdown";
|
||||
|
||||
const menuItems = [
|
||||
{ label: "View", icon: "eye", kbd: "V" },
|
||||
{ label: "Edit", icon: "edit", kbd: "E" },
|
||||
{ label: "Share", icon: "share", kbd: "⌘S" },
|
||||
{ label: "Delete", icon: "trash", destructive: true, kbd: "⌘⌫" },
|
||||
] as {
|
||||
label: string;
|
||||
icon: IconName;
|
||||
destructive?: boolean;
|
||||
kbd?: string;
|
||||
}[];
|
||||
|
||||
export const ButtonTriggerExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="flex flex-wrap items-center gap-8">
|
||||
{/* Default Button */}
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>Menu</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
{menuItems.map((item) => (
|
||||
<DropdownItem color={item.destructive ? "destructive" : "secondary"} key={item.label}>
|
||||
{item.label}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
<span className="text-subtle text-xs">Default</span>
|
||||
</div>
|
||||
|
||||
{/* Button with Icon */}
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>More Actions</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>Quick Actions</DropdownMenuLabel>
|
||||
{menuItems.map((item) => (
|
||||
<DropdownItem key={item.label} color={item.destructive ? "destructive" : undefined}>
|
||||
{item.label}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
<span className="text-subtle text-xs">With Icon</span>
|
||||
</div>
|
||||
|
||||
{/* Icon Button */}
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="icon" StartIcon="calendar" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{menuItems.map((item) => (
|
||||
<DropdownItem key={item.label} color={item.destructive ? "destructive" : undefined}>
|
||||
{item.label}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
<span className="text-subtle text-xs">Icon Only</span>
|
||||
</div>
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
@@ -1,86 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RenderComponentWithSnippet } from "@/app/components/render";
|
||||
|
||||
import { Avatar } from "@calcom/ui/components/avatar";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
} from "@calcom/ui/components/dropdown";
|
||||
|
||||
export const ComplexExample: React.FC = () => (
|
||||
<RenderComponentWithSnippet>
|
||||
<div className="flex flex-wrap items-center gap-8">
|
||||
{/* Example 1: Complex Menu Structure */}
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button color="secondary">Open</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>My account</DropdownMenuLabel>
|
||||
<DropdownItem>Profile</DropdownItem>
|
||||
<DropdownItem>Billing</DropdownItem>
|
||||
<DropdownItem>Settings</DropdownItem>
|
||||
<DropdownItem>Keyboard Shortcuts</DropdownItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Team</DropdownMenuLabel>
|
||||
<DropdownItem>Invite users</DropdownItem>
|
||||
<DropdownItem>New team</DropdownItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownItem>Github</DropdownItem>
|
||||
<DropdownItem>Support</DropdownItem>
|
||||
<DropdownItem>API</DropdownItem>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
<span className="text-subtle text-xs">Complex Menu</span>
|
||||
</div>
|
||||
|
||||
{/* Example 2: Avatar with Icons */}
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="cursor-pointer">
|
||||
<Avatar size="md" imageSrc="https://cal.com/stakeholder/peer.jpg" alt="Avatar" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>peer@cal.com</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownItem StartIcon="user">My account</DropdownItem>
|
||||
<DropdownItem StartIcon="map">Plan</DropdownItem>
|
||||
<DropdownItem StartIcon="credit-card">Billing</DropdownItem>
|
||||
<DropdownItem StartIcon="link">Integrations</DropdownItem>
|
||||
<DropdownItem StartIcon="upload">Module</DropdownItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownItem className="text-error" StartIcon="log-out">
|
||||
Log out
|
||||
</DropdownItem>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
<span className="text-subtle text-xs">With Icons</span>
|
||||
</div>
|
||||
|
||||
{/* Example 3: Simple Actions */}
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="icon" color="secondary" StartIcon="calendar" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownItem>Rename</DropdownItem>
|
||||
<DropdownItem>Duplicate</DropdownItem>
|
||||
<DropdownItem>Move</DropdownItem>
|
||||
<DropdownItem color="destructive">Delete</DropdownItem>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
<span className="text-subtle text-xs">Simple Actions</span>
|
||||
</div>
|
||||
</div>
|
||||
</RenderComponentWithSnippet>
|
||||
);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user