Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68663231d8 | ||
|
|
d3bbed01f6 | ||
|
|
9dfcb0d482 | ||
|
|
240405f849 | ||
|
|
bc9a9c0c24 | ||
|
|
f8abce44cb | ||
|
|
1368ffe55d | ||
|
|
397bf3cc0c | ||
|
|
98ec38aeaa | ||
|
|
d52e2d9fad | ||
|
|
bf348ee209 | ||
|
|
635c1febb0 | ||
|
|
8beb629b07 | ||
|
|
7cfb053026 | ||
|
|
50c5423ba7 | ||
|
|
7716f8c0a1 | ||
|
|
8a3944f987 | ||
|
|
be0b707296 | ||
|
|
fad64a012a | ||
|
|
14563f664e | ||
|
|
c7e66a3aa9 | ||
|
|
0d55939d13 | ||
|
|
06bb994145 | ||
|
|
b185cd231b | ||
|
|
4c804cdb00 | ||
|
|
98b6d63164 | ||
|
|
bc8ebb9213 | ||
|
|
6089f47d24 | ||
|
|
f6fa24de2b | ||
|
|
b75000ea7b | ||
|
|
dc7b9d91ae | ||
|
|
36713fc12e | ||
|
|
285f6d54a3 | ||
|
|
1e0cf3d0f6 | ||
|
|
681d191537 | ||
|
|
269ed87db4 | ||
|
|
0191f54519 | ||
|
|
dc3e681e01 | ||
|
|
30bba3da15 | ||
|
|
cfa0783ebc | ||
|
|
81e4241f85 | ||
|
|
f4abbb2de1 | ||
|
|
3908cda372 | ||
|
|
db76980644 | ||
|
|
d8e6ecc7a0 | ||
|
|
16eb96e27d | ||
|
|
83c42cfb2b |
@@ -209,11 +209,13 @@ NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE=
|
||||
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
|
||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
|
||||
STRIPE_TEAM_MONTHLY_PRICE_ID=
|
||||
STRIPE_TEAM_ANNUAL_PRICE_ID=
|
||||
NEXT_PUBLIC_STRIPE_CREDITS_PRICE_ID=
|
||||
ORG_MONTHLY_CREDITS=
|
||||
STRIPE_TEAM_PRODUCT_ID=
|
||||
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
|
||||
STRIPE_ORG_MONTHLY_PRICE_ID=
|
||||
STRIPE_ORG_ANNUAL_PRICE_ID=
|
||||
STRIPE_ORG_PRODUCT_ID=
|
||||
# Used to pass trial days for orgs during the Stripe checkout session
|
||||
STRIPE_ORG_TRIAL_DAYS=
|
||||
|
||||
@@ -7,4 +7,11 @@ GET /v2/bookings/{bookingUid} added 'subschema #3, subschema #6' to the 'data' r
|
||||
POST /v2/bookings/{bookingUid}/cancel added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200'
|
||||
POST /v2/bookings/{bookingUid}/confirm added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200'
|
||||
POST /v2/bookings/{bookingUid}/decline added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200'
|
||||
POST /v2/bookings/{bookingUid}/guests added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200'
|
||||
POST /v2/bookings/{bookingUid}/guests added 'subschema #3, subschema #6' to the 'data' response property 'oneOf' list for the response status '200'
|
||||
DELETE /v2/event-types/{eventTypeId}/webhooks/{webhookId} added the new path request parameter 'webhookId'
|
||||
GET /v2/event-types/{eventTypeId}/webhooks/{webhookId} added the new path request parameter 'webhookId'
|
||||
DELETE /v2/oauth-clients/{clientId}/webhooks/{webhookId} added the new path request parameter 'webhookId'
|
||||
GET /v2/oauth-clients/{clientId}/webhooks/{webhookId} added the new path request parameter 'webhookId'
|
||||
DELETE /v2/teams/{teamId}/event-types/{eventTypeId}/webhooks/{webhookId} added the new path request parameter 'webhookId'
|
||||
GET /v2/teams/{teamId}/event-types/{eventTypeId}/webhooks/{webhookId} added the new path request parameter 'webhookId'
|
||||
GET /v2/webhooks/{webhookId} added the new path request parameter 'webhookId'
|
||||
|
||||
@@ -108,6 +108,9 @@ packages/**/.yarn/ci-cache/
|
||||
.claude/plans/
|
||||
.claude/settings.local.json
|
||||
|
||||
# Spec drafts (rough work, not ready for review)
|
||||
specs/**/_drafts/
|
||||
|
||||
# trigger.dev
|
||||
.trigger
|
||||
agents/devin-knowledge.json
|
||||
|
||||
@@ -226,6 +226,14 @@ import { ProfileRepository } from "@calcom/features/profile/repositories/Profile
|
||||
- Fix type errors before test failures - they're often the root cause
|
||||
- Run `yarn prisma generate` if you see missing enum/type errors
|
||||
|
||||
## Spec-Driven Development (Opt-In)
|
||||
|
||||
For complex features, you can use spec-driven development when explicitly requested.
|
||||
|
||||
**To enable:** Tell the AI "use spec-driven development" or "follow the spec workflow"
|
||||
|
||||
See [SPEC-WORKFLOW.md](SPEC-WORKFLOW.md) for the full workflow documentation.
|
||||
|
||||
## Extended Documentation
|
||||
|
||||
For detailed information, see the `agents/` directory:
|
||||
|
||||
@@ -767,11 +767,14 @@ Cal.com, Inc. is a commercial open source company, which means some parts of thi
|
||||
|
||||
<img width="100%" src="https://repobeats.axiom.co/api/embed/6bfca2f20f39738048b6e70ca205efde46352c3d.svg" />
|
||||
|
||||
<!-- CONTRIBUTING -->
|
||||
|
||||
## Contributing
|
||||
We ❤️ contributions! Whether it’s fixing a typo, improving documentation, or building new features, your help makes Cal.com better.
|
||||
|
||||
Please see our [contributing guide](/CONTRIBUTING.md).
|
||||
- Check out our [Contributing Guide](./CONTRIBUTING.md) for detailed steps.
|
||||
- Join the discussion on [GitHub Discussions](https://github.com/calcom/cal.com/discussions) or our community channels.
|
||||
- Please follow our coding standards and commit message conventions to keep the project consistent.
|
||||
|
||||
Even small improvements matter — thank you for helping us grow!
|
||||
|
||||
### Good First Issues
|
||||
|
||||
@@ -984,12 +987,6 @@ An example of good readme is [atoms readme](https://github.com/calcom/cal.com/bl
|
||||
1. Follow semantic versioning when using changesets.
|
||||
2. Mark breaking changes using `❗️Breaking change`
|
||||
|
||||
<!-- LICENSE -->
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the [AGPLv3 License](https://github.com/calcom/cal.com/blob/main/LICENSE). See `LICENSE` for more information.
|
||||
|
||||
<!-- ACKNOWLEDGEMENTS -->
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# Spec-Driven Development Workflow
|
||||
|
||||
This workflow is **opt-in**. Use it when explicitly requested by saying "use spec-driven development" or "follow the spec workflow".
|
||||
|
||||
## Design Documents
|
||||
|
||||
The `specs/` folder contains design documents for features in development.
|
||||
|
||||
Each spec follows this structure:
|
||||
|
||||
```
|
||||
specs/{feature}/
|
||||
├── CLAUDE.md # Feature-specific instructions (read this first)
|
||||
├── design.md # The specification
|
||||
├── implementation.md # Current status and what's done
|
||||
├── decisions.md # Why decisions were made
|
||||
├── prompts.md # Reusable prompts
|
||||
├── future-work.md # What's deferred
|
||||
└── docs/ # Documentation with screenshots
|
||||
```
|
||||
|
||||
**Workflow:**
|
||||
|
||||
1. Read the spec's `CLAUDE.md` for specific instructions
|
||||
2. Read `design.md` to understand what we're building
|
||||
3. Check `implementation.md` for current status
|
||||
4. Find the relevant code in the codebase
|
||||
5. Implement in small pieces, update `implementation.md` after each
|
||||
|
||||
---
|
||||
|
||||
## When Implementing Features (Spec Mode)
|
||||
|
||||
1. **Check for design doc** in `specs/` — if it exists, follow it
|
||||
2. **If no spec exists** — ask if you should create one first
|
||||
3. **Look at existing patterns** — find similar code and follow conventions
|
||||
4. **Update implementation.md** — mark what's done after each piece
|
||||
5. **Update decisions.md** — when choosing between approaches
|
||||
|
||||
---
|
||||
|
||||
## Creating a New Spec
|
||||
|
||||
When user asks to build a new feature:
|
||||
|
||||
1. Copy the template: `cp -r specs/_templates specs/{feature-name}`
|
||||
2. Explore the codebase to understand existing patterns
|
||||
3. Write `design.md` with technical spec
|
||||
4. Write `CLAUDE.md` with feature-specific instructions
|
||||
5. Initialize `implementation.md` with "not-started" status
|
||||
6. Ask user to review before implementing
|
||||
|
||||
---
|
||||
|
||||
## Updating Spec Files
|
||||
|
||||
### implementation.md — After completing each piece:
|
||||
|
||||
```markdown
|
||||
## Status: in-progress
|
||||
|
||||
## Completed
|
||||
- [x] Database schema
|
||||
- [x] Migration file
|
||||
|
||||
## In Progress
|
||||
- tRPC endpoints
|
||||
|
||||
## Next Steps
|
||||
1. UI components
|
||||
2. Tests
|
||||
|
||||
## Session Notes
|
||||
### 2024-01-15
|
||||
- Done: Added schema, created migration
|
||||
- Next: Implement tRPC router
|
||||
```
|
||||
|
||||
### decisions.md — When choosing between approaches:
|
||||
|
||||
```markdown
|
||||
## ADR-001: Use Separate Table for Custom Locations
|
||||
|
||||
### Context
|
||||
Need to store user-defined locations.
|
||||
|
||||
### Options
|
||||
1. JSON field — simpler, but harder to query
|
||||
2. Separate table — more flexible, better indexing
|
||||
|
||||
### Decision
|
||||
Separate table for better querying.
|
||||
|
||||
### Consequences
|
||||
- Need migration
|
||||
- Need new tRPC router
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Don't (When Using Spec-Driven Development)
|
||||
|
||||
- Don't implement features without checking for a design doc first
|
||||
- Don't skip updating implementation.md after completing work
|
||||
- Don't make architectural decisions without recording them in decisions.md
|
||||
@@ -0,0 +1,193 @@
|
||||
---
|
||||
title: Prevent Circular Dependencies Between Packages
|
||||
impact: CRITICAL
|
||||
impactDescription: Prevents build failures, type errors, ensures honest implementation, and maintains clean dependency graph
|
||||
tags: architecture, dependencies, packages, imports, circular
|
||||
---
|
||||
|
||||
## Prevent Circular Dependencies Between Packages
|
||||
|
||||
**Impact: CRITICAL**
|
||||
|
||||
Circular dependencies between packages cause build failures, type errors, and make the codebase unmaintainable. The dependency graph must be acyclic, with clear layers from low-level utilities up to high-level features.
|
||||
|
||||
The dependency hierarchy (from lowest to highest):
|
||||
```
|
||||
packages/lib (lowest - no feature dependencies)
|
||||
↓
|
||||
packages/app-store
|
||||
↓
|
||||
packages/features
|
||||
↓
|
||||
packages/trpc
|
||||
↓
|
||||
apps/web (highest - can depend on all packages)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- **Honest implementation**: The dependency graph accurately reflects what code depends on what, with no hidden or circular relationships that create false expectations
|
||||
- **Predictable behavior**: Code behaves as the dependency structure suggests - no surprises from circular imports
|
||||
- **Clear mental model**: Developers can reason about the system without tracking complex circular relationships
|
||||
- **Build reliability**: No circular dependency errors during compilation or bundling
|
||||
- **Easier testing**: Lower-level packages can be tested independently without pulling in the entire codebase
|
||||
|
||||
### `packages/lib`
|
||||
|
||||
**Rules:**
|
||||
1. No files in `packages/lib` import from `@calcom/app-store` or `../app-store/**`
|
||||
2. No files in `packages/lib` import from `@calcom/features` or `../features/**`
|
||||
3. No files in `packages/lib` import from `@calcom/trpc` or `../trpc/**`
|
||||
4. No files in `packages/lib` import from `@trpc/server`
|
||||
5. **BONUS:** No repository files allowed in `packages/lib` (repositories belong in features or app-store)
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```typescript
|
||||
// Bad - lib importing from features
|
||||
import { getBooking } from "@calcom/features/bookings";
|
||||
import { EventRepository } from "@calcom/features/events/repositories";
|
||||
|
||||
// Bad - lib importing from app-store
|
||||
import { googleCalendarHandler } from "@calcom/app-store/googlecalendar";
|
||||
|
||||
// Bad - lib importing from trpc
|
||||
import { router } from "@calcom/trpc/server/trpc";
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```typescript
|
||||
// Good - lib only imports from other lib files or external packages
|
||||
import { logger } from "@calcom/lib/logger";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { User } from "@prisma/client";
|
||||
```
|
||||
|
||||
### `packages/app-store`
|
||||
|
||||
**Rules:**
|
||||
6. No files in `packages/app-store` import from `@calcom/features` or `../features/**`
|
||||
7. No files in `packages/app-store` import from `@calcom/trpc` or `../trpc/**`
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```typescript
|
||||
// Bad - app-store importing from features
|
||||
import { BookingService } from "@calcom/features/bookings/services";
|
||||
|
||||
// Bad - app-store importing from trpc
|
||||
import { publicProcedure } from "@calcom/trpc/server/trpc";
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```typescript
|
||||
// Good - app-store imports from lib and prisma
|
||||
import { logger } from "@calcom/lib/logger";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { Credential } from "@prisma/client";
|
||||
```
|
||||
|
||||
### `packages/features`
|
||||
|
||||
**Rules:**
|
||||
8. No files in `packages/features` import from `@calcom/trpc` or `../trpc/**`
|
||||
9. No files in `packages/features` import from `@calcom/web` or `@calcom/web/**`
|
||||
10. No files in `packages/features` import from `../../apps/web/**`
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```typescript
|
||||
// Bad - features importing from trpc
|
||||
import { router } from "@calcom/trpc/server/trpc";
|
||||
|
||||
// Bad - features importing from web app
|
||||
import { getServerSession } from "@calcom/web/lib/auth";
|
||||
import { WebComponent } from "../../apps/web/components/WebComponent";
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```typescript
|
||||
// Good - features import from lib, app-store, and prisma
|
||||
import { logger } from "@calcom/lib/logger";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { googleCalendarHandler } from "@calcom/app-store/googlecalendar";
|
||||
```
|
||||
|
||||
### `packages/trpc`
|
||||
|
||||
**Rules:**
|
||||
11. No files in `packages/trpc` import from `../apps/web/**`
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```typescript
|
||||
// Bad - trpc importing from web app
|
||||
import { getServerSession } from "../../apps/web/lib/auth";
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```typescript
|
||||
// Good - trpc imports from features, lib, and prisma
|
||||
import { BookingService } from "@calcom/features/bookings/services";
|
||||
import { logger } from "@calcom/lib/logger";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
```
|
||||
|
||||
### `packages/testing`
|
||||
|
||||
**Rules:**
|
||||
12. No files in `packages/testing` import from `@calcom/web` or `@calcom/web/**`
|
||||
13. No files in `packages/testing` import from `@calcom/features` or `@calcom/features/**`
|
||||
14. No files in `packages/testing` import from `../../apps/web/**`
|
||||
15. No files in `packages/testing` import from `../features/**`
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```typescript
|
||||
// Bad - testing importing from web or features
|
||||
import { WebComponent } from "@calcom/web/components";
|
||||
import { BookingService } from "@calcom/features/bookings";
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```typescript
|
||||
// Good - testing imports from lib and prisma
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { logger } from "@calcom/lib/logger";
|
||||
```
|
||||
|
||||
### `packages/platform/atoms`
|
||||
|
||||
**Rules:**
|
||||
16. No files in `packages/platform/atoms` import from `@calcom/trpc` or `@calcom/trpc/**`
|
||||
17. No files in `packages/platform/atoms` import from `../../trpc` or `../../trpc/**`
|
||||
18. No files in `packages/platform/atoms` import from `@calcom/web`
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```typescript
|
||||
// Bad - platform/atoms importing from trpc or web
|
||||
import { router } from "@calcom/trpc/server/trpc";
|
||||
import { trpc } from "../../trpc";
|
||||
import { WebComponent } from "@calcom/web";
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```typescript
|
||||
// Good - platform/atoms imports from lib, features, and ui
|
||||
import { logger } from "@calcom/lib/logger";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import type { Booking } from "@calcom/features/bookings/types";
|
||||
```
|
||||
|
||||
## Enforcement
|
||||
|
||||
These rules should be enforced through:
|
||||
- ESLint rules that detect forbidden import paths
|
||||
- CI checks that fail on circular dependencies
|
||||
- Code review guidelines that flag violations
|
||||
@@ -1 +1 @@
|
||||
export const TRIGGER_VERSION = '20260210.1';
|
||||
export const TRIGGER_VERSION = '20260212.3';
|
||||
|
||||
@@ -27,7 +27,7 @@ CALENDSO_ENCRYPTION_KEY="Lmqy57DlHKPvwwrtkwjL/8QtxJWxC/OV"
|
||||
# Service Account Encryption Key for encrypting/decrypting service account keys
|
||||
CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY="LDBFYLEVICRZBYQZUZCSTZRJFZUKZMEI"
|
||||
# url to redis instance running in Docker and used to cache data. Redis is setup in apps/api/v2/docker-compose.yaml
|
||||
REDIS_URL="rediss://localhost:6379"
|
||||
REDIS_URL="redis://localhost:6379"
|
||||
|
||||
# KEEP THIS EMPTY, DISABLE SENTRY CLIENT INSIDE OF LIBRARIES USED BY APIv2
|
||||
NEXT_PUBLIC_SENTRY_DSN=
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
Patch,
|
||||
ParseIntPipe,
|
||||
} from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
@@ -84,6 +84,7 @@ export class EventTypeWebhooksController {
|
||||
|
||||
@Get("/:webhookId")
|
||||
@ApiOperation({ summary: "Get a webhook" })
|
||||
@ApiParam({ name: "webhookId", type: String, required: true })
|
||||
async getEventTypeWebhook(@GetWebhook() webhook: Webhook): Promise<EventTypeWebhookOutputResponseDto> {
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -116,6 +117,7 @@ export class EventTypeWebhooksController {
|
||||
|
||||
@Delete("/:webhookId")
|
||||
@ApiOperation({ summary: "Delete a webhook" })
|
||||
@ApiParam({ name: "webhookId", type: String, required: true })
|
||||
async deleteEventTypeWebhook(@GetWebhook() webhook: Webhook): Promise<EventTypeWebhookOutputResponseDto> {
|
||||
await this.webhooksService.deleteWebhook(webhook.id);
|
||||
return {
|
||||
|
||||
+3
-1
@@ -3,7 +3,7 @@ import { MembershipRole } from "@calcom/platform-libraries";
|
||||
import { SkipTakePagination } from "@calcom/platform-types";
|
||||
import type { Webhook } from "@calcom/prisma/client";
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToClass } from "class-transformer";
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
|
||||
@@ -94,6 +94,7 @@ export class OAuthClientWebhooksController {
|
||||
summary: "Get a webhook",
|
||||
description: `<Warning>These endpoints are deprecated and will be removed in the future.</Warning>`,
|
||||
})
|
||||
@ApiParam({ name: "webhookId", type: String, required: true })
|
||||
@UseGuards(IsOAuthClientWebhookGuard)
|
||||
async getOAuthClientWebhook(@GetWebhook() webhook: Webhook): Promise<OAuthClientWebhookOutputResponseDto> {
|
||||
return {
|
||||
@@ -135,6 +136,7 @@ export class OAuthClientWebhooksController {
|
||||
summary: "Delete a webhook",
|
||||
description: `<Warning>These endpoints are deprecated and will be removed in the future.</Warning>`,
|
||||
})
|
||||
@ApiParam({ name: "webhookId", type: String, required: true })
|
||||
@UseGuards(IsOAuthClientWebhookGuard)
|
||||
async deleteOAuthClientWebhook(
|
||||
@GetWebhook() webhook: Webhook
|
||||
|
||||
+2
-2
@@ -549,7 +549,7 @@ describe("Slots 2024-04-15 Endpoints", () => {
|
||||
});
|
||||
|
||||
bookingSeatsRepositoryFixture.create({
|
||||
referenceUid: "100",
|
||||
referenceUid: randomString(),
|
||||
data: {},
|
||||
booking: {
|
||||
connect: {
|
||||
@@ -632,7 +632,7 @@ describe("Slots 2024-04-15 Endpoints", () => {
|
||||
});
|
||||
|
||||
bookingSeatsRepositoryFixture.create({
|
||||
referenceUid: "100",
|
||||
referenceUid: randomString(),
|
||||
data: {},
|
||||
booking: {
|
||||
connect: {
|
||||
|
||||
+2
-2
@@ -866,7 +866,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
});
|
||||
|
||||
bookingSeatsRepositoryFixture.create({
|
||||
referenceUid: "100",
|
||||
referenceUid: randomString(),
|
||||
data: {},
|
||||
booking: {
|
||||
connect: {
|
||||
@@ -995,7 +995,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
});
|
||||
|
||||
bookingSeatsRepositoryFixture.create({
|
||||
referenceUid: "100",
|
||||
referenceUid: randomString(),
|
||||
data: {},
|
||||
booking: {
|
||||
connect: {
|
||||
|
||||
+3
-1
@@ -13,7 +13,7 @@ import {
|
||||
Query,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToClass } from "class-transformer";
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { API_KEY_HEADER } from "@/lib/docs/headers";
|
||||
@@ -87,6 +87,7 @@ export class TeamsEventTypesWebhooksController {
|
||||
|
||||
@Get("/:webhookId")
|
||||
@ApiOperation({ summary: "Get a webhook for a team event type" })
|
||||
@ApiParam({ name: "webhookId", type: String, required: true })
|
||||
@Roles("TEAM_MEMBER")
|
||||
async getTeamEventTypeWebhook(@GetWebhook() webhook: Webhook): Promise<EventTypeWebhookOutputResponseDto> {
|
||||
return {
|
||||
@@ -122,6 +123,7 @@ export class TeamsEventTypesWebhooksController {
|
||||
@Delete("/:webhookId")
|
||||
@Roles("TEAM_ADMIN")
|
||||
@ApiOperation({ summary: "Delete a webhook for a team event type" })
|
||||
@ApiParam({ name: "webhookId", type: String, required: true })
|
||||
async deleteTeamEventTypeWebhook(
|
||||
@GetWebhook() webhook: Webhook
|
||||
): Promise<EventTypeWebhookOutputResponseDto> {
|
||||
|
||||
@@ -16,7 +16,7 @@ import { WebhookOutputPipe } from "@/modules/webhooks/pipes/WebhookOutputPipe";
|
||||
import { UserWebhooksService } from "@/modules/webhooks/services/user-webhooks.service";
|
||||
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
|
||||
import { Controller, Post, Body, UseGuards, Get, Param, Query, Delete, Patch } from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
@@ -75,6 +75,7 @@ export class WebhooksController {
|
||||
|
||||
@Get("/:webhookId")
|
||||
@ApiOperation({ summary: "Get a webhook" })
|
||||
@ApiParam({ name: "webhookId", type: String, required: true })
|
||||
@UseGuards(IsUserWebhookGuard)
|
||||
async getWebhook(@GetWebhook() webhook: Webhook): Promise<UserWebhookOutputResponseDto> {
|
||||
return {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const TRIGGER_VERSION = '20260210.1';
|
||||
export const TRIGGER_VERSION = '20260212.3';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Added as a separate route for now to ease the testing of the audit logs feature
|
||||
// It partially matches the figma design - https://www.figma.com/design/wleA2SR6rn60EK7ORxAfMy/Cal.com-New-Features?node-id=5641-6732&p=f
|
||||
// TOOD: Move it to the booking page side bar later
|
||||
// TOOD: Move it to the booking page side bar later
|
||||
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
@@ -14,35 +14,34 @@ import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
import { BookingHistoryPage } from "@calcom/web/modules/booking-audit/components/BookingHistoryPage";
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Promise<{ uid: string }> }) =>
|
||||
await _generateMetadata(
|
||||
(t) => t("booking_history"),
|
||||
(t) => t("booking_history_description"),
|
||||
undefined,
|
||||
undefined,
|
||||
`/booking/${(await params).uid}/logs`
|
||||
);
|
||||
await _generateMetadata(
|
||||
(t) => t("booking_history"),
|
||||
(t) => t("booking_history_description"),
|
||||
undefined,
|
||||
undefined,
|
||||
`/booking/${(await params).uid}/logs`
|
||||
);
|
||||
|
||||
const Page = async ({ params }: PageProps) => {
|
||||
const resolvedParams = await params;
|
||||
const bookingUid = resolvedParams.uid;
|
||||
const resolvedParams = await params;
|
||||
const bookingUid = resolvedParams.uid;
|
||||
|
||||
if (!bookingUid || typeof bookingUid !== "string") {
|
||||
redirect("/bookings/upcoming");
|
||||
}
|
||||
if (!bookingUid || typeof bookingUid !== "string") {
|
||||
redirect("/bookings/upcoming");
|
||||
}
|
||||
|
||||
const t = await getTranslate();
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
const t = await getTranslate();
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
redirect("/auth/login");
|
||||
}
|
||||
if (!session?.user?.id) {
|
||||
redirect("/auth/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<ShellMainAppDir heading={t("booking_history")} subtitle={t("booking_history_description")}>
|
||||
<BookingHistoryPage bookingUid={bookingUid} />
|
||||
</ShellMainAppDir>
|
||||
);
|
||||
return (
|
||||
<ShellMainAppDir heading={t("booking_history")} subtitle={t("booking_history_description")}>
|
||||
<BookingHistoryPage bookingUid={bookingUid} />
|
||||
</ShellMainAppDir>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
|
||||
@@ -5,11 +5,7 @@ import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { eventTypesRouter } from "@calcom/trpc/server/routers/viewer/eventTypes/_router";
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
import { createRouterCaller, getTRPCContext } from "app/_trpc/context";
|
||||
import type {
|
||||
PageProps,
|
||||
ReadonlyHeaders,
|
||||
ReadonlyRequestCookies,
|
||||
} from "app/_types";
|
||||
import type { PageProps, ReadonlyHeaders, ReadonlyRequestCookies } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { cookies, headers } from "next/headers";
|
||||
@@ -26,26 +22,25 @@ const getCachedEventGroups: (
|
||||
userIds?: number[] | undefined;
|
||||
upIds?: string[] | undefined;
|
||||
}
|
||||
) => Promise<RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"]> =
|
||||
unstable_cache(
|
||||
async (
|
||||
headers: ReadonlyHeaders,
|
||||
cookies: ReadonlyRequestCookies,
|
||||
filters?: {
|
||||
teamIds?: number[] | undefined;
|
||||
userIds?: number[] | undefined;
|
||||
upIds?: string[] | undefined;
|
||||
}
|
||||
): Promise<RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"]> => {
|
||||
const eventTypesCaller = await createRouterCaller(
|
||||
eventTypesRouter,
|
||||
await getTRPCContext(headers, cookies)
|
||||
);
|
||||
return await eventTypesCaller.getUserEventGroups({ filters });
|
||||
},
|
||||
["viewer.eventTypes.getUserEventGroups"],
|
||||
{ revalidate: 3600 } // seconds
|
||||
);
|
||||
) => Promise<RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"]> = unstable_cache(
|
||||
async (
|
||||
headers: ReadonlyHeaders,
|
||||
cookies: ReadonlyRequestCookies,
|
||||
filters?: {
|
||||
teamIds?: number[] | undefined;
|
||||
userIds?: number[] | undefined;
|
||||
upIds?: string[] | undefined;
|
||||
}
|
||||
): Promise<RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"]> => {
|
||||
const eventTypesCaller = await createRouterCaller(
|
||||
eventTypesRouter,
|
||||
await getTRPCContext(headers, cookies)
|
||||
);
|
||||
return await eventTypesCaller.getUserEventGroups({ filters });
|
||||
},
|
||||
["viewer.eventTypes.getUserEventGroups"],
|
||||
{ revalidate: 3600 } // seconds
|
||||
);
|
||||
|
||||
const Page = async ({ searchParams }: PageProps): Promise<ReactElement> => {
|
||||
const _searchParams = await searchParams;
|
||||
@@ -71,23 +66,12 @@ const Page = async ({ searchParams }: PageProps): Promise<ReactElement> => {
|
||||
}
|
||||
|
||||
const filters = getTeamsFiltersFromQuery(_searchParams);
|
||||
const userEventGroupsData = await getCachedEventGroups(
|
||||
_headers,
|
||||
_cookies,
|
||||
filters
|
||||
);
|
||||
const userEventGroupsData = await getCachedEventGroups(_headers, _cookies, filters);
|
||||
|
||||
return (
|
||||
<EventTypesWrapper
|
||||
userEventGroupsData={userEventGroupsData}
|
||||
user={session.user}
|
||||
/>
|
||||
);
|
||||
return <EventTypesWrapper userEventGroupsData={userEventGroupsData} user={session.user} />;
|
||||
};
|
||||
|
||||
export const generateMetadata = async (): Promise<
|
||||
ReturnType<typeof _generateMetadata>
|
||||
> =>
|
||||
export const generateMetadata = async (): Promise<ReturnType<typeof _generateMetadata>> =>
|
||||
await _generateMetadata(
|
||||
(t) => t("event_types_page_title"),
|
||||
(t) => t("event_types_page_subtitle"),
|
||||
|
||||
@@ -15,7 +15,7 @@ export const TeamsCTA = () => {
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
posthog.capture("add_team_button_clicked")
|
||||
posthog.capture("add_team_button_clicked");
|
||||
}}
|
||||
href={`${WEBAPP_URL}/settings/teams/new?returnTo=${WEBAPP_URL}/teams`}>
|
||||
{t("new")}
|
||||
|
||||
@@ -38,12 +38,15 @@ const ServerPage = async () => {
|
||||
const categoryQuery = appStore.map(({ categories }) => ({
|
||||
categories: categories || [],
|
||||
}));
|
||||
const categories = categoryQuery.reduce((c, app) => {
|
||||
for (const category of app.categories) {
|
||||
c[category] = c[category] ? c[category] + 1 : 1;
|
||||
}
|
||||
return c;
|
||||
}, {} as Record<string, number>);
|
||||
const categories = categoryQuery.reduce(
|
||||
(c, app) => {
|
||||
for (const category of app.categories) {
|
||||
c[category] = c[category] ? c[category] + 1 : 1;
|
||||
}
|
||||
return c;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
);
|
||||
|
||||
const props = {
|
||||
categories: Object.entries(categories)
|
||||
|
||||
@@ -14,10 +14,10 @@ import {
|
||||
SelectField,
|
||||
TextField,
|
||||
} from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
import type { getServerSidePropsForSingleFormView as getServerSideProps } from "@calcom/web/lib/apps/routing-forms/[...pages]/getServerSidePropsSingleForm";
|
||||
import SingleForm from "@components/apps/routing-forms/SingleForm";
|
||||
import { ChevronDownIcon, MenuIcon } from "@coss/ui/icons";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
@@ -93,8 +93,7 @@ function Field({
|
||||
}
|
||||
: null
|
||||
}
|
||||
deleteField={router ? null : deleteField}
|
||||
>
|
||||
deleteField={router ? null : deleteField}>
|
||||
<FormCardBody>
|
||||
<div className="mb-3 w-full">
|
||||
<TextField
|
||||
@@ -114,17 +113,12 @@ function Field({
|
||||
hookForm.setValue(`${hookFieldNamespace}.label`, newLabel, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
const currentIdentifier = hookForm.getValues(
|
||||
`${hookFieldNamespace}.identifier`
|
||||
);
|
||||
const currentIdentifier = hookForm.getValues(`${hookFieldNamespace}.identifier`);
|
||||
// Only auto-update identifier if it was auto-generated from the previous label
|
||||
// This preserves manual identifier changes
|
||||
const isIdentifierGeneratedFromPreviousLabel =
|
||||
currentIdentifier === getFieldIdentifier(previousLabel).toLowerCase();
|
||||
if (
|
||||
!currentIdentifier ||
|
||||
isIdentifierGeneratedFromPreviousLabel
|
||||
) {
|
||||
if (!currentIdentifier || isIdentifierGeneratedFromPreviousLabel) {
|
||||
hookForm.setValue(
|
||||
`${hookFieldNamespace}.identifier`,
|
||||
getFieldIdentifier(newLabel).toLowerCase(),
|
||||
@@ -142,19 +136,11 @@ function Field({
|
||||
name={`${hookFieldNamespace}.identifier`}
|
||||
required
|
||||
placeholder={t("identifies_name_field")}
|
||||
value={
|
||||
identifier ||
|
||||
routerField?.identifier ||
|
||||
label ||
|
||||
routerField?.label ||
|
||||
""
|
||||
}
|
||||
value={identifier || routerField?.identifier || label || routerField?.label || ""}
|
||||
onChange={(e) => {
|
||||
hookForm.setValue(
|
||||
`${hookFieldNamespace}.identifier`,
|
||||
e.target.value.toLowerCase(),
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
hookForm.setValue(`${hookFieldNamespace}.identifier`, e.target.value.toLowerCase(), {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -164,9 +150,7 @@ function Field({
|
||||
control={hookForm.control}
|
||||
defaultValue={routerField?.type}
|
||||
render={({ field: { value, onChange } }) => {
|
||||
const defaultValue = FieldTypes.find(
|
||||
(fieldType) => fieldType.value === value
|
||||
);
|
||||
const defaultValue = FieldTypes.find((fieldType) => fieldType.value === value);
|
||||
if (disableTypeChange) {
|
||||
return (
|
||||
<div className="data-testid-field-type">
|
||||
@@ -179,15 +163,9 @@ function Field({
|
||||
className={classNames(
|
||||
"h-8 w-full justify-between text-left text-sm",
|
||||
!!router && "bg-subtle cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<span className="text-default">
|
||||
{defaultValue?.label || "Select field type"}
|
||||
</span>
|
||||
<Icon
|
||||
name="chevron-down"
|
||||
className="text-default h-4 w-4"
|
||||
/>
|
||||
)}>
|
||||
<span className="text-default">{defaultValue?.label || t("select_field_type")}</span>
|
||||
<ChevronDownIcon className="text-default h-4 w-4" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -304,9 +282,7 @@ const FormEdit = ({
|
||||
<div className="w-full py-4 lg:py-8">
|
||||
<div ref={animationRef} className="flex w-full flex-col rounded-md">
|
||||
{hookFormFields.map((field, key) => {
|
||||
const existingField = Boolean(
|
||||
(form.fields || []).find((f) => f.id === field.id)
|
||||
);
|
||||
const existingField = Boolean((form.fields || []).find((f) => f.id === field.id));
|
||||
const hasFormResponses = (form._count?.responses ?? 0) > 0;
|
||||
return (
|
||||
<Field
|
||||
@@ -343,13 +319,7 @@ const FormEdit = ({
|
||||
</div>
|
||||
{hookFormFields.length ? (
|
||||
<div className={classNames("flex")}>
|
||||
<Button
|
||||
data-testid="add-field"
|
||||
type="button"
|
||||
StartIcon="plus"
|
||||
color="secondary"
|
||||
onClick={addField}
|
||||
>
|
||||
<Button data-testid="add-field" type="button" StartIcon="plus" color="secondary" onClick={addField}>
|
||||
Add question
|
||||
</Button>
|
||||
</div>
|
||||
@@ -363,7 +333,7 @@ const FormEdit = ({
|
||||
{/* Icon card - Top */}
|
||||
<div className="bg-default border-subtle z-30 col-start-1 col-end-1 row-start-1 row-end-1 h-10 w-10 transform rounded-md border shadow-sm">
|
||||
<div className="text-emphasis flex h-full items-center justify-center">
|
||||
<Icon name="menu" className="text-emphasis h-4 w-4" />
|
||||
<MenuIcon className="text-emphasis h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
{/* Left fanned card */}
|
||||
@@ -389,12 +359,7 @@ const FormEdit = ({
|
||||
Fields are the form fields that the booker would see.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
data-testid="add-field"
|
||||
onClick={addField}
|
||||
StartIcon="plus"
|
||||
className="mt-6"
|
||||
>
|
||||
<Button data-testid="add-field" onClick={addField} StartIcon="plus" className="mt-6">
|
||||
Add question
|
||||
</Button>
|
||||
</div>
|
||||
@@ -414,9 +379,7 @@ export default function FormEditPage({
|
||||
{...props}
|
||||
appUrl={appUrl}
|
||||
permissions={permissions}
|
||||
Page={({ hookForm, form }) => (
|
||||
<FormEdit appUrl={appUrl} hookForm={hookForm} form={form} />
|
||||
)}
|
||||
Page={({ hookForm, form }) => <FormEdit appUrl={appUrl} hookForm={hookForm} form={form} />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+19
-24
@@ -1,10 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import type z from "zod";
|
||||
|
||||
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
|
||||
import { WhenToWriteToRecord, SalesforceFieldType } from "@calcom/app-store/salesforce/lib/enums";
|
||||
import { SalesforceFieldType, WhenToWriteToRecord } from "@calcom/app-store/salesforce/lib/enums";
|
||||
import type { writeToRecordDataSchema as salesforceWriteToRecordDataSchema } from "@calcom/app-store/salesforce/zod";
|
||||
import { routingFormIncompleteBookingDataSchema as salesforceRoutingFormIncompleteBookingDataSchema } from "@calcom/app-store/salesforce/zod";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -12,15 +9,13 @@ import { IncompleteBookingActionType } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Switch } from "@calcom/ui/components/form";
|
||||
import { InputField } from "@calcom/ui/components/form";
|
||||
import { Select } from "@calcom/ui/components/form";
|
||||
import { Label } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { InputField, Label, Select, Switch } from "@calcom/ui/components/form";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import type { getServerSidePropsForSingleFormView as getServerSideProps } from "@calcom/web/lib/apps/routing-forms/[...pages]/getServerSidePropsSingleForm";
|
||||
|
||||
import SingleForm from "@components/apps/routing-forms/SingleForm";
|
||||
import { GlobeIcon } from "@coss/ui/icons";
|
||||
import { useEffect, useState } from "react";
|
||||
import type z from "zod";
|
||||
|
||||
function Page({ form }: { form: RoutingFormWithResponseCount }) {
|
||||
const { t } = useLocale();
|
||||
@@ -88,8 +83,8 @@ function Page({ form }: { form: RoutingFormWithResponseCount }) {
|
||||
|
||||
setSelectedCredential(
|
||||
credentialOptions
|
||||
? credentialOptions.find((option) => option.value === salesforceAction?.credentialId) ??
|
||||
selectedCredential
|
||||
? (credentialOptions.find((option) => option.value === salesforceAction?.credentialId) ??
|
||||
selectedCredential)
|
||||
: selectedCredential
|
||||
);
|
||||
}
|
||||
@@ -105,7 +100,7 @@ function Page({ form }: { form: RoutingFormWithResponseCount }) {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-0.5">
|
||||
<div className="border-subtle rounded-lg border p-1">
|
||||
<Icon name="globe" className="text-subtle h-4 w-4" />
|
||||
<GlobeIcon className="text-subtle h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-emphasis ml-2 text-sm font-medium">
|
||||
@@ -128,17 +123,17 @@ function Page({ form }: { form: RoutingFormWithResponseCount }) {
|
||||
<div className="bg-default mt-2 rounded-xl px-2 py-2">
|
||||
<Label>Credential to use</Label>
|
||||
<Select
|
||||
size="sm"
|
||||
options={credentialOptions}
|
||||
value={selectedCredential}
|
||||
onChange={(option) => {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
setSelectedCredential(option);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
size="sm"
|
||||
options={credentialOptions}
|
||||
value={selectedCredential}
|
||||
onChange={(option) => {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
setSelectedCredential(option);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,43 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import Link from "next/link";
|
||||
import React, { useCallback, useState, useEffect } from "react";
|
||||
import { Query, Builder, Utils as QbUtils } from "react-awesome-query-builder";
|
||||
import type { ImmutableTree, BuilderProps, Config } from "react-awesome-query-builder";
|
||||
import type { JsonTree } from "react-awesome-query-builder";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
import { buildEmptyQueryValue } from "@calcom/app-store/_utils/raqb/raqbUtils.client";
|
||||
import { raqbQueryValueUtils } from "@calcom/app-store/_utils/raqb/raqbUtils.server";
|
||||
import { routingFormAppComponents } from "@calcom/app-store/routing-forms/appComponents";
|
||||
import DynamicAppComponent from "@calcom/app-store/routing-forms/components/DynamicAppComponent";
|
||||
import { EmptyState } from "@calcom/app-store/routing-forms/components/_components/EmptyState";
|
||||
import { RoutingSkeleton } from "@calcom/app-store/routing-forms/components/_components/RoutingSkeleton";
|
||||
import DynamicAppComponent from "@calcom/app-store/routing-forms/components/DynamicAppComponent";
|
||||
import {
|
||||
withRaqbSettingsAndWidgets,
|
||||
ConfigFor,
|
||||
withRaqbSettingsAndWidgets,
|
||||
} from "@calcom/app-store/routing-forms/components/react-awesome-query-builder/config/uiConfig";
|
||||
import { RoutingPages } from "@calcom/app-store/routing-forms/lib/RoutingPages";
|
||||
import { createFallbackRoute } from "@calcom/app-store/routing-forms/lib/createFallbackRoute";
|
||||
import getEventTypeAppMetadata from "@calcom/app-store/routing-forms/lib/getEventTypeAppMetadata";
|
||||
import {
|
||||
getQueryBuilderConfigForFormFields,
|
||||
getQueryBuilderConfigForAttributes,
|
||||
type FormFieldsQueryBuilderConfigWithRaqbFields,
|
||||
type AttributesQueryBuilderConfigWithRaqbFields,
|
||||
type FormFieldsQueryBuilderConfigWithRaqbFields,
|
||||
getQueryBuilderConfigForAttributes,
|
||||
getQueryBuilderConfigForFormFields,
|
||||
isDynamicOperandField,
|
||||
} from "@calcom/app-store/routing-forms/lib/getQueryBuilderConfig";
|
||||
import isRouter from "@calcom/app-store/routing-forms/lib/isRouter";
|
||||
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
|
||||
import { RoutingPages } from "@calcom/app-store/routing-forms/lib/RoutingPages";
|
||||
import type {
|
||||
Attribute,
|
||||
AttributeRoutingConfig,
|
||||
EditFormRoute,
|
||||
GlobalRoute,
|
||||
LocalRoute,
|
||||
RoutingFormWithResponseCount,
|
||||
SerializableRoute,
|
||||
Attribute,
|
||||
EditFormRoute,
|
||||
AttributeRoutingConfig,
|
||||
} from "@calcom/app-store/routing-forms/types/types";
|
||||
import { RouteActionType } from "@calcom/app-store/routing-forms/zod";
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
@@ -52,15 +43,25 @@ import classNames from "@calcom/ui/classNames";
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { FormCard } from "@calcom/ui/components/card";
|
||||
import { SelectWithValidation as Select, TextArea } from "@calcom/ui/components/form";
|
||||
import { TextField } from "@calcom/ui/components/form";
|
||||
import { SelectField } from "@calcom/ui/components/form";
|
||||
import { Switch } from "@calcom/ui/components/form";
|
||||
import {
|
||||
SelectWithValidation as Select,
|
||||
SelectField,
|
||||
Switch,
|
||||
TextArea,
|
||||
TextField,
|
||||
} from "@calcom/ui/components/form";
|
||||
import type { IconName } from "@calcom/ui/components/icon";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import type { getServerSidePropsForSingleFormView as getServerSideProps } from "@calcom/web/lib/apps/routing-forms/[...pages]/getServerSidePropsSingleForm";
|
||||
|
||||
import SingleForm from "@components/apps/routing-forms/SingleForm";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import Link from "next/link";
|
||||
import type React from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import type { BuilderProps, Config, ImmutableTree, JsonTree } from "react-awesome-query-builder";
|
||||
import { Builder, Utils as QbUtils, Query } from "react-awesome-query-builder";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
type Form = inferSSRProps<typeof getServerSideProps>["form"];
|
||||
|
||||
@@ -342,6 +343,165 @@ const WeightedAttributesSelector = ({
|
||||
) : null;
|
||||
};
|
||||
|
||||
type RouteActionSelectorProps = {
|
||||
action: LocalRoute["action"] | undefined;
|
||||
onActionChange: (action: LocalRoute["action"]) => void;
|
||||
onEventTypeChange?: (action: LocalRoute["action"]) => void;
|
||||
eventTypeOptions: { label: string; value: string; eventTypeId: number }[];
|
||||
selectedEventTypeOption:
|
||||
| { label: string; value: string; eventTypeId: number }
|
||||
| { label: string; value: "custom"; eventTypeId: 0 }
|
||||
| undefined;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
showCustomEventTypeInput?: boolean;
|
||||
customEventTypeSlug?: string;
|
||||
onCustomEventTypeSlugChange?: (slug: string) => void;
|
||||
eventTypePrefix?: string;
|
||||
fieldIdentifiers?: string[];
|
||||
};
|
||||
|
||||
const RouteActionSelector = ({
|
||||
action,
|
||||
onActionChange,
|
||||
onEventTypeChange,
|
||||
eventTypeOptions,
|
||||
selectedEventTypeOption,
|
||||
disabled = false,
|
||||
className = "",
|
||||
showCustomEventTypeInput = false,
|
||||
customEventTypeSlug = "",
|
||||
onCustomEventTypeSlugChange,
|
||||
eventTypePrefix = "",
|
||||
fieldIdentifiers = [],
|
||||
}: RouteActionSelectorProps) => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<div className={classNames("flex w-full flex-col gap-2 text-sm lg:flex-row", className)}>
|
||||
<div className="flex grow items-center gap-2">
|
||||
<Select
|
||||
size="sm"
|
||||
isDisabled={disabled}
|
||||
className="data-testid-select-routing-action block w-full grow"
|
||||
required
|
||||
value={RoutingPages.find((page) => page.value === action?.type)}
|
||||
onChange={(item) => {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
const newAction: LocalRoute["action"] = {
|
||||
type: item.value,
|
||||
value: "",
|
||||
};
|
||||
|
||||
if (newAction.type === "customPageMessage") {
|
||||
newAction.value = t("default_custom_page_message");
|
||||
} else {
|
||||
newAction.value = "";
|
||||
}
|
||||
|
||||
onActionChange(newAction);
|
||||
}}
|
||||
options={RoutingPages}
|
||||
/>
|
||||
</div>
|
||||
{action?.type ? (
|
||||
action?.type === "customPageMessage" ? (
|
||||
<TextArea
|
||||
required
|
||||
disabled={disabled}
|
||||
name="customPageMessage"
|
||||
className="border-default flex grow lg:w-fit"
|
||||
style={{
|
||||
minHeight: "38px",
|
||||
}}
|
||||
value={action.value}
|
||||
onChange={(e) => {
|
||||
onActionChange({ ...action, value: e.target.value });
|
||||
}}
|
||||
/>
|
||||
) : action?.type === "externalRedirectUrl" ? (
|
||||
<TextField
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
name="externalRedirectUrl"
|
||||
className="border-default flex grow text-sm"
|
||||
containerClassName="grow"
|
||||
type="url"
|
||||
required
|
||||
labelSrOnly
|
||||
value={action.value}
|
||||
onChange={(e) => {
|
||||
onActionChange({ ...action, value: e.target.value });
|
||||
}}
|
||||
placeholder="https://example.com"
|
||||
/>
|
||||
) : (
|
||||
<div className="grow">
|
||||
<Select
|
||||
size="sm"
|
||||
required
|
||||
className="data-testid-eventTypeRedirectUrl-select"
|
||||
isDisabled={disabled}
|
||||
options={eventTypeOptions}
|
||||
onChange={(option) => {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
const newAction = {
|
||||
...action,
|
||||
value: option.value,
|
||||
eventTypeId: option.eventTypeId,
|
||||
};
|
||||
if (onEventTypeChange) {
|
||||
onEventTypeChange(newAction);
|
||||
} else {
|
||||
onActionChange(newAction);
|
||||
}
|
||||
}}
|
||||
value={selectedEventTypeOption}
|
||||
/>
|
||||
{showCustomEventTypeInput &&
|
||||
eventTypeOptions.length !== 0 &&
|
||||
action.value !== "" &&
|
||||
(action.value === "custom" ||
|
||||
!eventTypeOptions.find((eventOption) => eventOption.value === action.value) ||
|
||||
customEventTypeSlug.length) ? (
|
||||
<>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
className="border-default flex w-full grow text-sm"
|
||||
containerClassName="grow mt-2"
|
||||
addOnLeading={eventTypePrefix}
|
||||
required
|
||||
value={customEventTypeSlug}
|
||||
onChange={(e) => {
|
||||
onCustomEventTypeSlugChange?.(e.target.value);
|
||||
onActionChange({
|
||||
...action,
|
||||
value: `${eventTypePrefix}${e.target.value}`,
|
||||
});
|
||||
}}
|
||||
placeholder="event-url"
|
||||
/>
|
||||
<div className="mt-2 ">
|
||||
<p className="text-subtle text-xs">
|
||||
{fieldIdentifiers.length
|
||||
? t("field_identifiers_as_variables_with_example", {
|
||||
variable: `{${fieldIdentifiers[0]}}`,
|
||||
})
|
||||
: t("field_identifiers_as_variables")}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Route = ({
|
||||
form,
|
||||
route,
|
||||
@@ -395,10 +555,20 @@ const Route = ({
|
||||
? eventOptions[0].value.substring(0, eventOptions[0].value.lastIndexOf("/") + 1)
|
||||
: "";
|
||||
|
||||
const getCustomSlug = (actionValue: string | undefined) => {
|
||||
if (!actionValue) return "";
|
||||
const isCustom = !eventOptions.find((eventOption) => eventOption.value === actionValue);
|
||||
return isCustom ? (actionValue.split("/").pop() ?? "") : "";
|
||||
};
|
||||
|
||||
const [customEventTypeSlug, setCustomEventTypeSlug] = useState<string>(() => {
|
||||
const isCustom =
|
||||
!isRouter(route) && !eventOptions.find((eventOption) => eventOption.value === route.action.value);
|
||||
return isCustom && !isRouter(route) ? route.action.value.split("/").pop() ?? "" : "";
|
||||
if (isRouter(route)) return "";
|
||||
return getCustomSlug(route.action.value);
|
||||
});
|
||||
|
||||
const [customFallbackEventTypeSlug, setCustomFallbackEventTypeSlug] = useState<string>(() => {
|
||||
if (isRouter(route)) return "";
|
||||
return getCustomSlug(route.fallbackAction?.value);
|
||||
});
|
||||
|
||||
useEnsureEventTypeIdInRedirectUrlAction({
|
||||
@@ -558,7 +728,7 @@ const Route = ({
|
||||
const attributesQueryBuilder =
|
||||
// team member attributes are only available for organization teams
|
||||
route.action?.type === RouteActionType.EventTypeRedirectUrl && isTeamForm && isOrganization ? (
|
||||
<div className="mt-4">
|
||||
<div>
|
||||
{/* TODO: */}
|
||||
{eventTypeRedirectUrlSelectedOption?.eventTypeAppMetadata &&
|
||||
"salesforce" in eventTypeRedirectUrlSelectedOption.eventTypeAppMetadata ? (
|
||||
@@ -573,13 +743,13 @@ const Route = ({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="bg-default border-subtle cal-query-builder-container mt-2 rounded-2xl border p-2">
|
||||
<div className="cal-query-builder-container mt-4" data-testid="attributes-query-builder">
|
||||
<div className="ml-2 flex items-center gap-0.5">
|
||||
<div className="border-subtle rounded-lg border p-1">
|
||||
<Icon name="user-check" className="text-subtle h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-emphasis ml-2 text-sm font-medium">
|
||||
And connect with specific team members
|
||||
{t("connect_with_specific_team_members")}
|
||||
</span>
|
||||
</div>
|
||||
{route.attributesQueryBuilderState && attributesQueryBuilderConfigWithRaqbSettingsAndWidgets && (
|
||||
@@ -600,30 +770,70 @@ const Route = ({
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const fallbackAttributesQueryBuilder =
|
||||
route.action?.type === RouteActionType.EventTypeRedirectUrl && isTeamForm ? (
|
||||
const fallbackActionOptions = eventTypeRedirectUrlOptions;
|
||||
const fallbackActionSelectedOption = route.fallbackAction?.eventTypeId
|
||||
? eventTypeRedirectUrlOptions.find((option) => option.eventTypeId === route.fallbackAction?.eventTypeId)
|
||||
: eventTypeRedirectUrlOptions.find((option) => option.value === route.fallbackAction?.value);
|
||||
|
||||
// Only show fallback section when main action has a valid event type selected
|
||||
// This prevents showing an empty fallback action selector before user selects an event type
|
||||
const hasValidEventTypeSelected =
|
||||
route.action?.type === RouteActionType.EventTypeRedirectUrl &&
|
||||
(route.action?.eventTypeId || route.action?.value);
|
||||
|
||||
const matchingMembersFallbackRoute =
|
||||
hasValidEventTypeSelected && isTeamForm ? (
|
||||
<div className="bg-default border-subtle cal-query-builder-container mt-2 rounded-2xl border p-2">
|
||||
<div className="ml-2 flex items-center gap-0.5">
|
||||
<div className="border-subtle rounded-lg border p-1">
|
||||
<Icon name="blocks" className="text-subtle h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-emphasis ml-2 text-sm font-medium">Fallback</span>
|
||||
<span className="text-emphasis ml-2 text-sm font-medium">{t("fallback_action")}</span>
|
||||
</div>
|
||||
{route.fallbackAttributesQueryBuilderState &&
|
||||
attributesQueryBuilderConfigWithRaqbSettingsAndWidgets && (
|
||||
<Query
|
||||
{...attributesQueryBuilderConfigWithRaqbSettingsAndWidgets}
|
||||
value={route.fallbackAttributesQueryBuilderState.tree}
|
||||
onChange={(immutableTree, attributesQueryBuilderConfig) => {
|
||||
onChangeFallbackTeamMembersQuery(
|
||||
route,
|
||||
immutableTree,
|
||||
attributesQueryBuilderConfig as unknown as AttributesQueryBuilderConfigWithRaqbFields
|
||||
);
|
||||
}}
|
||||
renderBuilder={renderBuilder}
|
||||
/>
|
||||
)}
|
||||
<div className="bg-cal-muted mt-2 rounded-xl p-2">
|
||||
<RouteActionSelector
|
||||
action={route.fallbackAction}
|
||||
onActionChange={(newAction) => setRoute(route.id, { fallbackAction: newAction })}
|
||||
eventTypeOptions={fallbackActionOptions}
|
||||
selectedEventTypeOption={fallbackActionSelectedOption}
|
||||
disabled={disabled}
|
||||
showCustomEventTypeInput
|
||||
customEventTypeSlug={customFallbackEventTypeSlug}
|
||||
onCustomEventTypeSlugChange={setCustomFallbackEventTypeSlug}
|
||||
eventTypePrefix={eventTypePrefix}
|
||||
fieldIdentifiers={fieldIdentifiers}
|
||||
/>
|
||||
</div>
|
||||
{isOrganization && (
|
||||
<div className="mt-4">
|
||||
{route.fallbackAction?.type === RouteActionType.EventTypeRedirectUrl &&
|
||||
route.fallbackAttributesQueryBuilderState &&
|
||||
attributesQueryBuilderConfigWithRaqbSettingsAndWidgets && (
|
||||
<div>
|
||||
<div className="ml-2 flex items-center gap-0.5">
|
||||
<div className="border-subtle rounded-lg border p-1">
|
||||
<Icon name="user-check" className="text-subtle h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-emphasis ml-2 text-sm font-medium">
|
||||
{t("connect_with_specific_team_members")}
|
||||
</span>
|
||||
</div>
|
||||
<Query
|
||||
{...attributesQueryBuilderConfigWithRaqbSettingsAndWidgets}
|
||||
value={route.fallbackAttributesQueryBuilderState.tree}
|
||||
onChange={(immutableTree, attributesQueryBuilderConfig) => {
|
||||
onChangeFallbackTeamMembersQuery(
|
||||
route,
|
||||
immutableTree,
|
||||
attributesQueryBuilderConfig as unknown as AttributesQueryBuilderConfigWithRaqbFields
|
||||
);
|
||||
}}
|
||||
renderBuilder={renderBuilder}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
@@ -659,283 +869,85 @@ const Route = ({
|
||||
{formFieldsQueryBuilder}
|
||||
<div>
|
||||
{route.isFallback ? (
|
||||
<div className="flex w-full flex-col gap-2 text-sm lg:flex-row">
|
||||
<div className="flex grow items-center gap-2">
|
||||
{/* <div className="flex grow-0 whitespace-nowrap">
|
||||
<span>{t("send_booker_to")}</span>
|
||||
</div> */}
|
||||
<Select
|
||||
size="sm"
|
||||
isDisabled={disabled}
|
||||
className="data-testid-select-routing-action block w-full grow"
|
||||
required
|
||||
value={RoutingPages.find((page) => page.value === route.action?.type)}
|
||||
onChange={(item) => {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
const action: LocalRoute["action"] = {
|
||||
type: item.value,
|
||||
value: "",
|
||||
};
|
||||
|
||||
if (action.type === "customPageMessage") {
|
||||
action.value = "We are not ready for you yet :(";
|
||||
} else {
|
||||
action.value = "";
|
||||
}
|
||||
|
||||
setRoute(route.id, { action });
|
||||
}}
|
||||
options={RoutingPages}
|
||||
/>
|
||||
</div>
|
||||
{route.action?.type ? (
|
||||
route.action?.type === "customPageMessage" ? (
|
||||
<TextArea
|
||||
required
|
||||
disabled={disabled}
|
||||
name="customPageMessage"
|
||||
className="border-default flex grow lg:w-fit"
|
||||
style={{
|
||||
minHeight: "38px",
|
||||
}}
|
||||
value={route.action.value}
|
||||
onChange={(e) => {
|
||||
setRoute(route.id, { action: { ...route.action, value: e.target.value } });
|
||||
}}
|
||||
/>
|
||||
) : route.action?.type === "externalRedirectUrl" ? (
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
name="externalRedirectUrl"
|
||||
className="border-default flex grow text-sm"
|
||||
containerClassName="grow"
|
||||
type="url"
|
||||
required
|
||||
labelSrOnly
|
||||
value={route.action.value}
|
||||
onChange={(e) => {
|
||||
setRoute(route.id, { action: { ...route.action, value: e.target.value } });
|
||||
}}
|
||||
placeholder="https://example.com"
|
||||
/>
|
||||
) : (
|
||||
<div className="grow">
|
||||
<Select
|
||||
size="sm"
|
||||
required
|
||||
className="data-testid-eventTypeRedirectUrl-select"
|
||||
isDisabled={disabled}
|
||||
options={eventTypeRedirectUrlOptions}
|
||||
onChange={(option) => {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
if (option.value !== "custom") {
|
||||
setRoute(route.id, {
|
||||
action: {
|
||||
...route.action,
|
||||
value: option.value,
|
||||
eventTypeId: option.eventTypeId,
|
||||
},
|
||||
attributeRoutingConfig: {},
|
||||
});
|
||||
setCustomEventTypeSlug("");
|
||||
} else {
|
||||
setRoute(route.id, {
|
||||
action: { ...route.action, value: "custom", eventTypeId: 0 },
|
||||
attributeRoutingConfig: {},
|
||||
});
|
||||
setCustomEventTypeSlug("");
|
||||
}
|
||||
}}
|
||||
value={eventTypeRedirectUrlSelectedOption}
|
||||
/>
|
||||
{eventOptions.length !== 0 &&
|
||||
route.action.value !== "" &&
|
||||
(!eventOptions.find((eventOption) => eventOption.value === route.action.value) ||
|
||||
customEventTypeSlug.length) ? (
|
||||
<>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
className="border-default flex w-full grow text-sm"
|
||||
containerClassName="grow mt-2"
|
||||
addOnLeading={eventTypePrefix}
|
||||
required
|
||||
value={customEventTypeSlug}
|
||||
onChange={(e) => {
|
||||
setCustomEventTypeSlug(e.target.value);
|
||||
setRoute(route.id, {
|
||||
action: { ...route.action, value: `${eventTypePrefix}${e.target.value}` },
|
||||
});
|
||||
}}
|
||||
placeholder="event-url"
|
||||
/>
|
||||
<div className="mt-2 ">
|
||||
<p className="text-subtle text-xs">
|
||||
{fieldIdentifiers.length
|
||||
? t("field_identifiers_as_variables_with_example", {
|
||||
variable: `{${fieldIdentifiers[0]}}`,
|
||||
})
|
||||
: t("field_identifiers_as_variables")}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
<RouteActionSelector
|
||||
action={route.action}
|
||||
onActionChange={(newAction) => setRoute(route.id, { action: newAction })}
|
||||
onEventTypeChange={(newAction) => {
|
||||
// Initialize fallbackAction with the same event type if not already set with a valid event type
|
||||
// Check if fallbackAction has a valid event type (eventTypeId or non-empty value)
|
||||
const hasValidFallbackEventType =
|
||||
route.fallbackAction?.eventTypeId || route.fallbackAction?.value;
|
||||
const fallbackAction = hasValidFallbackEventType
|
||||
? route.fallbackAction
|
||||
: newAction.type === RouteActionType.EventTypeRedirectUrl
|
||||
? {
|
||||
type: newAction.type,
|
||||
value: newAction.value,
|
||||
eventTypeId: newAction.eventTypeId,
|
||||
}
|
||||
: undefined;
|
||||
setRoute(route.id, { action: newAction, attributeRoutingConfig: {}, fallbackAction });
|
||||
setCustomEventTypeSlug("");
|
||||
}}
|
||||
eventTypeOptions={eventTypeRedirectUrlOptions}
|
||||
selectedEventTypeOption={eventTypeRedirectUrlSelectedOption}
|
||||
disabled={disabled}
|
||||
showCustomEventTypeInput
|
||||
customEventTypeSlug={customEventTypeSlug}
|
||||
onCustomEventTypeSlugChange={setCustomEventTypeSlug}
|
||||
eventTypePrefix={eventTypePrefix}
|
||||
fieldIdentifiers={fieldIdentifiers}
|
||||
/>
|
||||
) : (
|
||||
<div className="bg-default border-subtle my-3 rounded-xl border p-2">
|
||||
<div className="mb-2 ml-2 flex items-center gap-0.5">
|
||||
<div className="border-subtle rounded-lg border p-1">
|
||||
<Icon name="arrow-right" className="text-subtle h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-emphasis ml-2 text-sm font-medium">Send booker to</span>
|
||||
<span className="text-emphasis ml-2 text-sm font-medium">{t("send_booker_to")}</span>
|
||||
</div>
|
||||
<div className="bg-cal-muted flex w-full flex-col gap-2 rounded-xl p-2 text-sm lg:flex-row">
|
||||
<div className="flex grow items-center gap-2">
|
||||
<Select
|
||||
size="sm"
|
||||
isDisabled={disabled}
|
||||
className="data-testid-select-routing-action block w-full grow"
|
||||
required
|
||||
value={RoutingPages.find((page) => page.value === route.action?.type)}
|
||||
onChange={(item) => {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
const action: LocalRoute["action"] = {
|
||||
type: item.value,
|
||||
value: "",
|
||||
};
|
||||
|
||||
if (action.type === "customPageMessage") {
|
||||
action.value = "We are not ready for you yet :(";
|
||||
} else {
|
||||
action.value = "";
|
||||
}
|
||||
|
||||
setRoute(route.id, { action });
|
||||
}}
|
||||
options={RoutingPages}
|
||||
/>
|
||||
</div>
|
||||
{route.action?.type ? (
|
||||
route.action?.type === "customPageMessage" ? (
|
||||
<TextArea
|
||||
required
|
||||
disabled={disabled}
|
||||
name="customPageMessage"
|
||||
className="border-default flex grow lg:w-fit"
|
||||
style={{
|
||||
minHeight: "38px",
|
||||
}}
|
||||
value={route.action.value}
|
||||
onChange={(e) => {
|
||||
setRoute(route.id, { action: { ...route.action, value: e.target.value } });
|
||||
}}
|
||||
/>
|
||||
) : route.action?.type === "externalRedirectUrl" ? (
|
||||
<TextField
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
name="externalRedirectUrl"
|
||||
className="border-default flex grow text-sm"
|
||||
containerClassName="grow"
|
||||
type="url"
|
||||
required
|
||||
labelSrOnly
|
||||
value={route.action.value}
|
||||
onChange={(e) => {
|
||||
setRoute(route.id, { action: { ...route.action, value: e.target.value } });
|
||||
}}
|
||||
placeholder="https://example.com"
|
||||
/>
|
||||
) : (
|
||||
<div className="grow">
|
||||
<Select
|
||||
size="sm"
|
||||
required
|
||||
className="data-testid-eventTypeRedirectUrl-select"
|
||||
isDisabled={disabled}
|
||||
options={eventTypeRedirectUrlOptions}
|
||||
onChange={(option) => {
|
||||
if (!option) {
|
||||
return;
|
||||
<div className="bg-cal-muted rounded-xl p-2">
|
||||
<RouteActionSelector
|
||||
action={route.action}
|
||||
onActionChange={(newAction) => setRoute(route.id, { action: newAction })}
|
||||
onEventTypeChange={(newAction) => {
|
||||
// Initialize fallbackAction with the same event type if not already set with a valid event type
|
||||
// Check if fallbackAction has a valid event type (eventTypeId or non-empty value)
|
||||
const hasValidFallbackEventType =
|
||||
route.fallbackAction?.eventTypeId || route.fallbackAction?.value;
|
||||
const fallbackAction = hasValidFallbackEventType
|
||||
? route.fallbackAction
|
||||
: newAction.type === RouteActionType.EventTypeRedirectUrl
|
||||
? {
|
||||
type: newAction.type,
|
||||
value: newAction.value,
|
||||
eventTypeId: newAction.eventTypeId,
|
||||
}
|
||||
if (option.value !== "custom") {
|
||||
setRoute(route.id, {
|
||||
action: {
|
||||
...route.action,
|
||||
value: option.value,
|
||||
eventTypeId: option.eventTypeId,
|
||||
},
|
||||
attributeRoutingConfig: {},
|
||||
});
|
||||
setCustomEventTypeSlug("");
|
||||
} else {
|
||||
setRoute(route.id, {
|
||||
action: { ...route.action, value: "custom", eventTypeId: 0 },
|
||||
attributeRoutingConfig: {},
|
||||
});
|
||||
setCustomEventTypeSlug("");
|
||||
}
|
||||
}}
|
||||
value={eventTypeRedirectUrlSelectedOption}
|
||||
/>
|
||||
{eventOptions.length !== 0 &&
|
||||
route.action.value !== "" &&
|
||||
(!eventOptions.find((eventOption) => eventOption.value === route.action.value) ||
|
||||
customEventTypeSlug.length) ? (
|
||||
<>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
className="border-default flex w-full grow text-sm"
|
||||
containerClassName="grow mt-2"
|
||||
addOnLeading={eventTypePrefix}
|
||||
required
|
||||
value={customEventTypeSlug}
|
||||
onChange={(e) => {
|
||||
setCustomEventTypeSlug(e.target.value);
|
||||
setRoute(route.id, {
|
||||
action: { ...route.action, value: `${eventTypePrefix}${e.target.value}` },
|
||||
});
|
||||
}}
|
||||
placeholder="event-url"
|
||||
/>
|
||||
<div className="mt-2 ">
|
||||
<p className="text-subtle text-xs">
|
||||
{fieldIdentifiers.length
|
||||
? t("field_identifiers_as_variables_with_example", {
|
||||
variable: `{${fieldIdentifiers[0]}}`,
|
||||
})
|
||||
: t("field_identifiers_as_variables")}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
: undefined;
|
||||
setRoute(route.id, { action: newAction, attributeRoutingConfig: {}, fallbackAction });
|
||||
setCustomEventTypeSlug("");
|
||||
}}
|
||||
eventTypeOptions={eventTypeRedirectUrlOptions}
|
||||
selectedEventTypeOption={eventTypeRedirectUrlSelectedOption}
|
||||
disabled={disabled}
|
||||
showCustomEventTypeInput
|
||||
customEventTypeSlug={customEventTypeSlug}
|
||||
onCustomEventTypeSlugChange={setCustomEventTypeSlug}
|
||||
eventTypePrefix={eventTypePrefix}
|
||||
fieldIdentifiers={fieldIdentifiers}
|
||||
/>
|
||||
</div>
|
||||
{attributesQueryBuilder}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{attributesQueryBuilder}
|
||||
<WeightedAttributesSelector
|
||||
attributes={attributes}
|
||||
route={route}
|
||||
eventTypeRedirectUrlSelectedOption={eventTypeRedirectUrlSelectedOption}
|
||||
setRoute={setRoute}
|
||||
/>
|
||||
{fallbackAttributesQueryBuilder ? <>{fallbackAttributesQueryBuilder}</> : null}
|
||||
{matchingMembersFallbackRoute ? <>{matchingMembersFallbackRoute}</> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -952,7 +964,7 @@ const buildState = <
|
||||
| {
|
||||
queryValue: AttributesQueryValue;
|
||||
config: AttributesQueryBuilderConfigWithRaqbFields;
|
||||
}
|
||||
},
|
||||
>({
|
||||
queryValue,
|
||||
config,
|
||||
@@ -1026,6 +1038,16 @@ function useRoutes({
|
||||
if (!r.fallbackAttributesQueryValue) {
|
||||
r.fallbackAttributesQueryValue = getEmptyQueryValue() as LocalRoute["fallbackAttributesQueryValue"];
|
||||
}
|
||||
|
||||
// Initialize fallbackAction with the main action for backwards compatibility
|
||||
// This ensures existing routes show the correct default (same event type as main route)
|
||||
if (!r.fallbackAction && r.action?.type === RouteActionType.EventTypeRedirectUrl) {
|
||||
r.fallbackAction = {
|
||||
type: r.action.type,
|
||||
value: r.action.value,
|
||||
eventTypeId: r.action.eventTypeId,
|
||||
};
|
||||
}
|
||||
});
|
||||
return _routes;
|
||||
};
|
||||
@@ -1056,6 +1078,17 @@ function useRoutes({
|
||||
if (isRouter(route)) {
|
||||
return route;
|
||||
}
|
||||
// Initialize fallbackAction from action if not set and action is EventTypeRedirectUrl
|
||||
// This handles the case where user creates a new route and changes action to an event type
|
||||
const fallbackAction =
|
||||
route.fallbackAction ??
|
||||
(route.action?.type === RouteActionType.EventTypeRedirectUrl
|
||||
? {
|
||||
type: route.action.type,
|
||||
value: route.action.value,
|
||||
eventTypeId: route.action.eventTypeId,
|
||||
}
|
||||
: undefined);
|
||||
return {
|
||||
id: route.id,
|
||||
name: route.name,
|
||||
@@ -1066,6 +1099,7 @@ function useRoutes({
|
||||
attributesQueryValue: route.attributesQueryValue,
|
||||
fallbackAttributesQueryValue: route.fallbackAttributesQueryValue,
|
||||
attributeIdForWeights: route.attributeIdForWeights,
|
||||
fallbackAction,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1087,10 +1121,21 @@ const useCreateRoute = ({
|
||||
}) => {
|
||||
const createRoute = useCallback(() => {
|
||||
const newEmptyRoute = getEmptyRoute();
|
||||
// Initialize fallbackAction with the main action for new routes
|
||||
// This ensures new routes have a valid fallbackAction when saved
|
||||
const fallbackAction =
|
||||
newEmptyRoute.action?.type === RouteActionType.EventTypeRedirectUrl
|
||||
? {
|
||||
type: newEmptyRoute.action.type,
|
||||
value: newEmptyRoute.action.value,
|
||||
eventTypeId: newEmptyRoute.action.eventTypeId,
|
||||
}
|
||||
: undefined;
|
||||
const newRoutes = [
|
||||
...routes,
|
||||
{
|
||||
...newEmptyRoute,
|
||||
fallbackAction,
|
||||
formFieldsQueryBuilderState: buildState({
|
||||
queryValue: newEmptyRoute.queryValue,
|
||||
config: formFieldsQueryBuilderConfig,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { routingFormsComponents } from "./app-routing.client-config";
|
||||
|
||||
const normalizePages = (pages: string[] | string | undefined) => {
|
||||
const normalizedPages = Array.isArray(pages) ? pages : pages?.split("/") ?? [];
|
||||
const normalizedPages = Array.isArray(pages) ? pages : (pages?.split("/") ?? []);
|
||||
return normalizedPages[0];
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import posthog from "posthog-js";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { isFallbackRoute } from "@calcom/app-store/routing-forms/lib/isFallbackRoute";
|
||||
import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-forms/types/types";
|
||||
import LicenseRequired from "~/ee/common/components/LicenseRequired";
|
||||
import { FilterResults } from "~/filters/components/FilterResults";
|
||||
import { TeamsFilter } from "~/filters/components/TeamsFilter";
|
||||
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
|
||||
@@ -21,22 +15,33 @@ import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
|
||||
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { List, ListLinkItem } from "@calcom/ui/components/list";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
import type {
|
||||
SetNewFormDialogState,
|
||||
NewFormDialogState,
|
||||
SetNewFormDialogState,
|
||||
} from "@calcom/web/components/apps/routing-forms/FormActions";
|
||||
import {
|
||||
FormAction,
|
||||
FormActionsDropdown,
|
||||
FormActionsProvider,
|
||||
} from "@calcom/web/components/apps/routing-forms/FormActions";
|
||||
|
||||
import {
|
||||
ChartBarIcon,
|
||||
CircleCheckIcon,
|
||||
DownloadIcon,
|
||||
FileTextIcon,
|
||||
MailIcon,
|
||||
ShuffleIcon,
|
||||
} from "@coss/ui/icons";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import posthog from "posthog-js";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { useHasPaidPlan } from "~/billing/hooks/useHasPaidPlan";
|
||||
import SkeletonLoaderTeamList from "~/ee/teams/components/SkeletonloaderTeamList";
|
||||
import LicenseRequired from "~/ee/common/components/LicenseRequired";
|
||||
import { CreateButtonWithTeamsList } from "~/ee/teams/components/createButton/CreateButtonWithTeamsList";
|
||||
import SkeletonLoaderTeamList from "~/ee/teams/components/SkeletonloaderTeamList";
|
||||
import { ShellMain } from "~/shell/Shell";
|
||||
import { UpgradeTip } from "~/shell/UpgradeTip";
|
||||
|
||||
@@ -92,32 +97,32 @@ export default function RoutingForms({ appUrl }: { appUrl: string }) {
|
||||
const forms = queryRes.data?.filtered;
|
||||
const features = [
|
||||
{
|
||||
icon: <Icon name="file-text" className="h-5 w-5 text-orange-500" />,
|
||||
icon: <FileTextIcon className="h-5 w-5 text-orange-500" />,
|
||||
title: t("create_your_first_form"),
|
||||
description: t("create_your_first_form_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="shuffle" className="h-5 w-5 text-lime-500" />,
|
||||
icon: <ShuffleIcon className="h-5 w-5 text-lime-500" />,
|
||||
title: t("create_your_first_route"),
|
||||
description: t("route_to_the_right_person"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="chart-bar" className="h-5 w-5 text-blue-500" />,
|
||||
icon: <ChartBarIcon className="h-5 w-5 text-blue-500" />,
|
||||
title: t("reporting"),
|
||||
description: t("reporting_feature"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="circle-check" className="h-5 w-5 text-teal-500" />,
|
||||
icon: <CircleCheckIcon className="h-5 w-5 text-teal-500" />,
|
||||
title: t("test_routing_form"),
|
||||
description: t("test_preview_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="mail" className="h-5 w-5 text-yellow-500" />,
|
||||
icon: <MailIcon className="h-5 w-5 text-yellow-500" />,
|
||||
title: t("routing_forms_send_email_owner"),
|
||||
description: t("routing_forms_send_email_owner_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="download" className="h-5 w-5 text-violet-500" />,
|
||||
icon: <DownloadIcon className="h-5 w-5 text-violet-500" />,
|
||||
title: t("download_responses"),
|
||||
description: t("download_responses_description"),
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import { z } from "zod";
|
||||
|
||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { XIcon } from "@coss/ui/icons";
|
||||
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
@@ -42,10 +42,10 @@ const ServerPage = async ({ searchParams }: PageProps) => {
|
||||
provider === IdentityProvider.GOOGLE
|
||||
? "Google"
|
||||
: provider === IdentityProvider.CAL
|
||||
? "Email and Password"
|
||||
: provider === IdentityProvider.SAML
|
||||
? "SAML (like Okta)"
|
||||
: "your original login method";
|
||||
? "Email and Password"
|
||||
: provider === IdentityProvider.SAML
|
||||
? "SAML (like Okta)"
|
||||
: "your original login method";
|
||||
return t("account_managed_by_identity_provider_error", { provider: providerName });
|
||||
} else if (error === "saml-idp-not-authoritative") {
|
||||
return t("saml_idp_not_authoritative_error");
|
||||
@@ -61,7 +61,7 @@ const ServerPage = async ({ searchParams }: PageProps) => {
|
||||
<AuthContainer>
|
||||
<div>
|
||||
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
|
||||
<Icon name="x" className="h-6 w-6 text-red-600" />
|
||||
<XIcon 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">
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
import React, { Suspense } from "react";
|
||||
|
||||
import { ErrorBoundary } from "@calcom/ui/components/errorBoundary";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { LoaderIcon } from "@coss/ui/icons";
|
||||
|
||||
export default function TroubleshooterLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-1 *:flex-1">
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<Icon name="loader" />}>{children}</Suspense>
|
||||
<Suspense fallback={<LoaderIcon />}>{children}</Suspense>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { RefreshCcwIcon, UserPlusIcon, UsersIcon } from "@coss/ui/icons";
|
||||
|
||||
import { UpgradeTip } from "~/shell/UpgradeTip";
|
||||
|
||||
@@ -22,17 +22,17 @@ export default function UpgradeTipWrapper({ children }: { children: React.ReactN
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <Icon name="users" className="h-5 w-5 text-red-500" />,
|
||||
icon: <UsersIcon className="h-5 w-5 text-red-500" />,
|
||||
title: t("view_bookings_across"),
|
||||
description: t("view_bookings_across_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="refresh-ccw" className="h-5 w-5 text-blue-500" />,
|
||||
icon: <RefreshCcwIcon className="h-5 w-5 text-blue-500" />,
|
||||
title: t("identify_booking_trends"),
|
||||
description: t("identify_booking_trends_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="user-plus" className="h-5 w-5 text-green-500" />,
|
||||
icon: <UserPlusIcon className="h-5 w-5 text-green-500" />,
|
||||
title: t("spot_popular_event_types"),
|
||||
description: t("spot_popular_event_types_description"),
|
||||
},
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import InsightsWrongRoutingPage from "~/insights/views/insights-wrong-routing-view";
|
||||
|
||||
import { checkInsightsPagePermission } from "../checkInsightsPagePermission";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("wrong_routing"),
|
||||
(t) => t("wrong_routing_description"),
|
||||
undefined,
|
||||
undefined,
|
||||
"/insights/wrong-routing"
|
||||
);
|
||||
|
||||
export default async function Page() {
|
||||
const session = await checkInsightsPagePermission();
|
||||
|
||||
const userRepository = new UserRepository(prisma);
|
||||
const user = await userRepository.getTimeZoneAndDefaultScheduleId({ userId: session?.user.id ?? -1 });
|
||||
|
||||
return <InsightsWrongRoutingPage timeZone={user?.timeZone ?? "UTC"} />;
|
||||
}
|
||||
@@ -27,7 +27,9 @@ const ServerPage = async () => {
|
||||
|
||||
// If user has any team membership (pending or accepted), redirect them directly to personal onboarding
|
||||
// This handles the case where users sign up with an invite token (membership is auto-accepted)
|
||||
const hasTeamMembership = await MembershipRepository.hasAnyTeamMembershipByUserId({ userId: session.user.id });
|
||||
const hasTeamMembership = await MembershipRepository.hasAnyTeamMembershipByUserId({
|
||||
userId: session.user.id,
|
||||
});
|
||||
if (hasTeamMembership) {
|
||||
return redirect("/onboarding/personal/settings");
|
||||
}
|
||||
|
||||
@@ -32,4 +32,3 @@ const ServerPage = async () => {
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
|
||||
|
||||
@@ -32,4 +32,3 @@ const ServerPage = async () => {
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
|
||||
|
||||
@@ -31,7 +31,9 @@ const ServerPage = async (props: { searchParams: Promise<{ fromTeamOnboarding?:
|
||||
const searchParams = await props.searchParams;
|
||||
const fromTeamOnboarding = searchParams?.fromTeamOnboarding === "true";
|
||||
|
||||
return <PersonalSettingsView userEmail={userEmail} userName={userName} fromTeamOnboarding={fromTeamOnboarding} />;
|
||||
return (
|
||||
<PersonalSettingsView userEmail={userEmail} userName={userName} fromTeamOnboarding={fromTeamOnboarding} />
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
|
||||
@@ -32,4 +32,3 @@ const ServerPage = async () => {
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
|
||||
|
||||
+1
-4
@@ -42,10 +42,7 @@ const Page = async ({ params }: { params: Params }) => {
|
||||
const org = await organizationRepository.adminFindById({ id: input.data.id });
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<SettingsHeader
|
||||
title={`${t("editing_org")}: ${org.name}`}
|
||||
description={t("admin_orgs_edit_description")}
|
||||
>
|
||||
<SettingsHeader title={`${t("editing_org")}: ${org.name}`} description={t("admin_orgs_edit_description")}>
|
||||
<LicenseRequired>
|
||||
<OrgForm org={org} />
|
||||
</LicenseRequired>
|
||||
|
||||
+1
-4
@@ -17,10 +17,7 @@ export const generateMetadata = async () =>
|
||||
const Page = async () => {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<SettingsHeader
|
||||
title={t("organizations")}
|
||||
description={t("orgs_page_description")}
|
||||
>
|
||||
<SettingsHeader title={t("organizations")} description={t("orgs_page_description")}>
|
||||
<LicenseRequired>
|
||||
<AdminOrgTable />
|
||||
</LicenseRequired>
|
||||
|
||||
+2
-1
@@ -3,7 +3,8 @@
|
||||
import { useReactTable, getCoreRowModel, createColumnHelper } from "@tanstack/react-table";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { ColumnFilterType, DataTableProvider } from "@calcom/features/data-table";
|
||||
import { ColumnFilterType } from "@calcom/features/data-table";
|
||||
import { DataTableProvider } from "~/data-table/DataTableProvider";
|
||||
import type { DateRangeFilterOptions } from "@calcom/features/data-table/lib/types";
|
||||
import { DateRangeFilter } from "~/data-table/components/filters/DateRangeFilter";
|
||||
|
||||
|
||||
+1
-4
@@ -16,10 +16,7 @@ export const generateMetadata = async () =>
|
||||
const Page = async () => {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<SettingsHeader
|
||||
title={t("workspace_platforms")}
|
||||
description={t("workspace_platforms_description")}
|
||||
>
|
||||
<SettingsHeader title={t("workspace_platforms")} description={t("workspace_platforms_description")}>
|
||||
<LicenseRequired>
|
||||
<WorkspacePlatformsPage />
|
||||
</LicenseRequired>
|
||||
|
||||
+8
-15
@@ -31,6 +31,7 @@ import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { ErrorBoundary } from "@calcom/ui/components/errorBoundary";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { ArrowLeftIcon, ChevronDownIcon, ChevronRightIcon } from "@coss/ui/icons";
|
||||
import type { VerticalTabItemProps } from "@calcom/ui/components/navigation";
|
||||
import { VerticalTabItem } from "@calcom/ui/components/navigation";
|
||||
import { Skeleton } from "@calcom/ui/components/skeleton";
|
||||
@@ -475,10 +476,7 @@ const BackButtonInSidebar = ({ name }: { name: string }) => {
|
||||
href="/event-types"
|
||||
className="hover:bg-subtle todesktop:mt-10 [&[aria-current='page']]:bg-emphasis [&[aria-current='page']]:text-emphasis group-hover:text-default text-emphasis group my-6 flex h-6 max-h-6 w-full flex-row items-center rounded-md px-3 py-2 text-sm font-medium leading-4 transition"
|
||||
data-testid={`vertical-tab-${name}`}>
|
||||
<Icon
|
||||
name="arrow-left"
|
||||
className="h-4 w-4 stroke-[2px] ltr:mr-[10px] rtl:ml-[10px] rtl:rotate-180 md:mt-0"
|
||||
/>
|
||||
<ArrowLeftIcon className="h-4 w-4 stroke-[2px] ltr:mr-[10px] rtl:ml-[10px] rtl:rotate-180 md:mt-0" />
|
||||
<Skeleton title={name} as="p" className="min-h-4 max-w-36 truncate" loadingClassName="ms-3">
|
||||
{name}
|
||||
</Skeleton>
|
||||
@@ -602,9 +600,9 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record<number, T
|
||||
}`}>
|
||||
<div className="me-3">
|
||||
{teamMenuState[index].teamMenuOpen ? (
|
||||
<Icon name="chevron-down" className="h-4 w-4" />
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<Icon name="chevron-right" className="h-4 w-4" />
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
{}
|
||||
@@ -949,9 +947,9 @@ const SettingsSidebarContainer = ({
|
||||
}`}>
|
||||
<div className="me-3">
|
||||
{otherTeamMenuState[index].teamMenuOpen ? (
|
||||
<Icon name="chevron-down" className="h-4 w-4" />
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<Icon name="chevron-right" className="h-4 w-4" />
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
{}
|
||||
@@ -1036,7 +1034,7 @@ const MobileSettingsContainer = (props: { onSideContainerOpen?: () => void }) =>
|
||||
<button
|
||||
className="hover:bg-emphasis flex items-center space-x-2 rounded-md px-3 py-1 rtl:space-x-reverse"
|
||||
onClick={() => router.back()}>
|
||||
<Icon name="arrow-left" className="text-default h-4 w-4" />
|
||||
<ArrowLeftIcon className="text-default h-4 w-4" />
|
||||
<p className="text-emphasis font-semibold">{t("settings")}</p>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1052,12 +1050,7 @@ type SettingsLayoutProps = {
|
||||
permissions?: SettingsPermissions;
|
||||
} & ComponentProps<typeof Shell>;
|
||||
|
||||
function SettingsLayoutAppDirClient({
|
||||
children,
|
||||
teamFeatures,
|
||||
permissions,
|
||||
...rest
|
||||
}: SettingsLayoutProps) {
|
||||
function SettingsLayoutAppDirClient({ children, teamFeatures, permissions, ...rest }: SettingsLayoutProps) {
|
||||
const pathname = usePathname();
|
||||
const state = useState(false);
|
||||
const [sideContainerOpen, setSideContainerOpen] = state;
|
||||
|
||||
@@ -25,9 +25,7 @@ const Page = async () => {
|
||||
redirect("/auth/login?callbackUrl=/settings/developer/oauth");
|
||||
}
|
||||
|
||||
return (
|
||||
<OAuthClientsView />
|
||||
);
|
||||
return <OAuthClientsView />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
+7
-8
@@ -11,8 +11,8 @@ import {
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Checkbox, Label } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
import { ChevronRightIcon, InfoIcon } from "@coss/ui/icons";
|
||||
|
||||
import { usePermissions } from "./usePermissions";
|
||||
|
||||
@@ -54,10 +54,10 @@ export function AdvancedPermissionGroup({
|
||||
const allPermissions = isAllResources
|
||||
? ["*.*"]
|
||||
: resourceConfig
|
||||
? Object.entries(resourceConfig)
|
||||
.filter(([action]) => action !== INTERNAL_DATAACCESS_KEY)
|
||||
.map(([action]) => `${resource}.${action}`)
|
||||
: [];
|
||||
? Object.entries(resourceConfig)
|
||||
.filter(([action]) => action !== INTERNAL_DATAACCESS_KEY)
|
||||
.map(([action]) => `${resource}.${action}`)
|
||||
: [];
|
||||
|
||||
// Check if all permissions for this resource are selected
|
||||
const isAllSelected = isAllResources
|
||||
@@ -98,8 +98,7 @@ export function AdvancedPermissionGroup({
|
||||
}
|
||||
}}>
|
||||
<div className="flex items-center gap-1.5" onClick={() => setIsExpanded(!isExpanded)}>
|
||||
<Icon
|
||||
name="chevron-right"
|
||||
<ChevronRightIcon
|
||||
className={classNames(
|
||||
"h-4 w-4 transition-transform",
|
||||
isExpanded && !isAllResources ? "rotate-90" : ""
|
||||
@@ -173,7 +172,7 @@ export function AdvancedPermissionGroup({
|
||||
</span>
|
||||
{isAutoEnabled && (
|
||||
<Tooltip content={t("read_permission_auto_enabled_tooltip")}>
|
||||
<Icon name="info" className="text-muted-foreground h-3 w-3" />
|
||||
<InfoIcon className="text-muted-foreground h-3 w-3" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+5
-5
@@ -7,8 +7,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Dialog, DialogContent } from "@calcom/ui/components/dialog";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { LockIcon, ShieldCheckIcon, ShieldIcon, UsersIcon } from "@coss/ui/icons";
|
||||
|
||||
import { FingerprintAnimation } from "./FingerprintAnimation";
|
||||
|
||||
@@ -59,7 +59,7 @@ export function PbacOptInModal({ open, onOpenChange, revalidateRolesPath }: Pbac
|
||||
<div className="bg-subtle border-subtle w-full stack-y-px overflow-hidden rounded-xl border">
|
||||
<div className="bg-default flex gap-3 p-3">
|
||||
<div className="bg-subtle flex h-8 w-8 shrink-0 items-center justify-center rounded-md">
|
||||
<Icon name="shield-check" className="text-subtle h-5 w-5" />
|
||||
<ShieldCheckIcon className="text-subtle h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-emphasis text-sm font-semibold">{t("pbac_opt_in_custom_roles_title")}</p>
|
||||
@@ -69,7 +69,7 @@ export function PbacOptInModal({ open, onOpenChange, revalidateRolesPath }: Pbac
|
||||
|
||||
<div className="bg-default flex gap-3 p-3">
|
||||
<div className="bg-subtle flex h-8 w-8 shrink-0 items-center justify-center rounded-md">
|
||||
<Icon name="lock" className="text-subtle h-5 w-5" />
|
||||
<LockIcon className="text-subtle h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-emphasis text-sm font-semibold">
|
||||
@@ -81,7 +81,7 @@ export function PbacOptInModal({ open, onOpenChange, revalidateRolesPath }: Pbac
|
||||
|
||||
<div className="bg-default flex gap-3 p-3">
|
||||
<div className="bg-subtle flex h-8 w-8 shrink-0 items-center justify-center rounded-md">
|
||||
<Icon name="users" className="text-subtle h-5 w-5" />
|
||||
<UsersIcon className="text-subtle h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-emphasis text-sm font-semibold">
|
||||
@@ -93,7 +93,7 @@ export function PbacOptInModal({ open, onOpenChange, revalidateRolesPath }: Pbac
|
||||
|
||||
<div className="bg-default flex gap-3 p-3">
|
||||
<div className="bg-subtle flex h-8 w-8 shrink-0 items-center justify-center rounded-md">
|
||||
<Icon name="shield" className="text-subtle h-5 w-5" />
|
||||
<ShieldIcon className="text-subtle h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-emphasis text-sm font-semibold">
|
||||
|
||||
+3
-3
@@ -27,9 +27,9 @@ const Page = async () => {
|
||||
const organizationRepository = getOrganizationRepository();
|
||||
const otherTeams = organizationId
|
||||
? await organizationRepository.findTeamsInOrgIamNotPartOf({
|
||||
userId: session?.user.id,
|
||||
parentId: organizationId,
|
||||
})
|
||||
userId: session?.user.id,
|
||||
parentId: organizationId,
|
||||
})
|
||||
: [];
|
||||
|
||||
return (
|
||||
|
||||
+1
-3
@@ -1,8 +1,6 @@
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
|
||||
import LegacyPage, {
|
||||
TeamMembersCTA,
|
||||
} from "~/ee/organizations/other-team-members-view";
|
||||
import LegacyPage, { TeamMembersCTA } from "~/ee/organizations/other-team-members-view";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
|
||||
import { validateUserHasOrg } from "../../../../actions/validateUserHasOrg";
|
||||
|
||||
@@ -23,9 +23,7 @@ const getData = withAppDirSsr<inferSSRProps<typeof getServerSideProps>>(getServe
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
||||
return (
|
||||
<SettingsNewView />
|
||||
);
|
||||
return <SettingsNewView />;
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
|
||||
@@ -8,13 +8,7 @@ interface GeoContextValue {
|
||||
|
||||
const GeoContext = createContext<GeoContextValue | undefined>(undefined);
|
||||
|
||||
export function GeoProvider({
|
||||
country,
|
||||
children,
|
||||
}: {
|
||||
country: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
export function GeoProvider({ country, children }: { country: string; children: React.ReactNode }) {
|
||||
return <GeoContext.Provider value={{ country }}>{children}</GeoContext.Provider>;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ const url =
|
||||
typeof window !== "undefined"
|
||||
? "/api/trpc"
|
||||
: process.env.VERCEL_URL
|
||||
? `https://${process.env.VERCEL_URL}/api/trpc`
|
||||
: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/trpc`;
|
||||
? `https://${process.env.VERCEL_URL}/api/trpc`
|
||||
: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/trpc`;
|
||||
|
||||
export const trpcClient = trpc.createClient({
|
||||
links: [
|
||||
|
||||
@@ -9,9 +9,8 @@ import { ErrorWithCode } from "@calcom/lib/errors";
|
||||
import { getHttpStatusCode } from "@calcom/lib/server/getServerErrorFromUnknown";
|
||||
|
||||
async function handler(req: NextRequest) {
|
||||
const { code, client_id, client_secret, grant_type, redirect_uri, code_verifier } = await parseUrlFormData(
|
||||
req
|
||||
);
|
||||
const { code, client_id, client_secret, grant_type, redirect_uri, code_verifier } =
|
||||
await parseUrlFormData(req);
|
||||
|
||||
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
|
||||
return NextResponse.json({ message: OAUTH_ERROR_REASONS["encryption_key_missing"] }, { status: 500 });
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import type { Mock } from "vitest";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import type { MockResponse } from "@calcom/features/auth/signup/handlers/__tests__/mocks/next.mocks";
|
||||
|
||||
import {
|
||||
prismaMock,
|
||||
resetPrismaMock,
|
||||
} from "@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks";
|
||||
import {
|
||||
createMockTeam,
|
||||
createMockFoundToken,
|
||||
} from "@calcom/features/auth/signup/handlers/__tests__/mocks/signup.factories";
|
||||
import type { SignupBody } from "@calcom/features/auth/signup/handlers/__tests__/mocks/signup.factories";
|
||||
import {
|
||||
createMockFoundToken,
|
||||
createMockTeam,
|
||||
} from "@calcom/features/auth/signup/handlers/__tests__/mocks/signup.factories";
|
||||
import type { Mock } from "vitest";
|
||||
import { vi } from "vitest";
|
||||
|
||||
const mockFindTokenByToken: Mock = vi.fn();
|
||||
const mockValidateAndGetCorrectedUsernameForTeam: Mock = vi.fn();
|
||||
@@ -27,19 +25,27 @@ type InnerHandler = (body: Record<string, string>, status: UsernameStatus) => Pr
|
||||
var mockCapturedHandler: InnerHandler | null;
|
||||
|
||||
vi.mock("next/server", async () => {
|
||||
const { createNextServerMock } = await import("@calcom/features/auth/signup/handlers/__tests__/mocks/next.mocks");
|
||||
const { createNextServerMock } = await import(
|
||||
"@calcom/features/auth/signup/handlers/__tests__/mocks/next.mocks"
|
||||
);
|
||||
return createNextServerMock();
|
||||
});
|
||||
vi.mock("next/headers", async () => {
|
||||
const { createNextHeadersMock } = await import("@calcom/features/auth/signup/handlers/__tests__/mocks/next.mocks");
|
||||
const { createNextHeadersMock } = await import(
|
||||
"@calcom/features/auth/signup/handlers/__tests__/mocks/next.mocks"
|
||||
);
|
||||
return createNextHeadersMock();
|
||||
});
|
||||
vi.mock("@calcom/prisma", async () => {
|
||||
const { createPrismaMock } = await import("@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks");
|
||||
const { createPrismaMock } = await import(
|
||||
"@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks"
|
||||
);
|
||||
return createPrismaMock();
|
||||
});
|
||||
vi.mock("@calcom/prisma/client", async () => {
|
||||
const { createPrismaMock } = await import("@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks");
|
||||
const { createPrismaMock } = await import(
|
||||
"@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks"
|
||||
);
|
||||
return createPrismaMock();
|
||||
});
|
||||
vi.mock("@calcom/lib/logger", () => ({
|
||||
@@ -49,9 +55,13 @@ vi.mock("@calcom/lib/auth/hashPassword", () => ({ hashPassword: vi.fn().mockReso
|
||||
vi.mock("@calcom/lib/constants", () => ({ WEBAPP_URL: "http://localhost:3000" }));
|
||||
vi.mock("@calcom/lib/tracking", () => ({ getTrackingFromCookies: vi.fn().mockReturnValue({}) }));
|
||||
vi.mock("@calcom/app-store/stripepayment/lib/utils", () => ({ getPremiumMonthlyPlanPriceId: vi.fn() }));
|
||||
vi.mock("@calcom/features/auth/lib/getLocaleFromRequest", () => ({ getLocaleFromRequest: vi.fn().mockResolvedValue("en") }));
|
||||
vi.mock("@calcom/features/auth/lib/getLocaleFromRequest", () => ({
|
||||
getLocaleFromRequest: vi.fn().mockResolvedValue("en"),
|
||||
}));
|
||||
vi.mock("@calcom/features/auth/lib/verifyEmail", () => ({ sendEmailVerification: vi.fn() }));
|
||||
vi.mock("@calcom/features/auth/signup/utils/createOrUpdateMemberships", () => ({ createOrUpdateMemberships: vi.fn() }));
|
||||
vi.mock("@calcom/features/auth/signup/utils/createOrUpdateMemberships", () => ({
|
||||
createOrUpdateMemberships: vi.fn(),
|
||||
}));
|
||||
vi.mock("@calcom/features/auth/signup/utils/prefillAvatar", () => ({ prefillAvatar: vi.fn() }));
|
||||
vi.mock("@calcom/features/auth/signup/utils/validateUsername", () => ({
|
||||
validateAndGetCorrectedUsernameAndEmail: vi.fn().mockResolvedValue({ isValid: true, username: "testuser" }),
|
||||
@@ -65,12 +75,29 @@ vi.mock("@calcom/features/watchlist/lib/telemetry", () => ({ sentrySpan: {} }));
|
||||
vi.mock("@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller", () => ({
|
||||
checkIfEmailIsBlockedInWatchlistController: vi.fn().mockResolvedValue(false),
|
||||
}));
|
||||
vi.mock("@calcom/features/di/containers/FeatureRepository", () => ({
|
||||
getFeatureRepository: vi.fn().mockReturnValue({
|
||||
checkIfFeatureIsEnabledGlobally: vi.fn().mockResolvedValue(false),
|
||||
}),
|
||||
}));
|
||||
vi.mock("@calcom/features/watchlist/lib/repository/GlobalWatchlistRepository", () => {
|
||||
return {
|
||||
GlobalWatchlistRepository: class {
|
||||
findBlockedEmail = vi.fn().mockResolvedValue(null);
|
||||
createEntry = vi.fn().mockResolvedValue({});
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock("@calcom/features/watchlist/lib/utils/normalization", () => ({
|
||||
normalizeEmail: vi.fn((e: string) => e.toLowerCase()),
|
||||
}));
|
||||
vi.mock("@calcom/web/lib/buildLegacyCtx", () => ({ buildLegacyRequest: vi.fn() }));
|
||||
vi.mock("@calcom/features/auth/signup/utils/organization", () => ({ joinAnyChildTeamOnOrgInvite: vi.fn() }));
|
||||
vi.mock("@calcom/features/auth/signup/utils/token", () => ({
|
||||
findTokenByToken: (...args: unknown[]) => mockFindTokenByToken(...args),
|
||||
throwIfTokenExpired: vi.fn(),
|
||||
validateAndGetCorrectedUsernameForTeam: (...args: unknown[]) => mockValidateAndGetCorrectedUsernameForTeam(...args),
|
||||
validateAndGetCorrectedUsernameForTeam: (...args: unknown[]) =>
|
||||
mockValidateAndGetCorrectedUsernameForTeam(...args),
|
||||
}));
|
||||
|
||||
// Capture inner handler from usernameHandler wrapper
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import process from "node:process";
|
||||
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
|
||||
import { getLocaleFromRequest } from "@calcom/features/auth/lib/getLocaleFromRequest";
|
||||
import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
|
||||
import { SIGNUP_ERROR_CODES } from "@calcom/features/auth/signup/constants";
|
||||
import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/createOrUpdateMemberships";
|
||||
import { joinAnyChildTeamOnOrgInvite } from "@calcom/features/auth/signup/utils/organization";
|
||||
import { prefillAvatar } from "@calcom/features/auth/signup/utils/prefillAvatar";
|
||||
import {
|
||||
findTokenByToken,
|
||||
throwIfTokenExpired,
|
||||
validateAndGetCorrectedUsernameForTeam,
|
||||
} from "@calcom/features/auth/signup/utils/token";
|
||||
import { validateAndGetCorrectedUsernameAndEmail } from "@calcom/features/auth/signup/utils/validateUsername";
|
||||
import { getBillingProviderService } from "@calcom/features/ee/billing/di/containers/Billing";
|
||||
import { getFeatureRepository } from "@calcom/features/di/containers/FeatureRepository";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import { GlobalWatchlistRepository } from "@calcom/features/watchlist/lib/repository/GlobalWatchlistRepository";
|
||||
import { sentrySpan } from "@calcom/features/watchlist/lib/telemetry";
|
||||
import { normalizeEmail } from "@calcom/features/watchlist/lib/utils/normalization";
|
||||
import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
|
||||
import { hashPassword } from "@calcom/lib/auth/hashPassword";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
@@ -19,19 +28,17 @@ import type { CustomNextApiHandler } from "@calcom/lib/server/username";
|
||||
import { usernameHandler } from "@calcom/lib/server/username";
|
||||
import { getTrackingFromCookies } from "@calcom/lib/tracking";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { CreationSource } from "@calcom/prisma/enums";
|
||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
import {
|
||||
CreationSource,
|
||||
IdentityProvider,
|
||||
WatchlistAction,
|
||||
WatchlistSource,
|
||||
WatchlistType,
|
||||
} from "@calcom/prisma/enums";
|
||||
import { signupSchema } from "@calcom/prisma/zod-utils";
|
||||
import { buildLegacyRequest } from "@calcom/web/lib/buildLegacyCtx";
|
||||
|
||||
import { joinAnyChildTeamOnOrgInvite } from "@calcom/features/auth/signup/utils/organization";
|
||||
import { SIGNUP_ERROR_CODES } from "@calcom/features/auth/signup/constants";
|
||||
|
||||
import {
|
||||
findTokenByToken,
|
||||
throwIfTokenExpired,
|
||||
validateAndGetCorrectedUsernameForTeam,
|
||||
} from "@calcom/features/auth/signup/utils/token";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["signupCalcomHandler"] });
|
||||
|
||||
@@ -291,6 +298,34 @@ const handler: CustomNextApiHandler = async (body, usernameStatus, query) => {
|
||||
}
|
||||
}
|
||||
|
||||
const featureRepository = getFeatureRepository();
|
||||
const signupWatchlistReviewEnabled =
|
||||
await featureRepository.checkIfFeatureIsEnabledGlobally("signup-watchlist-review");
|
||||
|
||||
if (signupWatchlistReviewEnabled && !token) {
|
||||
const globalWatchlistRepo = new GlobalWatchlistRepository(prisma);
|
||||
const normalizedEmail = normalizeEmail(email);
|
||||
const existing = await globalWatchlistRepo.findBlockedEmail(normalizedEmail);
|
||||
|
||||
if (!existing) {
|
||||
await globalWatchlistRepo.createEntry({
|
||||
type: WatchlistType.EMAIL,
|
||||
value: normalizedEmail,
|
||||
action: WatchlistAction.BLOCK,
|
||||
source: WatchlistSource.SIGNUP,
|
||||
description: "Auto-added during signup review",
|
||||
});
|
||||
}
|
||||
|
||||
const userRepository = new UserRepository(prisma);
|
||||
await userRepository.lockByEmail({ email });
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: "Created user", stripeCustomerId: customer.stripeCustomerId, accountUnderReview: true },
|
||||
{ status: 201 }
|
||||
);
|
||||
}
|
||||
|
||||
if (checkoutSessionId) {
|
||||
console.log("Created user but missing payment", checkoutSessionId);
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -15,15 +15,21 @@ const mockFindTokenByToken: Mock = vi.fn();
|
||||
const mockValidateAndGetCorrectedUsernameForTeam: Mock = vi.fn();
|
||||
|
||||
vi.mock("next/server", async () => {
|
||||
const { createNextServerMock } = await import("@calcom/features/auth/signup/handlers/__tests__/mocks/next.mocks");
|
||||
const { createNextServerMock } = await import(
|
||||
"@calcom/features/auth/signup/handlers/__tests__/mocks/next.mocks"
|
||||
);
|
||||
return createNextServerMock();
|
||||
});
|
||||
vi.mock("@calcom/prisma", async () => {
|
||||
const { createPrismaMock } = await import("@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks");
|
||||
const { createPrismaMock } = await import(
|
||||
"@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks"
|
||||
);
|
||||
return createPrismaMock();
|
||||
});
|
||||
vi.mock("@calcom/prisma/client", async () => {
|
||||
const { createPrismaMock } = await import("@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks");
|
||||
const { createPrismaMock } = await import(
|
||||
"@calcom/features/auth/signup/handlers/__tests__/mocks/prisma.mocks"
|
||||
);
|
||||
return createPrismaMock();
|
||||
});
|
||||
vi.mock("@calcom/lib/logger", () => ({
|
||||
@@ -32,9 +38,13 @@ vi.mock("@calcom/lib/logger", () => ({
|
||||
vi.mock("@calcom/lib/auth/hashPassword", () => ({ hashPassword: vi.fn().mockResolvedValue("hashed") }));
|
||||
vi.mock("@calcom/lib/slugify", () => ({ default: vi.fn((s: string) => s.toLowerCase()) }));
|
||||
vi.mock("@calcom/lib/constants", () => ({ IS_PREMIUM_USERNAME_ENABLED: false }));
|
||||
vi.mock("@calcom/lib/server/username", () => ({ isUsernameReservedDueToMigration: vi.fn().mockResolvedValue(false) }));
|
||||
vi.mock("@calcom/lib/server/username", () => ({
|
||||
isUsernameReservedDueToMigration: vi.fn().mockResolvedValue(false),
|
||||
}));
|
||||
vi.mock("@calcom/features/auth/lib/verifyEmail", () => ({ sendEmailVerification: vi.fn() }));
|
||||
vi.mock("@calcom/features/auth/signup/utils/createOrUpdateMemberships", () => ({ createOrUpdateMemberships: vi.fn() }));
|
||||
vi.mock("@calcom/features/auth/signup/utils/createOrUpdateMemberships", () => ({
|
||||
createOrUpdateMemberships: vi.fn(),
|
||||
}));
|
||||
vi.mock("@calcom/features/auth/signup/utils/validateUsername", () => ({
|
||||
validateAndGetCorrectedUsernameAndEmail: vi.fn().mockResolvedValue({ isValid: true, username: "testuser" }),
|
||||
}));
|
||||
@@ -43,7 +53,8 @@ vi.mock("@calcom/features/auth/signup/utils/prefillAvatar", () => ({ prefillAvat
|
||||
vi.mock("@calcom/features/auth/signup/utils/token", () => ({
|
||||
findTokenByToken: (...args: unknown[]) => mockFindTokenByToken(...args),
|
||||
throwIfTokenExpired: vi.fn(),
|
||||
validateAndGetCorrectedUsernameForTeam: (...args: unknown[]) => mockValidateAndGetCorrectedUsernameForTeam(...args),
|
||||
validateAndGetCorrectedUsernameForTeam: (...args: unknown[]) =>
|
||||
mockValidateAndGetCorrectedUsernameForTeam(...args),
|
||||
}));
|
||||
|
||||
// Import after mocks
|
||||
|
||||
@@ -100,7 +100,7 @@ export default async function handler(body: Record<string, string>) {
|
||||
}
|
||||
}
|
||||
|
||||
const organizationId = team.isOrganization ? team.id : team.parent?.id ?? null;
|
||||
const organizationId = team.isOrganization ? team.id : (team.parent?.id ?? null);
|
||||
|
||||
const existingUserByUsername = await prisma.user.findFirst({
|
||||
where: {
|
||||
@@ -200,10 +200,7 @@ export default async function handler(body: Record<string, string>) {
|
||||
if (isPrismaError(error) && error.code === "P2002") {
|
||||
const target = String(error.meta?.target ?? "");
|
||||
if (target.includes("email") || target.includes("username")) {
|
||||
return NextResponse.json(
|
||||
{ message: SIGNUP_ERROR_CODES.USER_ALREADY_EXISTS },
|
||||
{ status: 409 }
|
||||
);
|
||||
return NextResponse.json({ message: SIGNUP_ERROR_CODES.USER_ALREADY_EXISTS }, { status: 409 });
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
|
||||
@@ -91,8 +91,7 @@ async function deleteHandler(req: NextRequest) {
|
||||
const user = await authMiddleware();
|
||||
const searchParams = Object.fromEntries(req.nextUrl.searchParams.entries());
|
||||
|
||||
const { integration, externalId, eventTypeId } =
|
||||
selectedCalendarSelectSchema.parse(searchParams);
|
||||
const { integration, externalId, eventTypeId } = selectedCalendarSelectSchema.parse(searchParams);
|
||||
|
||||
await SelectedCalendarRepository.delete({
|
||||
where: {
|
||||
|
||||
@@ -71,20 +71,20 @@ export async function handleCreateCredentials() {
|
||||
`Creating credentials for ${toProcessMembers.length} members out of ${delegatedMembers.length} delegated members in organization ${organization.id}`
|
||||
);
|
||||
|
||||
const credentialCreationPromises = toProcessMembers.map(async (member) => {
|
||||
try {
|
||||
const credentialData = buildCredentialCreateData({
|
||||
type: "google_calendar",
|
||||
key: {
|
||||
// The in-memory credential that we create for Delegation Credential has access_token as "NOOP_UNUSED_DELEGATION_TOKEN. This is to mark it is in DB, in case we need to identify that.
|
||||
access_token: "NOOP_UNUSED_DELEGATION_TOKEN_DB",
|
||||
},
|
||||
userId: member.userId,
|
||||
appId: "google-calendar",
|
||||
delegationCredentialId: delegationCredential.id,
|
||||
});
|
||||
await CredentialRepository.create(credentialData);
|
||||
log.info(`Created credential for member ${member.userId}`);
|
||||
const credentialCreationPromises = toProcessMembers.map(async (member) => {
|
||||
try {
|
||||
const credentialData = buildCredentialCreateData({
|
||||
type: "google_calendar",
|
||||
key: {
|
||||
// The in-memory credential that we create for Delegation Credential has access_token as "NOOP_UNUSED_DELEGATION_TOKEN. This is to mark it is in DB, in case we need to identify that.
|
||||
access_token: "NOOP_UNUSED_DELEGATION_TOKEN_DB",
|
||||
},
|
||||
userId: member.userId,
|
||||
appId: "google-calendar",
|
||||
delegationCredentialId: delegationCredential.id,
|
||||
});
|
||||
await CredentialRepository.create(credentialData);
|
||||
log.info(`Created credential for member ${member.userId}`);
|
||||
} catch (error) {
|
||||
log.error(`Error creating credential for member ${member.userId}:`, safeStringify(error));
|
||||
throw error;
|
||||
|
||||
@@ -44,7 +44,7 @@ async function postHandler(request: NextRequest) {
|
||||
|
||||
const teamBillingFactory = getTeamBillingServiceFactory();
|
||||
const teamsBilling = teamBillingFactory.initMany(teams);
|
||||
const teamBillingPromises = teamsBilling.map((teamBilling) => teamBilling.updateQuantity());
|
||||
const teamBillingPromises = teamsBilling.map((teamBilling) => teamBilling.updateQuantity("sync"));
|
||||
await Promise.allSettled(teamBillingPromises);
|
||||
|
||||
pageNumber++;
|
||||
|
||||
@@ -12,12 +12,16 @@ const getPrimaryCalendarMock = vi.fn();
|
||||
vi.mock("@calcom/app-store/googlecalendar/lib/CalendarService", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
createGoogleCalendarServiceWithGoogleType: vi.fn().mockImplementation(function() { return {
|
||||
getPrimaryCalendar: getPrimaryCalendarMock,
|
||||
}; }),
|
||||
default: vi.fn().mockImplementation(function() { return {
|
||||
getPrimaryCalendar: getPrimaryCalendarMock,
|
||||
}; }),
|
||||
createGoogleCalendarServiceWithGoogleType: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
getPrimaryCalendar: getPrimaryCalendarMock,
|
||||
};
|
||||
}),
|
||||
default: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
getPrimaryCalendar: getPrimaryCalendarMock,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -267,10 +267,13 @@ export async function handleCreateSelectedCalendars() {
|
||||
}
|
||||
|
||||
// Groups delegationUserCredentials by delegationCredentialId
|
||||
const groupedDelegationUserCredentials = allDelegationUserCredentials.reduce((acc, curr) => {
|
||||
acc[curr.delegationCredentialId] = [...(acc[curr.delegationCredentialId] || []), curr];
|
||||
return acc;
|
||||
}, {} as Record<string, typeof allDelegationUserCredentials>);
|
||||
const groupedDelegationUserCredentials = allDelegationUserCredentials.reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr.delegationCredentialId] = [...(acc[curr.delegationCredentialId] || []), curr];
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, typeof allDelegationUserCredentials>
|
||||
);
|
||||
|
||||
let totalSuccess = 0;
|
||||
let totalFailures = 0;
|
||||
@@ -279,9 +282,8 @@ export async function handleCreateSelectedCalendars() {
|
||||
groupedDelegationUserCredentials
|
||||
)) {
|
||||
log.info(`Processing delegation user credentials for delegationCredentialId: ${delegationCredentialId}`);
|
||||
const delegationUserCredentialsToProcess = await getDelegationUserCredentialsToProcess(
|
||||
delegationUserCredentials
|
||||
);
|
||||
const delegationUserCredentialsToProcess =
|
||||
await getDelegationUserCredentialsToProcess(delegationUserCredentials);
|
||||
log.info(
|
||||
`Found ${delegationUserCredentialsToProcess.length} delegationUserCredentials to process for delegationCredentialId: ${delegationCredentialId}`
|
||||
);
|
||||
|
||||
@@ -47,9 +47,7 @@ describe("defaultResponderForAppDir", () => {
|
||||
});
|
||||
|
||||
it("should respond with status code 409 for NoAvailableUsersFound", async () => {
|
||||
const f = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error(ErrorCode.NoAvailableUsersFound));
|
||||
const f = vi.fn().mockRejectedValue(new Error(ErrorCode.NoAvailableUsersFound));
|
||||
const req = { method: "GET", url: "/api/test" } as unknown as NextRequest;
|
||||
const params = Promise.resolve<Params>({});
|
||||
|
||||
@@ -65,9 +63,7 @@ describe("defaultResponderForAppDir", () => {
|
||||
});
|
||||
|
||||
it("should respond with a 429 status code for rate limit errors", async () => {
|
||||
const f = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new TRPCError({ code: "TOO_MANY_REQUESTS" }));
|
||||
const f = vi.fn().mockRejectedValue(new TRPCError({ code: "TOO_MANY_REQUESTS" }));
|
||||
const req = { method: "POST", url: "/api/test" } as unknown as NextRequest;
|
||||
const params = Promise.resolve<Params>({});
|
||||
|
||||
|
||||
@@ -4,4 +4,4 @@ import getIP from "@calcom/lib/getIP";
|
||||
export async function GET(req: Request) {
|
||||
const requestorIp = getIP(req);
|
||||
return NextResponse.json({ ip: requestorIp });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,16 @@ import { getOGImageVersion } from "@calcom/lib/OgImages";
|
||||
import { GET } from "../route";
|
||||
|
||||
vi.mock("next/og", () => ({
|
||||
ImageResponse: vi.fn().mockImplementation(function() { return {
|
||||
body: new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new Uint8Array([1, 2, 3, 4]));
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
}; }),
|
||||
ImageResponse: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
body: new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new Uint8Array([1, 2, 3, 4]));
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/OgImages", async (importOriginal) => {
|
||||
|
||||
@@ -77,13 +77,11 @@ async function handler(req: NextRequest) {
|
||||
|
||||
const etag = await getOGImageVersion("meeting");
|
||||
const img = new ImageResponse(
|
||||
(
|
||||
<Meeting
|
||||
title={title}
|
||||
profile={{ name: meetingProfileName, image: meetingImage }}
|
||||
users={names.map((name, index) => ({ name, username: usernames[index] }))}
|
||||
/>
|
||||
),
|
||||
<Meeting
|
||||
title={title}
|
||||
profile={{ name: meetingProfileName, image: meetingImage }}
|
||||
users={names.map((name, index) => ({ name, username: usernames[index] }))}
|
||||
/>,
|
||||
ogConfig
|
||||
);
|
||||
|
||||
|
||||
@@ -76,10 +76,12 @@ const mockSendCreditBalanceLimitReachedEmails = vi.fn();
|
||||
const mockSendCreditBalanceLowWarningEmails = vi.fn();
|
||||
|
||||
vi.mock("@calcom/features/ee/billing/credit-service", () => ({
|
||||
CreditService: vi.fn().mockImplementation(function() { return {
|
||||
hasAvailableCredits: mockHasAvailableCredits,
|
||||
chargeCredits: mockChargeCredits,
|
||||
}; }),
|
||||
CreditService: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
hasAvailableCredits: mockHasAvailableCredits,
|
||||
chargeCredits: mockChargeCredits,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/emails/email-manager", () => ({
|
||||
@@ -92,15 +94,19 @@ const mockFindByPhoneNumber = vi.fn();
|
||||
const mockFindByProviderAgentId = vi.fn();
|
||||
|
||||
vi.mock("@calcom/features/calAIPhone/repositories/PrismaPhoneNumberRepository", () => ({
|
||||
PrismaPhoneNumberRepository: vi.fn().mockImplementation(function() { return {
|
||||
findByPhoneNumber: mockFindByPhoneNumber,
|
||||
}; }),
|
||||
PrismaPhoneNumberRepository: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
findByPhoneNumber: mockFindByPhoneNumber,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/features/calAIPhone/repositories/PrismaAgentRepository", () => ({
|
||||
PrismaAgentRepository: vi.fn().mockImplementation(function() { return {
|
||||
findByProviderAgentId: mockFindByProviderAgentId,
|
||||
}; }),
|
||||
PrismaAgentRepository: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
findByProviderAgentId: mockFindByProviderAgentId,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("next/server", () => ({
|
||||
|
||||
@@ -12,10 +12,7 @@ import { notFound } from "next/navigation";
|
||||
*/
|
||||
const Page = (): JSX.Element => {
|
||||
// Gate this page to E2E test mode or dev only
|
||||
if (
|
||||
process.env.NEXT_PUBLIC_IS_E2E !== "1" &&
|
||||
process.env.NODE_ENV !== "development"
|
||||
) {
|
||||
if (process.env.NEXT_PUBLIC_IS_E2E !== "1" && process.env.NODE_ENV !== "development") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
|
||||
@@ -41,8 +41,9 @@ function getPageInfo(pathname: string, host: string) {
|
||||
return {
|
||||
username: currentOrgDomain ?? "",
|
||||
pageType: PageType.ORG,
|
||||
url: `${WEBSITE_URL}/signup?callbackUrl=settings/organizations/new%3Fslug%3D${currentOrgDomain?.replace("/", "") ?? ""
|
||||
}`,
|
||||
url: `${WEBSITE_URL}/signup?callbackUrl=settings/organizations/new%3Fslug%3D${
|
||||
currentOrgDomain?.replace("/", "") ?? ""
|
||||
}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -162,8 +163,9 @@ export function NotFound({ host }: { host: string }) {
|
||||
<span className="focus:outline-none">
|
||||
<span className="absolute inset-0" aria-hidden="true" />
|
||||
{t("register")}{" "}
|
||||
<strong className="text-green-500">{`${pageType === PageType.TEAM ? `${new URL(WEBSITE_URL).host}/team/` : ""
|
||||
}${username}${pageType === PageType.ORG ? `.${subdomainSuffix()}` : ""}`}</strong>
|
||||
<strong className="text-green-500">{`${
|
||||
pageType === PageType.TEAM ? `${new URL(WEBSITE_URL).host}/team/` : ""
|
||||
}${username}${pageType === PageType.ORG ? `.${subdomainSuffix()}` : ""}`}</strong>
|
||||
</span>
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { XIcon } from "@coss/ui/icons";
|
||||
|
||||
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>
|
||||
<Icon name="x" className="text-inverted h-6 w-6 dark:text-white" aria-hidden="true" />
|
||||
<XIcon className="text-inverted h-6 w-6 dark:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { ChartBarIcon, CreditCardIcon, GlobeIcon, LockIcon, PaintbrushIcon, UsersIcon } from "@coss/ui/icons";
|
||||
|
||||
import Shell from "~/shell/Shell";
|
||||
import { UpgradeTip } from "~/shell/UpgradeTip";
|
||||
@@ -13,32 +13,32 @@ export default function EnterprisePage() {
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <Icon name="globe" className="h-5 w-5 text-red-500" />,
|
||||
icon: <GlobeIcon className="h-5 w-5 text-red-500" />,
|
||||
title: t("branded_subdomain"),
|
||||
description: t("branded_subdomain_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="chart-bar" className="h-5 w-5 text-blue-500" />,
|
||||
icon: <ChartBarIcon className="h-5 w-5 text-blue-500" />,
|
||||
title: t("org_insights"),
|
||||
description: t("org_insights_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="paintbrush" className="h-5 w-5 text-pink-500" />,
|
||||
icon: <PaintbrushIcon className="h-5 w-5 text-pink-500" />,
|
||||
title: t("extensive_whitelabeling"),
|
||||
description: t("extensive_whitelabeling_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="users" className="h-5 w-5 text-orange-500" />,
|
||||
icon: <UsersIcon className="h-5 w-5 text-orange-500" />,
|
||||
title: t("unlimited_teams"),
|
||||
description: t("unlimited_teams_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="credit-card" className="h-5 w-5 text-green-500" />,
|
||||
icon: <CreditCardIcon className="h-5 w-5 text-green-500" />,
|
||||
title: t("unified_billing"),
|
||||
description: t("advanced_managed_events_description"),
|
||||
},
|
||||
{
|
||||
icon: <Icon name="lock" className="h-5 w-5 text-purple-500" />,
|
||||
icon: <LockIcon className="h-5 w-5 text-purple-500" />,
|
||||
title: t("advanced_managed_events"),
|
||||
description: t("advanced_managed_events_description"),
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* PAGES ROUTER ONLY - Used exclusively by Next.js Pages Router
|
||||
*
|
||||
*
|
||||
* Currently only serves the /router endpoint (routing forms redirect page).
|
||||
* DO NOT add new features here - this file will be deprecated once we remove apps/web/pages.
|
||||
*
|
||||
*
|
||||
* For App Router, use PageWrapperAppDir.tsx instead.
|
||||
*/
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ export type PageWrapperProps = Readonly<{
|
||||
}>;
|
||||
|
||||
function PageWrapper(props: PageWrapperProps) {
|
||||
|
||||
// On client side don't let nonce creep into DOM
|
||||
// It also avoids hydration warning that says that Client has the nonce value but server has "" because browser removes nonce attributes before DOM is built
|
||||
// See https://github.com/kentcdodds/nonce-hydration-issues
|
||||
|
||||
@@ -19,7 +19,15 @@ import type { App as AppType } from "@calcom/types/App";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import {
|
||||
BookOpenIcon,
|
||||
CircleAlertIcon,
|
||||
ExternalLinkIcon,
|
||||
FileIcon,
|
||||
FlagIcon,
|
||||
MailIcon,
|
||||
ShieldIcon,
|
||||
} from "@coss/ui/icons";
|
||||
import { SkeletonButton, SkeletonText } from "@calcom/ui/components/skeleton";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
@@ -184,7 +192,9 @@ export const AppPage = ({
|
||||
enabled: !!dependencies,
|
||||
});
|
||||
|
||||
const disableInstall = dependencyData.data ? dependencyData.data.some((dependency) => !dependency.installed) : false;
|
||||
const disableInstall = dependencyData.data
|
||||
? dependencyData.data.some((dependency) => !dependency.installed)
|
||||
: false;
|
||||
|
||||
// const disableInstall = requiresGCal && !gCalInstalled.data;
|
||||
|
||||
@@ -383,7 +393,7 @@ export const AppPage = ({
|
||||
<div className="items-start space-x-2.5">
|
||||
<div className="text-info flex items-start">
|
||||
<div>
|
||||
<Icon name="circle-alert" className="mr-2 mt-1 font-semibold" />
|
||||
<CircleAlertIcon className="mr-2 mt-1 font-semibold" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">{t("msteams_calendar_warning_body")}</span>
|
||||
@@ -443,7 +453,7 @@ export const AppPage = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis text-sm font-normal no-underline hover:underline"
|
||||
href={docs}>
|
||||
<Icon name="book-open" className="text-subtle -mt-1 mr-1 inline h-4 w-4" />
|
||||
<BookOpenIcon className="text-subtle -mt-1 mr-1 inline h-4 w-4" />
|
||||
{t("documentation")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -455,7 +465,7 @@ export const AppPage = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
href={website}>
|
||||
<Icon name="external-link" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
<ExternalLinkIcon className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
{website.replace("https://", "")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -467,7 +477,7 @@ export const AppPage = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
href={`mailto:${email}`}>
|
||||
<Icon name="mail" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
<MailIcon className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
|
||||
{email}
|
||||
</a>
|
||||
@@ -480,7 +490,7 @@ export const AppPage = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
href={tos}>
|
||||
<Icon name="file" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
<FileIcon className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
{t("terms_of_service")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -492,7 +502,7 @@ export const AppPage = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
href={privacy}>
|
||||
<Icon name="shield" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
<ShieldIcon className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
{t("privacy_policy")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -503,7 +513,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}`}>
|
||||
<Icon name="flag" className="inline h-3 w-3" /> {t("report_app")}
|
||||
<FlagIcon className="inline h-3 w-3" /> {t("report_app")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -169,7 +169,11 @@ export function CalendarListContainer({
|
||||
})}
|
||||
description={t(`no_category_apps_description_calendar`)}
|
||||
buttonRaw={
|
||||
<Button EndIcon="external-link" color="secondary" data-testid="connect-calendar-apps" href="/apps/categories/calendar">
|
||||
<Button
|
||||
EndIcon="external-link"
|
||||
color="secondary"
|
||||
data-testid="connect-calendar-apps"
|
||||
href="/apps/categories/calendar">
|
||||
{t("connect_your_first_calendar")}
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -23,24 +23,14 @@ export const InstallAppButtonChild = ({
|
||||
// Also, cal.ai(the only paid app at the moment) doesn't support team install either
|
||||
if (paid) {
|
||||
return (
|
||||
<Button
|
||||
data-testid="install-app-button"
|
||||
{...props}
|
||||
disabled={isDisabled}
|
||||
color="primary"
|
||||
size="base">
|
||||
<Button data-testid="install-app-button" {...props} disabled={isDisabled} color="primary" size="base">
|
||||
{paid.trial ? t("start_paid_trial") : t("subscribe")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-testid="install-app-button"
|
||||
{...props}
|
||||
disabled={isDisabled}
|
||||
color="primary"
|
||||
size="base">
|
||||
<Button data-testid="install-app-button" {...props} disabled={isDisabled} color="primary" size="base">
|
||||
{multiInstall ? t("install_another") : t("install_app")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -12,8 +12,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { InfoIcon } from "@coss/ui/icons";
|
||||
|
||||
export default function AlbySetup(props: IAlbySetupProps) {
|
||||
const params = useCompatSearchParams();
|
||||
@@ -165,7 +165,7 @@ function AlbySetupPage(props: IAlbySetupProps) {
|
||||
|
||||
{/* TODO: remove when invoices are generated using user identifier */}
|
||||
<div className="mt-4 rounded bg-blue-50 p-3 text-sm text-blue-700 dark:bg-blue-950 dark:text-blue-300">
|
||||
<Icon name="info" className="mb-0.5 inline-flex h-4 w-4" /> Your Alby lightning address will be
|
||||
<InfoIcon className="mb-0.5 inline-flex h-4 w-4" /> Your Alby lightning address will be
|
||||
used to generate invoices. If you update your lightning address, please disconnect and setup the
|
||||
Alby app again.
|
||||
</div>
|
||||
|
||||
@@ -1,150 +1,150 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import z from "zod";
|
||||
|
||||
import { useBookingSuccessRedirect } from "@calcom/features/bookings/lib/bookingSuccessRedirect";
|
||||
import type { PaymentPageProps } from "@calcom/features/ee/payments/pages/payment";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useCopy } from "@calcom/lib/hooks/useCopy";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Spinner } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
interface IPaymentComponentProps {
|
||||
payment: {
|
||||
// Will be parsed on render
|
||||
data: unknown;
|
||||
};
|
||||
paymentPageProps: PaymentPageProps;
|
||||
}
|
||||
|
||||
// Create zod schema for data
|
||||
const PaymentBTCPayDataSchema = z.object({
|
||||
invoice: z.object({ checkoutLink: z.string() }).required(),
|
||||
});
|
||||
|
||||
export const BtcpayPaymentComponent = (props: IPaymentComponentProps) => {
|
||||
const { payment } = props;
|
||||
const { data } = payment;
|
||||
const [iframeLoaded, setIframeLoaded] = useState(false);
|
||||
const { copyToClipboard, isCopied } = useCopy();
|
||||
const wrongUrl = (
|
||||
<>
|
||||
<p className="mt-3 text-center">Couldn't obtain payment URL</p>
|
||||
</>
|
||||
);
|
||||
|
||||
const parsedData = PaymentBTCPayDataSchema.safeParse(data);
|
||||
if (!parsedData.success || !parsedData.data?.invoice?.checkoutLink) return wrongUrl;
|
||||
const checkoutUrl = parsedData.data.invoice.checkoutLink;
|
||||
const handleOpenInNewTab = () => {
|
||||
window.open(checkoutUrl, "_blank", "noopener,noreferrer");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4 mt-8 flex h-full w-full flex-col items-center justify-center gap-4">
|
||||
<PaymentChecker {...props.paymentPageProps} />
|
||||
|
||||
{!iframeLoaded && (
|
||||
<div className="flex items-center justify-center">
|
||||
<Spinner className="mr-2 h-5 w-5" />
|
||||
<p>Loading payment page...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`w-full ${iframeLoaded ? "block" : "hidden"}`}>
|
||||
<iframe
|
||||
src={checkoutUrl}
|
||||
title="BTCPay Payment"
|
||||
className="h-[1000px] w-full rounded-md border-0"
|
||||
onLoad={() => setIframeLoaded(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row flex-wrap items-center justify-center gap-4">
|
||||
<Button
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={() => copyToClipboard(checkoutUrl)}
|
||||
className="text-subtle rounded-md"
|
||||
StartIcon={isCopied ? "clipboard-check" : "clipboard"}>
|
||||
Copy Payment Link
|
||||
</Button>
|
||||
|
||||
<Button onClick={handleOpenInNewTab}>Open in New Tab</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type PaymentCheckerProps = PaymentPageProps;
|
||||
|
||||
function PaymentChecker(props: PaymentCheckerProps) {
|
||||
// TODO: move booking success code to a common lib function
|
||||
// TODO: subscribe rather than polling
|
||||
const searchParams = useCompatSearchParams();
|
||||
const bookingSuccessRedirect = useBookingSuccessRedirect();
|
||||
const utils = trpc.useUtils();
|
||||
const { t } = useLocale();
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// use closure to ensure non-nullability
|
||||
const sp = searchParams;
|
||||
const interval = setInterval(() => {
|
||||
(async () => {
|
||||
try {
|
||||
if (props.booking.status === "ACCEPTED") {
|
||||
return;
|
||||
}
|
||||
const { booking: bookingResult } = await utils.viewer.bookings.find.fetch({
|
||||
bookingUid: props.booking.uid,
|
||||
});
|
||||
|
||||
if (bookingResult?.paid) {
|
||||
showToast("Payment successful", "success");
|
||||
|
||||
const params: {
|
||||
uid: string;
|
||||
email: string | null;
|
||||
location: string;
|
||||
} = {
|
||||
uid: props.booking.uid,
|
||||
email: sp.get("email"),
|
||||
location: t("web_conferencing_details_to_follow"),
|
||||
};
|
||||
|
||||
bookingSuccessRedirect({
|
||||
successRedirectUrl: props.eventType.successRedirectUrl,
|
||||
query: params,
|
||||
booking: props.booking,
|
||||
forwardParamsSuccessRedirect: props.eventType.forwardParamsSuccessRedirect,
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
}, 2000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [
|
||||
bookingSuccessRedirect,
|
||||
props.booking,
|
||||
props.booking.id,
|
||||
props.booking.status,
|
||||
props.eventType.id,
|
||||
props.eventType.successRedirectUrl,
|
||||
props.eventType.forwardParamsSuccessRedirect,
|
||||
props.payment.success,
|
||||
searchParams,
|
||||
t,
|
||||
utils.viewer.bookings,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import z from "zod";
|
||||
|
||||
import { useBookingSuccessRedirect } from "@calcom/features/bookings/lib/bookingSuccessRedirect";
|
||||
import type { PaymentPageProps } from "@calcom/features/ee/payments/pages/payment";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useCopy } from "@calcom/lib/hooks/useCopy";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Spinner } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
interface IPaymentComponentProps {
|
||||
payment: {
|
||||
// Will be parsed on render
|
||||
data: unknown;
|
||||
};
|
||||
paymentPageProps: PaymentPageProps;
|
||||
}
|
||||
|
||||
// Create zod schema for data
|
||||
const PaymentBTCPayDataSchema = z.object({
|
||||
invoice: z.object({ checkoutLink: z.string() }).required(),
|
||||
});
|
||||
|
||||
export const BtcpayPaymentComponent = (props: IPaymentComponentProps) => {
|
||||
const { payment } = props;
|
||||
const { data } = payment;
|
||||
const [iframeLoaded, setIframeLoaded] = useState(false);
|
||||
const { copyToClipboard, isCopied } = useCopy();
|
||||
const wrongUrl = (
|
||||
<>
|
||||
<p className="mt-3 text-center">Couldn't obtain payment URL</p>
|
||||
</>
|
||||
);
|
||||
|
||||
const parsedData = PaymentBTCPayDataSchema.safeParse(data);
|
||||
if (!parsedData.success || !parsedData.data?.invoice?.checkoutLink) return wrongUrl;
|
||||
const checkoutUrl = parsedData.data.invoice.checkoutLink;
|
||||
const handleOpenInNewTab = () => {
|
||||
window.open(checkoutUrl, "_blank", "noopener,noreferrer");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4 mt-8 flex h-full w-full flex-col items-center justify-center gap-4">
|
||||
<PaymentChecker {...props.paymentPageProps} />
|
||||
|
||||
{!iframeLoaded && (
|
||||
<div className="flex items-center justify-center">
|
||||
<Spinner className="mr-2 h-5 w-5" />
|
||||
<p>Loading payment page...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`w-full ${iframeLoaded ? "block" : "hidden"}`}>
|
||||
<iframe
|
||||
src={checkoutUrl}
|
||||
title="BTCPay Payment"
|
||||
className="h-[1000px] w-full rounded-md border-0"
|
||||
onLoad={() => setIframeLoaded(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row flex-wrap items-center justify-center gap-4">
|
||||
<Button
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={() => copyToClipboard(checkoutUrl)}
|
||||
className="text-subtle rounded-md"
|
||||
StartIcon={isCopied ? "clipboard-check" : "clipboard"}>
|
||||
Copy Payment Link
|
||||
</Button>
|
||||
|
||||
<Button onClick={handleOpenInNewTab}>Open in New Tab</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type PaymentCheckerProps = PaymentPageProps;
|
||||
|
||||
function PaymentChecker(props: PaymentCheckerProps) {
|
||||
// TODO: move booking success code to a common lib function
|
||||
// TODO: subscribe rather than polling
|
||||
const searchParams = useCompatSearchParams();
|
||||
const bookingSuccessRedirect = useBookingSuccessRedirect();
|
||||
const utils = trpc.useUtils();
|
||||
const { t } = useLocale();
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// use closure to ensure non-nullability
|
||||
const sp = searchParams;
|
||||
const interval = setInterval(() => {
|
||||
(async () => {
|
||||
try {
|
||||
if (props.booking.status === "ACCEPTED") {
|
||||
return;
|
||||
}
|
||||
const { booking: bookingResult } = await utils.viewer.bookings.find.fetch({
|
||||
bookingUid: props.booking.uid,
|
||||
});
|
||||
|
||||
if (bookingResult?.paid) {
|
||||
showToast("Payment successful", "success");
|
||||
|
||||
const params: {
|
||||
uid: string;
|
||||
email: string | null;
|
||||
location: string;
|
||||
} = {
|
||||
uid: props.booking.uid,
|
||||
email: sp.get("email"),
|
||||
location: t("web_conferencing_details_to_follow"),
|
||||
};
|
||||
|
||||
bookingSuccessRedirect({
|
||||
successRedirectUrl: props.eventType.successRedirectUrl,
|
||||
query: params,
|
||||
booking: props.booking,
|
||||
forwardParamsSuccessRedirect: props.eventType.forwardParamsSuccessRedirect,
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
}, 2000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [
|
||||
bookingSuccessRedirect,
|
||||
props.booking,
|
||||
props.booking.id,
|
||||
props.booking.status,
|
||||
props.eventType.id,
|
||||
props.eventType.successRedirectUrl,
|
||||
props.eventType.forwardParamsSuccessRedirect,
|
||||
props.payment.success,
|
||||
searchParams,
|
||||
t,
|
||||
utils.viewer.bookings,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { LoaderIcon } from "@coss/ui/icons";
|
||||
|
||||
export default function BTCPaySetup(props: IBTCPaySetupProps) {
|
||||
const params = useCompatSearchParams();
|
||||
@@ -324,7 +324,7 @@ function BTCPaySetupPage(props: IBTCPaySetupProps) {
|
||||
disabled={loading || validating || !updatable}>
|
||||
{loading ? (
|
||||
<>
|
||||
<Icon name="loader" className="mr-2 animate-spin" />
|
||||
<LoaderIcon className="mr-2 animate-spin" />
|
||||
{t("connecting")}
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Switch } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { InfoIcon } from "@coss/ui/icons";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
export default function HitPaySetup(props: IHitPaySetupProps) {
|
||||
@@ -205,7 +205,7 @@ function HitPaySetupPage(props: IHitPaySetupProps) {
|
||||
{showContent ? (
|
||||
<div className="flex w-full w-full max-w-[43em] flex-col items-center justify-center stack-y-4 p-4 lg:stack-y-5">
|
||||
<div className="rounded bg-blue-50 p-3 text-sm text-blue-700 dark:bg-blue-950 dark:text-blue-300">
|
||||
<Icon name="info" className="mb-0.5 inline-flex h-4 w-4" /> Create or connect to an existing
|
||||
<InfoIcon className="mb-0.5 inline-flex h-4 w-4" /> Create or connect to an existing
|
||||
HitPay account to receive payments for your paid bookings.
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Alert } from "@calcom/ui/components/alert";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Form } from "@calcom/ui/components/form";
|
||||
import { TextField } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { PlusIcon, TrashIcon } from "@coss/ui/icons";
|
||||
|
||||
export default function ICSFeedSetup() {
|
||||
const { t } = useLocale();
|
||||
@@ -78,7 +78,7 @@ export default function ICSFeedSetup() {
|
||||
type="button"
|
||||
className="mb-2 h-min text-sm"
|
||||
onClick={() => setUrls((urls) => urls.filter((_, ii) => i !== ii))}>
|
||||
<Icon name="trash" size={16} />
|
||||
<TrashIcon size={16} />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -91,7 +91,7 @@ export default function ICSFeedSetup() {
|
||||
onClick={() => {
|
||||
setUrls((urls) => urls.concat(""));
|
||||
}}>
|
||||
{t("add")} <Icon className="inline" name="plus" size={16} />
|
||||
{t("add")} <PlusIcon className="inline" size={16} />
|
||||
</button>
|
||||
|
||||
{errorMessage && (
|
||||
|
||||
@@ -36,13 +36,15 @@ const EventTypeCard: FC<EventTypeCardProps> = ({
|
||||
userName,
|
||||
}) => {
|
||||
const parsedMetaData = EventTypeMetaDataSchema.safeParse(metadata);
|
||||
const multipleDuration = parsedMetaData.success && parsedMetaData.data?.multipleDuration
|
||||
? parsedMetaData.data.multipleDuration
|
||||
: [];
|
||||
const multipleDuration =
|
||||
parsedMetaData.success && parsedMetaData.data?.multipleDuration
|
||||
? parsedMetaData.data.multipleDuration
|
||||
: [];
|
||||
|
||||
const durations = multipleDuration.length > 0
|
||||
? [length, ...multipleDuration.filter((duration) => duration !== length)].sort()
|
||||
: [length];
|
||||
const durations =
|
||||
multipleDuration.length > 0
|
||||
? [length, ...multipleDuration.filter((duration) => duration !== length)].sort()
|
||||
: [length];
|
||||
return (
|
||||
<div
|
||||
data-testid={`select-event-type-${id}`}
|
||||
@@ -66,7 +68,6 @@ const EventTypeCard: FC<EventTypeCardProps> = ({
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: Content is sanitized via markdownToSafeHTML
|
||||
<div
|
||||
className="text-subtle line-clamp-4 wrap-break-word text-sm sm:max-w-[650px] [&>*:not(:first-child)]:hidden [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: markdownToSafeHTML(description),
|
||||
}}
|
||||
@@ -120,7 +121,9 @@ const EventTypeGroup: FC<EventTypeGroupProps> = ({ groupIndex, userName, ...prop
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="text-subtle bg-cal-muted w-full p-2 text-center text-sm">Team has no Events</div>
|
||||
<div className="text-subtle bg-cal-muted w-full p-2 text-center text-sm">
|
||||
Team has no Events
|
||||
</div>
|
||||
)}
|
||||
</ul>
|
||||
</ScrollableArea>
|
||||
|
||||
@@ -8,9 +8,9 @@ import ServerTrans from "@calcom/lib/components/ServerTrans";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
import { ClipboardIcon } from "@coss/ui/icons";
|
||||
|
||||
const MAKE = "make";
|
||||
|
||||
@@ -164,7 +164,7 @@ const CopyApiKey = ({ apiKey }: { apiKey: string }) => {
|
||||
}}
|
||||
type="button"
|
||||
className="mt-4 text-base sm:mt-0 sm:rounded-l-none">
|
||||
<Icon name="clipboard" className="h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||
<ClipboardIcon className="h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||
{t("copy")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { TextField } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { CircleAlertIcon } from "@coss/ui/icons";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
export default function PayPalSetup() {
|
||||
@@ -91,7 +91,7 @@ export default function PayPalSetup() {
|
||||
</p>
|
||||
|
||||
<p className="text-lgf text-default mt-5 inline-flex font-bold">
|
||||
<Icon name="circle-alert" className="mr-2 mt-1 h-4 w-4" /> Important requirements:
|
||||
<CircleAlertIcon className="mr-2 mt-1 h-4 w-4" /> Important requirements:
|
||||
</p>
|
||||
<ul className="text-default ml-1 mt-2 list-disc pl-2">
|
||||
<li>Paypal Business account</li>
|
||||
@@ -102,7 +102,8 @@ export default function PayPalSetup() {
|
||||
<a
|
||||
className="text-orange-600 underline"
|
||||
target="_blank"
|
||||
href="https://developer.paypal.com/api/rest/#link-getclientidandclientsecret" rel="noreferrer">
|
||||
href="https://developer.paypal.com/api/rest/#link-getclientidandclientsecret"
|
||||
rel="noreferrer">
|
||||
Link to Paypal developer API REST Setup Guide:
|
||||
https://developer.paypal.com/api/rest/#link-getclientidandclientsecret
|
||||
</a>
|
||||
@@ -121,7 +122,8 @@ export default function PayPalSetup() {
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://developer.paypal.com/dashboard/applications/live"
|
||||
className="text-orange-600 underline" rel="noreferrer">
|
||||
className="text-orange-600 underline"
|
||||
rel="noreferrer">
|
||||
{t("here")}
|
||||
</a>
|
||||
.
|
||||
@@ -143,7 +145,7 @@ export default function PayPalSetup() {
|
||||
<li>You should be all setup after this.</li>
|
||||
</ol>
|
||||
<p className="text-default mt-5 inline-flex font-bold">
|
||||
<Icon name="circle-alert" className="mr-2 mt-1 h-4 w-4" />
|
||||
<CircleAlertIcon className="mr-2 mt-1 h-4 w-4" />
|
||||
Reminder:
|
||||
</p>
|
||||
<p className="text-default mt-2">
|
||||
|
||||
@@ -8,7 +8,10 @@ import { Dialog } from "@calcom/features/components/controlled-dialog";
|
||||
import { dataTableQueryParamsSerializer } from "@calcom/features/data-table/lib/serializers";
|
||||
import { ColumnFilterType } from "@calcom/features/data-table/lib/types";
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { RoutingFormEmbedButton, RoutingFormEmbedDialog } from "@calcom/web/modules/embed/components/RoutingFormEmbed";
|
||||
import {
|
||||
RoutingFormEmbedButton,
|
||||
RoutingFormEmbedDialog,
|
||||
} from "@calcom/web/modules/embed/components/RoutingFormEmbed";
|
||||
import { EmbedDialogProvider } from "@calcom/features/embed/lib/hooks/useEmbedDialogCtx";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { RoutingFormWithResponseCount } from "@calcom/routing-forms/types/t
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { DropdownMenuSeparator } from "@calcom/ui/components/dropdown";
|
||||
import { ToggleGroup } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { MenuIcon, PencilIcon, WaypointsIcon } from "@coss/ui/icons";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
|
||||
import { FormAction, FormActionsDropdown } from "./FormActions";
|
||||
@@ -298,7 +298,7 @@ export function Header({
|
||||
color="minimal"
|
||||
onClick={() => setIsEditing(true)}
|
||||
CustomStartIcon={
|
||||
<Icon name="pencil" className="text-subtle group-hover:text-default h-3 w-3" />
|
||||
<PencilIcon className="text-subtle group-hover:text-default h-3 w-3" />
|
||||
}>
|
||||
<span className="sr-only">Edit</span>
|
||||
</Button>
|
||||
@@ -319,13 +319,13 @@ export function Header({
|
||||
{
|
||||
value: "form-edit",
|
||||
label: <span className="sr-only sm:not-sr-only">{t("form")}</span>,
|
||||
iconLeft: <Icon name="menu" className="h-3 w-3" />,
|
||||
iconLeft: <MenuIcon className="h-3 w-3" />,
|
||||
dataTestId: "toggle-group-item-form-edit",
|
||||
},
|
||||
{
|
||||
value: "route-builder",
|
||||
label: <span className="sr-only sm:not-sr-only">{t("routing")}</span>,
|
||||
iconLeft: <Icon name="waypoints" className="h-3 w-3" />,
|
||||
iconLeft: <WaypointsIcon className="h-3 w-3" />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -355,12 +355,12 @@ export function Header({
|
||||
{
|
||||
value: "form-edit",
|
||||
label: <span className="sr-only sm:not-sr-only">{t("form")}</span>,
|
||||
iconLeft: <Icon name="menu" className="h-3 w-3" />,
|
||||
iconLeft: <MenuIcon className="h-3 w-3" />,
|
||||
},
|
||||
{
|
||||
value: "route-builder",
|
||||
label: <span className="sr-only sm:not-sr-only">{t("routing")}</span>,
|
||||
iconLeft: <Icon name="waypoints" className="h-3 w-3" />,
|
||||
iconLeft: <WaypointsIcon className="h-3 w-3" />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,7 @@ import logger from "@calcom/lib/logger";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/components/dialog";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { ClockIcon } from "@coss/ui/icons";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
interface IConfirmDialogWipe {
|
||||
@@ -83,7 +83,7 @@ export const ConfirmDialog = (props: IConfirmDialogWipe) => {
|
||||
<DialogContent onOpenAutoFocus={(e) => e.preventDefault()}>
|
||||
<div className="flex flex-row space-x-3">
|
||||
<div className="flex h-10 w-10 shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
||||
<Icon name="clock" className="m-auto h-5 w-5" />
|
||||
<ClockIcon className="m-auto h-5 w-5" />
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<DialogHeader title="Wipe My Calendar" />
|
||||
|
||||
@@ -552,19 +552,14 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
userTimeZone={userTimeZone}
|
||||
isRescheduled={isRescheduled}
|
||||
onAssignmentReasonClick={
|
||||
booking.assignmentReason.length > 0 ? () => setIsOpenRoutingTraceSheet(true) : undefined
|
||||
booking.assignmentReasonSortedByCreatedAt.length > 0 ? () => setIsOpenRoutingTraceSheet(true) : undefined
|
||||
}
|
||||
/>
|
||||
{isBookingFromRoutingForm && (
|
||||
<WrongAssignmentDialog
|
||||
isOpenDialog={isOpenWrongAssignmentDialog}
|
||||
setIsOpenDialog={setIsOpenWrongAssignmentDialog}
|
||||
bookingUid={booking.uid}
|
||||
routingReason={booking.assignmentReason[0]?.reasonString ?? null}
|
||||
guestEmail={booking.attendees[0]?.email ?? ""}
|
||||
hostEmail={booking.user?.email ?? ""}
|
||||
hostName={booking.user?.name ?? null}
|
||||
teamId={booking.eventType?.team?.id ?? null}
|
||||
booking={booking}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -606,7 +601,7 @@ const BookingItemBadges = ({
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isRejected && !isRescheduled && booking.assignmentReason.length === 0 && (
|
||||
{isRejected && !isRescheduled && booking.assignmentReasonSortedByCreatedAt.length === 0 && (
|
||||
<Badge variant="gray" className="ltr:mr-2 rtl:ml-2">
|
||||
{t("rejected")}
|
||||
</Badge>
|
||||
@@ -616,9 +611,9 @@ const BookingItemBadges = ({
|
||||
{booking.eventType.team.name}
|
||||
</Badge>
|
||||
)}
|
||||
{booking?.assignmentReason.length > 0 && (
|
||||
{booking?.assignmentReasonSortedByCreatedAt.length > 0 && (
|
||||
<AssignmentReasonTooltip
|
||||
assignmentReason={booking.assignmentReason[0]}
|
||||
assignmentReason={booking.assignmentReasonSortedByCreatedAt[booking.assignmentReasonSortedByCreatedAt.length - 1]}
|
||||
onClick={onAssignmentReasonClick}
|
||||
/>
|
||||
)}
|
||||
@@ -804,20 +799,19 @@ const Attendee = (
|
||||
const { copyToClipboard, isCopied } = useCopy();
|
||||
|
||||
const noShowMutation = trpc.viewer.loggedInViewerRouter.markNoShow.useMutation({
|
||||
onSuccess: async (data) => {
|
||||
showToast(data.message, "success");
|
||||
await utils.viewer.bookings.invalidate();
|
||||
},
|
||||
onError: (err) => {
|
||||
showToast(err.message, "error");
|
||||
},
|
||||
});
|
||||
onSuccess: async (data) => {
|
||||
showToast(data.message, "success");
|
||||
await utils.viewer.bookings.invalidate();
|
||||
},
|
||||
onError: (err) => {
|
||||
showToast(err.message, "error");
|
||||
},
|
||||
});
|
||||
|
||||
const displayName = user?.name || name || user?.email || email;
|
||||
|
||||
const isTeamMemberOrHost =
|
||||
email === organizerEmail ||
|
||||
eventTypeHosts?.some((host) => host.user?.email === email);
|
||||
email === organizerEmail || eventTypeHosts?.some((host) => host.user?.email === email);
|
||||
const shouldHideEmail = hideOrganizerEmail && isTeamMemberOrHost;
|
||||
|
||||
return (
|
||||
@@ -858,7 +852,7 @@ const Attendee = (
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
const isEmailCopied = isSmsCalEmail(email);
|
||||
copyToClipboard(isEmailCopied ? email : phoneNumber ?? "");
|
||||
copyToClipboard(isEmailCopied ? email : (phoneNumber ?? ""));
|
||||
setOpenDropdown(false);
|
||||
showToast(isEmailCopied ? t("email_copied") : t("phone_number_copied"), "success");
|
||||
}}>
|
||||
@@ -1102,7 +1096,9 @@ const DisplayAttendees = ({
|
||||
|
||||
return (
|
||||
<div className="text-emphasis text-sm" onClick={(e) => e.stopPropagation()}>
|
||||
{user && <FirstAttendee user={user} currentEmail={currentEmail} hideOrganizerEmail={hideOrganizerEmail} />}
|
||||
{user && (
|
||||
<FirstAttendee user={user} currentEmail={currentEmail} hideOrganizerEmail={hideOrganizerEmail} />
|
||||
)}
|
||||
{attendees.length > 1 ? <span>, </span> : <span> {t("and")} </span>}
|
||||
<Attendee
|
||||
{...attendees[0]}
|
||||
|
||||
@@ -10,8 +10,8 @@ import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Label, Select, TextArea, CheckboxField } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { InfoIcon, XIcon } from "@coss/ui/icons";
|
||||
|
||||
interface InternalNotePresetsSelectProps {
|
||||
internalNotePresets: { id: number; name: string }[];
|
||||
@@ -182,7 +182,7 @@ export default function CancelBooking(props: Props) {
|
||||
{error && !props.showErrorAsToast && (
|
||||
<div className="mt-8">
|
||||
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
|
||||
<Icon name="x" className="h-6 w-6 text-red-600" />
|
||||
<XIcon 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">
|
||||
@@ -230,7 +230,7 @@ export default function CancelBooking(props: Props) {
|
||||
/>
|
||||
{isCancellationUserHost ? (
|
||||
<div className="-mt-2 mb-4 flex items-center gap-2">
|
||||
<Icon name="info" className="text-subtle h-4 w-4" />
|
||||
<InfoIcon className="text-subtle h-4 w-4" />
|
||||
<p className="text-subtle text-sm leading-none">
|
||||
{t("notify_attendee_cancellation_reason_warning")}
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetBody } from "@calcom/ui/components/sheet";
|
||||
|
||||
interface RoutingFormResponseSheetProps {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
formResponseId: number;
|
||||
}
|
||||
|
||||
export function RoutingFormResponseSheet({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
formResponseId,
|
||||
}: RoutingFormResponseSheetProps) {
|
||||
const { t } = useLocale();
|
||||
|
||||
const { data, isLoading } = trpc.viewer.appRoutingForms.getFormResponseDisplay.useQuery(
|
||||
{ formResponseId },
|
||||
{
|
||||
enabled: isOpen && !!formResponseId,
|
||||
staleTime: 10 * 60 * 1000,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Sheet open={isOpen} onOpenChange={setIsOpen}>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{t("form_submission")}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<SheetBody>
|
||||
{isLoading && (
|
||||
<div className="flex flex-col gap-4 py-2">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="flex flex-col gap-1.5">
|
||||
<div className="bg-muted h-3 w-24 animate-pulse rounded" />
|
||||
<div className="bg-muted h-5 w-full animate-pulse rounded" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && (!data?.responses || data.responses.length === 0) && (
|
||||
<p className="text-subtle text-sm">{t("no_responses_found")}</p>
|
||||
)}
|
||||
{!isLoading && data?.responses && data.responses.length > 0 && (
|
||||
<div className="flex flex-col gap-4">
|
||||
{data.form?.name && (
|
||||
<div className="border-subtle border-b pb-3">
|
||||
<p className="text-emphasis text-sm font-medium">{data.form.name}</p>
|
||||
{data.form.description && (
|
||||
<p className="text-subtle text-xs">{data.form.description}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{data.responses.map((item) => (
|
||||
<div key={item.fieldId} className="flex flex-col gap-1">
|
||||
<label className="text-subtle text-xs font-medium uppercase tracking-wide">
|
||||
{item.label}
|
||||
</label>
|
||||
<p className="text-emphasis text-sm">{item.displayValue}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</SheetBody>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -5,16 +5,34 @@ import { DomainIcon } from "@calcom/features/routing-trace/components/DomainIcon
|
||||
import { getDomainLabel } from "@calcom/features/routing-trace/presenters/getDomainLabel";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetBody } from "@calcom/ui/components/sheet";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetBody,
|
||||
} from "@calcom/ui/components/sheet";
|
||||
|
||||
|
||||
interface RoutingTraceSheetProps {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
bookingUid: string;
|
||||
/** Callback to open the wrong assignment report dialog. If not provided, report button is hidden */
|
||||
onReport?: () => void;
|
||||
hasExistingReport?: boolean;
|
||||
}
|
||||
|
||||
export function RoutingTraceSheet({ isOpen, setIsOpen, bookingUid }: RoutingTraceSheetProps) {
|
||||
export function RoutingTraceSheet({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
bookingUid,
|
||||
onReport,
|
||||
hasExistingReport,
|
||||
}: RoutingTraceSheetProps) {
|
||||
const { t } = useLocale();
|
||||
const showReportButton = !!onReport;
|
||||
|
||||
const { data, isLoading } = trpc.viewer.bookings.getRoutingTrace.useQuery(
|
||||
{ bookingUid },
|
||||
@@ -27,8 +45,24 @@ export function RoutingTraceSheet({ isOpen, setIsOpen, bookingUid }: RoutingTrac
|
||||
return (
|
||||
<Sheet open={isOpen} onOpenChange={setIsOpen}>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{t("routing_trace")}</SheetTitle>
|
||||
<SheetHeader showCloseButton={!showReportButton} className="w-full">
|
||||
{showReportButton ? (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<SheetTitle>{t("routing_trace")}</SheetTitle>
|
||||
<div className="flex items-center gap-2 pl-2">
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
StartIcon="flag"
|
||||
disabled={hasExistingReport}
|
||||
onClick={onReport}>
|
||||
{t("report")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<SheetTitle>{t("routing_trace")}</SheetTitle>
|
||||
)}
|
||||
</SheetHeader>
|
||||
<SheetBody>
|
||||
{isLoading && (
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { useState } from "react";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { MeetingSessionDetailsDialog } from "@calcom/web/modules/ee/video/components/MeetingSessionDetailsDialog";
|
||||
import ViewRecordingsDialog from "~/ee/video/components/ViewRecordingsDialog";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogClose } from "@calcom/ui/components/dialog";
|
||||
import { Dialog, DialogClose, DialogContent, DialogFooter } from "@calcom/ui/components/dialog";
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
@@ -19,10 +14,10 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@calcom/ui/components/dropdown";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
import type { ActionType } from "@calcom/ui/components/table";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
import { MeetingSessionDetailsDialog } from "@calcom/web/modules/ee/video/components/MeetingSessionDetailsDialog";
|
||||
import { AddGuestsDialog } from "@components/dialog/AddGuestsDialog";
|
||||
import { CancelBookingDialog } from "@components/dialog/CancelBookingDialog";
|
||||
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
|
||||
@@ -33,20 +28,22 @@ import { ReportBookingDialog } from "@components/dialog/ReportBookingDialog";
|
||||
import { RerouteDialog } from "@components/dialog/RerouteDialog";
|
||||
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
||||
import { WrongAssignmentDialog } from "@components/dialog/WrongAssignmentDialog";
|
||||
import { RoutingTraceSheet } from "../RoutingTraceSheet";
|
||||
|
||||
import { useState } from "react";
|
||||
import type { z } from "zod";
|
||||
import ViewRecordingsDialog from "~/ee/video/components/ViewRecordingsDialog";
|
||||
import { useBookingConfirmation } from "../hooks/useBookingConfirmation";
|
||||
import { RoutingTraceSheet } from "../RoutingTraceSheet";
|
||||
import type { BookingItemProps } from "../types";
|
||||
import { useBookingActionsStoreContext } from "./BookingActionsStoreProvider";
|
||||
import {
|
||||
type BookingActionContext,
|
||||
getAfterEventActions,
|
||||
getCancelEventAction,
|
||||
getEditEventActions,
|
||||
getAfterEventActions,
|
||||
getPendingActions,
|
||||
getReportAction,
|
||||
shouldShowEditActions,
|
||||
shouldShowPendingActions,
|
||||
getPendingActions,
|
||||
type BookingActionContext,
|
||||
} from "./bookingActions";
|
||||
|
||||
interface BookingActionsDropdownProps {
|
||||
@@ -278,8 +275,8 @@ export function BookingActionsDropdown({
|
||||
recurringEventId: booking.recurringEventId,
|
||||
})
|
||||
: action.id === "reject"
|
||||
? () => handleReject()
|
||||
: undefined,
|
||||
? () => handleReject()
|
||||
: undefined,
|
||||
})) as ActionType[];
|
||||
|
||||
const shouldShowEdit = shouldShowEditActions(actionContext);
|
||||
@@ -291,14 +288,14 @@ export function BookingActionsDropdown({
|
||||
action.id === "reschedule_request"
|
||||
? () => setIsOpenRescheduleDialog(true)
|
||||
: action.id === "reroute"
|
||||
? () => setRerouteDialogIsOpen(true)
|
||||
: action.id === "change_location"
|
||||
? () => setIsOpenLocationDialog(true)
|
||||
: action.id === "add_members"
|
||||
? () => setIsOpenAddGuestsDialog(true)
|
||||
: action.id === "reassign"
|
||||
? () => setIsOpenReassignDialog(true)
|
||||
: undefined,
|
||||
? () => setRerouteDialogIsOpen(true)
|
||||
: action.id === "change_location"
|
||||
? () => setIsOpenLocationDialog(true)
|
||||
: action.id === "add_members"
|
||||
? () => setIsOpenAddGuestsDialog(true)
|
||||
: action.id === "reassign"
|
||||
? () => setIsOpenReassignDialog(true)
|
||||
: undefined,
|
||||
})) as ActionType[];
|
||||
|
||||
const baseAfterEventActions = getAfterEventActions(actionContext);
|
||||
@@ -308,22 +305,22 @@ export function BookingActionsDropdown({
|
||||
action.id === "view_recordings"
|
||||
? () => setViewRecordingsDialogIsOpen(true)
|
||||
: action.id === "meeting_session_details"
|
||||
? () => setMeetingSessionDetailsDialogIsOpen(true)
|
||||
: action.id === "charge_card"
|
||||
? () => setChargeCardDialogIsOpen(true)
|
||||
: action.id === "no_show"
|
||||
? () => {
|
||||
if (attendeeList.length === 1) {
|
||||
const attendee = attendeeList[0];
|
||||
noShowMutation.mutate({
|
||||
bookingUid: booking.uid,
|
||||
attendees: [{ email: attendee.email, noShow: !attendee.noShow }],
|
||||
});
|
||||
return;
|
||||
}
|
||||
setIsNoShowDialogOpen(true);
|
||||
}
|
||||
: undefined,
|
||||
? () => setMeetingSessionDetailsDialogIsOpen(true)
|
||||
: action.id === "charge_card"
|
||||
? () => setChargeCardDialogIsOpen(true)
|
||||
: action.id === "no_show"
|
||||
? () => {
|
||||
if (attendeeList.length === 1) {
|
||||
const attendee = attendeeList[0];
|
||||
noShowMutation.mutate({
|
||||
bookingUid: booking.uid,
|
||||
attendees: [{ email: attendee.email, noShow: !attendee.noShow }],
|
||||
});
|
||||
return;
|
||||
}
|
||||
setIsNoShowDialogOpen(true);
|
||||
}
|
||||
: undefined,
|
||||
disabled:
|
||||
action.disabled ||
|
||||
(action.id === "no_show" && !(isBookingInPast || isOngoing)) ||
|
||||
@@ -455,23 +452,20 @@ export function BookingActionsDropdown({
|
||||
status={getBookingStatus()}
|
||||
/>
|
||||
{isBookingFromRoutingForm && (
|
||||
<WrongAssignmentDialog
|
||||
isOpenDialog={isOpenWrongAssignmentDialog}
|
||||
setIsOpenDialog={setIsOpenWrongAssignmentDialog}
|
||||
bookingUid={booking.uid}
|
||||
routingReason={booking.assignmentReason[0]?.reasonString ?? null}
|
||||
guestEmail={booking.attendees[0]?.email ?? ""}
|
||||
hostEmail={booking.user?.email ?? ""}
|
||||
hostName={booking.user?.name ?? null}
|
||||
teamId={booking.eventType?.team?.id ?? null}
|
||||
/>
|
||||
)}
|
||||
{booking.assignmentReason.length > 0 && (
|
||||
<RoutingTraceSheet
|
||||
isOpen={isOpenRoutingTraceSheet}
|
||||
setIsOpen={setIsOpenRoutingTraceSheet}
|
||||
bookingUid={booking.uid}
|
||||
/>
|
||||
<>
|
||||
<WrongAssignmentDialog
|
||||
isOpenDialog={isOpenWrongAssignmentDialog}
|
||||
setIsOpenDialog={setIsOpenWrongAssignmentDialog}
|
||||
booking={booking}
|
||||
/>
|
||||
<RoutingTraceSheet
|
||||
isOpen={isOpenRoutingTraceSheet}
|
||||
setIsOpen={setIsOpenRoutingTraceSheet}
|
||||
bookingUid={booking.uid}
|
||||
onReport={() => setIsOpenWrongAssignmentDialog(true)}
|
||||
hasExistingReport={!!booking.report}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{booking.paid && booking.payment[0] && (
|
||||
<ChargeCardDialog
|
||||
|
||||
@@ -84,7 +84,7 @@ function createMockContext(overrides: Partial<BookingActionContext> = {}): Booki
|
||||
location: "integrations:daily",
|
||||
payment: [],
|
||||
seatsReferences: [],
|
||||
assignmentReason: [],
|
||||
assignmentReasonSortedByCreatedAt: [],
|
||||
metadata: null,
|
||||
routedFromRoutingFormReponse: null,
|
||||
listingStatus: "upcoming",
|
||||
|
||||
@@ -237,7 +237,15 @@ export function shouldShowIndividualReportButton(context: BookingActionContext):
|
||||
}
|
||||
|
||||
export function isActionDisabled(actionId: string, context: BookingActionContext): boolean {
|
||||
const { booking, isBookingInPast, isDisabledRescheduling, isDisabledCancelling, isAttendee, isCancelled, isRejected } = context;
|
||||
const {
|
||||
booking,
|
||||
isBookingInPast,
|
||||
isDisabledRescheduling,
|
||||
isDisabledCancelling,
|
||||
isAttendee,
|
||||
isCancelled,
|
||||
isRejected,
|
||||
} = context;
|
||||
|
||||
switch (actionId) {
|
||||
case "reschedule":
|
||||
|
||||
@@ -8,8 +8,8 @@ import { trpc } from "@calcom/trpc/react";
|
||||
import { MultiEmail } from "@calcom/ui/components/address";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/components/dialog";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { TriangleAlertIcon, UserPlusIcon } from "@coss/ui/icons";
|
||||
|
||||
interface IAddGuestsDialog {
|
||||
isOpenDialog: boolean;
|
||||
@@ -59,7 +59,7 @@ export const AddGuestsDialog = (props: IAddGuestsDialog) => {
|
||||
<DialogContent enableOverflow>
|
||||
<div className="flex flex-row md:space-x-3">
|
||||
<div className="bg-subtle hidden h-10 w-10 shrink-0 justify-center rounded-full md:flex">
|
||||
<Icon name="user-plus" className="m-auto h-6 w-6" />
|
||||
<UserPlusIcon className="m-auto h-6 w-6" />
|
||||
</div>
|
||||
<div className="w-full md:pt-1">
|
||||
<DialogHeader title={t("additional_guests")} />
|
||||
@@ -75,7 +75,7 @@ export const AddGuestsDialog = (props: IAddGuestsDialog) => {
|
||||
{isInvalidEmail && (
|
||||
<div className="my-4 flex text-sm text-red-700">
|
||||
<div className="shrink-0">
|
||||
<Icon name="triangle-alert" className="h-5 w-5" />
|
||||
<TriangleAlertIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="font-medium">{t("emails_must_be_unique_valid")}</p>
|
||||
|
||||
@@ -6,8 +6,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { DialogContent, DialogFooter, DialogHeader, DialogClose } from "@calcom/ui/components/dialog";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { CreditCardIcon, TriangleAlertIcon } from "@coss/ui/icons";
|
||||
|
||||
interface IRescheduleDialog {
|
||||
isOpenDialog: boolean;
|
||||
@@ -45,7 +45,7 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
|
||||
<DialogContent>
|
||||
<div className="flex flex-row space-x-3">
|
||||
<div className=" bg-subtle flex h-10 w-10 shrink-0 justify-center rounded-full">
|
||||
<Icon name="credit-card" className="m-auto h-6 w-6" />
|
||||
<CreditCardIcon className="m-auto h-6 w-6" />
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<DialogHeader title={t("charge_card")} />
|
||||
@@ -53,7 +53,7 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
|
||||
|
||||
{chargeError && (
|
||||
<div className="mt-4 flex text-red-500">
|
||||
<Icon name="triangle-alert" className="mr-2 h-5 w-5 " aria-hidden="true" />
|
||||
<TriangleAlertIcon className="mr-2 h-5 w-5" />
|
||||
<p className="text-sm">{chargeError}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/components/dialog";
|
||||
import { Form, Input } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { MapPinIcon } from "@coss/ui/icons";
|
||||
|
||||
import { QueryCell } from "../../lib/QueryCell";
|
||||
|
||||
@@ -258,7 +258,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
}}>
|
||||
<div className="flex flex-row md:space-x-3">
|
||||
<div className="bg-subtle hidden h-10 w-10 shrink-0 justify-center rounded-full md:flex">
|
||||
<Icon name="map-pin" className="m-auto h-6 w-6" />
|
||||
<MapPinIcon className="m-auto h-6 w-6" />
|
||||
</div>
|
||||
<div className="w-full md:pt-1">
|
||||
<DialogHeader title={t("edit_location")} />
|
||||
|
||||
@@ -20,9 +20,9 @@ import {
|
||||
ConfirmationDialogContent,
|
||||
} from "@calcom/ui/components/dialog";
|
||||
import { TextAreaField, Form, Label, Input } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { RadioAreaGroup as RadioArea } from "@calcom/ui/components/radio";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { CheckIcon, LoaderIcon } from "@coss/ui/icons";
|
||||
|
||||
enum ReassignType {
|
||||
AUTO = "auto",
|
||||
@@ -252,7 +252,9 @@ export const ReassignDialog = ({
|
||||
<strong className="mb-1 block">
|
||||
{isManagedEvent ? t("auto_reassign") : t("round_robin")}
|
||||
</strong>
|
||||
<p>{isManagedEvent ? t("auto_reassign_description") : t("round_robin_reassign_description")}</p>
|
||||
<p>
|
||||
{isManagedEvent ? t("auto_reassign_description") : t("round_robin_reassign_description")}
|
||||
</p>
|
||||
</RadioArea.Item>
|
||||
) : null}
|
||||
<RadioArea.Item
|
||||
@@ -285,7 +287,7 @@ export const ReassignDialog = ({
|
||||
{isFetching && teamMemberOptions.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Icon name="loader" className="text-subtle h-5 w-5 animate-spin" />
|
||||
<LoaderIcon className="text-subtle h-5 w-5 animate-spin" />
|
||||
<p className="text-subtle text-sm">{t("loading")}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -295,42 +297,42 @@ export const ReassignDialog = ({
|
||||
</div>
|
||||
) : (
|
||||
teamMemberOptions.map((member) => (
|
||||
<label
|
||||
key={member.value}
|
||||
tabIndex={watchedTeamMemberId === member.value ? -1 : 0}
|
||||
role="radio"
|
||||
aria-checked={watchedTeamMemberId === member.value}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
form.setValue("teamMemberId", member.value);
|
||||
}
|
||||
}}
|
||||
className={classNames(
|
||||
"hover:bg-subtle focus:bg-subtle focus:ring-emphasis cursor-pointer items-center justify-between gap-0.5 rounded-sm py-2 outline-none focus:ring-2",
|
||||
watchedTeamMemberId === member.value && "bg-subtle"
|
||||
)}>
|
||||
<div className="flex flex-1 items-center space-x-3">
|
||||
<input
|
||||
type="radio"
|
||||
className="hidden"
|
||||
checked={watchedTeamMemberId === member.value}
|
||||
onChange={() => form.setValue("teamMemberId", member.value)}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
"h-3 w-3 shrink-0 rounded-full",
|
||||
member.status === "unavailable" ? "bg-red-500" : "bg-green-500"
|
||||
<label
|
||||
key={member.value}
|
||||
tabIndex={watchedTeamMemberId === member.value ? -1 : 0}
|
||||
role="radio"
|
||||
aria-checked={watchedTeamMemberId === member.value}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
form.setValue("teamMemberId", member.value);
|
||||
}
|
||||
}}
|
||||
className={classNames(
|
||||
"hover:bg-subtle focus:bg-subtle focus:ring-emphasis cursor-pointer items-center justify-between gap-0.5 rounded-sm py-2 outline-none focus:ring-2",
|
||||
watchedTeamMemberId === member.value && "bg-subtle"
|
||||
)}>
|
||||
<div className="flex flex-1 items-center space-x-3">
|
||||
<input
|
||||
type="radio"
|
||||
className="hidden"
|
||||
checked={watchedTeamMemberId === member.value}
|
||||
onChange={() => form.setValue("teamMemberId", member.value)}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
"h-3 w-3 shrink-0 rounded-full",
|
||||
member.status === "unavailable" ? "bg-red-500" : "bg-green-500"
|
||||
)}
|
||||
/>
|
||||
<span className="text-emphasis w-full text-sm">{member.label}</span>
|
||||
{watchedTeamMemberId === member.value && (
|
||||
<div className="place-self-end pr-2">
|
||||
<CheckIcon className="text-emphasis h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<span className="text-emphasis w-full text-sm">{member.label}</span>
|
||||
{watchedTeamMemberId === member.value && (
|
||||
<div className="place-self-end pr-2">
|
||||
<Icon name="check" className="text-emphasis h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</label>
|
||||
))
|
||||
)}
|
||||
{teamMemberOptions.length > 0 && (
|
||||
|
||||
@@ -903,7 +903,7 @@ export const RerouteDialog = ({ isOpenDialog, setIsOpenDialog, booking }: Rerout
|
||||
|
||||
return (
|
||||
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
||||
<DialogContent preventCloseOnOutsideClick enableOverflow>
|
||||
<DialogContent preventCloseOnOutsideClick enableOverflow>
|
||||
<DialogHeader title={t("reroute_booking")} subtitle={t("reroute_booking_description")} />
|
||||
<RerouteDialogContentAndFooter
|
||||
booking={teamEventTypeBooking}
|
||||
|
||||
@@ -7,8 +7,8 @@ import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/components/dialog";
|
||||
import { TextArea } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { ClockIcon } from "@coss/ui/icons";
|
||||
|
||||
interface IRescheduleDialog {
|
||||
isOpenDialog: boolean;
|
||||
@@ -39,7 +39,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
||||
<DialogContent enableOverflow data-testid="reschedule-dialog">
|
||||
<div className="flex flex-row md:space-x-3">
|
||||
<div className="bg-subtle hidden h-10 w-10 shrink-0 justify-center rounded-full md:flex">
|
||||
<Icon name="clock" className="m-auto h-6 w-6" />
|
||||
<ClockIcon className="m-auto h-6 w-6" />
|
||||
</div>
|
||||
<div className="w-full md:pt-1">
|
||||
<DialogHeader title={t("send_reschedule_request")} />
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Dialog } from "@calcom/features/components/controlled-dialog";
|
||||
import { useCopy } from "@calcom/lib/hooks/useCopy";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { AssignmentReasonEnum } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Alert } from "@calcom/ui/components/alert";
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/components/dialog";
|
||||
import { Label, Select, TextArea } from "@calcom/ui/components/form";
|
||||
@@ -12,15 +14,32 @@ import type { Dispatch, SetStateAction } from "react";
|
||||
import type { Control, ControllerRenderProps } from "react-hook-form";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import assignmentReasonBadgeTitleMap from "@lib/booking/assignmentReasonBadgeTitleMap";
|
||||
|
||||
interface BookingData {
|
||||
uid: string;
|
||||
eventType?: {
|
||||
team?: {
|
||||
id: number;
|
||||
} | null;
|
||||
} | null;
|
||||
user?: {
|
||||
email: string;
|
||||
name: string | null;
|
||||
} | null;
|
||||
assignmentReasonSortedByCreatedAt: Array<{
|
||||
reasonString: string | null;
|
||||
reasonEnum: AssignmentReasonEnum | null;
|
||||
}>;
|
||||
attendees: Array<{
|
||||
email: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface IWrongAssignmentDialog {
|
||||
isOpenDialog: boolean;
|
||||
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
|
||||
bookingUid: string;
|
||||
routingReason: string | null;
|
||||
guestEmail: string;
|
||||
hostEmail: string;
|
||||
hostName: string | null;
|
||||
teamId: number | null;
|
||||
booking: BookingData;
|
||||
}
|
||||
|
||||
interface FormValues {
|
||||
@@ -36,6 +55,7 @@ interface TeamMemberOption {
|
||||
|
||||
interface RoutingInfoSectionProps {
|
||||
routingReason: string | null;
|
||||
routingReasonEnum: AssignmentReasonEnum | null;
|
||||
noRoutingReasonText: string;
|
||||
routingReasonLabel: string;
|
||||
guestEmail: string;
|
||||
@@ -48,8 +68,10 @@ interface RoutingInfoSectionProps {
|
||||
}
|
||||
|
||||
function RoutingInfoSection(props: RoutingInfoSectionProps): JSX.Element {
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
routingReason,
|
||||
routingReasonEnum,
|
||||
noRoutingReasonText,
|
||||
routingReasonLabel,
|
||||
guestEmail,
|
||||
@@ -79,9 +101,14 @@ function RoutingInfoSection(props: RoutingInfoSectionProps): JSX.Element {
|
||||
<div className="-mt-2 mb-4 space-y-3">
|
||||
<div>
|
||||
<Label className="text-emphasis mb-1 block text-sm font-medium">{routingReasonLabel}</Label>
|
||||
<p className="text-default bg-muted rounded-md px-3 py-2 text-sm">
|
||||
{routingReason || noRoutingReasonText}
|
||||
</p>
|
||||
<div className="text-default bg-muted flex items-center gap-2 rounded-md px-3 py-2 text-sm">
|
||||
{routingReasonEnum && (
|
||||
<Badge variant="gray" className="shrink-0">
|
||||
{t(assignmentReasonBadgeTitleMap(routingReasonEnum))}
|
||||
</Badge>
|
||||
)}
|
||||
<span className="flex-1">{routingReason || noRoutingReasonText}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -212,16 +239,15 @@ export function WrongAssignmentDialog(props: IWrongAssignmentDialog): JSX.Elemen
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
const { copyToClipboard, isCopied } = useCopy();
|
||||
const {
|
||||
isOpenDialog,
|
||||
setIsOpenDialog,
|
||||
bookingUid,
|
||||
routingReason,
|
||||
guestEmail,
|
||||
hostEmail,
|
||||
hostName,
|
||||
teamId,
|
||||
} = props;
|
||||
const { isOpenDialog, setIsOpenDialog, booking } = props;
|
||||
|
||||
const bookingUid = booking.uid;
|
||||
const teamId = booking.eventType?.team?.id ?? null;
|
||||
const routingReason = booking.assignmentReasonSortedByCreatedAt[0]?.reasonString ?? null;
|
||||
const routingReasonEnum = booking.assignmentReasonSortedByCreatedAt[0]?.reasonEnum ?? null;
|
||||
const guestEmail = booking.attendees[0]?.email ?? "";
|
||||
const hostEmail = booking.user?.email ?? "";
|
||||
const hostName = booking.user?.name ?? null;
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -234,10 +260,8 @@ export function WrongAssignmentDialog(props: IWrongAssignmentDialog): JSX.Elemen
|
||||
},
|
||||
});
|
||||
|
||||
const { data: existingReport, isPending: isCheckingReport } = trpc.viewer.bookings.hasWrongAssignmentReport.useQuery(
|
||||
{ bookingUid },
|
||||
{ enabled: isOpenDialog }
|
||||
);
|
||||
const { data: existingReport, isPending: isCheckingReport } =
|
||||
trpc.viewer.bookings.hasWrongAssignmentReport.useQuery({ bookingUid }, { enabled: isOpenDialog });
|
||||
const alreadyReported = existingReport?.hasReport ?? false;
|
||||
|
||||
const teamIdForQuery = teamId ?? 0;
|
||||
@@ -288,8 +312,9 @@ export function WrongAssignmentDialog(props: IWrongAssignmentDialog): JSX.Elemen
|
||||
|
||||
<RoutingInfoSection
|
||||
routingReason={routingReason}
|
||||
routingReasonEnum={routingReasonEnum}
|
||||
noRoutingReasonText={t("no_routing_reason")}
|
||||
routingReasonLabel={t("routing_reason")}
|
||||
routingReasonLabel={t("first_assignment_reason")}
|
||||
guestEmail={guestEmail}
|
||||
whoBookedItLabel={t("who_booked_it")}
|
||||
hostEmail={hostEmail}
|
||||
@@ -329,7 +354,11 @@ export function WrongAssignmentDialog(props: IWrongAssignmentDialog): JSX.Elemen
|
||||
<Button type="button" color="secondary" onClick={handleCloseClick} disabled={isPending}>
|
||||
{t("close")}
|
||||
</Button>
|
||||
<Button type="submit" color="primary" disabled={isPending || alreadyReported || isCheckingReport} loading={isPending || isCheckingReport}>
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
disabled={isPending || alreadyReported || isCheckingReport}
|
||||
loading={isPending || isCheckingReport}>
|
||||
{t("submit")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -10,8 +10,8 @@ import { trpc } from "@calcom/trpc/react";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { ArrowRightIcon, CircleAlertIcon } from "@coss/ui/icons";
|
||||
|
||||
interface IAppConnectionItem {
|
||||
title: string;
|
||||
@@ -79,7 +79,7 @@ const AppConnectionItem = (props: IAppConnectionItem) => {
|
||||
<div className="items-start space-x-2.5">
|
||||
<div className="flex items-start">
|
||||
<div>
|
||||
<Icon name="circle-alert" className="mr-2 mt-1 font-semibold" />
|
||||
<CircleAlertIcon className="mr-2 mt-1 font-semibold" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs font-semibold">
|
||||
@@ -99,7 +99,7 @@ const AppConnectionItem = (props: IAppConnectionItem) => {
|
||||
<span className="mr-1">
|
||||
{t("connect_app", { dependencyName: dependency.name })}
|
||||
</span>
|
||||
<Icon name="arrow-right" className="inline-block h-3 w-3" />
|
||||
<ArrowRightIcon className="inline-block h-3 w-3" />
|
||||
</Link>
|
||||
</>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,7 @@ const EventReservationSchema: FC<EventReservationSchemaInterface> = ({
|
||||
? ({ "@type": "Person", name: organizer.name, email: organizer.email } as Person)
|
||||
: undefined,
|
||||
attendee: attendees?.map(
|
||||
(person) => ({ "@type": "Person", name: person.name, email: person.email } as Person)
|
||||
(person) => ({ "@type": "Person", name: person.name, email: person.email }) as Person
|
||||
),
|
||||
location: location || undefined,
|
||||
description: description || undefined,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { ShieldIcon } from "@coss/ui/icons";
|
||||
|
||||
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
|
||||
return (
|
||||
<div className="mb-4 sm:flex sm:items-start">
|
||||
<div className="bg-brand/5 text-brandcontrast dark:bg-darkmodebrand/5 dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
||||
<Icon name="shield" className="text-inverted h-6 w-6" />
|
||||
<ShieldIcon 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">
|
||||
|
||||
@@ -5,8 +5,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants/permissions";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Label, TextField } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
import { InfoIcon } from "@coss/ui/icons";
|
||||
|
||||
type OAuthClientFormProps = {
|
||||
defaultValues?: Partial<FormValues>;
|
||||
@@ -227,7 +227,7 @@ export const OAuthClientForm = ({
|
||||
className="max-w-[400px] whitespace-normal"
|
||||
content="If enabled and the managed user has calendar connected, an event in the calendar will be created. By default true. Disable it if you want to create events in the calendar manually.">
|
||||
<div className="ml-1">
|
||||
<Icon name="info" className="h-4 w-4 text-gray-500" aria-hidden="true" />
|
||||
<InfoIcon className="h-4 w-4 text-gray-500" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -252,7 +252,7 @@ export const OAuthClientForm = ({
|
||||
className="max-w-[400px] whitespace-normal"
|
||||
content="If enabled, when creating a managed user the managed user will have 4 default event types: 30 and 60 minutes without Cal video, 30 and 60 minutes with Cal video. Leave this disabled if you want to create a managed user and then manually create event types for the user.">
|
||||
<div className="ml-1">
|
||||
<Icon name="info" className="h-4 w-4 text-gray-500" aria-hidden="true" />
|
||||
<InfoIcon className="h-4 w-4 text-gray-500" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user