Merge branch 'main' into feat/prevent-duplicate-bookings
This commit is contained in:
commit
829bc32c53
|
@ -133,6 +133,11 @@ NEXT_PUBLIC_SENDGRID_SENDER_NAME=
|
|||
# Used for capturing exceptions and logging messages
|
||||
NEXT_PUBLIC_SENTRY_DSN=
|
||||
|
||||
# Formbricks Experience Management Integration
|
||||
FORMBRICKS_HOST_URL=https://app.formbricks.com
|
||||
FORMBRICKS_ENVIRONMENT_ID=
|
||||
FORMBRICKS_FEEDBACK_SURVEY_ID=
|
||||
|
||||
# Twilio
|
||||
# Used to send SMS reminders in workflows
|
||||
TWILIO_SID=
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
name: "Apply issue labels to PR"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
label_on_pr:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: none
|
||||
issues: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Apply labels from linked issue to PR
|
||||
uses: actions/github-script@v5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
async function getLinkedIssues(owner, repo, prNumber) {
|
||||
const query = `query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $prNumber) {
|
||||
closingIssuesReferences(first: 10) {
|
||||
nodes {
|
||||
number
|
||||
labels(first: 10) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const variables = {
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
prNumber: prNumber,
|
||||
};
|
||||
|
||||
const result = await github.graphql(query, variables);
|
||||
return result.repository.pullRequest.closingIssuesReferences.nodes;
|
||||
}
|
||||
|
||||
const pr = context.payload.pull_request;
|
||||
const linkedIssues = await getLinkedIssues(
|
||||
context.repo.owner,
|
||||
context.repo.repo,
|
||||
pr.number
|
||||
);
|
||||
|
||||
const labelsToAdd = new Set();
|
||||
for (const issue of linkedIssues) {
|
||||
if (issue.labels && issue.labels.nodes) {
|
||||
for (const label of issue.labels.nodes) {
|
||||
labelsToAdd.add(label.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (labelsToAdd.size) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
labels: Array.from(labelsToAdd),
|
||||
});
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
name: "Pull Request Labeler"
|
||||
on:
|
||||
- pull_request_target
|
||||
- workflow_call
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
@ -16,3 +17,81 @@ jobs:
|
|||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
|
||||
sync-labels: ""
|
||||
team-labels:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: equitybee/team-label-action@main
|
||||
with:
|
||||
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
||||
organization-name: calcom
|
||||
ignore-labels: "app-store, ai, authentication, automated-testing, platform, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
|
||||
apply-labels-from-issue:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: none
|
||||
issues: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Apply labels from linked issue to PR
|
||||
uses: actions/github-script@v5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
async function getLinkedIssues(owner, repo, prNumber) {
|
||||
const query = `query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $prNumber) {
|
||||
closingIssuesReferences(first: 10) {
|
||||
nodes {
|
||||
number
|
||||
labels(first: 10) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const variables = {
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
prNumber: prNumber,
|
||||
};
|
||||
|
||||
const result = await github.graphql(query, variables);
|
||||
return result.repository.pullRequest.closingIssuesReferences.nodes;
|
||||
}
|
||||
|
||||
const pr = context.payload.pull_request;
|
||||
const linkedIssues = await getLinkedIssues(
|
||||
context.repo.owner,
|
||||
context.repo.repo,
|
||||
pr.number
|
||||
);
|
||||
|
||||
const labelsToAdd = new Set();
|
||||
for (const issue of linkedIssues) {
|
||||
if (issue.labels && issue.labels.nodes) {
|
||||
for (const label of issue.labels.nodes) {
|
||||
labelsToAdd.add(label.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (labelsToAdd.size) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
labels: Array.from(labelsToAdd),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
name: Assign PR team labels
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
team-labels:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: equitybee/team-label-action@main
|
||||
with:
|
||||
repo-token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
organization-name: calcom
|
||||
ignore-labels: "app-store, ai, authentication, automated-testing, platform, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
|
|
@ -4,9 +4,6 @@ on:
|
|||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- ".github/CODEOWNERS"
|
||||
merge_group:
|
||||
workflow_dispatch:
|
||||
|
||||
|
@ -15,36 +12,62 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect changes
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
permissions:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
has-files-requiring-all-checks: ${{ steps.filter.outputs.has-files-requiring-all-checks }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
has-files-requiring-all-checks:
|
||||
- "!(**.md|.github/CODEOWNERS)"
|
||||
type-check:
|
||||
name: Type check
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/check-types.yml
|
||||
secrets: inherit
|
||||
|
||||
test:
|
||||
name: Unit tests
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
lint:
|
||||
name: Linters
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/lint.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
name: Production build
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
analyze:
|
||||
needs: build
|
||||
name: Analyze Build
|
||||
needs: [changes, build]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs: [lint, type-check, test, build]
|
||||
needs: [changes, lint, type-check, test, build]
|
||||
if: always()
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: fail if conditional jobs failed
|
||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
||||
if: needs.changes.outputs.has-files-requiring-all-checks == 'true' && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled'))
|
||||
run: exit 1
|
||||
|
|
|
@ -48,17 +48,21 @@ Here is the full architecture:
|
|||
|
||||
### Email Router
|
||||
|
||||
To expose the AI app, run `ngrok http 3005` (or the AI app's port number) in a new terminal. You may need to install [nGrok](https://ngrok.com/).
|
||||
To expose the AI app, you can use either [Tunnelmole](https://github.com/robbie-cahill/tunnelmole-client), an open source tunnelling tool; or [nGrok](https://ngrok.com/), a popular closed source tunnelling tool.
|
||||
|
||||
For Tunnelmole, run `tmole 3005` (or the AI app's port number) in a new terminal. Please replace `3005` with the port number if it is different. In the output, you'll see two URLs, one http and a https (we recommend using the https url for privacy and security). To install Tunnelmole, use `curl -O https://install.tunnelmole.com/8dPBw/install && sudo bash install`. (On Windows, download [tmole.exe](https://tunnelmole.com/downloads/tmole.exe))
|
||||
|
||||
For nGrok, run `ngrok http 3005` (or the AI app's port number) in a new terminal. You may need to install nGrok first.
|
||||
|
||||
To forward incoming emails to the serverless function at `/agent`, we use [SendGrid's Inbound Parse](https://docs.sendgrid.com/for-developers/parsing-email/setting-up-the-inbound-parse-webhook).
|
||||
|
||||
1. Ensure you have a [SendGrid account](https://signup.sendgrid.com/)
|
||||
2. Ensure you have an authenticated domain. Go to Settings > Sender Authentication > Authenticate. For DNS host, select `I'm not sure`. Click Next and add your domain, eg. `example.com`. Choose Manual Setup. You'll be given three CNAME records to add to your DNS settings, eg. in [Vercel Domains](https://vercel.com/dashboard/domains). After adding those records, click Verify. To troubleshoot, see the [full instructions](https://docs.sendgrid.com/ui/account-and-settings/how-to-set-up-domain-authentication).
|
||||
3. Authorize your domain for email with MX records: one with name `[your domain].com` and value `mx.sendgrid.net.`, and another with name `bounces.[your domain].com` and value `feedback-smtp.us-east-1.amazonses.com`, both with priority `10` if prompted.
|
||||
3. Authorize your domain for email with MX records: one with name `[your domain].com` and value `mx.sendgrid.net.`, and another with name `bounces.[your domain].com` and value `feedback-smtp.us-east-1.amazonses.com`. Set the priority to `10` if prompted.
|
||||
4. Go to Settings > [Inbound Parse](https://app.sendgrid.com/settings/parse) > Add Host & URL. Choose your authenticated domain.
|
||||
5. In the Destination URL field, use the nGrok URL from above along with the path, `/api/receive`, and one param, `parseKey`, which lives in [this app's .env](/apps/ai/.env.example) under `PARSE_KEY`. The full URL should look like `https://abc.ngrok.io/api/receive?parseKey=ABC-123`.
|
||||
5. In the Destination URL field, use the Tunnelmole or ngrok URL from above along with the path, `/api/receive`, and one param, `parseKey`, which lives in [this app's .env](/apps/ai/.env.example) under `PARSE_KEY`. The full URL should look like `https://abc.tunnelmole.net/api/receive?parseKey=ABC-123` or `https://abc.ngrok.io/api/receive?parseKey=ABC-123`.
|
||||
6. Activate "POST the raw, full MIME message".
|
||||
7. Send an email to `[anyUsername]@example.com`. You should see a ping on the nGrok listener and server.
|
||||
7. Send an email to `[anyUsername]@example.com`. You should see a ping on the Tunnelmole or ngrok listener and server.
|
||||
8. Adjust the logic in [receive/route.ts](/apps/ai/src/app/api/receive/route.ts), save to hot-reload, and send another email to test the behaviour.
|
||||
|
||||
Please feel free to improve any part of this architecture!
|
||||
|
|
|
@ -84,7 +84,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|||
`}</style>
|
||||
</head>
|
||||
<body
|
||||
className="dark:bg-darkgray-50 desktop-transparent bg-subtle antialiased"
|
||||
className="dark:bg-darkgray-50 todesktop:!bg-transparent bg-subtle antialiased"
|
||||
style={
|
||||
isEmbed
|
||||
? {
|
||||
|
|
|
@ -26,7 +26,7 @@ export default function AdminLayout({
|
|||
const isAppsPage = pathname?.startsWith("/settings/admin/apps");
|
||||
return (
|
||||
<SettingsLayout {...rest}>
|
||||
<div className="divide-subtle mx-auto flex max-w-4xl flex-row divide-y">
|
||||
<div className="divide-subtle bg-default mx-auto flex max-w-4xl flex-row divide-y">
|
||||
<div className={isAppsPage ? "min-w-0" : "flex flex-1 [&>*]:flex-1"}>
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
</div>
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
"@calcom/tsconfig": "*",
|
||||
"@calcom/ui": "*",
|
||||
"@daily-co/daily-js": "^0.37.0",
|
||||
"@formkit/auto-animate": "^1.0.0-beta.5",
|
||||
"@formkit/auto-animate": "^0.8.1",
|
||||
"@glidejs/glide": "^3.5.2",
|
||||
"@hookform/error-message": "^2.0.0",
|
||||
"@hookform/resolvers": "^2.9.7",
|
||||
|
|
|
@ -87,7 +87,7 @@ class MyDocument extends Document<Props> {
|
|||
</Head>
|
||||
|
||||
<body
|
||||
className="dark:bg-darkgray-50 desktop-transparent bg-subtle antialiased"
|
||||
className="dark:bg-darkgray-50 todesktop:!bg-transparent bg-subtle antialiased"
|
||||
style={
|
||||
isEmbed
|
||||
? {
|
||||
|
|
|
@ -21,16 +21,16 @@ const schema = z
|
|||
id: z.string(),
|
||||
payload: z.object({
|
||||
recording_id: z.string(),
|
||||
end_ts: z.number(),
|
||||
end_ts: z.number().optional(),
|
||||
room_name: z.string(),
|
||||
start_ts: z.number(),
|
||||
start_ts: z.number().optional(),
|
||||
status: z.string(),
|
||||
|
||||
max_participants: z.number(),
|
||||
duration: z.number(),
|
||||
s3_key: z.string(),
|
||||
max_participants: z.number().optional(),
|
||||
duration: z.number().optional(),
|
||||
s3_key: z.string().optional(),
|
||||
}),
|
||||
event_ts: z.number(),
|
||||
event_ts: z.number().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
|
|
|
@ -522,6 +522,32 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
const [slugExistsChildrenDialogOpen, setSlugExistsChildrenDialogOpen] = useState<ChildrenEventType[]>([]);
|
||||
const slug = formMethods.watch("slug") ?? eventType.slug;
|
||||
|
||||
// Optional prerender all tabs after 300 ms on mount
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
const Components = [
|
||||
EventSetupTab,
|
||||
EventAvailabilityTab,
|
||||
EventTeamTab,
|
||||
EventLimitsTab,
|
||||
EventAdvancedTab,
|
||||
EventInstantTab,
|
||||
EventRecurringTab,
|
||||
EventAppsTab,
|
||||
EventWorkflowsTab,
|
||||
EventWebhooksTab,
|
||||
];
|
||||
|
||||
Components.forEach((C) => {
|
||||
// @ts-expect-error Property 'render' does not exist on type 'ComponentClass
|
||||
C.render.preload();
|
||||
});
|
||||
}, 300);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<EventTypeSingleLayout
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable playwright/no-conditional-in-test */
|
||||
import { loginUser } from "../fixtures/regularBookings";
|
||||
import { test } from "../lib/fixtures";
|
||||
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
test.describe("Booking with recurring checked", () => {
|
||||
test.beforeEach(async ({ page, users, bookingPage }) => {
|
||||
await loginUser(users);
|
||||
await page.goto("/event-types");
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("recurring");
|
||||
});
|
||||
|
||||
test("Updates event type with recurring events", async ({ page, bookingPage }) => {
|
||||
await bookingPage.updateRecurringTab("2", "3");
|
||||
await bookingPage.updateEventType();
|
||||
await page.getByRole("link", { name: "Event Types" }).click();
|
||||
await bookingPage.assertRepeatEventType();
|
||||
});
|
||||
|
||||
test("Updates and shows recurring schedule correctly in booking page", async ({ bookingPage }) => {
|
||||
await bookingPage.updateRecurringTab("2", "3");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.fillRecurringFieldAndConfirm(eventTypePage);
|
||||
});
|
||||
});
|
|
@ -2,6 +2,7 @@ import { expect, type Page } from "@playwright/test";
|
|||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
||||
import { localize } from "../lib/testUtils";
|
||||
import type { createUsersFixture } from "./users";
|
||||
|
||||
const reschedulePlaceholderText = "Let others know why you need to reschedule";
|
||||
|
@ -220,6 +221,23 @@ export function createBookingPageFixture(page: Page) {
|
|||
}
|
||||
await page.getByTestId("field-add-save").click();
|
||||
},
|
||||
updateRecurringTab: async (repeatWeek: string, maxEvents: string) => {
|
||||
const repeatText = (await localize("en"))("repeats_every");
|
||||
const maximumOf = (await localize("en"))("for_a_maximum_of");
|
||||
await page.getByTestId("recurring-event-check").click();
|
||||
await page
|
||||
.getByTestId("recurring-event-collapsible")
|
||||
.locator("div")
|
||||
.filter({ hasText: repeatText })
|
||||
.getByRole("spinbutton")
|
||||
.fill(repeatWeek);
|
||||
await page
|
||||
.getByTestId("recurring-event-collapsible")
|
||||
.locator("div")
|
||||
.filter({ hasText: maximumOf })
|
||||
.getByRole("spinbutton")
|
||||
.fill(maxEvents);
|
||||
},
|
||||
updateEventType: async () => {
|
||||
await page.getByTestId("update-eventtype").click();
|
||||
},
|
||||
|
@ -246,6 +264,14 @@ export function createBookingPageFixture(page: Page) {
|
|||
await page.getByTestId("confirm-reschedule-button").click();
|
||||
},
|
||||
|
||||
fillRecurringFieldAndConfirm: async (eventTypePage: Page) => {
|
||||
await eventTypePage.getByTestId("occurrence-input").click();
|
||||
await eventTypePage.getByTestId("occurrence-input").fill("2");
|
||||
await goToNextMonthIfNoAvailabilities(eventTypePage);
|
||||
await eventTypePage.getByTestId("time").first().click();
|
||||
await expect(eventTypePage.getByTestId("recurring-dates")).toBeVisible();
|
||||
},
|
||||
|
||||
cancelBookingWithReason: async (page: Page) => {
|
||||
await page.getByTestId("cancel").click();
|
||||
await page.getByTestId("cancel_reason").fill("Test cancel");
|
||||
|
@ -279,6 +305,10 @@ export function createBookingPageFixture(page: Page) {
|
|||
await expect(page.getByText(scheduleSuccessfullyText)).toBeVisible();
|
||||
},
|
||||
|
||||
assertRepeatEventType: async () => {
|
||||
await expect(page.getByTestId("repeat-eventtype")).toBeVisible();
|
||||
},
|
||||
|
||||
cancelBooking: async (eventTypePage: Page) => {
|
||||
await eventTypePage.getByTestId("cancel").click();
|
||||
await eventTypePage.getByTestId("cancel_reason").fill("Test cancel");
|
||||
|
|
|
@ -142,43 +142,11 @@ html.todesktop div {
|
|||
html.todesktop header {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
html.todesktop header button {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
html.todesktop .logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.desktop-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html.todesktop .desktop-only {
|
||||
display: block;
|
||||
}
|
||||
|
||||
html.todesktop .desktop-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html.todesktop header {
|
||||
margin-top: -14px;
|
||||
}
|
||||
|
||||
html.todesktop header nav {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
html.todesktop aside {
|
||||
margin: 0px -6px;
|
||||
}
|
||||
|
||||
html.todesktop-platform-darwin .desktop-transparent {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
html.todesktop-platform-darwin.dark main.bg-default {
|
||||
background: rgba(0, 0, 0, 0.6) !important;
|
||||
}
|
||||
|
@ -187,26 +155,14 @@ html.todesktop-platform-darwin.light main.bg-default {
|
|||
background: rgba(255, 255, 255, 0.8) !important;
|
||||
}
|
||||
|
||||
/*
|
||||
html.todesktop aside a {
|
||||
height: 28px;
|
||||
padding: 0px 8px;
|
||||
font-size: 12px;
|
||||
color: #383438 !important
|
||||
html.todesktop.light {
|
||||
--cal-bg-emphasis: hsla(0, 0%, 11%, 0.1);
|
||||
}
|
||||
|
||||
html.todesktop nav a:hover{
|
||||
background-color: inherit !important
|
||||
html.todesktop.dark {
|
||||
--cal-bg-emphasis: hsla(220, 2%, 26%, 0.3);
|
||||
}
|
||||
|
||||
html.todesktop nav a[aria-current="page"]{
|
||||
background: rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
html.todesktop nav a svg{
|
||||
color: #0272F7 !important
|
||||
} */
|
||||
|
||||
/*
|
||||
Adds Utility to hide scrollbar to tailwind
|
||||
https://github.com/tailwindlabs/tailwindcss/discussions/2394
|
||||
|
|
|
@ -8,7 +8,7 @@ import { expect, vi } from "vitest";
|
|||
import "vitest-fetch-mock";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
@ -382,7 +382,7 @@ export function expectSuccessfulBookingCreationEmails({
|
|||
bookingTimeRange?: { start: Date; end: Date };
|
||||
booking: { uid: string; urlOrigin?: string };
|
||||
}) {
|
||||
const bookingUrlOrigin = booking.urlOrigin || WEBAPP_URL;
|
||||
const bookingUrlOrigin = booking.urlOrigin || CAL_URL;
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
titleTag: "confirmed_event_type_subject",
|
||||
|
@ -742,7 +742,7 @@ export function expectBookingRequestRescheduledEmails({
|
|||
booking: { uid: string; urlOrigin?: string };
|
||||
bookNewTimePath: string;
|
||||
}) {
|
||||
const bookingUrlOrigin = booking.urlOrigin || WEBAPP_URL;
|
||||
const bookingUrlOrigin = booking.urlOrigin || CAL_URL;
|
||||
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@calcom/eslint-plugin-eslint": "*",
|
||||
"@todesktop/tailwind-variants": "^1.0.0",
|
||||
"eslint-config-next": "^13.2.1",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-config-turbo": "^0.0.7",
|
||||
|
|
|
@ -150,6 +150,7 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
plugins: [
|
||||
require("@todesktop/tailwind-variants"),
|
||||
require("@tailwindcss/forms"),
|
||||
require("@tailwindcss/typography"),
|
||||
require("tailwind-scrollbar")({ nocompatible: true }),
|
||||
|
|
|
@ -338,12 +338,11 @@ export const BookEventFormChild = ({
|
|||
|
||||
// Ensures that duration is an allowed value, if not it defaults to the
|
||||
// default eventQuery duration.
|
||||
const validDuration =
|
||||
duration &&
|
||||
eventQuery.data.metadata?.multipleDuration &&
|
||||
eventQuery.data.metadata?.multipleDuration.includes(duration)
|
||||
? duration
|
||||
: eventQuery.data.length;
|
||||
const validDuration = eventQuery.data.isDynamic
|
||||
? duration || eventQuery.data.length
|
||||
: duration && eventQuery.data.metadata?.multipleDuration?.includes(duration)
|
||||
? duration
|
||||
: eventQuery.data.length;
|
||||
|
||||
const bookingInput = {
|
||||
values,
|
||||
|
|
|
@ -47,7 +47,7 @@ export const EventOccurences = ({ event }: { event: PublicEvent }) => {
|
|||
i18n.language
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div data-testid="recurring-dates">
|
||||
{recurringStrings.slice(0, 5).map((timeFormatted, key) => (
|
||||
<p key={key}>{timeFormatted}</p>
|
||||
))}
|
||||
|
@ -59,7 +59,7 @@ export const EventOccurences = ({ event }: { event: PublicEvent }) => {
|
|||
<p className=" text-sm">+ {t("plus_more", { count: recurringStrings.length - 5 })}</p>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -73,6 +73,7 @@ export const EventOccurences = ({ event }: { event: PublicEvent }) => {
|
|||
min="1"
|
||||
max={event.recurringEvent.count}
|
||||
defaultValue={occurenceCount || event.recurringEvent.count}
|
||||
data-testid="occurrence-input"
|
||||
onChange={(event) => {
|
||||
const pattern = /^(?=.*[0-9])\S+$/;
|
||||
const inputValue = parseInt(event.target.value);
|
||||
|
|
|
@ -9,6 +9,7 @@ import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
|||
import { scheduleTrigger } from "@calcom/features/webhooks/lib/scheduleTrigger";
|
||||
import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
|
||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
|
@ -138,6 +139,7 @@ export async function handleConfirmation(args: {
|
|||
}[] = [];
|
||||
|
||||
const videoCallUrl = metadata.hangoutLink ? metadata.hangoutLink : evt.videoCallData?.url || "";
|
||||
const meetingUrl = getVideoCallUrlFromCalEvent(evt) || videoCallUrl;
|
||||
|
||||
if (recurringEventId) {
|
||||
// The booking to confirm is a recurring event and comes from /booking/recurring, proceeding to mark all related
|
||||
|
@ -162,7 +164,7 @@ export async function handleConfirmation(args: {
|
|||
paid,
|
||||
metadata: {
|
||||
...(typeof recurringBooking.metadata === "object" ? recurringBooking.metadata : {}),
|
||||
videoCallUrl,
|
||||
videoCallUrl: meetingUrl,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
|
@ -215,7 +217,10 @@ export async function handleConfirmation(args: {
|
|||
references: {
|
||||
create: scheduleResult.referencesToCreate,
|
||||
},
|
||||
metadata: { ...(typeof booking.metadata === "object" ? booking.metadata : {}), videoCallUrl },
|
||||
metadata: {
|
||||
...(typeof booking.metadata === "object" ? booking.metadata : {}),
|
||||
videoCallUrl: meetingUrl,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
eventType: {
|
||||
|
@ -258,7 +263,11 @@ export async function handleConfirmation(args: {
|
|||
try {
|
||||
for (let index = 0; index < updatedBookings.length; index++) {
|
||||
const eventTypeSlug = updatedBookings[index].eventType?.slug || "";
|
||||
const evtOfBooking = { ...evt, metadata: { videoCallUrl }, eventType: { slug: eventTypeSlug } };
|
||||
const evtOfBooking = {
|
||||
...evt,
|
||||
metadata: { videoCallUrl: meetingUrl },
|
||||
eventType: { slug: eventTypeSlug },
|
||||
};
|
||||
evtOfBooking.startTime = updatedBookings[index].startTime.toISOString();
|
||||
evtOfBooking.endTime = updatedBookings[index].endTime.toISOString();
|
||||
evtOfBooking.uid = updatedBookings[index].uid;
|
||||
|
@ -341,7 +350,7 @@ export async function handleConfirmation(args: {
|
|||
eventTypeId: booking.eventType?.id,
|
||||
status: "ACCEPTED",
|
||||
smsReminderNumber: booking.smsReminderNumber || undefined,
|
||||
metadata: evt.videoCallData?.url ? { videoCallUrl: evt.videoCallData.url } : undefined,
|
||||
metadata: meetingUrl ? { videoCallUrl: meetingUrl } : undefined,
|
||||
}).catch((e) => {
|
||||
console.error(
|
||||
`Error executing webhook for event: ${WebhookTriggerEvents.BOOKING_CREATED}, URL: ${sub.subscriberUrl}`,
|
||||
|
|
|
@ -12,7 +12,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
import { describe, expect } from "vitest";
|
||||
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { resetTestEmails } from "@calcom/lib/testEmails";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
@ -218,7 +218,7 @@ describe("handleNewBooking", () => {
|
|||
expectSuccessfulBookingCreationEmails({
|
||||
booking: {
|
||||
uid: createdBooking.uid!,
|
||||
urlOrigin: org ? org.urlOrigin : WEBAPP_URL,
|
||||
urlOrigin: org ? org.urlOrigin : CAL_URL,
|
||||
},
|
||||
booker,
|
||||
organizer,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { v4 as uuidv4 } from "uuid";
|
||||
import { describe, expect } from "vitest";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL, CAL_URL } from "@calcom/lib/constants";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
@ -209,7 +209,7 @@ describe("handleNewBooking", () => {
|
|||
booking: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
uid: createdBookings[0].uid!,
|
||||
urlOrigin: WEBAPP_URL,
|
||||
urlOrigin: CAL_URL,
|
||||
},
|
||||
organizer,
|
||||
emails,
|
||||
|
@ -555,7 +555,7 @@ describe("handleNewBooking", () => {
|
|||
booking: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
uid: createdBookings[0].uid!,
|
||||
urlOrigin: WEBAPP_URL,
|
||||
urlOrigin: CAL_URL,
|
||||
},
|
||||
organizer,
|
||||
emails,
|
||||
|
@ -769,7 +769,7 @@ describe("handleNewBooking", () => {
|
|||
booking: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
uid: createdBookings[0].uid!,
|
||||
urlOrigin: WEBAPP_URL,
|
||||
urlOrigin: CAL_URL,
|
||||
},
|
||||
booker,
|
||||
organizer,
|
||||
|
|
|
@ -3,7 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
import { describe, expect } from "vitest";
|
||||
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
@ -1279,7 +1279,7 @@ describe("handleNewBooking", () => {
|
|||
booking: {
|
||||
uid: createdBooking.uid!,
|
||||
// All booking links are of WEBAPP_URL and not of the org because the team isn't part of the org
|
||||
urlOrigin: WEBAPP_URL,
|
||||
urlOrigin: CAL_URL,
|
||||
},
|
||||
booker,
|
||||
organizer,
|
||||
|
|
|
@ -37,7 +37,35 @@ Browsers do not allow camera/mic access on any non-HTTPS hosts except for localh
|
|||
|
||||
For eg:- Use `http://localhost:3000/video/nAjnkjejuzis99NhN72rGt` instead of `http://app.cal.local:3000/video/nAjnkjejuzis99NhN72rGt`.
|
||||
|
||||
You can also use `ngrok` or you can generate SSL certificate for your local domain using `mkcert`.
|
||||
To get an HTTPS URL for localhost, you can use a tunneling tool such as `ngrok` or [Tunnelmole](https://github.com/robbie-cahill/tunnelmole-client) . Alternatively, you can generate an SSL certificate for your local domain using `mkcert`. Turn off any SSL certificate validation in your HTTPS client (be sure to do this for local only, otherwise its a security risk).
|
||||
|
||||
#### Tunnelmole - Open Source Tunnelling Tool:
|
||||
|
||||
To install Tunnelmole, execute the command:
|
||||
|
||||
```
|
||||
curl -O https://install.tunnelmole.com/8dPBw/install && sudo bash install
|
||||
```
|
||||
|
||||
After a successful installation, you can run Tunnelmole using the following command, replacing `8000` with your actual port number if it is different:
|
||||
|
||||
```
|
||||
tmole 8000
|
||||
```
|
||||
|
||||
In the output, you'll see two URLs, one HTTP and an HTTPS URL. For privacy and security reasons, it is recommended to use the HTTPS URL.
|
||||
|
||||
View the Tunnelmole [README](https://github.com/robbie-cahill/tunnelmole-client) for additional information and other installation methods such as `npm` or building your own binaries from source.
|
||||
|
||||
#### ngrok - Closed Source Tunnelling Tool:
|
||||
|
||||
ngrok is a popular closed source tunneling tool. You can run ngrok using the same port, using the format `ngrok http <port>` replacing `<port>` with your actual port number. For example:
|
||||
|
||||
```
|
||||
ngrok http 8000
|
||||
```
|
||||
|
||||
This will generate a public URL that you can use to access your localhost server.
|
||||
|
||||
|
||||
## DNS setup
|
||||
|
|
|
@ -86,7 +86,7 @@ export const EventTypeDescription = ({
|
|||
</Badge>
|
||||
)}
|
||||
{recurringEvent?.count && recurringEvent.count > 0 && (
|
||||
<li className="hidden xl:block">
|
||||
<li className="hidden xl:block" data-testid="repeat-eventtype">
|
||||
<Badge variant="gray" startIcon={RefreshCw}>
|
||||
{t("repeats_up_to", {
|
||||
count: recurringEvent.count,
|
||||
|
|
|
@ -273,7 +273,7 @@ export const KBarTrigger = () => {
|
|||
<button
|
||||
color="minimal"
|
||||
onClick={query.toggle}
|
||||
className="text-default hover:bg-subtle lg:hover:bg-emphasis lg:hover:text-emphasis group flex rounded-md px-3 py-2 text-sm font-medium transition lg:px-2">
|
||||
className="text-default hover:bg-subtle todesktop:hover:!bg-transparent lg:hover:bg-emphasis lg:hover:text-emphasis group flex rounded-md px-3 py-2 text-sm font-medium transition lg:px-2">
|
||||
<Search className="h-4 w-4 flex-shrink-0 text-inherit" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
|
|
@ -103,7 +103,7 @@ const tabs: VerticalTabItemProps[] = [
|
|||
},
|
||||
{
|
||||
name: "teams",
|
||||
href: "/settings/teams",
|
||||
href: "/teams",
|
||||
icon: Users,
|
||||
children: [],
|
||||
},
|
||||
|
@ -175,7 +175,7 @@ const BackButtonInSidebar = ({ name }: { name: string }) => {
|
|||
return (
|
||||
<Link
|
||||
href="/"
|
||||
className="hover:bg-subtle [&[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"
|
||||
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"
|
||||
data-testid={`vertical-tab-${name}`}>
|
||||
<ArrowLeft 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="max-w-36 min-h-4 truncate" loadingClassName="ms-3">
|
||||
|
|
|
@ -105,7 +105,7 @@ const tabs: VerticalTabItemProps[] = [
|
|||
},
|
||||
{
|
||||
name: "teams",
|
||||
href: "/settings/teams",
|
||||
href: "/teams",
|
||||
icon: Users,
|
||||
children: [],
|
||||
},
|
||||
|
|
|
@ -421,7 +421,7 @@ function UserDropdown({ small }: UserDropdownProps) {
|
|||
<DropdownMenuTrigger asChild onClick={() => setMenuOpen((menuOpen) => !menuOpen)}>
|
||||
<button
|
||||
className={classNames(
|
||||
"hover:bg-emphasis group mx-0 flex cursor-pointer appearance-none items-center rounded-full text-left outline-none transition focus:outline-none focus:ring-0 md:rounded-none lg:rounded",
|
||||
"hover:bg-emphasis todesktop:!bg-transparent group mx-0 flex w-full cursor-pointer appearance-none items-center rounded-full text-left outline-none transition focus:outline-none focus:ring-0 md:rounded-none lg:rounded",
|
||||
small ? "p-2" : "px-2 py-1.5"
|
||||
)}>
|
||||
<span
|
||||
|
@ -525,7 +525,7 @@ function UserDropdown({ small }: UserDropdownProps) {
|
|||
{t("help")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="desktop-hidden hidden lg:flex">
|
||||
<DropdownMenuItem className="todesktop:hidden hidden lg:flex">
|
||||
<DropdownItem StartIcon={Download} target="_blank" rel="noreferrer" href={DESKTOP_APP_LINK}>
|
||||
{t("download_desktop_app")}
|
||||
</DropdownItem>
|
||||
|
@ -717,19 +717,21 @@ const NavigationItem: React.FC<{
|
|||
href={item.href}
|
||||
aria-label={t(item.name)}
|
||||
className={classNames(
|
||||
"text-default group flex items-center rounded-md px-2 py-1.5 text-sm font-medium transition",
|
||||
item.child ? `[&[aria-current='page']]:bg-transparent` : `[&[aria-current='page']]:bg-emphasis`,
|
||||
"todesktop:py-[7px] text-default group flex items-center rounded-md px-2 py-1.5 text-sm font-medium transition",
|
||||
item.child ? `[&[aria-current='page']]:!bg-transparent` : `[&[aria-current='page']]:bg-emphasis`,
|
||||
isChild
|
||||
? `[&[aria-current='page']]:text-emphasis [&[aria-current='page']]:bg-emphasis hidden h-8 pl-16 lg:flex lg:pl-11 ${
|
||||
props.index === 0 ? "mt-0" : "mt-px"
|
||||
}`
|
||||
: "[&[aria-current='page']]:text-emphasis mt-0.5 text-sm",
|
||||
isLocaleReady ? "hover:bg-subtle hover:text-emphasis" : ""
|
||||
isLocaleReady
|
||||
? "hover:bg-subtle todesktop:[&[aria-current='page']]:bg-emphasis todesktop:hover:bg-transparent hover:text-emphasis"
|
||||
: ""
|
||||
)}
|
||||
aria-current={current ? "page" : undefined}>
|
||||
{item.icon && (
|
||||
<item.icon
|
||||
className="mr-2 h-4 w-4 flex-shrink-0 rtl:ml-2 md:ltr:mx-auto lg:ltr:mr-2 [&[aria-current='page']]:text-inherit"
|
||||
className="todesktop:!text-blue-500 mr-2 h-4 w-4 flex-shrink-0 rtl:ml-2 md:ltr:mx-auto lg:ltr:mr-2 [&[aria-current='page']]:text-inherit"
|
||||
aria-hidden="true"
|
||||
aria-current={current ? "page" : undefined}
|
||||
/>
|
||||
|
@ -887,9 +889,9 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
|
|||
<div className="relative">
|
||||
<aside
|
||||
style={{ maxHeight: `calc(100vh - ${bannersHeight}px)`, top: `${bannersHeight}px` }}
|
||||
className="desktop-transparent bg-muted border-muted fixed left-0 hidden h-full max-h-screen w-14 flex-col overflow-y-auto overflow-x-hidden border-r md:sticky md:flex lg:w-56 lg:px-3">
|
||||
className="todesktop:!bg-transparent bg-muted border-muted fixed left-0 hidden h-full max-h-screen w-14 flex-col overflow-y-auto overflow-x-hidden border-r md:sticky md:flex lg:w-56 lg:px-3">
|
||||
<div className="flex h-full flex-col justify-between py-3 lg:pt-4">
|
||||
<header className="items-center justify-between md:hidden lg:flex">
|
||||
<header className="todesktop:-mt-3 todesktop:flex-col-reverse todesktop:[-webkit-app-region:drag] items-center justify-between md:hidden lg:flex">
|
||||
{orgBranding ? (
|
||||
<Link href="/settings/organizations/profile" className="px-1.5">
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
|
@ -904,7 +906,7 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
|
|||
</div>
|
||||
</Link>
|
||||
) : (
|
||||
<div data-testid="user-dropdown-trigger">
|
||||
<div data-testid="user-dropdown-trigger" className="todesktop:mt-4 w-full">
|
||||
<span className="hidden lg:inline">
|
||||
<UserDropdown />
|
||||
</span>
|
||||
|
@ -913,17 +915,17 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
|
|||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex space-x-0.5 rtl:space-x-reverse">
|
||||
<div className="flex w-full justify-end space-x-2 rtl:space-x-reverse">
|
||||
<button
|
||||
color="minimal"
|
||||
onClick={() => window.history.back()}
|
||||
className="desktop-only hover:text-emphasis text-subtle group flex text-sm font-medium">
|
||||
className="todesktop:block hover:text-emphasis text-subtle group hidden text-sm font-medium">
|
||||
<ArrowLeft className="group-hover:text-emphasis text-subtle h-4 w-4 flex-shrink-0" />
|
||||
</button>
|
||||
<button
|
||||
color="minimal"
|
||||
onClick={() => window.history.forward()}
|
||||
className="desktop-only hover:text-emphasis text-subtle group flex text-sm font-medium">
|
||||
className="todesktop:block hover:text-emphasis text-subtle group hidden text-sm font-medium">
|
||||
<ArrowRight className="group-hover:text-emphasis text-subtle h-4 w-4 flex-shrink-0" />
|
||||
</button>
|
||||
{!!orgBranding && (
|
||||
|
@ -935,8 +937,6 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<hr className="desktop-only border-subtle absolute -left-3 -right-3 mt-4 block w-full" />
|
||||
|
||||
{/* logo icon for tablet */}
|
||||
<Link href="/event-types" className="text-center md:inline lg:hidden">
|
||||
<Logo small icon />
|
||||
|
@ -976,7 +976,7 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
|
|||
<div className="flex">{t(item.name)}</div>
|
||||
</span>
|
||||
) : (
|
||||
<SkeletonText style={{ width: `${item.name.length * 10}px` }} className="h-[20px]" />
|
||||
<SkeletonText className="h-[20px] w-full" />
|
||||
)}
|
||||
</ButtonOrLink>
|
||||
</Tooltip>
|
||||
|
@ -1043,7 +1043,7 @@ export function ShellMain(props: LayoutProps) {
|
|||
props.backPath
|
||||
? "relative"
|
||||
: "pwa:bottom-[max(7rem,_calc(5rem_+_env(safe-area-inset-bottom)))] fixed bottom-20 z-40 ltr:right-4 rtl:left-4 md:z-auto md:ltr:right-0 md:rtl:left-0",
|
||||
"flex-shrink-0 md:relative md:bottom-auto md:right-auto"
|
||||
"flex-shrink-0 [-webkit-app-region:no-drag] md:relative md:bottom-auto md:right-auto"
|
||||
)}>
|
||||
{isLocaleReady && props.CTA}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import { FormbricksAPI } from "@formbricks/api";
|
||||
|
||||
import type { Feedback } from "@calcom/emails/templates/feedback-email";
|
||||
|
||||
enum Rating {
|
||||
"Extremely unsatisfied" = 1,
|
||||
"Unsatisfied" = 2,
|
||||
"Satisfied" = 3,
|
||||
"Extremely satisfied" = 4,
|
||||
}
|
||||
|
||||
export const sendFeedbackFormbricks = async (userId: number, feedback: Feedback) => {
|
||||
if (!process.env.FORMBRICKS_HOST_URL || !process.env.FORMBRICKS_ENVIRONMENT_ID)
|
||||
throw new Error("Missing FORMBRICKS_HOST_URL or FORMBRICKS_ENVIRONMENT_ID env variable");
|
||||
const api = new FormbricksAPI({
|
||||
apiHost: process.env.FORMBRICKS_HOST_URL,
|
||||
environmentId: process.env.FORMBRICKS_ENVIRONMENT_ID,
|
||||
});
|
||||
if (process.env.FORMBRICKS_FEEDBACK_SURVEY_ID) {
|
||||
const formbricksUserId = userId.toString();
|
||||
const ratingValue = Object.keys(Rating).includes(feedback.rating)
|
||||
? Rating[feedback.rating as keyof typeof Rating]
|
||||
: undefined;
|
||||
if (ratingValue === undefined) throw new Error("Invalid rating value");
|
||||
|
||||
await api.client.response.create({
|
||||
surveyId: process.env.FORMBRICKS_FEEDBACK_SURVEY_ID,
|
||||
userId: formbricksUserId,
|
||||
finished: true,
|
||||
data: {
|
||||
"formbricks-share-comments-question": feedback.comment,
|
||||
"formbricks-rating-question": ratingValue,
|
||||
},
|
||||
});
|
||||
await api.client.people.update(formbricksUserId, {
|
||||
attributes: {
|
||||
email: feedback.email,
|
||||
username: feedback.username,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
|
@ -1,12 +1,12 @@
|
|||
import { WEBAPP_URL } from "../constants";
|
||||
import { CAL_URL } from "../constants";
|
||||
import { getBrand } from "../server/getBrand";
|
||||
|
||||
export const getBookerBaseUrl = async (user: { organizationId: number | null }) => {
|
||||
const orgBrand = await getBrand(user.organizationId);
|
||||
return orgBrand?.fullDomain ?? WEBAPP_URL;
|
||||
return orgBrand?.fullDomain ?? CAL_URL;
|
||||
};
|
||||
|
||||
export const getTeamBookerUrl = async (team: { organizationId: number | null }) => {
|
||||
const orgBrand = await getBrand(team.organizationId);
|
||||
return orgBrand?.fullDomain ?? WEBAPP_URL;
|
||||
return orgBrand?.fullDomain ?? CAL_URL;
|
||||
};
|
||||
|
|
|
@ -15,7 +15,7 @@ import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-u
|
|||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { WEBAPP_URL } from "./constants";
|
||||
import { CAL_URL } from "./constants";
|
||||
import { getBookerBaseUrl } from "./getBookerUrl/server";
|
||||
|
||||
interface getEventTypeByIdProps {
|
||||
|
@ -271,7 +271,7 @@ export default async function getEventTypeById({
|
|||
? await getBookerBaseUrl({ organizationId: restEventType.team.parentId })
|
||||
: restEventType.owner
|
||||
? await getBookerBaseUrl(restEventType.owner)
|
||||
: WEBAPP_URL,
|
||||
: CAL_URL,
|
||||
children: restEventType.children.flatMap((ch) =>
|
||||
ch.owner !== null
|
||||
? {
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"dependencies": {
|
||||
"@calcom/config": "*",
|
||||
"@calcom/dayjs": "*",
|
||||
"@formbricks/api": "^1.1.0",
|
||||
"@sendgrid/client": "^7.7.0",
|
||||
"@vercel/og": "^0.5.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import dayjs from "@calcom/dayjs";
|
||||
import { sendFeedbackEmail } from "@calcom/emails";
|
||||
import { sendFeedbackFormbricks } from "@calcom/lib/formbricks";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
|
@ -30,6 +31,8 @@ export const submitFeedbackHandler = async ({ ctx, input }: SubmitFeedbackOption
|
|||
comment: comment,
|
||||
},
|
||||
});
|
||||
if (process.env.FORMBRICKS_HOST_URL && process.env.FORMBRICKS_ENVIRONMENT_ID)
|
||||
sendFeedbackFormbricks(ctx.user.id, feedback);
|
||||
|
||||
if (process.env.SEND_FEEDBACK_EMAIL && comment) sendFeedbackEmail(feedback);
|
||||
};
|
||||
|
|
|
@ -4,6 +4,8 @@ import type { Table } from "@tanstack/react-table";
|
|||
import type { LucideIcon } from "lucide-react";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import { Button } from "../button";
|
||||
import { Input } from "../form";
|
||||
import { DataTableFilter } from "./DataTableFilter";
|
||||
|
@ -35,8 +37,10 @@ export function DataTableToolbar<TData>({
|
|||
// If you select ALL filters for a column, the table is not filtered and we dont get a reset button
|
||||
const isFiltered = table.getState().columnFilters.length > 0;
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div className="bg-default sticky top-[3rem] z-10 flex items-center justify-end space-x-2 py-4 md:top-0">
|
||||
<div className="sticky top-[3rem] z-10 flex items-center justify-end space-x-2 py-4 md:top-0">
|
||||
{searchKey && (
|
||||
<Input
|
||||
className="max-w-64 mb-0 mr-auto rounded-md"
|
||||
|
@ -51,7 +55,7 @@ export function DataTableToolbar<TData>({
|
|||
EndIcon={X}
|
||||
onClick={() => table.resetColumnFilters()}
|
||||
className="h-8 px-2 lg:px-3">
|
||||
Reset
|
||||
{t("clear")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ export const AnimatedPopover = ({
|
|||
prefix?: string;
|
||||
}) => {
|
||||
const [open, setOpen] = React.useState(defaultOpen ?? false);
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const ref = React.useRef<HTMLButtonElement>(null);
|
||||
// calculate which aligment to open the popover with based on which half of the screen it is on (left or right)
|
||||
const [align, setAlign] = React.useState<"start" | "end">("start");
|
||||
React.useEffect(() => {
|
||||
|
@ -50,7 +50,7 @@ export const AnimatedPopover = ({
|
|||
return (
|
||||
<Popover.Root defaultOpen={defaultOpen} onOpenChange={setOpen} modal={true}>
|
||||
<Popover.Trigger asChild>
|
||||
<div
|
||||
<button
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
"hover:border-emphasis border-default text-default hover:text-emphasis radix-state-open:border-emphasis radix-state-open:outline-none radix-state-open:ring-2 radix-state-open:ring-emphasis mb-4 flex h-9 max-h-72 items-center justify-between whitespace-nowrap rounded-md border px-3 py-2 text-sm hover:cursor-pointer",
|
||||
|
@ -77,7 +77,7 @@ export const AnimatedPopover = ({
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content side="bottom" align={align} asChild>
|
||||
<div
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/trpc": "*",
|
||||
"@formkit/auto-animate": "^1.0.0-beta.5",
|
||||
"@formkit/auto-animate": "^0.8.1",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-popover": "^1.0.2",
|
||||
|
|
|
@ -256,6 +256,9 @@
|
|||
"EMAIL_SERVER_USER",
|
||||
"EMAIL_SERVER",
|
||||
"EXCHANGE_DEFAULT_EWS_URL",
|
||||
"FORMBRICKS_HOST_URL",
|
||||
"FORMBRICKS_ENVIRONMENT_ID",
|
||||
"FORMBRICKS_FEEDBACK_SURVEY_ID",
|
||||
"GIPHY_API_KEY",
|
||||
"GITHUB_API_REPO_TOKEN",
|
||||
"GOOGLE_API_CREDENTIALS",
|
||||
|
|
Loading…
Reference in New Issue
Block a user