Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4335c9ab92 | ||
|
|
20df7156cf | ||
|
|
9c35425e57 | ||
|
|
65df5ac0e4 | ||
|
|
84fdd03603 | ||
|
|
e6ec8bdf4d | ||
|
|
86c8197104 | ||
|
|
bb13badcf4 | ||
|
|
64b2397fa2 | ||
|
|
90130457e2 | ||
|
|
260521fb28 | ||
|
|
6390d595f5 | ||
|
|
74dc46f8b2 | ||
|
|
67262910cd | ||
|
|
d59e68de6d | ||
|
|
b50318fbca | ||
|
|
1ce7df324d | ||
|
|
e3b7264208 | ||
|
|
5e3f6e6c2c | ||
|
|
5b6c43118e | ||
|
|
beee545568 | ||
|
|
a597defd03 | ||
|
|
e3562cdfb5 | ||
|
|
86a7824ec9 | ||
|
|
7001515a44 | ||
|
|
36044c4ab3 | ||
|
|
4933295810 | ||
|
|
ed0cd30092 | ||
|
|
1132d9d0dc | ||
|
|
9bd3a362a2 | ||
|
|
844076badb | ||
|
|
c0523a31a7 | ||
|
|
1fff642860 | ||
|
|
89fe383c9f | ||
|
|
4a173c48b8 | ||
|
|
a72a3e45e9 | ||
|
|
a8d8c992a4 | ||
|
|
99f9a121cb | ||
|
|
6a0e2392fe | ||
|
|
addec46feb | ||
|
|
a4fe9a51b5 | ||
|
|
78b8bbb5aa | ||
|
|
d7ece4f583 | ||
|
|
8fc9b9ae9d | ||
|
|
d1175bf7de | ||
|
|
3900453482 | ||
|
|
68e7e76b31 | ||
|
|
986d196a37 | ||
|
|
d30fb213d2 | ||
|
|
3f02de8214 | ||
|
|
7421cea0da | ||
|
|
507daa6a0c | ||
|
|
61f68af435 | ||
|
|
b3f4cfab32 | ||
|
|
c3b1de498d | ||
|
|
cdf059361e | ||
|
|
2aada9a7da |
+5
-2
@@ -169,8 +169,7 @@ STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_WEBHOOK_SECRET_APPS=
|
||||
STRIPE_PRIVATE_KEY=
|
||||
STRIPE_CLIENT_ID=
|
||||
PAYMENT_FEE_FIXED=
|
||||
PAYMENT_FEE_PERCENTAGE=
|
||||
|
||||
|
||||
# Use for internal Public API Keys and optional
|
||||
API_KEY_PREFIX=cal_
|
||||
@@ -213,6 +212,10 @@ E2E_TEST_MAILHOG_ENABLED=
|
||||
|
||||
# **********************************************************************************************************
|
||||
|
||||
# Cloudflare Turnstile
|
||||
NEXT_PUBLIC_CLOUDFLARE_SITEKEY=
|
||||
CLOUDFLARE_TURNSTILE_SECRET=
|
||||
|
||||
# Set the following value to true if you wish to enable Team Impersonation
|
||||
NEXT_PUBLIC_TEAM_IMPERSONATION=false
|
||||
|
||||
|
||||
@@ -4,3 +4,4 @@ node_modules
|
||||
**/**/public
|
||||
packages/prisma/zod
|
||||
apps/web/public/embed
|
||||
packages/ui/components/icon/dynamicIconImports.tsx
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
name: Production Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
||||
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
|
||||
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
|
||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
|
||||
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
|
||||
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
|
||||
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
|
||||
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
|
||||
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
|
||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
|
||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build API V1
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 30
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
- run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
yarn turbo run build --filter=@calcom/api...
|
||||
shell: bash
|
||||
@@ -56,6 +56,13 @@ jobs:
|
||||
uses: ./.github/workflows/production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-api-v1:
|
||||
name: Production build
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/api-v1-production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-without-database:
|
||||
name: Production build (without database)
|
||||
needs: [changes]
|
||||
@@ -99,7 +106,7 @@ jobs:
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs: [changes, lint, type-check, test, build, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
needs: [changes, lint, type-check, test, build, build-api-v1, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
if: always()
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
|
||||
@@ -17,3 +17,4 @@ packages/prisma/zod
|
||||
packages/prisma/enums
|
||||
apps/web/public/embed
|
||||
apps/api/v2/swagger/documentation.json
|
||||
packages/ui/components/icon/dynamicIconImports.tsx
|
||||
|
||||
+13
-2
@@ -13,8 +13,6 @@ Contributions are what makes the open source community such an amazing place to
|
||||
- Issues from non-core members automatically receive the `🚨 needs approval` label.
|
||||
- We greatly value new feature ideas. To ensure consistency in the product's direction, they undergo review and approval.
|
||||
|
||||
|
||||
|
||||
## Priorities
|
||||
|
||||
<table>
|
||||
@@ -178,11 +176,15 @@ If you get errors, be sure to fix them before committing.
|
||||
Do not commit your `yarn.lock` unless you've made changes to the `package.json`. If you've already committed `yarn.lock` unintentionally, follow these steps to undo:
|
||||
|
||||
If your last commit has the `yarn.lock` file alongside other files and you only wish to uncommit the `yarn.lock`:
|
||||
|
||||
```bash
|
||||
git checkout HEAD~1 yarn.lock
|
||||
git commit -m "Revert yarn.lock changes"
|
||||
```
|
||||
|
||||
_NB_: You may have to bypass the pre-commit hook with by appending `--no-verify` to the git commit
|
||||
If you've pushed the commit with the `yarn.lock`:
|
||||
|
||||
1. Correct the commit locally using the above method.
|
||||
2. Carefully force push:
|
||||
|
||||
@@ -194,26 +196,35 @@ If `yarn.lock` was committed a while ago and there have been several commits sin
|
||||
|
||||
1. **Checkout a Previous Version**:
|
||||
- Find the commit hash before the `yarn.lock` was unintentionally committed. You can do this by viewing the Git log:
|
||||
|
||||
```bash
|
||||
git log yarn.lock
|
||||
```
|
||||
|
||||
- Once you have identified the commit hash, use it to checkout the previous version of `yarn.lock`:
|
||||
|
||||
```bash
|
||||
git checkout <commit_hash> yarn.lock
|
||||
```
|
||||
|
||||
2. **Commit the Reverted Version**:
|
||||
- After checking out the previous version of the `yarn.lock`, commit this change:
|
||||
|
||||
```bash
|
||||
git commit -m "Revert yarn.lock to its state before unintended changes"
|
||||
```
|
||||
|
||||
3. **Proceed with Caution**:
|
||||
- If you need to push this change, first pull the latest changes from your remote branch to ensure you're not overwriting other recent changes:
|
||||
|
||||
```bash
|
||||
git pull origin <your-branch-name>
|
||||
```
|
||||
|
||||
- Then push the updated branch:
|
||||
|
||||
```bash
|
||||
git push origin <your-branch-name>
|
||||
```
|
||||
|
||||
Lastly, make sure to keep the branches updated (e.g. click the `Update branch` button on GitHub PR).
|
||||
|
||||
@@ -113,7 +113,7 @@ To get a local copy up and running, please follow these simple steps.
|
||||
Here is what you need to be able to run Cal.com.
|
||||
|
||||
- Node.js (Version: >=18.x)
|
||||
- PostgreSQL
|
||||
- PostgreSQL (Version: >=13.x)
|
||||
- Yarn _(recommended)_
|
||||
|
||||
> If you want to enable any of the available integrations, you may want to obtain additional credentials for each one. More details on this can be found below under the [integrations section](#integrations).
|
||||
@@ -210,7 +210,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
||||

|
||||
|
||||
5. Now extract all the info and add it to your DATABASE_URL. The url would look something like this
|
||||
`postgresql://postgres:postgres@localhost:5432/Your-DB-Name`.
|
||||
`postgresql://postgres:postgres@localhost:5432/Your-DB-Name`. The port is configurable and does not have to be 5432.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { API_KEY_RATE_LIMIT } from "@calcom/lib/rateLimit";
|
||||
|
||||
export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
|
||||
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
|
||||
@@ -11,7 +10,7 @@ export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
|
||||
identifier: req.query.apiKey as string,
|
||||
rateLimitingType: "api",
|
||||
onRateLimiterResponse: (response) => {
|
||||
res.setHeader("X-RateLimit-Limit", API_KEY_RATE_LIMIT);
|
||||
res.setHeader("X-RateLimit-Limit", response.limit);
|
||||
res.setHeader("X-RateLimit-Remaining", response.remaining);
|
||||
res.setHeader("X-RateLimit-Reset", response.reset);
|
||||
},
|
||||
|
||||
@@ -93,6 +93,7 @@ async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
|
||||
await canAccessTeamEventOrThrow(req, {
|
||||
in: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER],
|
||||
});
|
||||
return true;
|
||||
}
|
||||
if (eventType?.userId === req.userId) return true; // is owner.
|
||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { purchaseTeamSubscription } from "@calcom/features/ee/teams/lib/payments";
|
||||
import { purchaseTeamOrOrgSubscription } from "@calcom/features/ee/teams/lib/payments";
|
||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
@@ -105,10 +105,11 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
};
|
||||
delete data.slug;
|
||||
if (IS_TEAM_BILLING_ENABLED) {
|
||||
const checkoutSession = await purchaseTeamSubscription({
|
||||
const checkoutSession = await purchaseTeamOrOrgSubscription({
|
||||
teamId: _team.id,
|
||||
seats: _team.members.length,
|
||||
userId,
|
||||
pricePerSeat: null,
|
||||
});
|
||||
if (!checkoutSession.url)
|
||||
throw new TRPCError({
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { NextApiRequest } from "next";
|
||||
|
||||
import { getStripeCustomerIdFromUserId } from "@calcom/app-store/stripepayment/lib/customer";
|
||||
import stripe from "@calcom/app-store/stripepayment/lib/server";
|
||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
@@ -197,8 +198,9 @@ const generateTeamCheckoutSession = async ({
|
||||
customer_update: {
|
||||
address: "auto",
|
||||
},
|
||||
// Disabled when testing locally as usually developer doesn't setup Tax in Stripe Test mode
|
||||
automatic_tax: {
|
||||
enabled: true,
|
||||
enabled: IS_PRODUCTION,
|
||||
},
|
||||
metadata: {
|
||||
pendingPaymentTeamId,
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"paths": {
|
||||
"~/*": ["*"],
|
||||
"@prisma/client/*": ["@calcom/prisma/client/*"]
|
||||
}
|
||||
},
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
|
||||
@@ -8,7 +8,7 @@ export class AppLoggerMiddleware implements NestMiddleware {
|
||||
private logger = new Logger("HTTP");
|
||||
|
||||
use(request: Request, response: Response, next: NextFunction): void {
|
||||
const { ip, method, protocol, originalUrl, path: url } = request;
|
||||
const { ip, method, originalUrl } = request;
|
||||
const userAgent = request.get("user-agent") || "";
|
||||
|
||||
response.on("close", () => {
|
||||
|
||||
@@ -11,7 +11,9 @@ const loadConfig = (): AppConfig => {
|
||||
port: Number(getEnv("API_PORT", "5555")),
|
||||
path: getEnv("API_URL", "http://localhost"),
|
||||
url: `${getEnv("API_URL", "http://localhost")}${
|
||||
process.env.API_PORT ? `:${Number(getEnv("API_PORT", "5555"))}` : ""
|
||||
process.env.API_PORT && getEnv("NODE_ENV", "development") === "development"
|
||||
? `:${Number(getEnv("API_PORT", "5555"))}`
|
||||
: ""
|
||||
}/api/v2`,
|
||||
},
|
||||
db: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
||||
import { IsBoolean, IsNumber, IsString, IsOptional, IsArray } from "class-validator";
|
||||
import { IsBoolean, IsNumber, IsOptional } from "class-validator";
|
||||
|
||||
import type { AppsStatus } from "@calcom/platform-libraries";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Controller, Get, Logger, UseGuards, Query } from "@nestjs/common";
|
||||
import { Controller, Get, UseGuards, Query } from "@nestjs/common";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { ConnectedDestinationCalendars } from "@calcom/platform-libraries";
|
||||
@@ -16,8 +16,6 @@ import { EventBusyDate } from "@calcom/types/Calendar";
|
||||
})
|
||||
@UseGuards(AccessTokenGuard)
|
||||
export class CalendarsController {
|
||||
private readonly logger = new Logger("ee overlay calendars controller");
|
||||
|
||||
constructor(private readonly calendarsService: CalendarsService) {}
|
||||
|
||||
@Get("/busy-times")
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
||||
import { CreateEventTypeOutput } from "@/ee/event-types/outputs/create-event-type.output";
|
||||
import { DeleteEventTypeOutput } from "@/ee/event-types/outputs/delete-event-type.output";
|
||||
import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output";
|
||||
import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-types-public.output";
|
||||
import { GetEventTypesOutput } from "@/ee/event-types/outputs/get-event-types.output";
|
||||
import { UpdateEventTypeOutput } from "@/ee/event-types/outputs/update-event-type.output";
|
||||
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
|
||||
import { ForAtom } from "@/lib/atoms/decorators/for-atom.decorator";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
@@ -22,17 +26,9 @@ import {
|
||||
Delete,
|
||||
} from "@nestjs/common";
|
||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { EventType } from "@prisma/client";
|
||||
|
||||
import { EventTypesByViewer } from "@calcom/lib";
|
||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type {
|
||||
EventType as AtomEventType,
|
||||
EventTypesPublic,
|
||||
UpdateEventTypeReturn,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { getEventTypesByViewer } from "@calcom/platform-libraries";
|
||||
import { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "event-types",
|
||||
@@ -63,12 +59,9 @@ export class EventTypesController {
|
||||
@UseGuards(AccessTokenGuard)
|
||||
async getEventType(
|
||||
@Param("eventTypeId") eventTypeId: string,
|
||||
@ForAtom() forAtom: boolean,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<ApiSuccessResponse<EventType | AtomEventType>> {
|
||||
const eventType = forAtom
|
||||
? await this.eventTypesService.getUserEventTypeForAtom(user, Number(eventTypeId))
|
||||
: await this.eventTypesService.getUserEventType(user.id, Number(eventTypeId));
|
||||
): Promise<GetEventTypeOutput> {
|
||||
const eventType = await this.eventTypesService.getUserEventTypeForAtom(user, Number(eventTypeId));
|
||||
|
||||
if (!eventType) {
|
||||
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||
@@ -83,7 +76,7 @@ export class EventTypesController {
|
||||
@Get("/")
|
||||
@Permissions([EVENT_TYPE_READ])
|
||||
@UseGuards(AccessTokenGuard)
|
||||
async getEventTypes(@GetUser() user: UserWithProfile): Promise<ApiSuccessResponse<EventTypesByViewer>> {
|
||||
async getEventTypes(@GetUser() user: UserWithProfile): Promise<GetEventTypesOutput> {
|
||||
const eventTypes = await getEventTypesByViewer({
|
||||
id: user.id,
|
||||
profile: {
|
||||
@@ -99,9 +92,7 @@ export class EventTypesController {
|
||||
|
||||
@Get("/:username/public")
|
||||
@Permissions([EVENT_TYPE_READ])
|
||||
async getPublicEventTypes(
|
||||
@Param("username") username: string
|
||||
): Promise<ApiSuccessResponse<EventTypesPublic>> {
|
||||
async getPublicEventTypes(@Param("username") username: string): Promise<GetEventTypesPublicOutput> {
|
||||
const eventTypes = await this.eventTypesService.getEventTypesPublicByUsername(username);
|
||||
|
||||
return {
|
||||
@@ -118,7 +109,7 @@ export class EventTypesController {
|
||||
@Param("eventTypeId") eventTypeId: number,
|
||||
@Body() body: UpdateEventTypeInput,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<ApiResponse<UpdateEventTypeReturn["eventType"]>> {
|
||||
): Promise<UpdateEventTypeOutput> {
|
||||
const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user);
|
||||
|
||||
return {
|
||||
@@ -133,12 +124,17 @@ export class EventTypesController {
|
||||
async deleteEventType(
|
||||
@Param("eventTypeId") eventTypeId: number,
|
||||
@GetUser("id") userId: number
|
||||
): Promise<ApiResponse<EventType>> {
|
||||
): Promise<DeleteEventTypeOutput> {
|
||||
const eventType = await this.eventTypesService.deleteEventType(eventTypeId, userId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: eventType,
|
||||
data: {
|
||||
id: eventType.id,
|
||||
length: eventType.length,
|
||||
slug: eventType.slug,
|
||||
title: eventType.title,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
CREATE_EVENT_LENGTH_EXAMPLE,
|
||||
CREATE_EVENT_SLUG_EXAMPLE,
|
||||
CREATE_EVENT_TITLE_EXAMPLE,
|
||||
} from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsInt, IsString } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class DeleteData {
|
||||
@IsInt()
|
||||
@DocsProperty({ example: 1 })
|
||||
id!: number;
|
||||
|
||||
@IsInt()
|
||||
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
||||
length!: number;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
|
||||
slug!: string;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
|
||||
title!: string;
|
||||
}
|
||||
|
||||
export class DeleteEventTypeOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Type(() => DeleteData)
|
||||
data!: DeleteData;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class Data {
|
||||
@ApiProperty({
|
||||
type: EventTypeOutput,
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => EventTypeOutput)
|
||||
eventType!: EventTypeOutput;
|
||||
}
|
||||
|
||||
export class GetEventTypeOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: Data,
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => Data)
|
||||
data!: Data;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
CREATE_EVENT_LENGTH_EXAMPLE,
|
||||
CREATE_EVENT_SLUG_EXAMPLE,
|
||||
CREATE_EVENT_TITLE_EXAMPLE,
|
||||
} from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsArray, IsEnum, IsInt, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class PublicEventType {
|
||||
@IsInt()
|
||||
@DocsProperty({ example: 1 })
|
||||
id!: number;
|
||||
|
||||
@IsInt()
|
||||
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
||||
length!: number;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
|
||||
slug!: string;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export class GetEventTypesPublicOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => PublicEventType)
|
||||
@IsArray()
|
||||
data!: PublicEventType[];
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsArray, IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class EventTypeGroup {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeOutput)
|
||||
@IsArray()
|
||||
eventTypes!: EventTypeOutput[];
|
||||
}
|
||||
|
||||
class GetEventTypesData {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeGroup)
|
||||
@IsArray()
|
||||
eventTypeGroups!: EventTypeGroup[];
|
||||
}
|
||||
|
||||
export class GetEventTypesOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => GetEventTypesData)
|
||||
@IsArray()
|
||||
data!: GetEventTypesData;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class UpdateEventTypeOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: EventTypeOutput,
|
||||
})
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
@Type(() => EventTypeOutput)
|
||||
data!: EventTypeOutput;
|
||||
}
|
||||
@@ -101,11 +101,7 @@ export class EventTypesService {
|
||||
}
|
||||
|
||||
async createUserDefaultEventTypes(userId: number) {
|
||||
const thirtyMinutes = DEFAULT_EVENT_TYPES.thirtyMinutes;
|
||||
const thirtyMinutesVideo = DEFAULT_EVENT_TYPES.thirtyMinutesVideo;
|
||||
|
||||
const sixtyMinutes = DEFAULT_EVENT_TYPES.sixtyMinutes;
|
||||
const sixtyMinutesVideo = DEFAULT_EVENT_TYPES.sixtyMinutesVideo;
|
||||
const { sixtyMinutes, sixtyMinutesVideo, thirtyMinutes, thirtyMinutesVideo } = DEFAULT_EVENT_TYPES;
|
||||
|
||||
const defaultEventTypes = await Promise.all([
|
||||
this.eventTypesRepository.createUserEventType(userId, thirtyMinutes),
|
||||
@@ -120,7 +116,7 @@ export class EventTypesService {
|
||||
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput, user: UserWithProfile) {
|
||||
this.checkCanUpdateEventType(user.id, eventTypeId);
|
||||
const eventTypeUser = await this.getUserToUpdateEvent(user);
|
||||
const { eventType } = await updateEventType({
|
||||
await updateEventType({
|
||||
input: { id: eventTypeId, ...body },
|
||||
ctx: {
|
||||
user: eventTypeUser,
|
||||
@@ -130,6 +126,8 @@ export class EventTypesService {
|
||||
},
|
||||
});
|
||||
|
||||
const { eventType } = await this.getUserEventTypeForAtom(user, eventTypeId);
|
||||
|
||||
return eventType;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ describe("Platform Gcal Endpoints", () => {
|
||||
let gcalCredentials: Credential;
|
||||
let accessTokenSecret: string;
|
||||
let refreshTokenSecret: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { GcalService } from "@/modules/apps/services/gcal.service";
|
||||
import { GCalService } from "@/modules/apps/services/gcal.service";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
@@ -47,12 +46,11 @@ export class GcalController {
|
||||
private readonly logger = new Logger("Platform Gcal Provider");
|
||||
|
||||
constructor(
|
||||
private readonly appRepository: AppsRepository,
|
||||
private readonly credentialRepository: CredentialsRepository,
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
||||
private readonly config: ConfigService,
|
||||
private readonly gcalService: GcalService
|
||||
private readonly gcalService: GCalService
|
||||
) {}
|
||||
|
||||
private redirectUri = `${this.config.get("api.url")}/ee/gcal/oauth/save`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { GcalController } from "@/ee/gcal/gcal.controller";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { GcalService } from "@/modules/apps/services/gcal.service";
|
||||
import { GCalService } from "@/modules/apps/services/gcal.service";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
@@ -11,7 +11,7 @@ import { ConfigService } from "@nestjs/config";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, TokensModule, OAuthClientModule],
|
||||
providers: [AppsRepository, ConfigService, CredentialsRepository, SelectedCalendarsRepository, GcalService],
|
||||
providers: [AppsRepository, ConfigService, CredentialsRepository, SelectedCalendarsRepository, GCalService],
|
||||
controllers: [GcalController],
|
||||
})
|
||||
export class GcalModule {}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
BadRequestException,
|
||||
@@ -9,7 +8,6 @@ import {
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
Param,
|
||||
UnauthorizedException,
|
||||
@@ -24,12 +22,7 @@ import { ApiResponse } from "@calcom/platform-types";
|
||||
version: "2",
|
||||
})
|
||||
export class CalProviderController {
|
||||
private readonly logger = new Logger("Platform Provider Controller");
|
||||
|
||||
constructor(
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly oauthClientRepository: OAuthClientRepository
|
||||
) {}
|
||||
constructor(private readonly oauthClientRepository: OAuthClientRepository) {}
|
||||
|
||||
@Get("/:clientId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { Response } from "@calcom/platform-types";
|
||||
@@ -10,11 +11,14 @@ export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
|
||||
catch(exception: HttpException, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest();
|
||||
const request = ctx.getRequest<Request>();
|
||||
const statusCode = exception.getStatus();
|
||||
this.logger.error(`Http Exception Filter: ${exception?.message}`, {
|
||||
exception,
|
||||
request,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
});
|
||||
response.status(statusCode).json({
|
||||
status: ERROR_STATUS,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
PrismaClientUnknownRequestError,
|
||||
PrismaClientValidationError,
|
||||
} from "@prisma/client/runtime/library";
|
||||
import { Request } from "express";
|
||||
|
||||
import { ERROR_STATUS, INTERNAL_SERVER_ERROR } from "@calcom/platform-constants";
|
||||
import { Response } from "@calcom/platform-types";
|
||||
@@ -31,10 +32,13 @@ export class PrismaExceptionFilter implements ExceptionFilter {
|
||||
catch(error: PrismaError, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest();
|
||||
const request = ctx.getRequest<Request>();
|
||||
this.logger.error(`PrismaError: ${error.message}`, {
|
||||
error,
|
||||
request,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
});
|
||||
response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
|
||||
status: ERROR_STATUS,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ArgumentsHost, Catch, Logger, HttpStatus } from "@nestjs/common";
|
||||
import { BaseExceptionFilter } from "@nestjs/core";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import { Request } from "express";
|
||||
|
||||
import { ERROR_STATUS, INTERNAL_SERVER_ERROR } from "@calcom/platform-constants";
|
||||
import { Response } from "@calcom/platform-types";
|
||||
@@ -12,11 +13,14 @@ export class SentryFilter extends BaseExceptionFilter {
|
||||
handleUnknownError(exception: any, host: ArgumentsHost): void {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
this.logger.error(`Sentry Exception Filter: ${exception?.message}`, {
|
||||
exception,
|
||||
request,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
});
|
||||
|
||||
// capture if client has been init
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { TRPCError } from "@calcom/platform-libraries";
|
||||
@@ -11,7 +12,8 @@ export class TRPCExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: TRPCError, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
let statusCode = 500;
|
||||
switch (exception.code) {
|
||||
case "UNAUTHORIZED":
|
||||
@@ -38,10 +40,15 @@ export class TRPCExceptionFilter implements ExceptionFilter {
|
||||
statusCode = 500;
|
||||
break;
|
||||
}
|
||||
|
||||
this.logger.error(`TRPC Exception Filter: ${exception?.message}`, {
|
||||
exception,
|
||||
request,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
});
|
||||
|
||||
response.status(statusCode).json({
|
||||
status: ERROR_STATUS,
|
||||
timestamp: new Date().toISOString(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
|
||||
import { Catch, HttpStatus, Logger } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { BAD_REQUEST, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
@@ -12,11 +13,16 @@ export class ZodExceptionFilter implements ExceptionFilter {
|
||||
catch(error: ZodError, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
this.logger.error(`ZodError: ${error.message}`, {
|
||||
error,
|
||||
request,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
});
|
||||
|
||||
response.status(HttpStatus.BAD_REQUEST).json({
|
||||
status: ERROR_STATUS,
|
||||
timestamp: new Date().toISOString(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
|
||||
|
||||
export const ForAtom = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
|
||||
export const ForAtom = createParamDecorator((_data: unknown, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.query.for === "atom";
|
||||
});
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
|
||||
export class BaseStrategy {
|
||||
success!: (user: unknown) => void;
|
||||
error!: (error: Error) => void;
|
||||
}
|
||||
|
||||
export class NextAuthPassportStrategy {
|
||||
success!: (user: UserWithProfile) => void;
|
||||
error!: (error: Error) => void;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ import { google } from "googleapis";
|
||||
import { z } from "zod";
|
||||
|
||||
@Injectable()
|
||||
export class GcalService {
|
||||
export class GCalService {
|
||||
private logger = new Logger("GcalService");
|
||||
|
||||
private gcalResponseSchema = z.object({ client_id: z.string(), client_secret: z.string() });
|
||||
|
||||
constructor(private readonly appsRepository: AppsRepository) {}
|
||||
|
||||
async getOAuthClient(redirectUri: string) {
|
||||
@@ -17,9 +19,7 @@ export class GcalService {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
const { client_id, client_secret } = z
|
||||
.object({ client_id: z.string(), client_secret: z.string() })
|
||||
.parse(app.keys);
|
||||
const { client_id, client_secret } = this.gcalResponseSchema.parse(app.keys);
|
||||
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
|
||||
return oAuth2Client;
|
||||
|
||||
@@ -38,7 +38,7 @@ export class OrganizationRolesGuard implements CanActivate {
|
||||
async isPlatform(organizationId: number) {
|
||||
const isPlatform = await this.organizationsService.isPlatform(organizationId);
|
||||
if (!isPlatform) {
|
||||
throw new ForbiddenException("Organization is not a platform.");
|
||||
throw new ForbiddenException("Organization is not a platform (SHP).");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
||||
import { NextAuthPassportStrategy } from "@/lib/passport/strategies/types";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
@@ -7,7 +7,7 @@ import type { Request } from "express";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
|
||||
@Injectable()
|
||||
export class NextAuthStrategy extends PassportStrategy(BaseStrategy, "next-auth") {
|
||||
export class NextAuthStrategy extends PassportStrategy(NextAuthPassportStrategy, "next-auth") {
|
||||
constructor(private readonly userRepository: UsersRepository, private readonly config: ConfigService) {
|
||||
super();
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export class NextAuthStrategy extends PassportStrategy(BaseStrategy, "next-auth"
|
||||
throw new UnauthorizedException("Email not found in the authentication token.");
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findByEmail(payload.email);
|
||||
const user = await this.userRepository.findByEmailWithProfile(payload.email);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException("User associated with the authentication token email not found.");
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ export class EventsController {
|
||||
queryParams.eventSlug,
|
||||
queryParams.isTeamEvent,
|
||||
queryParams.org || null,
|
||||
this.prismaReadService.prisma as unknown as PrismaClient
|
||||
this.prismaReadService.prisma as unknown as PrismaClient,
|
||||
// We should be fine allowing unpublished orgs events to be servable through platform because Platform access is behind license
|
||||
// If there is ever a need to restrict this, we can introduce a new query param `fromRedirectOfNonOrgLink`
|
||||
true
|
||||
);
|
||||
return {
|
||||
data: event,
|
||||
|
||||
+15
@@ -179,6 +179,21 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
it(`/GET: return list of managed users`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/oauth-clients/${oAuthClient.id}/users?limit=10&offset=0`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.set("Origin", `${CLIENT_REDIRECT_URI}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<UserReturned[]> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toBeGreaterThan(0);
|
||||
expect(responseBody.data[0].email).toEqual(postResponseData.user.email);
|
||||
});
|
||||
|
||||
it(`/GET/:id`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
|
||||
|
||||
+23
-1
@@ -20,11 +20,12 @@ import {
|
||||
Patch,
|
||||
BadRequestException,
|
||||
Delete,
|
||||
Query,
|
||||
} from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
import { ApiResponse, Pagination } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "oauth-clients/:clientId/users",
|
||||
@@ -39,6 +40,27 @@ export class OAuthClientUsersController {
|
||||
private readonly oauthRepository: OAuthClientRepository
|
||||
) {}
|
||||
|
||||
@Get("/")
|
||||
@UseGuards(OAuthClientCredentialsGuard)
|
||||
async getManagedUsers(
|
||||
@Param("clientId") oAuthClientId: string,
|
||||
@Query() queryParams: Pagination
|
||||
): Promise<ApiResponse<User[]>> {
|
||||
this.logger.log(`getting managed users with data for OAuth Client with ID ${oAuthClientId}`);
|
||||
const { offset, limit } = queryParams;
|
||||
|
||||
const existingUsers = await this.userRepository.findManagedUsersByOAuthClientId(
|
||||
oAuthClientId,
|
||||
offset ?? 0,
|
||||
limit ?? 50
|
||||
);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: existingUsers,
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/")
|
||||
@UseGuards(OAuthClientCredentialsGuard)
|
||||
async createUser(
|
||||
|
||||
+6
-4
@@ -8,6 +8,7 @@ import { GetOAuthClientResponseDto } from "@/modules/oauth-clients/controllers/o
|
||||
import { GetOAuthClientsResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientsResponse.dto";
|
||||
import { UpdateOAuthClientInput } from "@/modules/oauth-clients/inputs/update-oauth-client.input";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
@@ -57,9 +58,10 @@ export class OAuthClientsController {
|
||||
type: CreateOAuthClientResponseDto,
|
||||
})
|
||||
async createOAuthClient(
|
||||
@GetUser("organizationId") organizationId: number,
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Body() body: CreateOAuthClientInput
|
||||
): Promise<CreateOAuthClientResponseDto> {
|
||||
const organizationId = (user.movedToProfile?.organizationId ?? user.organizationId) as number;
|
||||
this.logger.log(
|
||||
`For organisation ${organizationId} creating OAuth Client with data: ${JSON.stringify(body)}`
|
||||
);
|
||||
@@ -77,9 +79,9 @@ export class OAuthClientsController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Roles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
async getOAuthClients(
|
||||
@GetUser("organizationId") organizationId: number
|
||||
): Promise<GetOAuthClientsResponseDto> {
|
||||
async getOAuthClients(@GetUser() user: UserWithProfile): Promise<GetOAuthClientsResponseDto> {
|
||||
const organizationId = (user.movedToProfile?.organizationId ?? user.organizationId) as number;
|
||||
|
||||
const clients = await this.oauthClientRepository.getOrganizationOAuthClients(organizationId);
|
||||
return { status: SUCCESS_STATUS, data: clients };
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { EventTypesService } from "@/ee/event-types/services/event-types.service
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { CreateManagedPlatformUserInput } from "@/modules/users/inputs/create-managed-platform-user.input";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
import * as crypto from "crypto";
|
||||
|
||||
@@ -24,24 +24,24 @@ export class OAuthClientUsersService {
|
||||
) {
|
||||
let user: User;
|
||||
if (!organizationId) {
|
||||
const username = generateShortHash(body.email, oAuthClientId);
|
||||
user = await this.userRepository.create(body, username, oAuthClientId, isPlatformManaged);
|
||||
throw new BadRequestException("You cannot create a managed user outside of an organization");
|
||||
} else {
|
||||
const [_, emailDomain] = body.email.split("@");
|
||||
const [username, emailDomain] = body.email.split("@");
|
||||
const email = `${username}+${oAuthClientId}@${emailDomain}`;
|
||||
user = (
|
||||
await createNewUsersConnectToOrgIfExists({
|
||||
usernamesOrEmails: [body.email],
|
||||
usernamesOrEmails: [email],
|
||||
input: {
|
||||
teamId: organizationId,
|
||||
role: "MEMBER",
|
||||
usernameOrEmail: [body.email],
|
||||
usernameOrEmail: [email],
|
||||
isOrg: true,
|
||||
language: "en",
|
||||
},
|
||||
parentId: null,
|
||||
autoAcceptEmailDomain: emailDomain,
|
||||
autoAcceptEmailDomain: "never-auto-accept-email-domain-for-managed-users",
|
||||
connectionInfoMap: {
|
||||
[body.email]: {
|
||||
[email]: {
|
||||
orgId: organizationId,
|
||||
autoAccept: true,
|
||||
},
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { TokenExpiredException } from "@/modules/auth/guards/access-token/token-expired.exception";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { BadRequestException, Injectable, Logger, UnauthorizedException } from "@nestjs/common";
|
||||
import { BadRequestException, Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
|
||||
import { INVALID_ACCESS_TOKEN } from "@calcom/platform-constants";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthFlowService {
|
||||
private logger = new Logger("OAuthFlowService");
|
||||
|
||||
constructor(
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly oAuthClientRepository: OAuthClientRepository
|
||||
) //private readonly redisService: RedisIOService
|
||||
{}
|
||||
private readonly oAuthClientRepository: OAuthClientRepository //private readonly redisService: RedisIOService
|
||||
) {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async propagateAccessToken(accessToken: string) {
|
||||
// this.logger.log("Propagating access token to redis", accessToken);
|
||||
// TODO propagate
|
||||
@@ -48,6 +46,7 @@ export class OAuthFlowService {
|
||||
return true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private async readFromCache(secret: string) {
|
||||
return { status: "CACHE_MISS" };
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { PrismaClient } from "@prisma/client";
|
||||
export class PrismaReadService implements OnModuleInit {
|
||||
public prisma: PrismaClient;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
constructor(readonly configService: ConfigService) {
|
||||
const dbUrl = configService.get("db.readUrl", { infer: true });
|
||||
|
||||
this.prisma = new PrismaClient({
|
||||
|
||||
@@ -7,7 +7,7 @@ import { PrismaClient } from "@prisma/client";
|
||||
export class PrismaWriteService implements OnModuleInit {
|
||||
public prisma: PrismaClient;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
constructor(readonly configService: ConfigService) {
|
||||
const dbUrl = configService.get("db.writeUrl", { infer: true });
|
||||
|
||||
this.prisma = new PrismaClient({
|
||||
|
||||
@@ -101,7 +101,7 @@ export class TokensRepository {
|
||||
}
|
||||
|
||||
async refreshOAuthTokens(clientId: string, refreshTokenSecret: string, tokenUserId: number) {
|
||||
const accessExpiry = DateTime.now().plus({ minute: 1 }).startOf("minute").toJSDate();
|
||||
const accessExpiry = DateTime.now().plus({ minute: 60 }).startOf("minute").toJSDate();
|
||||
const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
@@ -83,6 +83,17 @@ export class UsersRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async findByEmailWithProfile(email: string) {
|
||||
return this.dbRead.prisma.user.findUnique({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
include: {
|
||||
movedToProfile: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findByUsername(username: string) {
|
||||
return this.dbRead.prisma.user.findFirst({
|
||||
where: {
|
||||
@@ -91,6 +102,21 @@ export class UsersRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async findManagedUsersByOAuthClientId(oauthClientId: string, cursor: number, limit: number) {
|
||||
return this.dbRead.prisma.user.findMany({
|
||||
where: {
|
||||
platformOAuthClients: {
|
||||
some: {
|
||||
id: oauthClientId,
|
||||
},
|
||||
},
|
||||
isPlatformManaged: true,
|
||||
},
|
||||
take: limit,
|
||||
skip: cursor,
|
||||
});
|
||||
}
|
||||
|
||||
async update(userId: number, updateData: UpdateManagedPlatformUserInput) {
|
||||
this.formatInput(updateData);
|
||||
|
||||
|
||||
@@ -71,6 +71,31 @@
|
||||
}
|
||||
},
|
||||
"/api/v2/oauth-clients/{clientId}/users": {
|
||||
"get": {
|
||||
"operationId": "OAuthClientUsersController_getManagedUsers",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "clientId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"operationId": "OAuthClientUsersController_createUser",
|
||||
"parameters": [
|
||||
@@ -552,7 +577,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"$ref": "#/components/schemas/GetEventTypesOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -582,7 +607,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"$ref": "#/components/schemas/GetEventTypeOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -620,7 +645,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"$ref": "#/components/schemas/UpdateEventTypeOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -648,7 +673,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"$ref": "#/components/schemas/DeleteEventTypeOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -678,7 +703,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"$ref": "#/components/schemas/GetEventTypesPublicOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1720,6 +1745,139 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"Data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"eventType": {
|
||||
"$ref": "#/components/schemas/EventTypeOutput"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"eventType"
|
||||
]
|
||||
},
|
||||
"GetEventTypeOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/Data"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"EventTypeGroup": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"eventTypes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EventTypeOutput"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"eventTypes"
|
||||
]
|
||||
},
|
||||
"GetEventTypesData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"eventTypeGroups": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EventTypeGroup"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"eventTypeGroups"
|
||||
]
|
||||
},
|
||||
"GetEventTypesOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/GetEventTypesData"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"PublicEventType": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"length": {
|
||||
"type": "number",
|
||||
"example": 60
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"example": "cooking-class"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Learn the secrets of masterchief!"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"length",
|
||||
"slug",
|
||||
"title"
|
||||
]
|
||||
},
|
||||
"GetEventTypesPublicOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PublicEventType"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"UpdateEventTypeInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1747,6 +1905,73 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdateEventTypeOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/EventTypeOutput"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"DeleteData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"length": {
|
||||
"type": "number",
|
||||
"example": 60
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"example": "cooking-class"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Learn the secrets of masterchief!"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"length",
|
||||
"slug",
|
||||
"title"
|
||||
]
|
||||
},
|
||||
"DeleteEventTypeOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/DeleteData"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"CreateAvailabilityInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
||||
import { NextAuthPassportStrategy } from "@/lib/passport/strategies/types";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
|
||||
@Injectable()
|
||||
export class NextAuthMockStrategy extends PassportStrategy(BaseStrategy, "next-auth") {
|
||||
export class NextAuthMockStrategy extends PassportStrategy(NextAuthPassportStrategy, "next-auth") {
|
||||
constructor(private readonly email: string, private readonly userRepository: UsersRepository) {
|
||||
super();
|
||||
}
|
||||
async authenticate() {
|
||||
try {
|
||||
const user = await this.userRepository.findByEmail(this.email);
|
||||
const user = await this.userRepository.findByEmailWithProfile(this.email);
|
||||
if (!user) {
|
||||
throw new Error("User with the provided email not found");
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ const env: Partial<Omit<Environment, "NODE_ENV">> = {
|
||||
LOG_LEVEL: "trace",
|
||||
REDIS_URL: "redis://localhost:9199",
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
process.env = {
|
||||
...env,
|
||||
...process.env,
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import LegacyPage, { WrappedSetPasswordPage } from "@pages/settings/organizations/[id]/set-password";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("set_a_password"),
|
||||
(t) => t("set_a_password_description")
|
||||
);
|
||||
|
||||
export default WithLayout({ Page: LegacyPage, getLayout: WrappedSetPasswordPage });
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { X } from "@calcom/ui/components/icon";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
export default function AddToHomescreen() {
|
||||
const { t } = useLocale();
|
||||
@@ -40,7 +40,7 @@ export default function AddToHomescreen() {
|
||||
type="button"
|
||||
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
|
||||
<span className="sr-only">{t("dismiss")}</span>
|
||||
<X className="text-inverted h-6 w-6 dark:text-white" aria-hidden="true" />
|
||||
<Icon name="x" className="text-inverted h-6 w-6 dark:text-white" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,8 +9,7 @@ import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { Badge, ListItemText, Avatar } from "@calcom/ui";
|
||||
import { AlertCircle } from "@calcom/ui/components/icon";
|
||||
import { Avatar, Badge, Icon, ListItemText } from "@calcom/ui";
|
||||
|
||||
type ShouldHighlight =
|
||||
| {
|
||||
@@ -105,7 +104,7 @@ export default function AppListCard(props: AppListCardProps) {
|
||||
<ListItemText component="p">{description}</ListItemText>
|
||||
{invalidCredential && (
|
||||
<div className="flex gap-x-2 pt-2">
|
||||
<AlertCircle className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
|
||||
<Icon name="circle-alert" className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
|
||||
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
||||
{t("invalid_credential")}
|
||||
</ListItemText>
|
||||
|
||||
@@ -3,40 +3,39 @@
|
||||
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||
import { UpgradeTip } from "@calcom/features/tips";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, ButtonGroup } from "@calcom/ui";
|
||||
import { BarChart, CreditCard, Globe, Lock, Paintbrush, Users } from "@calcom/ui/components/icon";
|
||||
import { Button, ButtonGroup, Icon } from "@calcom/ui";
|
||||
|
||||
export default function EnterprisePage() {
|
||||
const { t } = useLocale();
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <Globe className="h-5 w-5 text-red-500" />,
|
||||
icon: <Icon name="globe" className="h-5 w-5 text-red-500" />,
|
||||
title: t("branded_subdomain"),
|
||||
description: t("branded_subdomain_description"),
|
||||
},
|
||||
{
|
||||
icon: <BarChart className="h-5 w-5 text-blue-500" />,
|
||||
icon: <Icon name="bar-chart" className="h-5 w-5 text-blue-500" />,
|
||||
title: t("org_insights"),
|
||||
description: t("org_insights_description"),
|
||||
},
|
||||
{
|
||||
icon: <Paintbrush className="h-5 w-5 text-pink-500" />,
|
||||
icon: <Icon name="paintbrush" className="h-5 w-5 text-pink-500" />,
|
||||
title: t("extensive_whitelabeling"),
|
||||
description: t("extensive_whitelabeling_description"),
|
||||
},
|
||||
{
|
||||
icon: <Users className="h-5 w-5 text-orange-500" />,
|
||||
icon: <Icon name="users" className="h-5 w-5 text-orange-500" />,
|
||||
title: t("unlimited_teams"),
|
||||
description: t("unlimited_teams_description"),
|
||||
},
|
||||
{
|
||||
icon: <CreditCard className="h-5 w-5 text-green-500" />,
|
||||
icon: <Icon name="credit-card" className="h-5 w-5 text-green-500" />,
|
||||
title: t("unified_billing"),
|
||||
description: t("unified_billing_description"),
|
||||
},
|
||||
{
|
||||
icon: <Lock className="h-5 w-5 text-purple-500" />,
|
||||
icon: <Icon name="lock" className="h-5 w-5 text-purple-500" />,
|
||||
title: t("advanced_managed_events"),
|
||||
description: t("advanced_managed_events_description"),
|
||||
},
|
||||
|
||||
@@ -6,12 +6,11 @@ import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuItem,
|
||||
DropdownItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@calcom/ui";
|
||||
import { Plus } from "@calcom/ui/components/icon";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
@@ -42,7 +41,7 @@ const AdditionalCalendarSelector = ({ isPending }: AdditionalCalendarSelectorPro
|
||||
return (
|
||||
<Dropdown modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button StartIcon={Plus} color="secondary" {...(isPending && { loading: isPending })}>
|
||||
<Button StartIcon="plus" color="secondary" {...(isPending && { loading: isPending })}>
|
||||
{t("add")}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -50,7 +49,7 @@ const AdditionalCalendarSelector = ({ isPending }: AdditionalCalendarSelectorPro
|
||||
{options.map((data) => (
|
||||
<DropdownMenuItem key={data.slug} className="focus:outline-none">
|
||||
{data.slug === "add-new" ? (
|
||||
<DropdownItem StartIcon={Plus} color="minimal" href="/apps/categories/calendar">
|
||||
<DropdownItem StartIcon="plus" color="minimal" href="/apps/categories/calendar">
|
||||
{t("install_new_calendar_app")}
|
||||
</DropdownItem>
|
||||
) : (
|
||||
|
||||
@@ -11,17 +11,16 @@ import type { AppCategories } from "@calcom/prisma/enums";
|
||||
import { trpc, type RouterOutputs } from "@calcom/trpc";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
List,
|
||||
showToast,
|
||||
Button,
|
||||
DropdownMenuItem,
|
||||
Alert,
|
||||
} from "@calcom/ui";
|
||||
import { MoreHorizontal, Trash, Video } from "@calcom/ui/components/icon";
|
||||
|
||||
import AppListCard from "@components/AppListCard";
|
||||
|
||||
@@ -82,7 +81,7 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL
|
||||
<div className="flex justify-end">
|
||||
<Dropdown modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button StartIcon={MoreHorizontal} variant="icon" color="secondary" />
|
||||
<Button StartIcon="ellipsis" variant="icon" color="secondary" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{!appIsDefault && variant === "conferencing" && !item.credentialOwner?.teamId && (
|
||||
@@ -90,7 +89,7 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color="secondary"
|
||||
StartIcon={Video}
|
||||
StartIcon="video"
|
||||
onClick={() => {
|
||||
const locationType = getEventLocationTypeFromApp(item?.locationOption?.value ?? "");
|
||||
if (locationType?.linkType === "static") {
|
||||
@@ -214,7 +213,7 @@ function ConnectOrDisconnectIntegrationMenuItem(props: {
|
||||
color="destructive"
|
||||
onClick={() => handleDisconnect(credentialId, teamId)}
|
||||
disabled={isGlobal}
|
||||
StartIcon={Trash}>
|
||||
StartIcon="trash">
|
||||
{t("remove_app")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import type { IframeHTMLAttributes } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||
import { AppDependencyComponent, InstallAppButton } from "@calcom/app-store/components";
|
||||
@@ -10,8 +10,7 @@ import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/consta
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { App as AppType } from "@calcom/types/App";
|
||||
import { Badge, Button, showToast, SkeletonButton, SkeletonText } from "@calcom/ui";
|
||||
import { BookOpen, Check, ExternalLink, File, Flag, Mail, Shield } from "@calcom/ui/components/icon";
|
||||
import { Badge, Button, Icon, SkeletonButton, SkeletonText, showToast } from "@calcom/ui";
|
||||
|
||||
import { InstallAppButtonChild } from "./InstallAppButtonChild";
|
||||
|
||||
@@ -196,7 +195,7 @@ export const AppPage = ({
|
||||
isGlobal ||
|
||||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
|
||||
<div className="flex space-x-3">
|
||||
<Button StartIcon={Check} color="secondary" disabled>
|
||||
<Button StartIcon="check" color="secondary" disabled>
|
||||
{existingCredentials.length > 0
|
||||
? t("active_install", { count: existingCredentials.length })
|
||||
: t("default")}
|
||||
@@ -323,7 +322,7 @@ export const AppPage = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis text-sm font-normal no-underline hover:underline"
|
||||
href={docs}>
|
||||
<BookOpen className="text-subtle -mt-1 mr-1 inline h-4 w-4" />
|
||||
<Icon name="book-open" className="text-subtle -mt-1 mr-1 inline h-4 w-4" />
|
||||
{t("documentation")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -335,7 +334,7 @@ export const AppPage = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
href={website}>
|
||||
<ExternalLink className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
<Icon name="external-link" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
{website.replace("https://", "")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -347,7 +346,7 @@ export const AppPage = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
href={`mailto:${email}`}>
|
||||
<Mail className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
<Icon name="mail" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
|
||||
{email}
|
||||
</a>
|
||||
@@ -360,7 +359,7 @@ export const AppPage = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
href={tos}>
|
||||
<File className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
<Icon name="file" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
{t("terms_of_service")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -372,7 +371,7 @@ export const AppPage = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
href={privacy}>
|
||||
<Shield className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
<Icon name="shield" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
{t("privacy_policy")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -383,7 +382,7 @@ export const AppPage = ({
|
||||
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
|
||||
</span>
|
||||
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
|
||||
<Flag className="inline h-3 w-3" /> {t("report_app")}
|
||||
<Icon name="flag" className="inline h-3 w-3" /> {t("report_app")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,13 +11,12 @@ import {
|
||||
Alert,
|
||||
Button,
|
||||
EmptyScreen,
|
||||
List,
|
||||
AppSkeletonLoader as SkeletonLoader,
|
||||
ShellSubHeading,
|
||||
Label,
|
||||
List,
|
||||
ShellSubHeading,
|
||||
AppSkeletonLoader as SkeletonLoader,
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
import { Calendar } from "@calcom/ui/components/icon";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
@@ -267,7 +266,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
</>
|
||||
) : (
|
||||
<EmptyScreen
|
||||
Icon={Calendar}
|
||||
Icon="calendar"
|
||||
headline={t("no_category_apps", {
|
||||
category: t("calendar").toLowerCase(),
|
||||
})}
|
||||
|
||||
@@ -122,14 +122,13 @@ export const InstallAppButtonChild = ({
|
||||
data-testid={team.isUser ? "install-app-button-personal" : "anything else"}
|
||||
key={team.id}
|
||||
disabled={isInstalled}
|
||||
StartIcon={(props: { className?: string }) => (
|
||||
CustomStartIcon={
|
||||
<Avatar
|
||||
alt={team.logo || ""}
|
||||
imageSrc={team.logo || `${WEBAPP_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
|
||||
size="sm"
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
}
|
||||
onClick={() => {
|
||||
mutation.mutate(
|
||||
team.isUser ? addAppMutationInput : { ...addAppMutationInput, teamId: team.id }
|
||||
|
||||
@@ -6,7 +6,6 @@ import React from "react";
|
||||
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { EmptyScreen } from "@calcom/ui";
|
||||
import { AlertCircle } from "@calcom/ui/components/icon";
|
||||
|
||||
type AppsLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -27,7 +26,7 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
|
||||
<main className="w-full">
|
||||
{emptyStore ? (
|
||||
<EmptyScreen
|
||||
Icon={AlertCircle}
|
||||
Icon="circle-alert"
|
||||
headline={isAdmin ? t("no_apps") : t("no_apps_configured")}
|
||||
description={isAdmin ? t("enable_in_settings") : t("please_contact_admin")}
|
||||
buttonText={isAdmin ? t("apps_settings") : ""}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { TurnstileProps } from "react-turnstile";
|
||||
import Turnstile from "react-turnstile";
|
||||
|
||||
import { CLOUDFLARE_SITE_ID } from "@calcom/lib/constants";
|
||||
|
||||
type Props = Omit<TurnstileProps, "sitekey">;
|
||||
|
||||
export default function TurnstileWidget(props: Props) {
|
||||
if (!CLOUDFLARE_SITE_ID || process.env.NEXT_PUBLIC_IS_E2E) return null;
|
||||
|
||||
return <Turnstile {...props} sitekey={CLOUDFLARE_SITE_ID} theme="auto" />;
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
import React from "react";
|
||||
|
||||
import { Button, SkeletonText } from "@calcom/ui";
|
||||
import { MoreHorizontal } from "@calcom/ui/components/icon";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
|
||||
@@ -31,7 +28,7 @@ function SkeletonItem() {
|
||||
type="button"
|
||||
variant="icon"
|
||||
color="secondary"
|
||||
StartIcon={MoreHorizontal}
|
||||
StartIcon="ellipsis"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -29,13 +29,13 @@ import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
Icon,
|
||||
MeetingTimeInTimezones,
|
||||
showToast,
|
||||
TableActions,
|
||||
TextAreaField,
|
||||
Tooltip,
|
||||
} from "@calcom/ui";
|
||||
import { Ban, Check, Clock, CreditCard, MapPin, RefreshCcw, Send, X } from "@calcom/ui/components/icon";
|
||||
|
||||
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
|
||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||
@@ -138,7 +138,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
onClick: () => {
|
||||
setRejectionDialogIsOpen(true);
|
||||
},
|
||||
icon: Ban,
|
||||
icon: "ban",
|
||||
disabled: mutation.isPending,
|
||||
},
|
||||
// For bookings with payment, only confirm if the booking is paid for
|
||||
@@ -152,7 +152,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
onClick: () => {
|
||||
bookingConfirm(true);
|
||||
},
|
||||
icon: Check,
|
||||
icon: "check" as const,
|
||||
disabled: mutation.isPending,
|
||||
},
|
||||
]
|
||||
@@ -169,7 +169,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
||||
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
|
||||
`,
|
||||
icon: X,
|
||||
icon: "x" as const,
|
||||
},
|
||||
{
|
||||
id: "edit_booking",
|
||||
@@ -177,7 +177,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
actions: [
|
||||
{
|
||||
id: "reschedule",
|
||||
icon: Clock,
|
||||
icon: "clock" as const,
|
||||
label: t("reschedule_booking"),
|
||||
href: `${bookerUrl}/reschedule/${booking.uid}${
|
||||
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
|
||||
@@ -185,7 +185,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
},
|
||||
{
|
||||
id: "reschedule_request",
|
||||
icon: Send,
|
||||
icon: "send" as const,
|
||||
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
|
||||
label: t("send_reschedule_request"),
|
||||
onClick: () => {
|
||||
@@ -198,7 +198,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
onClick: () => {
|
||||
setIsOpenLocationDialog(true);
|
||||
},
|
||||
icon: MapPin,
|
||||
icon: "map-pin" as const,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -212,7 +212,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
onClick: () => {
|
||||
setChargeCardDialogIsOpen(true);
|
||||
},
|
||||
icon: CreditCard,
|
||||
icon: "credit-card" as const,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -226,7 +226,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
|
||||
const RequestSentMessage = () => {
|
||||
return (
|
||||
<Badge startIcon={Send} size="md" variant="gray" data-testid="request_reschedule_sent">
|
||||
<Badge startIcon="send" size="md" variant="gray" data-testid="request_reschedule_sent">
|
||||
{t("reschedule_request_sent")}
|
||||
</Badge>
|
||||
);
|
||||
@@ -599,7 +599,8 @@ const RecurringBookingsTooltip = ({
|
||||
);
|
||||
})}>
|
||||
<div className="text-default">
|
||||
<RefreshCcw
|
||||
<Icon
|
||||
name="refresh-ccw"
|
||||
strokeWidth="3"
|
||||
className="text-muted float-left mr-1 mt-1.5 inline-block h-3 w-3"
|
||||
/>
|
||||
|
||||
@@ -5,8 +5,7 @@ import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { Button, TextArea } from "@calcom/ui";
|
||||
import { X } from "@calcom/ui/components/icon";
|
||||
import { Button, Icon, TextArea } from "@calcom/ui";
|
||||
|
||||
type Props = {
|
||||
booking: {
|
||||
@@ -57,7 +56,7 @@ export default function CancelBooking(props: Props) {
|
||||
{error && (
|
||||
<div className="mt-8">
|
||||
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
|
||||
<X className="h-6 w-6 text-red-600" />
|
||||
<Icon name="x" className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
Icon,
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
import { CreditCard, AlertTriangle } from "@calcom/ui/components/icon";
|
||||
|
||||
interface IRescheduleDialog {
|
||||
isOpenDialog: boolean;
|
||||
@@ -48,7 +48,7 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
|
||||
<DialogContent>
|
||||
<div className="flex flex-row space-x-3">
|
||||
<div className=" bg-subtle flex h-10 w-10 flex-shrink-0 justify-center rounded-full">
|
||||
<CreditCard className="m-auto h-6 w-6" />
|
||||
<Icon name="credit-card" className="m-auto h-6 w-6" />
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<DialogHeader title={t("charge_card")} />
|
||||
@@ -56,7 +56,7 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
|
||||
|
||||
{chargeError && (
|
||||
<div className="mt-4 flex text-red-500">
|
||||
<AlertTriangle className="mr-2 h-5 w-5 " aria-hidden="true" />
|
||||
<Icon name="triangle-alert" className="mr-2 h-5 w-5 " aria-hidden="true" />
|
||||
<p className="text-sm">{t("error_charging_card")}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -18,9 +18,7 @@ import {
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Input } from "@calcom/ui";
|
||||
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
|
||||
import { MapPin } from "@calcom/ui/components/icon";
|
||||
import { Button, Icon, Input, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
@@ -237,7 +235,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
<DialogContent>
|
||||
<div className="flex flex-row space-x-3">
|
||||
<div className="bg-subtle mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
||||
<MapPin className="text-emphasis h-6 w-6" />
|
||||
<Icon name="map-pin" className="text-emphasis h-6 w-6" />
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
|
||||
@@ -10,10 +10,10 @@ import {
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
Icon,
|
||||
showToast,
|
||||
TextArea,
|
||||
} from "@calcom/ui";
|
||||
import { Clock } from "@calcom/ui/components/icon";
|
||||
|
||||
interface IRescheduleDialog {
|
||||
isOpenDialog: boolean;
|
||||
@@ -44,7 +44,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
||||
<DialogContent enableOverflow>
|
||||
<div className="flex flex-row space-x-3">
|
||||
<div className="bg-subtle flex h-10 w-10 flex-shrink-0 justify-center rounded-full ">
|
||||
<Clock className="m-auto h-6 w-6" />
|
||||
<Icon name="clock" className="m-auto h-6 w-6" />
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<DialogHeader title={t("send_reschedule_request")} />
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
PhoneInput,
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
import { Sparkles } from "@calcom/ui/components/icon";
|
||||
|
||||
type AIEventControllerProps = {
|
||||
eventType: EventTypeSetup;
|
||||
@@ -44,7 +43,7 @@ export default function AIEventController({ eventType, isTeamEvent }: AIEventCon
|
||||
{!isOrg || !isTeamEvent ? (
|
||||
<EmptyScreen
|
||||
headline={t("Cal.ai")}
|
||||
Icon={Sparkles}
|
||||
Icon="sparkles"
|
||||
description={t("upgrade_to_cal_ai_phone_number_description")}
|
||||
buttonRaw={<Button href="/enterprise">{t("upgrade")}</Button>}
|
||||
/>
|
||||
@@ -127,8 +126,15 @@ const AISettings = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
const values = formMethods.getValues("aiPhoneCallConfig");
|
||||
|
||||
const data = await AIPhoneSettingSchema.parseAsync({
|
||||
...values,
|
||||
generalPrompt: values.generalPrompt,
|
||||
beginMessage: values.beginMessage,
|
||||
enabled: values.enabled,
|
||||
guestName: values.guestName,
|
||||
guestEmail: values.guestEmail.trim().length ? values.guestEmail : undefined,
|
||||
guestCompany: values.guestCompany.trim().length ? values.guestCompany : undefined,
|
||||
eventTypeId: eventType.id,
|
||||
numberToCall: values.numberToCall,
|
||||
yourPhoneNumber: values.yourPhoneNumber,
|
||||
calApiKey,
|
||||
});
|
||||
|
||||
@@ -191,12 +197,28 @@ const AISettings = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
|
||||
<TextField
|
||||
type="text"
|
||||
hint="Variable: {name}"
|
||||
hint="Variable: {{name}}"
|
||||
label={t("guest_name")}
|
||||
placeholder="Jane Doe"
|
||||
{...formMethods.register("aiPhoneCallConfig.guestName")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
type="text"
|
||||
hint="Variable: {{email}}"
|
||||
label={t("guest_email")}
|
||||
placeholder="jane@acme.com"
|
||||
{...formMethods.register("aiPhoneCallConfig.guestEmail")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
type="text"
|
||||
hint="Variable: {{company}}"
|
||||
label={t("guest_company")}
|
||||
placeholder="Acme"
|
||||
{...formMethods.register("aiPhoneCallConfig.guestCompany")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
type="text"
|
||||
hint="For eg:- cal_live_0123.."
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -30,15 +29,15 @@ import {
|
||||
Button,
|
||||
Badge,
|
||||
CheckboxField,
|
||||
Icon,
|
||||
Label,
|
||||
SelectField,
|
||||
SettingsToggle,
|
||||
showToast,
|
||||
Switch,
|
||||
TextField,
|
||||
Tooltip,
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
import { Copy, Edit, Info } from "@calcom/ui/components/icon";
|
||||
|
||||
import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||
|
||||
@@ -180,7 +179,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
aria-label="edit custom name"
|
||||
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
|
||||
onClick={() => setShowEventNameTip((old) => !old)}>
|
||||
<Edit className="h-4 w-4" />
|
||||
<Icon name="pencil" className="h-4 w-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
@@ -194,7 +193,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
label={
|
||||
<>
|
||||
{t("display_add_to_calendar_organizer")}
|
||||
<InfoIcon className="text-default hover:text-attention hover:bg-attention ms-1 inline h-4 w-4 rounded-md" />
|
||||
<Icon
|
||||
name="info"
|
||||
className="text-default hover:text-attention hover:bg-attention ms-1 inline h-4 w-4 rounded-md"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
checked={useEventTypeDestinationCalendarEmail}
|
||||
@@ -348,7 +350,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://cal.com/docs/core-features/event-types/single-use-private-links">
|
||||
<Info className="ml-1.5 h-4 w-4 cursor-pointer" />
|
||||
<Icon name="info" className="ml-1.5 h-4 w-4 cursor-pointer" />
|
||||
</a>
|
||||
}
|
||||
{...shouldLockDisableProps("hashedLink")}
|
||||
@@ -389,7 +391,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
showToast(t("enabled_after_update_description"), "warning");
|
||||
}
|
||||
}}>
|
||||
<Copy className="h-4 w-4" />
|
||||
<Icon name="copy" className="h-4 w-4" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@ import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hook
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, EmptyScreen, Alert } from "@calcom/ui";
|
||||
import { Grid, Lock } from "@calcom/ui/components/icon";
|
||||
import { Alert, Button, EmptyScreen } from "@calcom/ui";
|
||||
|
||||
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
|
||||
EventTypeAppCardComponentProps["eventType"];
|
||||
@@ -161,12 +160,12 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
)}
|
||||
{!isPending && !installedApps?.length ? (
|
||||
<EmptyScreen
|
||||
Icon={Grid}
|
||||
Icon="grid-3x3"
|
||||
headline={t("empty_installed_apps_headline")}
|
||||
description={t("empty_installed_apps_description")}
|
||||
buttonRaw={
|
||||
appsDisableProps.disabled ? (
|
||||
<Button StartIcon={Lock} color="secondary" disabled>
|
||||
<Button StartIcon="lock" color="secondary" disabled>
|
||||
{t("locked_by_team_admin")}
|
||||
</Button>
|
||||
) : (
|
||||
|
||||
@@ -13,8 +13,7 @@ import { weekdayNames } from "@calcom/lib/weekday";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
||||
import { ExternalLink, Globe } from "@calcom/ui/components/icon";
|
||||
import { Badge, Button, Icon, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
||||
|
||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||
|
||||
@@ -130,7 +129,7 @@ const EventTypeScheduleDetails = memo(
|
||||
</div>
|
||||
<div className="bg-muted border-subtle flex flex-col justify-center gap-2 rounded-b-md border p-6 sm:flex-row sm:justify-between">
|
||||
<span className="text-default flex items-center justify-center text-sm sm:justify-start">
|
||||
<Globe className="h-3.5 w-3.5 ltr:mr-2 rtl:ml-2" />
|
||||
<Icon name="globe" className="h-3.5 w-3.5 ltr:mr-2 rtl:ml-2" />
|
||||
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
||||
</span>
|
||||
{!!schedule?.id && !schedule.isManaged && !schedule.readOnly && (
|
||||
@@ -138,7 +137,7 @@ const EventTypeScheduleDetails = memo(
|
||||
href={`/availability/${schedule.id}`}
|
||||
disabled={isPending}
|
||||
color="minimal"
|
||||
EndIcon={ExternalLink}
|
||||
EndIcon="external-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
{t("edit_availability")}
|
||||
|
||||
@@ -18,7 +18,6 @@ import { ascendingLimitKeys, intervalLimitKeyToUnit } from "@calcom/lib/interval
|
||||
import type { PeriodType } from "@calcom/prisma/enums";
|
||||
import type { IntervalLimit } from "@calcom/types/Calendar";
|
||||
import { Button, DateRangePicker, InputField, Label, Select, SettingsToggle, TextField } from "@calcom/ui";
|
||||
import { Plus, Trash2 } from "@calcom/ui/components/icon";
|
||||
|
||||
const MinimumBookingNoticeInput = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
@@ -598,7 +597,7 @@ const IntervalLimitItem = ({
|
||||
{hasDeleteButton && !disabled && (
|
||||
<Button
|
||||
variant="icon"
|
||||
StartIcon={Trash2}
|
||||
StartIcon="trash-2"
|
||||
color="destructive"
|
||||
className="border-none"
|
||||
onClick={() => onDelete(limitKey)}
|
||||
@@ -708,7 +707,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
||||
);
|
||||
})}
|
||||
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && !disabled && (
|
||||
<Button color="minimal" StartIcon={Plus} onClick={addLimit}>
|
||||
<Button color="minimal" StartIcon="plus" onClick={addLimit}>
|
||||
{t("add_limit")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
SettingsToggle,
|
||||
Skeleton,
|
||||
TextField,
|
||||
Icon,
|
||||
Editor,
|
||||
SkeletonContainer,
|
||||
SkeletonText,
|
||||
@@ -31,7 +32,6 @@ import {
|
||||
Button,
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
import { Plus, X, Check, CornerDownRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import CheckboxField from "@components/ui/form/CheckboxField";
|
||||
import type { SingleValueLocationOption } from "@components/ui/form/LocationSelect";
|
||||
@@ -313,7 +313,7 @@ export const EventSetupTab = (
|
||||
onClick={() => remove(index)}
|
||||
aria-label={t("remove")}>
|
||||
<div className="h-4 w-4">
|
||||
<X className="border-l-1 hover:text-emphasis text-subtle h-4 w-4" />
|
||||
<Icon name="x" className="border-l-1 hover:text-emphasis text-subtle h-4 w-4" />
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
@@ -324,7 +324,7 @@ export const EventSetupTab = (
|
||||
<div className="w-full">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex items-center justify-center">
|
||||
<CornerDownRight className="h-4 w-4" />
|
||||
<Icon name="corner-down-right" className="h-4 w-4" />
|
||||
</div>
|
||||
<LocationInput
|
||||
defaultValue={
|
||||
@@ -414,7 +414,7 @@ export const EventSetupTab = (
|
||||
) && (
|
||||
<div className="text-default flex items-center text-sm">
|
||||
<div className="mr-1.5 h-3 w-3">
|
||||
<Check className="h-3 w-3" />
|
||||
<Icon name="check" className="h-3 w-3" />
|
||||
</div>
|
||||
<Trans i18nKey="event_type_requres_google_cal">
|
||||
<p>
|
||||
@@ -442,7 +442,7 @@ export const EventSetupTab = (
|
||||
<li>
|
||||
<Button
|
||||
data-testid="add-location"
|
||||
StartIcon={Plus}
|
||||
StartIcon="plus"
|
||||
color="minimal"
|
||||
onClick={() => setShowEmptyLocationSelect(true)}>
|
||||
{t("add_location")}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Webhook as TbWebhook } from "lucide-react";
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { Trans } from "next-i18next";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -15,7 +14,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||
import type { DialogProps } from "@calcom/ui";
|
||||
import type { DialogProps, VerticalTabItemProps } from "@calcom/ui";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
@@ -29,6 +28,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
HorizontalTabs,
|
||||
Label,
|
||||
Icon,
|
||||
showToast,
|
||||
Skeleton,
|
||||
Switch,
|
||||
@@ -36,23 +36,6 @@ import {
|
||||
VerticalDivider,
|
||||
VerticalTabs,
|
||||
} from "@calcom/ui";
|
||||
import {
|
||||
Link as LinkIcon,
|
||||
Calendar,
|
||||
Clock,
|
||||
Sliders,
|
||||
Repeat,
|
||||
Grid,
|
||||
Zap,
|
||||
Sparkles,
|
||||
Users,
|
||||
ExternalLink,
|
||||
Code,
|
||||
Trash,
|
||||
PhoneCall,
|
||||
MoreHorizontal,
|
||||
Loader,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
@@ -97,41 +80,41 @@ function getNavigation({
|
||||
{
|
||||
name: "event_setup_tab_title",
|
||||
href: `/event-types/${id}?tabName=setup`,
|
||||
icon: LinkIcon,
|
||||
icon: "link",
|
||||
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
|
||||
},
|
||||
{
|
||||
name: "event_limit_tab_title",
|
||||
href: `/event-types/${id}?tabName=limits`,
|
||||
icon: Clock,
|
||||
icon: "clock",
|
||||
info: `event_limit_tab_description`,
|
||||
},
|
||||
{
|
||||
name: "event_advanced_tab_title",
|
||||
href: `/event-types/${id}?tabName=advanced`,
|
||||
icon: Sliders,
|
||||
icon: "sliders-vertical",
|
||||
info: `event_advanced_tab_description`,
|
||||
},
|
||||
{
|
||||
name: "recurring",
|
||||
href: `/event-types/${id}?tabName=recurring`,
|
||||
icon: Repeat,
|
||||
icon: "repeat",
|
||||
info: `recurring_event_tab_description`,
|
||||
},
|
||||
{
|
||||
name: "apps",
|
||||
href: `/event-types/${id}?tabName=apps`,
|
||||
icon: Grid,
|
||||
icon: "grid-3x3",
|
||||
//TODO: Handle proper translation with count handling
|
||||
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
|
||||
},
|
||||
{
|
||||
name: "workflows",
|
||||
href: `/event-types/${id}?tabName=workflows`,
|
||||
icon: Zap,
|
||||
icon: "zap",
|
||||
info: `${enabledWorkflowsNumber} ${t("active")}`,
|
||||
},
|
||||
];
|
||||
] satisfies VerticalTabItemProps[];
|
||||
}
|
||||
|
||||
function DeleteDialog({
|
||||
@@ -228,7 +211,7 @@ function EventTypeSingleLayout({
|
||||
const watchChildrenCount = formMethods.watch("children").length;
|
||||
// Define tab navigation here
|
||||
const EventTypeTabs = useMemo(() => {
|
||||
const navigation = getNavigation({
|
||||
const navigation: VerticalTabItemProps[] = getNavigation({
|
||||
t,
|
||||
length,
|
||||
multipleDuration,
|
||||
@@ -242,7 +225,7 @@ function EventTypeSingleLayout({
|
||||
navigation.splice(1, 0, {
|
||||
name: "availability",
|
||||
href: `/event-types/${formMethods.getValues("id")}?tabName=availability`,
|
||||
icon: Calendar,
|
||||
icon: "calendar",
|
||||
info:
|
||||
isManagedEventType || isChildrenManagedEventType
|
||||
? formMethods.getValues("schedule") === null
|
||||
@@ -261,7 +244,7 @@ function EventTypeSingleLayout({
|
||||
navigation.splice(2, 0, {
|
||||
name: "assignment",
|
||||
href: `/event-types/${formMethods.getValues("id")}?tabName=team`,
|
||||
icon: Users,
|
||||
icon: "users",
|
||||
info: `${t(watchSchedulingType?.toLowerCase() ?? "")}${
|
||||
isManagedEventType ? ` - ${t("number_member", { count: watchChildrenCount || 0 })}` : ""
|
||||
}`,
|
||||
@@ -273,14 +256,14 @@ function EventTypeSingleLayout({
|
||||
navigation.push({
|
||||
name: "instant_tab_title",
|
||||
href: `/event-types/${eventType.id}?tabName=instant`,
|
||||
icon: PhoneCall,
|
||||
icon: "phone-call",
|
||||
info: `instant_event_tab_description`,
|
||||
});
|
||||
}
|
||||
navigation.push({
|
||||
name: "webhooks",
|
||||
href: `/event-types/${formMethods.getValues("id")}?tabName=webhooks`,
|
||||
icon: TbWebhook,
|
||||
icon: "webhook",
|
||||
info: `${activeWebhooksNumber} ${t("active")}`,
|
||||
});
|
||||
}
|
||||
@@ -289,7 +272,7 @@ function EventTypeSingleLayout({
|
||||
navigation.push({
|
||||
name: "Cal.ai",
|
||||
href: `/event-types/${eventType.id}?tabName=ai`,
|
||||
icon: Sparkles,
|
||||
icon: "sparkles",
|
||||
info: "cal_ai_event_tab_description", // todo `cal_ai_event_tab_description`,
|
||||
});
|
||||
}
|
||||
@@ -378,14 +361,14 @@ function EventTypeSingleLayout({
|
||||
variant="icon"
|
||||
href={permalink}
|
||||
rel="noreferrer"
|
||||
StartIcon={ExternalLink}
|
||||
StartIcon="external-link"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon={LinkIcon}
|
||||
StartIcon="link"
|
||||
tooltip={t("copy_link")}
|
||||
tooltipSide="bottom"
|
||||
tooltipOffset={4}
|
||||
@@ -396,7 +379,7 @@ function EventTypeSingleLayout({
|
||||
/>
|
||||
<EventTypeEmbedButton
|
||||
embedUrl={encodeURIComponent(embedLink)}
|
||||
StartIcon={Code}
|
||||
StartIcon="code"
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
namespace=""
|
||||
@@ -411,7 +394,7 @@ function EventTypeSingleLayout({
|
||||
<Button
|
||||
color="destructive"
|
||||
variant="icon"
|
||||
StartIcon={Trash}
|
||||
StartIcon="trash"
|
||||
tooltip={t("delete")}
|
||||
tooltipSide="bottom"
|
||||
tooltipOffset={4}
|
||||
@@ -425,14 +408,14 @@ function EventTypeSingleLayout({
|
||||
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="lg:hidden" StartIcon={MoreHorizontal} variant="icon" color="secondary" />
|
||||
<Button className="lg:hidden" StartIcon="ellipsis" variant="icon" color="secondary" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent style={{ minWidth: "200px" }}>
|
||||
<DropdownMenuItem className="focus:ring-muted">
|
||||
<DropdownItem
|
||||
target="_blank"
|
||||
type="button"
|
||||
StartIcon={ExternalLink}
|
||||
StartIcon="external-link"
|
||||
href={permalink}
|
||||
rel="noreferrer">
|
||||
{t("preview")}
|
||||
@@ -441,7 +424,7 @@ function EventTypeSingleLayout({
|
||||
<DropdownMenuItem className="focus:ring-muted">
|
||||
<DropdownItem
|
||||
type="button"
|
||||
StartIcon={LinkIcon}
|
||||
StartIcon="link"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(permalink);
|
||||
showToast("Link copied!", "success");
|
||||
@@ -453,7 +436,7 @@ function EventTypeSingleLayout({
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color="destructive"
|
||||
StartIcon={Trash}
|
||||
StartIcon="trash"
|
||||
disabled={!hasPermsToDelete}
|
||||
onClick={() => setDeleteDialogOpen(true)}>
|
||||
{t("delete")}
|
||||
@@ -489,7 +472,7 @@ function EventTypeSingleLayout({
|
||||
</Button>
|
||||
</div>
|
||||
}>
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Suspense fallback={<Icon name="loader" />}>
|
||||
<div className="flex flex-col xl:flex-row xl:space-x-6">
|
||||
<div className="hidden xl:block">
|
||||
<VerticalTabs
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Webhook } from "@prisma/client";
|
||||
import { Webhook as TbWebhook } from "lucide-react";
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
@@ -16,7 +15,6 @@ import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Alert, Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
|
||||
import { Plus, Lock } from "@calcom/ui/components/icon";
|
||||
|
||||
export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
||||
const { t } = useLocale();
|
||||
@@ -92,7 +90,7 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
||||
<Button
|
||||
color="secondary"
|
||||
data-testid="new_webhook"
|
||||
StartIcon={Plus}
|
||||
StartIcon="plus"
|
||||
onClick={() => setCreateModalOpen(true)}>
|
||||
{t("new_webhook")}
|
||||
</Button>
|
||||
@@ -160,12 +158,12 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
||||
</>
|
||||
) : (
|
||||
<EmptyScreen
|
||||
Icon={TbWebhook}
|
||||
Icon="webhook"
|
||||
headline={t("create_your_first_webhook")}
|
||||
description={t("first_event_type_webhook_description")}
|
||||
buttonRaw={
|
||||
isChildrenManagedEventType && !isManagedEventType ? (
|
||||
<Button StartIcon={Lock} color="secondary" disabled>
|
||||
<Button StartIcon="lock" color="secondary" disabled>
|
||||
{t("locked_by_team_admin")}
|
||||
</Button>
|
||||
) : (
|
||||
|
||||
@@ -16,7 +16,6 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Alert, Button, EmptyScreen, SettingsToggle, Dialog, DialogContent, showToast } from "@calcom/ui";
|
||||
import { PhoneCall, Plus, Lock, Webhook as TbWebhook } from "@calcom/ui/components/icon";
|
||||
|
||||
type InstantEventControllerProps = {
|
||||
eventType: EventTypeSetup;
|
||||
@@ -48,7 +47,7 @@ export default function InstantEventController({
|
||||
{!isOrg || !isTeamEvent ? (
|
||||
<EmptyScreen
|
||||
headline={t("instant_tab_title")}
|
||||
Icon={PhoneCall}
|
||||
Icon="phone-call"
|
||||
description={t("uprade_to_create_instant_bookings")}
|
||||
buttonRaw={<Button href="/enterprise">{t("upgrade")}</Button>}
|
||||
/>
|
||||
@@ -171,7 +170,7 @@ const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) =>
|
||||
<Button
|
||||
color="secondary"
|
||||
data-testid="new_webhook"
|
||||
StartIcon={Plus}
|
||||
StartIcon="plus"
|
||||
onClick={() => setCreateModalOpen(true)}>
|
||||
{t("new_webhook")}
|
||||
</Button>
|
||||
@@ -218,12 +217,12 @@ const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) =>
|
||||
{t("warning_payment_instant_meeting_event")}
|
||||
</p>
|
||||
<EmptyScreen
|
||||
Icon={TbWebhook}
|
||||
Icon="webhook"
|
||||
headline={t("create_your_first_webhook")}
|
||||
description={t("create_instant_meeting_webhook_description")}
|
||||
buttonRaw={
|
||||
isChildrenManagedEventType && !isManagedEventType ? (
|
||||
<Button StartIcon={Lock} color="secondary" disabled>
|
||||
<Button StartIcon="lock" color="secondary" disabled>
|
||||
{t("locked_by_admin")}
|
||||
</Button>
|
||||
) : (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||
import { Clock, User } from "@calcom/ui/components/icon";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
function SkeletonLoader() {
|
||||
return (
|
||||
@@ -32,11 +32,11 @@ function SkeletonItem() {
|
||||
<div className="">
|
||||
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
|
||||
<li className="flex items-center whitespace-nowrap">
|
||||
<Clock className="text-subtle mr-1.5 mt-0.5 inline h-4 w-4" />
|
||||
<Icon name="clock" className="text-subtle mr-1.5 mt-0.5 inline h-4 w-4" />
|
||||
<SkeletonText className="h-4 w-12" />
|
||||
</li>
|
||||
<li className="flex items-center whitespace-nowrap">
|
||||
<User className="text-subtle mr-1.5 mt-0.5 inline h-4 w-4" />
|
||||
<Icon name="user" className="text-subtle mr-1.5 mt-0.5 inline h-4 w-4" />
|
||||
<SkeletonText className="h-4 w-12" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { List } from "@calcom/ui";
|
||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||
import { Icon, List } from "@calcom/ui";
|
||||
|
||||
import { AppConnectionItem } from "../components/AppConnectionItem";
|
||||
import { ConnectedCalendarItem } from "../components/ConnectedCalendarItem";
|
||||
@@ -88,7 +87,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
||||
onClick={() => nextStep()}
|
||||
disabled={disabledNextButton}>
|
||||
{firstCalendar ? `${t("continue")}` : `${t("next_step_text")}`}
|
||||
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||
<Icon name="arrow-right" className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { List } from "@calcom/ui";
|
||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||
import { Icon, List } from "@calcom/ui";
|
||||
|
||||
import { AppConnectionItem } from "../components/AppConnectionItem";
|
||||
import { StepConnectionLoader } from "../components/StepConnectionLoader";
|
||||
@@ -59,7 +58,7 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
|
||||
disabled={!hasAnyInstalledVideoApps}
|
||||
onClick={() => nextStep()}>
|
||||
{t("next_step_text")}
|
||||
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||
<Icon name="arrow-right" className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -6,8 +6,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { TRPCClientErrorLike } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import { Button, Form } from "@calcom/ui";
|
||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||
import { Button, Form, Icon } from "@calcom/ui";
|
||||
|
||||
interface ISetupAvailabilityProps {
|
||||
nextStep: () => void;
|
||||
@@ -78,7 +77,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
|
||||
className="mt-2 w-full justify-center p-2 text-sm sm:mt-8"
|
||||
loading={availabilityForm.formState.isSubmitting}
|
||||
disabled={availabilityForm.formState.isSubmitting}>
|
||||
{t("next_step_text")} <ArrowRight className="ml-2 h-4 w-4 self-center" />
|
||||
{t("next_step_text")} <Icon name="arrow-right" className="ml-2 h-4 w-4 self-center" />
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
@@ -10,7 +10,6 @@ import turndown from "@calcom/lib/turndownService";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
||||
import { UserAvatar } from "@calcom/ui";
|
||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
type FormData = {
|
||||
bio: string;
|
||||
@@ -147,7 +146,7 @@ const UserProfile = () => {
|
||||
</fieldset>
|
||||
<Button
|
||||
loading={mutation.isPending}
|
||||
EndIcon={ArrowRight}
|
||||
EndIcon="arrow-right"
|
||||
type="submit"
|
||||
className="mt-8 w-full items-center justify-center">
|
||||
{t("finish")}
|
||||
|
||||
@@ -9,8 +9,7 @@ import { FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, TimezoneSelect, Input } from "@calcom/ui";
|
||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||
import { Button, TimezoneSelect, Icon, Input } from "@calcom/ui";
|
||||
|
||||
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||
|
||||
@@ -116,7 +115,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
||||
loading={mutation.isPending}
|
||||
disabled={mutation.isPending}>
|
||||
{t("next_step_text")}
|
||||
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||
<Icon name="arrow-right" className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Shield } from "@calcom/ui/components/icon";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
|
||||
return (
|
||||
<div className="mb-4 sm:flex sm:items-start">
|
||||
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<Shield className="text-inverted h-6 w-6" />
|
||||
<Icon name="shield" className="text-inverted h-6 w-6" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||
<h3 className="font-cal text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
Button,
|
||||
InputError,
|
||||
} from "@calcom/ui";
|
||||
import { MoreHorizontal, Flag, Trash, Send } from "@calcom/ui/components/icon";
|
||||
|
||||
type CustomEmailTextFieldProps = {
|
||||
formMethods: UseFormReturn<FormValues>;
|
||||
@@ -71,7 +70,7 @@ const CustomEmailTextField = ({
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
StartIcon={MoreHorizontal}
|
||||
StartIcon="ellipsis"
|
||||
variant="icon"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
@@ -82,7 +81,7 @@ const CustomEmailTextField = ({
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
StartIcon={Flag}
|
||||
StartIcon="flag"
|
||||
color="secondary"
|
||||
className="disabled:opacity-40"
|
||||
onClick={handleChangePrimary}
|
||||
@@ -94,7 +93,7 @@ const CustomEmailTextField = ({
|
||||
{!emailVerified && (
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
StartIcon={Send}
|
||||
StartIcon="send"
|
||||
color="secondary"
|
||||
className="disabled:opacity-40"
|
||||
onClick={handleVerifyEmail}
|
||||
@@ -106,7 +105,7 @@ const CustomEmailTextField = ({
|
||||
)}
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
StartIcon={Trash}
|
||||
StartIcon="trash"
|
||||
color="destructive"
|
||||
className="disabled:opacity-40"
|
||||
onClick={handleItemDelete}
|
||||
|
||||
+6
-5
@@ -1,10 +1,9 @@
|
||||
import { Asterisk, Clipboard } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants";
|
||||
import type { Avatar } from "@calcom/prisma/client";
|
||||
import { Button, showToast } from "@calcom/ui";
|
||||
import { Button, Icon, showToast } from "@calcom/ui";
|
||||
|
||||
import { hasPermission } from "../../../../../../../packages/platform/utils/permissions";
|
||||
|
||||
@@ -75,7 +74,8 @@ export const OAuthClientCard = ({
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<div className="font-semibold">Client Id:</div>
|
||||
<div>{id}</div>
|
||||
<Clipboard
|
||||
<Icon
|
||||
name="clipboard"
|
||||
type="button"
|
||||
className="h-4 w-4 cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -89,9 +89,10 @@ export const OAuthClientCard = ({
|
||||
<div className="font-semibold">Client Secret:</div>
|
||||
<div className="flex items-center justify-center rounded-md">
|
||||
{[...new Array(20)].map((_, index) => (
|
||||
<Asterisk key={`${index}asterisk`} className="h-2 w-2" />
|
||||
<Icon name="asterisk" key={`${index}asterisk`} className="h-2 w-2" />
|
||||
))}
|
||||
<Clipboard
|
||||
<Icon
|
||||
name="clipboard"
|
||||
type="button"
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
onClick={() => {
|
||||
|
||||
+3
-4
@@ -7,7 +7,6 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants/permissions";
|
||||
import { showToast } from "@calcom/ui";
|
||||
import { Meta, Button, TextField, Label } from "@calcom/ui";
|
||||
import { Plus, Trash } from "@calcom/ui/components/icon";
|
||||
|
||||
import { useCreateOAuthClient } from "@lib/hooks/settings/organizations/platform/oauth-clients/usePersistOAuthClient";
|
||||
|
||||
@@ -156,7 +155,7 @@ export const OAuthClientForm: FC = () => {
|
||||
type="button"
|
||||
color="minimal"
|
||||
variant="icon"
|
||||
StartIcon={Plus}
|
||||
StartIcon="plus"
|
||||
className="text-default mx-2 mb-2"
|
||||
onClick={() => {
|
||||
append({ uri: "" });
|
||||
@@ -168,7 +167,7 @@ export const OAuthClientForm: FC = () => {
|
||||
type="button"
|
||||
color="destructive"
|
||||
variant="icon"
|
||||
StartIcon={Trash}
|
||||
StartIcon="trash"
|
||||
className="text-default mx-2 mb-2"
|
||||
onClick={() => {
|
||||
remove(index);
|
||||
@@ -191,7 +190,7 @@ export const OAuthClientForm: FC = () => {
|
||||
<Avatar
|
||||
alt=""
|
||||
imageSrc={value}
|
||||
fallback={<Plus className="text-subtle h-4 w-4" />}
|
||||
fallback={<Icon name="plus" className="text-subtle h-4 w-4" />}
|
||||
size="sm"
|
||||
/>
|
||||
<div className="ms-4">
|
||||
|
||||
@@ -9,7 +9,6 @@ import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
|
||||
import { UserCheck } from "@calcom/ui/components/icon";
|
||||
|
||||
export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser> & { userCount: number }) => {
|
||||
const { t } = useLocale();
|
||||
@@ -24,7 +23,7 @@ export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser>
|
||||
props.onSuccess();
|
||||
}}>
|
||||
<EmptyScreen
|
||||
Icon={UserCheck}
|
||||
Icon="user-check"
|
||||
headline={t("admin_user_created")}
|
||||
description={t("admin_user_created_description")}
|
||||
/>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, TextField } from "@calcom/ui";
|
||||
import { Check, ExternalLink, Loader } from "@calcom/ui/components/icon";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
type EnterpriseLicenseFormValues = {
|
||||
licenseKey: string;
|
||||
@@ -92,7 +92,7 @@ const EnterpriseLicense = (
|
||||
<div>
|
||||
<Button
|
||||
className="w-full justify-center text-lg"
|
||||
EndIcon={ExternalLink}
|
||||
EndIcon="external-link"
|
||||
href="https://console.cal.com"
|
||||
target="_blank">
|
||||
{t("purchase_license")}
|
||||
@@ -122,9 +122,9 @@ const EnterpriseLicense = (
|
||||
)}
|
||||
addOnSuffix={
|
||||
checkLicenseLoading ? (
|
||||
<Loader className="h-5 w-5 animate-spin" />
|
||||
<Icon name="loader" className="h-5 w-5 animate-spin" />
|
||||
) : errors.licenseKey === undefined && isDirty ? (
|
||||
<Check className="h-5 w-5 text-green-700" />
|
||||
<Icon name="check" className="h-5 w-5 text-green-700" />
|
||||
) : undefined
|
||||
}
|
||||
color={errors.licenseKey ? "warn" : ""}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useRouter } from "next/navigation";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Check } from "@calcom/ui/components/icon";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
const StepDone = (props: {
|
||||
currentStep: number;
|
||||
@@ -24,7 +24,10 @@ const StepDone = (props: {
|
||||
}}>
|
||||
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
||||
<div className="dark:bg-default flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600">
|
||||
<Check className="text-inverted dark:bg-default dark:text-default inline-block h-10 w-10" />
|
||||
<Icon
|
||||
name="check"
|
||||
className="text-inverted dark:bg-default dark:text-default inline-block h-10 w-10"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-w-[420px] text-center">
|
||||
<h2 className="mb-1 mt-6 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
import type { LucideIcon } from "@calcom/ui/components/icon";
|
||||
|
||||
import type { SVGComponent } from "@lib/types/SVGComponent";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
interface LinkIconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
Icon: SVGComponent | LucideIcon;
|
||||
Icon: React.ComponentProps<typeof Icon>["name"];
|
||||
}
|
||||
|
||||
export default function LinkIconButton(props: LinkIconButtonProps) {
|
||||
@@ -15,7 +13,7 @@ export default function LinkIconButton(props: LinkIconButtonProps) {
|
||||
type="button"
|
||||
{...props}
|
||||
className="text-md hover:bg-emphasis hover:text-emphasis text-default flex items-center rounded-sm px-2 py-1 text-sm font-medium">
|
||||
<props.Icon className="text-subtle h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||
<Icon name={props.Icon} className="text-subtle h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||
{props.children}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import { Button, Dialog, DialogClose, DialogContent, DialogFooter, Input, Label } from "@calcom/ui";
|
||||
import { Check, Edit2, ExternalLink, Star as StarSolid } from "@calcom/ui/components/icon";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
export enum UsernameChangeStatusEnum {
|
||||
UPGRADE = "UPGRADE",
|
||||
@@ -239,9 +239,13 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
isInputUsernamePremium ? "text-transparent" : "",
|
||||
usernameIsAvailable ? "" : ""
|
||||
)}>
|
||||
{isInputUsernamePremium ? <StarSolid className="mt-[2px] h-4 w-4 fill-orange-400" /> : <></>}
|
||||
{isInputUsernamePremium ? (
|
||||
<Icon name="star" className="mt-[2px] h-4 w-4 fill-orange-400" />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{!isInputUsernamePremium && usernameIsAvailable ? (
|
||||
<Check className="mt-[2px] h-4 w-4" />
|
||||
<Icon name="check" className="mt-[2px] h-4 w-4" />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
@@ -260,7 +264,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
|
||||
<Dialog open={openDialogSaveUsername}>
|
||||
<DialogContent
|
||||
Icon={Edit2}
|
||||
Icon="pencil"
|
||||
title={t("confirm_username_change_dialog_title")}
|
||||
description={
|
||||
<>
|
||||
@@ -297,7 +301,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
data-testid="go-to-billing"
|
||||
href={paymentLink}>
|
||||
<>
|
||||
{t("go_to_stripe_billing")} <ExternalLink className="ml-1 h-4 w-4" />
|
||||
{t("go_to_stripe_billing")} <Icon name="external-link" className="ml-1 h-4 w-4" />
|
||||
</>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import { Button, Dialog, DialogClose, DialogContent, TextField, DialogFooter, Tooltip } from "@calcom/ui";
|
||||
import { Check, Edit2 } from "@calcom/ui/components/icon";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
interface ICustomUsernameProps {
|
||||
currentUsername: string | undefined;
|
||||
@@ -137,7 +137,11 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
|
||||
{currentUsername !== inputUsernameValue && (
|
||||
<div className="absolute right-[2px] top-6 flex h-7 flex-row">
|
||||
<span className={classNames("bg-default mx-0 p-3")}>
|
||||
{usernameIsAvailable ? <Check className="relative bottom-[6px] h-4 w-4" /> : <></>}
|
||||
{usernameIsAvailable ? (
|
||||
<Icon name="check" className="relative bottom-[6px] h-4 w-4" />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -154,7 +158,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
|
||||
</div>
|
||||
)}
|
||||
<Dialog open={openDialogSaveUsername}>
|
||||
<DialogContent type="confirmation" Icon={Edit2} title={t("confirm_username_change_dialog_title")}>
|
||||
<DialogContent type="confirmation" Icon="pencil" title={t("confirm_username_change_dialog_title")}>
|
||||
<div className="flex flex-row">
|
||||
<div className="mb-4 w-full pt-1">
|
||||
<div className="bg-subtle flex w-full flex-wrap justify-between gap-6 rounded-sm px-4 py-3 text-sm">
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Props } from "react-select";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Avatar } from "@calcom/ui";
|
||||
import { X } from "@calcom/ui/components/icon";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
import Select from "@components/ui/form/Select";
|
||||
|
||||
@@ -43,7 +43,8 @@ export const CheckedSelect = ({
|
||||
alt={option.label}
|
||||
/>
|
||||
{option.label}
|
||||
<X
|
||||
<Icon
|
||||
name="x"
|
||||
onClick={() => props.onChange(value.filter((item) => item.value !== option.value))}
|
||||
className="text-subtle float-right mt-0.5 h-5 w-5 cursor-pointer"
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,7 @@ import "react-calendar/dist/Calendar.css";
|
||||
import "react-date-picker/dist/DatePicker.css";
|
||||
import PrimitiveDatePicker from "react-date-picker/dist/entry.nostyle";
|
||||
|
||||
import { Calendar } from "@calcom/ui/components/icon";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
|
||||
@@ -23,7 +23,7 @@ export const DatePicker = ({ minDate, disabled, date, onDatesChange, className }
|
||||
className
|
||||
)}
|
||||
clearIcon={null}
|
||||
calendarIcon={<Calendar className="text-subtle h-5 w-5" />}
|
||||
calendarIcon={<Icon name="calendar" className="text-subtle h-5 w-5" />}
|
||||
value={date}
|
||||
minDate={minDate}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -8,7 +8,9 @@ import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomain
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { RedirectType } from "@calcom/prisma/enums";
|
||||
|
||||
import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||
|
||||
@@ -57,6 +59,19 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
return notFound;
|
||||
}
|
||||
|
||||
if (!org) {
|
||||
const redirect = await getTemporaryOrgRedirect({
|
||||
slugs: [username],
|
||||
redirectType: RedirectType.User,
|
||||
eventTypeSlug: slug,
|
||||
currentQuery: context.query,
|
||||
});
|
||||
|
||||
if (redirect) {
|
||||
return redirect;
|
||||
}
|
||||
}
|
||||
|
||||
const [user] = await UserRepository.findUsersByUsername({
|
||||
usernameList: [username],
|
||||
orgSlug: org,
|
||||
|
||||
@@ -63,7 +63,7 @@ describe("getTemporaryOrgRedirect", () => {
|
||||
expect(redirect).toEqual({
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "https://calcom.cal.com/30min",
|
||||
destination: "https://calcom.cal.com/30min?orgRedirection=true",
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -83,7 +83,7 @@ describe("getTemporaryOrgRedirect", () => {
|
||||
expect(redirect).toEqual({
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "https://calcom.cal.com/30min?abc=1",
|
||||
destination: "https://calcom.cal.com/30min?abc=1&orgRedirection=true",
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -103,7 +103,7 @@ describe("getTemporaryOrgRedirect", () => {
|
||||
expect(redirect).toEqual({
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "https://calcom.cal.com?abc=1",
|
||||
destination: "https://calcom.cal.com?abc=1&orgRedirection=true",
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -127,7 +127,7 @@ describe("getTemporaryOrgRedirect", () => {
|
||||
expect(redirect).toEqual({
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "https://calcom.cal.com?abc=1",
|
||||
destination: "https://calcom.cal.com?abc=1&orgRedirection=true",
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -151,7 +151,7 @@ describe("getTemporaryOrgRedirect", () => {
|
||||
expect(redirect).toEqual({
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "https://calcom.cal.com/30min?abc=1",
|
||||
destination: "https://calcom.cal.com/30min?abc=1&orgRedirection=true",
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -173,7 +173,7 @@ describe("getTemporaryOrgRedirect", () => {
|
||||
expect(redirect).toEqual({
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "https://calcom.cal.com/30min",
|
||||
destination: "https://calcom.cal.com/30min?orgRedirection=true",
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -200,7 +200,7 @@ describe("getTemporaryOrgRedirect", () => {
|
||||
expect(redirect).toEqual({
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "https://calcom.cal.com/first-in-org1+second-in-org1",
|
||||
destination: "https://calcom.cal.com/first-in-org1+second-in-org1?orgRedirection=true",
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -227,7 +227,7 @@ describe("getTemporaryOrgRedirect", () => {
|
||||
expect(redirect).toEqual({
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "https://calcom.cal.com/first-in-org1+second-in-org1",
|
||||
destination: "https://calcom.cal.com/first-in-org1+second-in-org1?orgRedirection=true",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -241,7 +241,7 @@ describe("getTemporaryOrgRedirect", () => {
|
||||
expect(redirect1).toEqual({
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "https://calcom.cal.com/second-in-org1+first-in-org1",
|
||||
destination: "https://calcom.cal.com/second-in-org1+first-in-org1?orgRedirection=true",
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -270,7 +270,7 @@ describe("getTemporaryOrgRedirect", () => {
|
||||
expect(redirect).toEqual({
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "https://calcom.cal.com/first-in-org1+second-in-org1?abc=1",
|
||||
destination: "https://calcom.cal.com/first-in-org1+second-in-org1?abc=1&orgRedirection=true",
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -297,7 +297,7 @@ describe("getTemporaryOrgRedirect", () => {
|
||||
expect(redirect).toEqual({
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "https://calcom.cal.com/first-in-org1+second-in-org1/30min",
|
||||
destination: "https://calcom.cal.com/first-in-org1+second-in-org1/30min?orgRedirection=true",
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -326,7 +326,7 @@ describe("getTemporaryOrgRedirect", () => {
|
||||
expect(redirect).toEqual({
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "https://calcom.cal.com/first-in-org1+second-in-org1/30min?abc=1",
|
||||
destination: "https://calcom.cal.com/first-in-org1+second-in-org1/30min?abc=1&orgRedirection=true",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ export const getTemporaryOrgRedirect = async ({
|
||||
|
||||
// Use the first redirect origin as the new origin as we aren't supposed to handle different org usernames in a group
|
||||
const newOrigin = new URL(redirects[0].toUrl).origin;
|
||||
const query = currentQueryString ? `?${currentQueryString}` : "";
|
||||
const query = currentQueryString ? `?${currentQueryString}&orgRedirection=true` : "?orgRedirection=true";
|
||||
// Use the same order as in input slugs - It is important from Dynamic Group perspective as the first user's settings are used for various things
|
||||
const newSlugs = slugs.map((slug) => {
|
||||
const redirect = redirects.find((redirect) => redirect.from === slug);
|
||||
|
||||
@@ -19,7 +19,17 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
},
|
||||
});
|
||||
if (team) {
|
||||
return GSSTeamPage({ ...ctx, query: { slug: ctx.query.user } });
|
||||
return GSSTeamPage({
|
||||
...ctx,
|
||||
query: { slug: ctx.query.user, orgRedirection: ctx.query.orgRedirection },
|
||||
});
|
||||
}
|
||||
return GSSUserPage({ ...ctx, query: { user: ctx.query.user, redirect: ctx.query.redirect } });
|
||||
return GSSUserPage({
|
||||
...ctx,
|
||||
query: {
|
||||
user: ctx.query.user,
|
||||
redirect: ctx.query.redirect,
|
||||
orgRedirection: ctx.query.orgRedirection,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -24,8 +24,10 @@ const querySchema = z.object({
|
||||
|
||||
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||
const emailVerificationEnabled = await getFeatureFlag(prisma, "email-verification");
|
||||
await ssrInit(ctx);
|
||||
const signupDisabled = await getFeatureFlag(prisma, "disable-signup");
|
||||
const ssr = await ssrInit(ctx);
|
||||
|
||||
const token = z.string().optional().parse(ctx.query.token);
|
||||
const redirectUrlData = z
|
||||
.string()
|
||||
@@ -42,8 +44,8 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
redirectUrl,
|
||||
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
|
||||
isSAMLLoginEnabled,
|
||||
trpcState: ssr.dehydrate(),
|
||||
prepopulateFormValues: undefined,
|
||||
emailVerificationEnabled,
|
||||
};
|
||||
|
||||
// username + email prepopulated from query params
|
||||
|
||||
@@ -72,6 +72,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
eventSlug: meetingSlug,
|
||||
isTeamEvent: true,
|
||||
org,
|
||||
fromRedirectOfNonOrgLink: context.query.orgRedirection === "true",
|
||||
});
|
||||
|
||||
if (!eventData) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user