v1.6.2 (#2926)
* Turbo fixes * Make apps single pages public * fixed /booking skeleton (#2722) * fixed /booking skeleton * nit * Type fixes * Test fixes * Update playwright.config.ts * More test fixes * Update dynamic-booking-pages.test.ts * add invite link to Zapier setup page (#2696) * add invite link and toaster to zapier setup page * create env variable for invite link and save in database * fetch invite link form getStaticProps * add getStaticPath method * clean code * Moves app setup and index page * Moves Loader to ui * Trying new way to handle dynamic app store pages * Cleanup * Update tailwind.config.js * zapier invite link fixes * Tests fixes Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com> * Add more embed events (#2719) * Add more embed events * Add more embed events Co-authored-by: Peer Richelsen <peeroke@gmail.com> * adds availability select loader (#2718) * Improve logs and Fix unwanted 500 to reduce noise in logs (#2674) * Improve logs * Fix unintentional 500 Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Change date format for RecurringBookings (#2707) * Change date format for RecurringBookings * Missing bookingId query param Co-authored-by: Leo Giovanetti <hello@leog.me> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * added giphy description (#2730) * fixes #2732 (#2732) * Hotfix : Fix Infinite loading of Bookings (#2729) * Add more embed events * Add more embed events * Fix nextCursor calculation logic Co-authored-by: Peer Richelsen <peeroke@gmail.com> * Hotfix: Success page for recurring event (#2725) * Merge pull request #2672 from calcom/main v1.5.4 * Turbo fixes * Make apps single pages public * Fix preview.html not built and thus served during depooy (#2713) * Hotfix: Success page layout broken due to duplicate "When" (#2716) * Update BookingPage.tsx * Reverting unchaged lines * Fixing recurrenceRule for ICS files Co-authored-by: Omar López <zomars@me.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> * Fix preview.html not built and thus served during depooy (#2727) Co-authored-by: Omar López <zomars@me.com> * Allow deletion of a disabled event (#2737) * allows deletion of disabled event * some visual fixes Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Multiple E2E improvements * Parallelizes some tests * Update booking-pages.test.ts * E2E and paid bookings fixes * Add 'free' and 'workingElsewhere' as a non-blocking event (#2652) * Add 'free' and 'workingElsewhere' to non-blocking event - this will allow bookings at these times * Update CalendarService.ts Co-authored-by: zomars <zomars@me.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * TODO marks blocking test to be fixed * Update testUtils.ts * Update testUtils.ts * getBusyTimes consolidation * Fixes delete-me test * E2E fixing attemps * Adjusting Zapier endpoints for publishing integration (#2728) * add /me endpoint for zapier API testing * remove cacellationReason from listBookings response Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Omar López <zomars@me.com> * Fix reschedule not happening in calendar if two calendards are there (#2733) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> * Increases timeout temporarily * Merge pull request #2745 from calcom/apps/multiple-categories Allow apps to belong to multiple categories * Build fixes * Populate msteams key in db (#2743) * Populate msteams key in db * Fix calendar credentials to teams * Clarify account dialog Co-authored-by: Omar López <zomars@me.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Bailey Pumfleet <pumfleet@hey.com> * Improving Email DRYness (#2486) * Email DRY * WIP * Improve email DRYness Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * E2E fixtures (#2747) * Sign in button should be changed or disabled after click #2654 (#2749) Co-authored-by: gitstart <gitstart@users.noreply.github.com> * Fixing ESLint warnings (#2723) * Fixing warnings * Reverting and disabling ESLint in some cases * Reverting Next Images * Reverting file, bad merge * Targeting ESLint to line * Additional warnings * New warning squished * More tweaks and major fixes * Uneeded conf Co-authored-by: Omar López <zomars@me.com> * Fix/avoid multiple schedule deletions (#2602) * Prevents users from deleting the same schedule multiple times due to delay before the schedule disappears. It also applies the same fix to team disband. Schedule deletion: ![schedule_deletion_new_behaving](https://user-images.githubusercontent.com/42497300/165126805-b3090268-c1a6-418a-b06e-06bd8446da03.gif) Team disband: ![team_disband_new_behaving](https://user-images.githubusercontent.com/42497300/165127043-7e083e94-e4c9-4e88-90a2-47d31bdd92e6.gif) Fixes issue [#2569](https://github.com/calcom/cal.com/issues/2569) Bug fix (non-breaking change which fixes an issue) **apps/web/components/LightLoader.tsx** → this file was created in order to make a light color loading spinner available. It's necessary when we need to display a loading spinner above dark backgrounds. **apps/web/components/availability/ScheduleListItem.tsx** → this component was created in order to give a schedule list item its own state. * Removing a "setTimeout" that was only used for testing purposes * Adding a code review suggestion to my modifications * Changing loading style * Cleanup * Avoids using unnecessary state * Revert "Adding a code review suggestion to my modifications" This reverts commitb5e40062d7
. * Reverts some changes * Renames isLoading Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: Omar López <zomars@me.com> * Added check on create eventtype to see is user has slug that already exists (#2757) * Added check on create eventtype to see is user has slug that already exists, added error check on onError * revert yarn.lock back * Catches prisma known error instead of making an additional query Co-authored-by: zomars <zomars@me.com> * Fix white border (#2761) * fix: remove hardcoded redirect in signin url email verification (#2764) Co-authored-by: Agusti Fernandez Pardo <git@agusti.me> * Bad UX when user wants to set the default Event Type Title #2245 (#2760) Co-authored-by: gitstart <gitstart@users.noreply.github.com> * Skip sending emails in E2E * Users Phone Number Option (#2669) * Users Phone Number Option * Implemented improvments * Add validation to form Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com> * Fix/unpaid unconfirmed (#2553) * Fix merge errors * Errors prettier/prettier * Update apps/web/pages/api/book/event.ts Co-authored-by: Miguel Nieto A <39246879+miguelnietoa@users.noreply.github.com> * Update apps/web/pages/api/book/event.ts Co-authored-by: Miguel Nieto A <39246879+miguelnietoa@users.noreply.github.com> * Update apps/web/pages/api/integrations.ts Co-authored-by: Miguel Nieto A <39246879+miguelnietoa@users.noreply.github.com> * Fix merge errors * Errors prettier/prettier * Update apps/web/pages/api/book/confirm.ts Co-authored-by: alannnc <alannnc@gmail.com> * Modal window before delete stripe integration * ESLint Report * Test fixes Co-authored-by: Miguel Nieto A <39246879+miguelnietoa@users.noreply.github.com> Co-authored-by: alannnc <alannnc@gmail.com> Co-authored-by: zomars <zomars@me.com> * Mutually exclusive options (#2755) Co-authored-by: Omar López <zomars@me.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Readd steps to create a new user #2665 (#2759) Co-authored-by: gitstart <gitstart@users.noreply.github.com> * remove redundant conditional expressions (#2756) * remove redundant conditional expressions * remove redundant conditional expression Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> * Fix adds redirect callback that support app.cal.com (#2768) * Fix adds redirect callback that support app.cal.com * Update apps/web/pages/api/auth/[...nextauth].tsx Check origin of website and baseurl Co-authored-by: Omar López <zomars@me.com> * fix: lint issue extra space removed Co-authored-by: Agusti Fernandez Pardo <git@agusti.me> Co-authored-by: Omar López <zomars@me.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * respect local set timezone and update url on mismatch (#2506) * ensure `timeZone()` will make its way to the URL fixes https://github.com/calcom/cal.com/issues/2482 * keep `timeZone()` and the offset from URL in sync Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Squashed commit of the following: commit27540b09ce
Author: Agusti Fernandez Pardo <me@agusti.me> Date: Mon May 16 17:34:13 2022 +0200 fix: remove hardcoded redirect in signin url email verification (#2764) Co-authored-by: Agusti Fernandez Pardo <git@agusti.me> commitae15a7d739
Author: Hariom Balhara <hariombalhara@gmail.com> Date: Tue May 10 14:30:43 2022 +0530 Fix time issue commit2a5a89fe50
Author: Leo Giovanetti <hello@leog.me> Date: Wed May 11 10:21:46 2022 -0300 Missing fix for success page commit2ce1e78053
Author: Leo Giovanetti <hello@leog.me> Date: Wed May 11 10:12:59 2022 -0300 Hotfix: Success page for recurring event (#2725) * Merge pull request #2672 from calcom/main v1.5.4 * Turbo fixes * Make apps single pages public * Fix preview.html not built and thus served during depooy (#2713) * Hotfix: Success page layout broken due to duplicate "When" (#2716) * Update BookingPage.tsx * Reverting unchaged lines * Fixing recurrenceRule for ICS files Co-authored-by: Omar López <zomars@me.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> commit2d6d1cb444
Author: Hariom Balhara <hariombalhara@gmail.com> Date: Tue May 10 14:49:46 2022 +0530 Hotfix: Success page layout broken due to duplicate "When" (#2716) commitef68f4f4f8
Author: Hariom Balhara <hariombalhara@gmail.com> Date: Tue May 10 10:54:20 2022 +0530 Fix preview.html not built and thus served during depooy (#2713) commit18c28cc3fd
Author: zomars <zomars@me.com> Date: Mon May 9 16:17:07 2022 -0600 Make apps single pages public commitd40e8caff9
Author: zomars <zomars@me.com> Date: Mon May 9 16:08:03 2022 -0600 Turbo fixes commit3161cc4d45
Merge:ed808c3be
4099a477d
Author: zomars <zomars@me.com> Date: Mon May 9 14:58:33 2022 -0600 Merge branch 'main' into production commited808c3be6
Author: Omar López <zomars@me.com> Date: Mon May 9 14:56:23 2022 -0600 Merge pull request #2672 from calcom/main v1.5.4 * Typo * Typo * Update apps/web/pages/apps/categories/[category].tsx * Apply suggestions from code review * Alert to describe exclusion of options (#2770) * Alert to describe exclusion of options * Update apps/web/pages/event-types/[type].tsx Co-authored-by: Omar López <zomars@me.com> * Update apps/web/components/eventtype/RecurringEventController.tsx Co-authored-by: Omar López <zomars@me.com> * Formatting Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Omar López <zomars@me.com> * fix: split time correctly if the local working hours are just across mid night (#2766) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Update crowdin.yml * New Crowdin translations by Github Action (#2773) * New Crowdin translations by Github Action * Update vital.json * Update vital.json Co-authored-by: Crowdin Bot <support+bot@crowdin.com> Co-authored-by: Omar López <zomars@me.com> * Add Google cal extneral calendar id to booking reference (#2671) * Set google cal event id to use our uid * Save calendar external id to bookingRef * Pass external calendar ids to update and delete * Create migration * Fix type errors * Fix prisma url Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Omar López <zomars@me.com> * v1.6 * v1.6 * 2FA submit disabled (#2790) * fixing the hyperlink for open startup (#2777) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Add login event (#2784) Co-authored-by: Bailey Pumfleet <pumfleet@hey.com> * Fixes infinite loop * Fixes infinite loop * Fixes infinite loop * Update all Yarn dependencies (2022-05-16) (#2769) * Update all Yarn dependencies (2022-05-16) * Upgrade dependencies * Removes deprecated packages * Upgrades deps * Updates submodules * Update yarn.lock * Linting * Linting * Update website * Build fixes * TODO: fix this * Module resolving * Type fixes * Intercom fixes on SSG * Fixes infinite loop * Upgrades to React 18 * Type fixes * Locks node version to 14 * Upgrades daily-js * Readds missing types * Upgrades playwright * Noop when intercom is not installed * Update website * Removed yarn.lock in favor of monorepo Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com> * Create ci.yml * Update ci.yml * Reintroduces typescript-eslint Buckle up! * Type fixes * Update ci.yml * Update api * Update admin * Reusable inferSSRProps * Linting * Linting * Prisma fixes * Update ci.yml * Cache testing * Update e2e.yml * Update DatePicker.tsx * Update e2e.yml * Revert "Linting" This reverts commitadf817766e
. * Revert "Linting" This reverts commit1b59dacd64
. * Linting * Update e2e.yml * Ci updates * Add team Id to hash url (#2803) * Fix missing tabs - Embed (#2804) * Fix missing tabs * Fix Eslint error * Fix Eslint errors * Add import statement (#2812) * Add import statement * Update apps/docs/next.config.js Co-authored-by: Omar López <zomars@me.com> * Show success page if booking was deleted on calendar (#2808) * Add exception to 410 * Fix type error * Add GoogelCalError type * only show invite link for app.cal.dev (#2807) Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Omar López <zomars@me.com> * fix: update eslint config to test .ts and .js separately (#2805) * fix: update eslint config * fix: update ts ignore * fix: update eslint config * Update TeamAvailabilityScreen.tsx * Type fixes * Update useIntercom.ts Co-authored-by: Omar López <zomars@me.com> * fix: sync api to latest commit (#2810) Co-authored-by: Agusti Fernandez Pardo <git@agusti.me> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Embed React improvements (#2782) * Add off support. Add getApi export. * Add publish command * Add embed-snippet in prod deps * Update README * Update package.json Co-authored-by: Bailey Pumfleet <pumfleet@hey.com> Co-authored-by: zomars <zomars@me.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Consolidates test-results * Add vscode tasks.json (#2801) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * save additional inputs as json + view details of booking (#2796) * move custom inputs from description to own json object * show custom inputs on success page * fix type error * add custom inputs to email and webhook * add custom inputs to all emails * add values for custom inputs when rescheduling * add custom input everywhere description is shown * fix bug with boolean value * fix issues with null values * disable custom inputs and add notes for organizer * don't show custom input with empty string * don't show custom inputs with empty string in calender event and email * add link to booking details page * redirect to success page to see booking details * add functionality to cancel and reschedule booking * fix bookings that require confirmation * clean code * fix infinite lopp in useEffect of success page * show web conference details message when integration as location * improve design of cancelling event * clean code * disable darkmode for organizer on booking details page * fix dark mode for cancelling booking * fix build error * Fixes infinite loop * Fixes infinite loop * Fixes infinite loop * Update all Yarn dependencies (2022-05-16) (#2769) * Update all Yarn dependencies (2022-05-16) * Upgrade dependencies * Removes deprecated packages * Upgrades deps * Updates submodules * Update yarn.lock * Linting * Linting * Update website * Build fixes * TODO: fix this * Module resolving * Type fixes * Intercom fixes on SSG * Fixes infinite loop * Upgrades to React 18 * Type fixes * Locks node version to 14 * Upgrades daily-js * Readds missing types * Upgrades playwright * Noop when intercom is not installed * Update website * Removed yarn.lock in favor of monorepo Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com> * Create ci.yml * Update ci.yml * Reintroduces typescript-eslint Buckle up! * Type fixes * Update ci.yml * Update api * Update admin * Reusable inferSSRProps * Linting * Linting * Prisma fixes * Update ci.yml * Cache testing * Update e2e.yml * Update DatePicker.tsx * Update e2e.yml * Revert "Linting" This reverts commitadf817766e
. * Revert "Linting" This reverts commit1b59dacd64
. * Linting * Update e2e.yml * Ci updates * Add team Id to hash url (#2803) * Fix missing tabs - Embed (#2804) * Fix missing tabs * Fix Eslint error * Fix Eslint errors * Add import statement (#2812) * Add import statement * Update apps/docs/next.config.js Co-authored-by: Omar López <zomars@me.com> * Show success page if booking was deleted on calendar (#2808) * Add exception to 410 * Fix type error * Add GoogelCalError type * only show invite link for app.cal.dev (#2807) Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Omar López <zomars@me.com> * fix: update eslint config to test .ts and .js separately (#2805) * fix: update eslint config * fix: update ts ignore * fix: update eslint config * Update TeamAvailabilityScreen.tsx * Type fixes * Update useIntercom.ts Co-authored-by: Omar López <zomars@me.com> * fix: sync api to latest commit (#2810) Co-authored-by: Agusti Fernandez Pardo <git@agusti.me> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Embed React improvements (#2782) * Add off support. Add getApi export. * Add publish command * Add embed-snippet in prod deps * Update README * Update package.json Co-authored-by: Bailey Pumfleet <pumfleet@hey.com> Co-authored-by: zomars <zomars@me.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Consolidates test-results * Type fixes * Abstracts minimal booking select * Type fixes * Update listBookings.ts * Update common.json * Update bookingReminder.ts * Consolidates isOutOfBounds * Update webhookResponse-chromium.txt * Update TableActions.tsx * Type fixes * Update BookingPage.tsx * Update webhookResponse-chromium.txt Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: Bailey Pumfleet <pumfleet@hey.com> Co-authored-by: zomars <zomars@me.com> Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: iamkun <kunhello@outlook.com> Co-authored-by: Agusti Fernandez Pardo <me@agusti.me> Co-authored-by: Agusti Fernandez Pardo <git@agusti.me> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Update check-types.yml * adding organizer as attendee to google calendar events (#2779) * Fix auto-select and close of dropdown (#2819) * fixes dynamic color and typefix for tfunction after react upgrade (#2821) * New Crowdin translations by Github Action (#2791) Co-authored-by: Crowdin Bot <support+bot@crowdin.com> * Relocates admin to console # Conflicts: # apps/admin * Relocates admin to console * Fix login submit (#2849) * fix: long string overflowing calendar div (#2842) Co-authored-by: gitstart <gitstart@users.noreply.github.com> Co-authored-by: Júlio Piubello da Silva Cabral <julio.piubello@gitstart.dev> * Adding labels (#2783) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Wrong username is identified if query params are present and user doesn't exist (#2838) Co-authored-by: gitstart <gitstart@users.noreply.github.com> Co-authored-by: Júlio Piubello da Silva Cabral <julio.piubello@gitstart.dev> * Meet/Zoom Email Clarification (#2828) * Add clarificaiton to email * Update apps/web/lib/emails/templates/organizer-scheduled-email.ts * Add to attendee scheduled email Co-authored-by: Omar López <zomars@me.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Request the inclusion Assuncion Time Zone (#2840) Co-authored-by: gitstart <gitstart@users.noreply.github.com> Co-authored-by: Júlio Piubello da Silva Cabral <julio.piubello@gitstart.dev> * Fix UI of dialog (#2788) * removed large mandatory height and scroll * added z index using css * cleanup * fixed TS errors * extract dialog out of dropdown * Adds custom loading text to confirmation dialog * rename update * utilizing mutation loading state Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Hotfix: Fixing Security Issues (#2848) * Fixing Privilege Escalation * Fixing critical obj ref in availability * Fixing reschedule security issue Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> * Merge production to main * Update vital.json * Update vital.json * Fix login page CTA disabled state (#2832) * fix(ui/login): better disabled state for login CTA The fix involves tracking the form submission with a dedicated state `submitInProgress` with React. `formState` from `react-hook-form` does not take time taken for the network call into account. For example, if the api takes 5 seconds to complete, we would expect the `formState.isSubmitting` to be true for `5` seconds. But, surprisingly this is not the case and `formState` from `react-hook-form` resolves immediately after it makes a successful connection to the endpoint. A dedicated state (with `useState`) is introduced that is enabled when the user clicks on the login CTA, and disabled when the api call is resolved, either successfully or with an error. * Update login.tsx * Update login.tsx * Fixes isSubmitting state Co-authored-by: zomars <zomars@me.com> * Playwright binaries shouldn't be on deps * Playwright binaries shouldn't be on deps * Fix infinite renders on event-type edit page (#2820) * Updates submodules * Makes sure to hash post-install cache * Add seats to event types (#2485) * Add seatsPerTimeSlot to event type schema * Add seats per time slot to event type form * Book event and render seats * Pass booking uid for seats * Disable requires confirmation if seats are enabled * Fix type errors * Update submodules * Fix type errors * Fix type errors * Fix duplicate string * Fix duplicate string * Fix schema and migration file * Fix render seats * Fix bookinguid typos * Remove console.log * Fix type error * Fix mobile formatting * Update apps/web/lib/hooks/useSlots.ts Co-authored-by: Omar López <zomars@me.com> * Update apps/web/lib/hooks/useSlots.ts Co-authored-by: Omar López <zomars@me.com> * Added translation for seats available text Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com> Co-authored-by: alannnc <alannnc@gmail.com> * Redesign help menu item (#2710) * Seperate help menu item and contact menu item * Add menu items * Install react-popover * Render contact only if support keys are present * Adjust contact support links * Add translations * Add embed changes * Adjust menu if helped is pressed * Add items to help menu * Change button color on selection * Create endpoint * Create feedback table * Create migration file * Write feedback to db * Remove logs * Add response message * Send feedback email * Disable submit if no rating and after submit * Add translations * Fix padding * Clean up * Clean up * Add user feedback email to .env example * Lint fixes and styles * Changed onClick function to a named function and fix style * Fix ids order * Removed commented code and changed textarea id and name * Fix id orders * Change to AND operator Co-authored-by: Omar López <zomars@me.com> * Add user relation to feedback Co-authored-by: Omar López <zomars@me.com> * Add migration files * Change rating to strings * Change rating to strings * Fix type errors * WIP success & error messages * Change success and error to boolans * Style messages * Add await Co-authored-by: Omar López <zomars@me.com> * Remove duplicate string * Refactor import statement Co-authored-by: Omar López <zomars@me.com> * Change opacity of emojis * added support@cal.com email for feedback * Add success toast * Update .env.example Co-authored-by: Omar López <zomars@me.com> * Add tCRP route * tCRP send email * tCRP send email Co-authored-by: Alan <alannnc@gmail.com> Co-authored-by: Omar López <zomars@me.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> * fixed labels for seats, removes shadow from event-type inputs (#2862) * fixed labels for seats * minor fixes, removed shadow from all event-type inputs * Render input field on pro accounts (#2859) Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> * Submodule sync * sec-001 fixes (#2866) (#2867) * sec-001 fixes (#2866) * sec-001 fixes * Typo * Fixes paid bookings # Conflicts: # apps/web/ee/components/stripe/Payment.tsx * Hotfix: Fixing Security Issues (#2848) (#2850) * sec-001 fixes (#2866) * sec-001 fixes * Typo * Fixes paid bookings * fixes issues with date of recurring events on booking detail page (#2872) * fixes date not showing for confirmed recurring event * only show all events on upcoming bookings Co-authored-by: CarinaWolli <wollencarina@gmail.com> * Resolve if can't find event on google cal (#2860) * Resolve if can't find event on google cal * Change order of console.error Co-authored-by: Omar López <zomars@me.com> Co-authored-by: Omar López <zomars@me.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> * Disable Impersonation Option (#2880) * Disable Impersonation * Update Description Copy Co-authored-by: Peer Richelsen <peeroke@gmail.com> * Fix deleted event when on different cal (404) (#2882) * Fix/email shows false (#2879) * Webhook sec fixes (#2883) * Webhook sec fixes * Revert changes * Feat/Display location information publicly (#2752) * Updating checkbox field to reflect new designs * Include Infobadge option checkbox * Checkbox Field + i18n * Default checked - true * Sync with router * Extracting Types * Update filtering logic * Add UI to booking page * Default address/link * Update hashedlink page * Tidy up * Video icon * Add nullish check * Update to use RHF controller Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Fix 500 errors if data is invalid (#2886) * Check in middleware to ensure authorization for all endpoints (#2885) Co-authored-by: Alex van Andel <me@alexvanandel.com> * Fix bug to check opt in bookings (#2889) * Fix bug to check opt in bookings * Implemented value, onChange instead of form access Co-authored-by: Alex van Andel <me@alexvanandel.com> * remove unused imports (#2892) * Cascades impersonations on user delete (#2891) Co-authored-by: Alex van Andel <me@alexvanandel.com> * Ensures json fields on each call (#2893) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Redundant conditional expression (#2894) * remove redundant conditional expressions * remove redundant conditional expressions * License server (#2379) * WIP License server * WIP * Moves locations to App Store and Core * LocationType fixes * Runs db migrations post-deploy * WIP * WIP * Cleanup * WIP * WIP * Decouples translations from NavTabs * Adds admin submodule * Adds admin submodule * Sync dependencies * WIP * WIP * Updates submodules * Renames package * Updates submodules * Adds scripts for console * Updates license checker URL * Updates admin * Adds staging/prod admin console links * Update yarn.lock * Update NavTabs.tsx * WIP * Update admin * WIP * Adds hint to InputField * Update admin * Adds turbo admin dependecies * Update admin * Prevents redirection on form submit * Form warning fixes * Update admin * Form fixes * Update yarn.lock * Update admin * Update admin * Update admin * Adds withLicenseRequired HOC * Adds LicenseRequired to EE components * Admin deploy fix? * Updates submodules * Use relative inside lib * type fixes * Fixes turbo race condition * Relocates admin to console * Relocates admin to console * Update console * Update api * Update turbo.json * Update ErrorBoundary.tsx * Update defaultEvents.ts * Update checkLicense.ts * Update yarn.lock * Skip on E2E Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Update turbo.json * Reverts license check on payment Since it's a public page * Submodule sync * fix: overflowing text on teams page - using tailwind line-clamp (#2843) Co-authored-by: gitstart <gitstart@users.noreply.github.com> Co-authored-by: Júlio Piubello da Silva Cabral <julio.piubello@gitstart.dev> * Fix @calcom/embed-react installation with TS project (#2870) * Fix failing builds (#2908) * Update turbo.json * Change location of booking (#2658) * add functionality to change location in booking and send out mail * add i18n * change location with dropdown like in event-types * small fixes and code clean up * clean code * improve format of current Location string * clean code * clear selection when dialog closed * added mutation and changed props (first working verison) * clean code * clean code * clean code * clean code * fix typo * change maxHeight of select * use useWatch for selectedLocation * pass default values with props * set current location directly in useState * clear selected values when updating location * fix trpc query for credentialst * change icons for editing booking * improve naming of variables * remove unnecessary orderBy * use locationOptionsToString method * fix current location naming for Cal Video * add phone input * save phone number as location of booking * remove input field for phone number for event-types * fix redirection issue * show previous selected location in event-type * remove attendee number from selection for booking * make first letter of location lowercase * remove input field for attendee phone number * clear Errors when changing location type * set location details to optional * clean code * fixing issue that dropdown doesn't close when dialog opens * clean code * make overflow visibile in dialog * fix existing bug with address not showing in event-type settings * fix issue with losing focus after validation * close rejection dialog * small spelling fixes * fix issue with LocationChangeEmail * fix failing E2E test * fix failing E2E test * fix E2E test * bug fix for saving user phone, and other minor changes * merge main * improve text * fix UI of booking list * Delete admin * remove selection after update and submit * add translation for error message * add default values for checkbox * add "your phone number" to locations on booking page * remove duplicate attributes from viewer.bookings Co-authored-by: Omar López <zomars@me.com> * check if user is authorized to make changes to booking * remove location string * clan code for displayLocaitonPublicly checkbox * fetch locationOptions on server side * remove trpc query for credentials * fix phone number input * fix labels of host and attendee phone number for booking page * Migrates edit location to tRPC * Link elemnt should only be used in `a` tags * Adds missin router * Migrates locationOptions to tRPC query * Type fixes Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Alan <alannnc@gmail.com> Co-authored-by: Omar López <zomars@me.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * added checkly and twitch badges * Update README.md * areas of expertise test * Update CONTRIBUTING.md * Update README.md (#2912) * Update CONTRIBUTING.md * Update CONTRIBUTING.md * Update CONTRIBUTING.md * Update CONTRIBUTING.md * Update CONTRIBUTING.md * Update CONTRIBUTING.md * remove unsed imports (#2895) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Fix event-type preview links on Vercel Previews (#2919) * Fix website url * Fix avatar paths everywhere * Fix linting errors Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Cleanup & removed usages of sparingly used Text/ files (#2904) * Update CONTRIBUTING.md * Update CONTRIBUTING.md * Fixed typo in CheckboxField and wrapped description in <label> (#2924) * Fixed typo in CheckboxField and wrapped description in <label> * Make functionality identical to before * Fixed use of infomationIconText * Fix lint error (needs refactor, out of scope) * Payment amount input leading 0 (#2836) Co-authored-by: gitstart <gitstart@users.noreply.github.com> Co-authored-by: Júlio Piubello da Silva Cabral <julio.piubello@gitstart.dev> * Allow less than 6 bookings and add proper min validation of 2+ (#2921) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * fix(app-store): Giphy Ux fixes (#2746) * fix(app-store): Giphy Ux fixes * Add search placeholder * Min height image on event type page * Use regex in zod validation and fix design issues for figma row 1 * Add paddings as per discussion with Ciarán * Fix getGiphyApiKey nomenclature * Update apps/web/public/static/locales/en/common.json * Update regex to be more readable and allow enter to search Co-authored-by: Omar López <zomars@me.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> * Add embed tests in CI (#2887) * Add embed tests in CI * Update e2e-embed.yml * Run quick tests only * Ignore certain folders * Add embed-react tests as well * Fix commands * Dont run unit tests * Update playwright.config.ts * Update playwright.config.ts * Update playwright.config.ts * Create correct artifcats * Create correct artifcats * Fi ignore path * Update package.json * Add back embed-react typecheck Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * remove subtitle (#2925) Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> * Fix booking detail page for cancelled events (#2905) * fix booking detail page for cancelled events * Fix typo * fix bug when now attendees exist in booking Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Alan <alannnc@gmail.com> * v1.6.2 * Add reschedule reason to DB and emails (#2909) * Write reschedule reason to db * Add reschedule reason to emails * Add reschedule reason to request reschedule email * Add reschedule reason to request reschedule email * Add reschedule reason to request reschedule email * Add reschedule reason to request reschedule email * Write reschedule reason to db * Add reschedule reason to emails * No longer using rescheduleReason in favor of cancellationReason * Update apps emails accordingly with reschedule reason * Update reschedule.test.ts Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Alan <alannnc@gmail.com> Co-authored-by: zomars <zomars@me.com> * Update website * Update HelpMenuItem.tsx * Update event.ts Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Leo Giovanetti <hello@leog.me> Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: Bailey Pumfleet <pumfleet@hey.com> Co-authored-by: GitStart <1501599+gitstart@users.noreply.github.com> Co-authored-by: gitstart <gitstart@users.noreply.github.com> Co-authored-by: Arthur Cruz <42497300+arthur1041@users.noreply.github.com> Co-authored-by: Mitchell Moore <47459168+Mitchell-Moore@users.noreply.github.com> Co-authored-by: Agusti Fernandez Pardo <me@agusti.me> Co-authored-by: Agusti Fernandez Pardo <git@agusti.me> Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: andreaestefania12 <19562383+andreaestefania12@users.noreply.github.com> Co-authored-by: Miguel Nieto A <39246879+miguelnietoa@users.noreply.github.com> Co-authored-by: alannnc <alannnc@gmail.com> Co-authored-by: Hashen <37979557+Hashen110@users.noreply.github.com> Co-authored-by: buschco <colin@busch.dev> Co-authored-by: iamkun <kunhello@outlook.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Crowdin Bot <support+bot@crowdin.com> Co-authored-by: Ankit Gordhandas <agordhandas@gmail.com> Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> Co-authored-by: Ken Miller <kemiller@users.noreply.github.com> Co-authored-by: Júlio Piubello da Silva Cabral <julio.piubello@gitstart.dev> Co-authored-by: Arun Kumar <palerdot@users.noreply.github.com> Co-authored-by: Shrey Gupta <connectwithshrey@gmail.com>
This commit is contained in:
parent
411497575a
commit
01631e808f
|
@ -15,6 +15,8 @@
|
|||
# - You can not repackage or sell the codebase
|
||||
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
|
||||
NEXT_PUBLIC_LICENSE_CONSENT=''
|
||||
# To enable enterprise-only features, fill your license key in here
|
||||
CALCOM_LICENSE_KEY=
|
||||
# ***********************************************************************************************************
|
||||
|
||||
# - DATABASE ************************************************************************************************
|
||||
|
@ -25,6 +27,7 @@ NEXT_PUBLIC_LICENSE_CONSENT=''
|
|||
NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000'
|
||||
# Change to 'http://localhost:3001' if running the website simultaneously
|
||||
NEXT_PUBLIC_WEBSITE_URL='http://localhost:3000'
|
||||
NEXT_PUBLIC_CONSOLE_URL='http://localhost:3004'
|
||||
NEXT_PUBLIC_EMBED_LIB_URL='http://localhost:3000/embed/embed.js'
|
||||
|
||||
# To enable SAML login, set both these variables
|
||||
|
@ -70,6 +73,10 @@ NEXT_PUBLIC_ZENDESK_KEY=
|
|||
# Help Scout Config
|
||||
NEXT_PUBLIC_HELPSCOUT_KEY=
|
||||
|
||||
# Inbox to send user feedback
|
||||
SEND_FEEDBACK_EMAIL=
|
||||
|
||||
|
||||
# This is used so we can bypass emails in auth flows for E2E testing
|
||||
# Set it to "1" if you need to run E2E tests locally
|
||||
NEXT_PUBLIC_IS_E2E=
|
||||
|
|
|
@ -16,7 +16,8 @@ jobs:
|
|||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Use Node ${{ matrix.node }}
|
||||
uses: actions/setup-node@v3
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
name: E2E test - embed
|
||||
on:
|
||||
push:
|
||||
branches: [ tests/ci-embed ]
|
||||
pull_request_target: # So we can test on forks
|
||||
branches:
|
||||
- main
|
||||
# Embed e2e - tests verify booking flow which is applicable to non-embed case also. So, don't ignore apps/web changes.
|
||||
paths-ignore:
|
||||
- apps/api/**
|
||||
- apps/console/**
|
||||
- apps/docs/**
|
||||
- apps/swagger/**
|
||||
- apps/website/**
|
||||
- apps/web/public/**
|
||||
- tests/**
|
||||
- playwright/**
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 20
|
||||
name: Testing Embeds
|
||||
strategy:
|
||||
matrix:
|
||||
node: ["14.x"]
|
||||
os: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
||||
NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
|
||||
NEXTAUTH_SECRET: secret
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
GOOGLE_LOGIN_ENABLED: true
|
||||
# CRON_API_KEY: xxx
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||
PAYMENT_FEE_PERCENTAGE: 0.005
|
||||
PAYMENT_FEE_FIXED: 10
|
||||
SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||
SAML_ADMINS: pro@example.com
|
||||
NEXTAUTH_URL: http://localhost:3000/api/auth
|
||||
NEXT_PUBLIC_IS_E2E: 1
|
||||
# EMAIL_FROM: e2e@cal.com
|
||||
# EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
||||
# EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
|
||||
# EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
|
||||
# EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD }}
|
||||
# MS_GRAPH_CLIENT_ID: xxx
|
||||
# MS_GRAPH_CLIENT_SECRET: xxx
|
||||
# ZOOM_CLIENT_ID: xxx
|
||||
# ZOOM_CLIENT_SECRET: xxx
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12.1
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: calendso
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Use Node ${{ matrix.node }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: "yarn"
|
||||
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@v2
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
~/Library/Caches/ms-playwright
|
||||
~/.cache/ms-playwright
|
||||
${{ github.workspace }}/node_modules/playwright
|
||||
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: cache-playwright-
|
||||
- run: yarn --frozen-lockfile
|
||||
- name: Install playwright deps
|
||||
# if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: yarn playwright install --with-deps
|
||||
- run: yarn embed-tests-prepare
|
||||
- run: yarn workspace @calcom/embed-core embed-tests-update-snapshots:ci
|
||||
- run: yarn workspace @calcom/embed-react embed-tests-update-snapshots:ci
|
||||
|
||||
- name: Upload embed-core results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: test-results-core
|
||||
path: packages/embeds/embed-core/playwright/results
|
||||
|
||||
- name: Upload embed-react results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: test-results-react
|
||||
path: packages/embeds/embed-react/playwright/results
|
|
@ -4,7 +4,7 @@ on:
|
|||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- public/static/locales/**
|
||||
- apps/web/public/static/locales/**
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 20
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
"Website(3001)",
|
||||
"Embed Core(3100)",
|
||||
"Embed React(3101)",
|
||||
"Prisma Studio(5555)"
|
||||
"Prisma Studio(5555)",
|
||||
"Maildev(587)"
|
||||
],
|
||||
// Mark as the default build task so cmd/ctrl+shift+b will create them
|
||||
"group": {
|
||||
|
@ -65,6 +66,13 @@
|
|||
"command": "yarn db-studio",
|
||||
"isBackground": false,
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Maildev(587)",
|
||||
"type": "shell",
|
||||
"command": "maildev -s 587",
|
||||
"isBackground": false,
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -4,6 +4,26 @@ Contributions are what make the open source community such an amazing place to b
|
|||
|
||||
- Before jumping into a PR be sure to search [existing PRs](https://github.com/calcom/cal.com/pulls) or [issues](https://github.com/calcom/cal.com/issues) for an open or closed item that relates to your submission.
|
||||
|
||||
## Areas of expertise
|
||||
|
||||
### Legend
|
||||
|
||||
✅ = has knowledge
|
||||
|
||||
🥇 = is their main priority
|
||||
|
||||
⚠️ = is the only one with knowledge
|
||||
|
||||
👀 = has no knowledge but wants to be onboarded
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://dynamic-svgs.vercel.app/image.svg?dark">
|
||||
<img alt="Areas of expertise table" src="https://dynamic-svgs.vercel.app/image.svg">
|
||||
</picture>
|
||||
|
||||
|
||||
|
||||
|
||||
## Developing
|
||||
|
||||
The development branch is `main`. This is the branch that all pull
|
||||
|
|
24
README.md
24
README.md
|
@ -18,22 +18,26 @@
|
|||
<a href="https://cal.com">Website</a>
|
||||
·
|
||||
<a href="https://github.com/calcom/cal.com/issues">Issues</a>
|
||||
·
|
||||
<a href="https://cal.com/roadmap">Roadmap</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cal.com/slack"><img src="https://img.shields.io/badge/Slack-calendso.slack.com-%234A154B" alt="Join Cal.com Slack"></a>
|
||||
<a href="https://www.producthunt.com/posts/calendso"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
|
||||
<a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a>
|
||||
<a href="https://news.ycombinator.com/item?id=26817795"><img src="https://img.shields.io/badge/Hacker%20News-311-%23FF6600" alt="Hacker News"></a>
|
||||
<a href="https://github.com/calcom/cal.com/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
|
||||
<a href="https://github.com/calcom/cal.com/pulse"><img src="https://img.shields.io/github/commit-activity/m/calcom/cal.com" alt="Commits-per-month"></a>
|
||||
<a href="https://cal.com/pricing"><img src="https://img.shields.io/badge/Pricing-%2412%2Fmonth-brightgreen" alt="Pricing"></a>
|
||||
<a href="https://jitsu.com?utm_source=github/calcom/cal.com"><img src="https://img.shields.io/badge/Metrics_tracked_by-JITSU-AA00FF?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACKSURBVHgBrZDRCYAwDEQv6gCOoKO4hOCXI9QVnEZwiY5iF5GaVClaBNtioCSUvCR3tMJaxIfZgW4AGUoEPVwgPZoS0Dmgg3NBVDFNbMIsmYCak3J1jDk9iCQvsKJvkzr71N81Gj6vDT/LU2P6RhY63jcafk3YJEbgeZpiFyc/5HJKv8Ef273NSfABGbQfUZhnOSAAAAAASUVORK5CYII=" alt="Jitsu Tracked"></a>
|
||||
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
|
||||
<a href="https://twitter.com/calcom"><img src="https://img.shields.io/twitter/follow/calcom?style=social"></a>
|
||||
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a>
|
||||
<a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
|
||||
<a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a>
|
||||
<a href="https://news.ycombinator.com/item?id=26817795"><img src="https://img.shields.io/badge/Hacker%20News-311-%23FF6600" alt="Hacker News"></a>
|
||||
<a href="https://github.com/calcom/cal.com/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
|
||||
<a href="https://github.com/calcom/cal.com/pulse"><img src="https://img.shields.io/github/commit-activity/m/calcom/cal.com" alt="Commits-per-month"></a>
|
||||
<a href="https://cal.com/pricing"><img src="https://img.shields.io/badge/Pricing-%2412%2Fmonth-brightgreen" alt="Pricing"></a>
|
||||
<a href="https://jitsu.com?utm_source=github/calcom/cal.com"><img src="https://img.shields.io/badge/Metrics_tracked_by-JITSU-AA00FF?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACKSURBVHgBrZDRCYAwDEQv6gCOoKO4hOCXI9QVnEZwiY5iF5GaVClaBNtioCSUvCR3tMJaxIfZgW4AGUoEPVwgPZoS0Dmgg3NBVDFNbMIsmYCak3J1jDk9iCQvsKJvkzr71N81Gj6vDT/LU2P6RhY63jcafk3YJEbgeZpiFyc/5HJKv8Ef273NSfABGbQfUZhnOSAAAAAASUVORK5CYII=" alt="Jitsu Tracked"></a>
|
||||
<img src="https://api.checklyhq.com/v1/badges/checks/5e048048-1b51-47ba-9209-60607507622e?responseTime=true" alt="Checkly Availability" />
|
||||
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
|
||||
<a href="https://twitter.com/calcom"><img src="https://img.shields.io/twitter/follow/calcom?style=flat"></a>
|
||||
<a href="https://twitch.tv/calcomtv"><img src="https://img.shields.io/twitch/status/calcomtv?style=flat"></a>
|
||||
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a>
|
||||
<a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
|
||||
</p>
|
||||
|
||||
<!-- ABOUT THE PROJECT -->
|
||||
|
|
2
apps/api
2
apps/api
|
@ -1 +1 @@
|
|||
Subproject commit a7889b34368eb37981b5c78953315a6ed5fc97cd
|
||||
Subproject commit ed2f42fb0195b1afa0bf2edbab1df2126038b273
|
|
@ -1 +1 @@
|
|||
Subproject commit 67476f0e24871730e4a7b06da99ee18d4f5179ce
|
||||
Subproject commit b6b26f47922a5404086bf34635338dc6afa9c1d3
|
|
@ -1,23 +1,21 @@
|
|||
import { useSession } from "next-auth/react";
|
||||
import React from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import NavTabs from "./NavTabs";
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
name: "app_store",
|
||||
href: "/apps",
|
||||
},
|
||||
{
|
||||
name: "installed_apps",
|
||||
href: "/apps/installed",
|
||||
},
|
||||
];
|
||||
|
||||
export default function AppsShell({ children }: { children: React.ReactNode }) {
|
||||
const { t } = useLocale();
|
||||
const { status } = useSession();
|
||||
const tabs = [
|
||||
{
|
||||
name: t("app_store"),
|
||||
href: "/apps",
|
||||
},
|
||||
{
|
||||
name: t("installed_apps"),
|
||||
href: "/apps/installed",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -1,30 +1,27 @@
|
|||
import React from "react";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import NavTabs from "./NavTabs";
|
||||
|
||||
export default function BookingsShell({ children }: { children: React.ReactNode }) {
|
||||
const { t } = useLocale();
|
||||
const tabs = [
|
||||
{
|
||||
name: t("upcoming"),
|
||||
href: "/bookings/upcoming",
|
||||
},
|
||||
{
|
||||
name: t("recurring"),
|
||||
href: "/bookings/recurring",
|
||||
},
|
||||
{
|
||||
name: t("past"),
|
||||
href: "/bookings/past",
|
||||
},
|
||||
{
|
||||
name: t("cancelled"),
|
||||
href: "/bookings/cancelled",
|
||||
},
|
||||
];
|
||||
const tabs = [
|
||||
{
|
||||
name: "upcoming",
|
||||
href: "/bookings/upcoming",
|
||||
},
|
||||
{
|
||||
name: "recurring",
|
||||
href: "/bookings/recurring",
|
||||
},
|
||||
{
|
||||
name: "past",
|
||||
href: "/bookings/past",
|
||||
},
|
||||
{
|
||||
name: "cancelled",
|
||||
href: "/bookings/cancelled",
|
||||
},
|
||||
];
|
||||
|
||||
export default function BookingsShell({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<NavTabs tabs={tabs} linkProps={{ shallow: true }} />
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
import { useBrandColors } from "@calcom/embed-core";
|
||||
import { useBrandColors } from "@calcom/embed-core/embed-iframe";
|
||||
|
||||
const brandColor = "#292929";
|
||||
const brandTextColor = "#ffffff";
|
||||
|
|
|
@ -9,17 +9,17 @@ export default function EmptyScreen({
|
|||
}: {
|
||||
Icon: SVGComponent;
|
||||
headline: string;
|
||||
description: string;
|
||||
description: string | React.ReactElement;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-80 my-6 flex flex-col items-center justify-center rounded-sm border border-dashed">
|
||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-white">
|
||||
<Icon className="inline-block h-10 w-10 bg-white" />
|
||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
||||
<Icon className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
|
||||
</div>
|
||||
<div className="max-w-[420px] text-center">
|
||||
<h2 className="mt-6 mb-1 text-lg font-medium">{headline}</h2>
|
||||
<p className="text-sm leading-6 text-gray-600">{description}</p>
|
||||
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{headline}</h2>
|
||||
<p className="text-sm leading-6 text-gray-600 dark:text-gray-300">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -4,6 +4,8 @@ import Link, { LinkProps } from "next/link";
|
|||
import { useRouter } from "next/router";
|
||||
import { FC, Fragment, MouseEventHandler } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
import { SVGComponent } from "@lib/types/SVGComponent";
|
||||
|
||||
|
@ -22,6 +24,7 @@ export interface NavTabProps {
|
|||
|
||||
const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
||||
const router = useRouter();
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<>
|
||||
<nav
|
||||
|
@ -77,7 +80,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
|||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
<span>{tab.name}</span>
|
||||
<span>{t(tab.name)}</span>
|
||||
</a>
|
||||
</Link>
|
||||
</Component>
|
||||
|
|
|
@ -1,48 +1,54 @@
|
|||
import { CreditCardIcon, KeyIcon, LockClosedIcon, UserGroupIcon, UserIcon } from "@heroicons/react/solid";
|
||||
import React from "react";
|
||||
import React, { ComponentProps } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import ErrorBoundary from "@lib/ErrorBoundary";
|
||||
|
||||
import NavTabs from "./NavTabs";
|
||||
import Shell from "./Shell";
|
||||
|
||||
export default function SettingsShell({ children }: { children: React.ReactNode }) {
|
||||
const { t } = useLocale();
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
name: t("profile"),
|
||||
href: "/settings/profile",
|
||||
icon: UserIcon,
|
||||
},
|
||||
{
|
||||
name: t("security"),
|
||||
href: "/settings/security",
|
||||
icon: KeyIcon,
|
||||
},
|
||||
{
|
||||
name: t("teams"),
|
||||
href: "/settings/teams",
|
||||
icon: UserGroupIcon,
|
||||
},
|
||||
{
|
||||
name: t("billing"),
|
||||
href: "/settings/billing",
|
||||
icon: CreditCardIcon,
|
||||
},
|
||||
{
|
||||
name: t("admin"),
|
||||
href: "/settings/admin",
|
||||
icon: LockClosedIcon,
|
||||
adminRequired: true,
|
||||
},
|
||||
];
|
||||
const tabs = [
|
||||
{
|
||||
name: "profile",
|
||||
href: "/settings/profile",
|
||||
icon: UserIcon,
|
||||
},
|
||||
{
|
||||
name: "security",
|
||||
href: "/settings/security",
|
||||
icon: KeyIcon,
|
||||
},
|
||||
{
|
||||
name: "teams",
|
||||
href: "/settings/teams",
|
||||
icon: UserGroupIcon,
|
||||
},
|
||||
{
|
||||
name: "billing",
|
||||
href: "/settings/billing",
|
||||
icon: CreditCardIcon,
|
||||
},
|
||||
{
|
||||
name: "admin",
|
||||
href: "/settings/admin",
|
||||
icon: LockClosedIcon,
|
||||
adminRequired: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default function SettingsShell({
|
||||
children,
|
||||
...rest
|
||||
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||
return (
|
||||
<>
|
||||
<Shell {...rest}>
|
||||
<div className="sm:mx-auto">
|
||||
<NavTabs tabs={tabs} />
|
||||
</div>
|
||||
<main className="max-w-4xl">{children}</main>
|
||||
</>
|
||||
<main className="max-w-4xl">
|
||||
<>
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
</>
|
||||
</main>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,15 +10,16 @@ import {
|
|||
MapIcon,
|
||||
MoonIcon,
|
||||
ViewGridIcon,
|
||||
QuestionMarkCircleIcon,
|
||||
} from "@heroicons/react/solid";
|
||||
import { UserPlan } from "@prisma/client";
|
||||
import { SessionContextValue, signOut, useSession } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { Fragment, ReactNode, useEffect } from "react";
|
||||
import React, { Fragment, ReactNode, useEffect, useState } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { useIsEmbed } from "@calcom/embed-core";
|
||||
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import Button from "@calcom/ui/Button";
|
||||
import Dropdown, {
|
||||
|
@ -31,6 +32,7 @@ import LicenseBanner from "@ee/components/LicenseBanner";
|
|||
import TrialBanner from "@ee/components/TrialBanner";
|
||||
import HelpMenuItem from "@ee/components/support/HelpMenuItem";
|
||||
|
||||
import ErrorBoundary from "@lib/ErrorBoundary";
|
||||
import classNames from "@lib/classNames";
|
||||
import { WEBAPP_URL } from "@lib/config/constants";
|
||||
import { shouldShowOnboarding } from "@lib/getting-started";
|
||||
|
@ -348,7 +350,7 @@ const Layout = ({
|
|||
"px-4 sm:px-6 md:px-8",
|
||||
props.flexChildrenContainer && "flex flex-1 flex-col"
|
||||
)}>
|
||||
{!props.isLoading ? props.children : props.customLoader}
|
||||
<ErrorBoundary>{!props.isLoading ? props.children : props.customLoader}</ErrorBoundary>
|
||||
</div>
|
||||
{/* show bottom navigation for md and smaller (tablet and phones) */}
|
||||
{status === "authenticated" && (
|
||||
|
@ -461,8 +463,10 @@ function UserDropdown({ small }: { small?: boolean }) {
|
|||
},
|
||||
});
|
||||
const utils = trpc.useContext();
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dropdown>
|
||||
<Dropdown onOpenChange={() => setHelpOpen(false)}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="group flex w-full cursor-pointer appearance-none items-center text-left">
|
||||
<span
|
||||
|
@ -474,7 +478,7 @@ function UserDropdown({ small }: { small?: boolean }) {
|
|||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
className="rounded-full"
|
||||
src={process.env.NEXT_PUBLIC_WEBSITE_URL + "/" + user?.username + "/avatar.png"}
|
||||
src={WEBAPP_URL + "/" + user?.username + "/avatar.png"}
|
||||
alt={user?.username || "Nameless User"}
|
||||
/>
|
||||
}
|
||||
|
@ -504,96 +508,115 @@ function UserDropdown({ small }: { small?: boolean }) {
|
|||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent portalled={true}>
|
||||
<DropdownMenuItem>
|
||||
<a
|
||||
onClick={() => {
|
||||
mutation.mutate({ away: !user?.away });
|
||||
utils.invalidateQueries("viewer.me");
|
||||
}}
|
||||
className="flex min-w-max cursor-pointer px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900">
|
||||
<MoonIcon
|
||||
className={classNames(
|
||||
user?.away
|
||||
? "text-purple-500 group-hover:text-purple-700"
|
||||
: "text-gray-500 group-hover:text-gray-700",
|
||||
"h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{user?.away ? t("set_as_free") : t("set_as_away")}
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||
{user?.username && (
|
||||
<DropdownMenuItem>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}`}
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700">
|
||||
<ExternalLinkIcon className="h-5 w-5 text-gray-500 ltr:mr-3 rtl:ml-3" /> {t("view_public_page")}
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
{helpOpen ? (
|
||||
<HelpMenuItem />
|
||||
) : (
|
||||
<>
|
||||
<DropdownMenuItem>
|
||||
<a
|
||||
onClick={() => {
|
||||
mutation.mutate({ away: !user?.away });
|
||||
utils.invalidateQueries("viewer.me");
|
||||
}}
|
||||
className="flex min-w-max cursor-pointer px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900">
|
||||
<MoonIcon
|
||||
className={classNames(
|
||||
user?.away
|
||||
? "text-purple-500 group-hover:text-purple-700"
|
||||
: "text-gray-500 group-hover:text-gray-700",
|
||||
"h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{user?.away ? t("set_as_free") : t("set_as_away")}
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||
{user?.username && (
|
||||
<DropdownMenuItem>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}`}
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700">
|
||||
<ExternalLinkIcon className="h-5 w-5 text-gray-500 ltr:mr-3 rtl:ml-3" />{" "}
|
||||
{t("view_public_page")}
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||
<DropdownMenuItem>
|
||||
<a
|
||||
href="https://cal.com/slack"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900">
|
||||
<svg
|
||||
viewBox="0 0 2447.6 2452.5"
|
||||
className={classNames(
|
||||
"text-gray-500 group-hover:text-gray-700",
|
||||
"mt-0.5 h-4 w-4 flex-shrink-0 ltr:mr-4 rtl:ml-4"
|
||||
)}
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipRule="evenodd" fillRule="evenodd">
|
||||
<path
|
||||
d="m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z"
|
||||
fill="currentColor"></path>
|
||||
<path
|
||||
d="m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z"
|
||||
fill="currentColor"></path>
|
||||
<path
|
||||
d="m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z"
|
||||
fill="currentColor"></path>
|
||||
<path
|
||||
d="m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0"
|
||||
fill="currentColor"></path>
|
||||
</g>
|
||||
</svg>
|
||||
{t("join_our_slack")}
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://cal.com/roadmap"
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700">
|
||||
<MapIcon className="h-5 w-5 text-gray-500 ltr:mr-3 rtl:ml-3" /> {t("visit_roadmap")}
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<button
|
||||
className="flex w-full px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-gray-100 hover:text-gray-900"
|
||||
onClick={() => setHelpOpen(true)}>
|
||||
<QuestionMarkCircleIcon
|
||||
className={classNames(
|
||||
"text-gray-500 group-hover:text-neutral-500",
|
||||
"h-5 w-5 flex-shrink-0 ltr:mr-3"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{t("help")}
|
||||
</button>
|
||||
|
||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||
<DropdownMenuItem>
|
||||
<a
|
||||
onClick={() => signOut({ callbackUrl: "/auth/logout" })}
|
||||
className="flex cursor-pointer px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900">
|
||||
<LogoutIcon
|
||||
className={classNames(
|
||||
"text-gray-500 group-hover:text-gray-700",
|
||||
"h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t("sign_out")}
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||
<DropdownMenuItem>
|
||||
<a
|
||||
href="https://cal.com/slack"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900">
|
||||
<svg
|
||||
viewBox="0 0 2447.6 2452.5"
|
||||
className={classNames(
|
||||
"text-gray-500 group-hover:text-gray-700",
|
||||
"mt-0.5 h-4 w-4 flex-shrink-0 ltr:mr-4 rtl:ml-4"
|
||||
)}
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipRule="evenodd" fillRule="evenodd">
|
||||
<path
|
||||
d="m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z"
|
||||
fill="currentColor"></path>
|
||||
<path
|
||||
d="m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z"
|
||||
fill="currentColor"></path>
|
||||
<path
|
||||
d="m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z"
|
||||
fill="currentColor"></path>
|
||||
<path
|
||||
d="m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0"
|
||||
fill="currentColor"></path>
|
||||
</g>
|
||||
</svg>
|
||||
{t("join_our_slack")}
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://cal.com/roadmap"
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700">
|
||||
<MapIcon className="h-5 w-5 text-gray-500 ltr:mr-3 rtl:ml-3" /> {t("visit_roadmap")}
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<HelpMenuItem />
|
||||
|
||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||
<DropdownMenuItem>
|
||||
<a
|
||||
onClick={() => signOut({ callbackUrl: "/auth/logout" })}
|
||||
className="flex cursor-pointer px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900">
|
||||
<LogoutIcon
|
||||
className={classNames(
|
||||
"text-gray-500 group-hover:text-gray-700",
|
||||
"h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t("sign_out")}
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
);
|
||||
|
|
|
@ -46,20 +46,17 @@ export function NewScheduleButton({ name = "new-schedule" }: { name?: string })
|
|||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<div className="mb-4">
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
||||
{t("add_new_schedule")}
|
||||
</h3>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">{t("new_event_type_to_book_description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={(values) => {
|
||||
createMutation.mutate(values);
|
||||
}}>
|
||||
<div className="mt-3 space-y-4">
|
||||
<div className="mt-3 space-y-2">
|
||||
<label htmlFor="label" className="block text-sm font-medium text-gray-700">
|
||||
{t("name")}
|
||||
</label>
|
||||
|
|
|
@ -29,6 +29,7 @@ type AvailableTimesProps = {
|
|||
username: string | null;
|
||||
}[];
|
||||
schedulingType: SchedulingType | null;
|
||||
seatsPerTimeSlot?: number | null;
|
||||
};
|
||||
|
||||
const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
|
@ -44,6 +45,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||
schedulingType,
|
||||
beforeBufferTime,
|
||||
afterBufferTime,
|
||||
seatsPerTimeSlot,
|
||||
}) => {
|
||||
const { t, i18n } = useLocale();
|
||||
const router = useRouter();
|
||||
|
@ -105,18 +107,48 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||
bookingUrl.query.user = slot.users;
|
||||
}
|
||||
|
||||
// If event already has an attendee add booking id
|
||||
if (slot.bookingUid) {
|
||||
bookingUrl.query.bookingUid = slot.bookingUid;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={slot.time.format()}>
|
||||
<Link href={bookingUrl}>
|
||||
<a
|
||||
{/* Current there is no way to disable Next.js Links */}
|
||||
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
||||
<div
|
||||
className={classNames(
|
||||
"text-bookingdarker hover:bg-brand hover:text-brandcontrast dark:hover:bg-darkmodebrand dark:hover:text-darkmodebrandcontrast mb-2 block rounded-sm border bg-white py-4 font-medium hover:text-white dark:border-transparent dark:bg-gray-600 dark:text-neutral-200 dark:hover:border-black",
|
||||
"text-primary-500 mb-2 block rounded-sm border bg-white py-4 font-medium opacity-25 dark:border-transparent dark:bg-gray-600 dark:text-neutral-200 ",
|
||||
brand === "#fff" || brand === "#ffffff" ? "border-brandcontrast" : "border-brand"
|
||||
)}
|
||||
data-testid="time">
|
||||
{dayjs.tz(slot.time, timeZone()).format(timeFormat)}
|
||||
</a>
|
||||
</Link>
|
||||
)}>
|
||||
{slot.time.format(timeFormat)}
|
||||
{seatsPerTimeSlot && <p className={`text-sm`}>{t("booking_full")}</p>}
|
||||
</div>
|
||||
) : (
|
||||
<Link href={bookingUrl}>
|
||||
<a
|
||||
className={classNames(
|
||||
"text-primary-500 hover:bg-brand hover:text-brandcontrast dark:hover:bg-darkmodebrand dark:hover:text-darkmodebrandcontrast mb-2 block rounded-sm border bg-white py-4 font-medium hover:text-white dark:border-transparent dark:bg-gray-600 dark:text-neutral-200 dark:hover:border-black",
|
||||
brand === "#fff" || brand === "#ffffff" ? "border-brandcontrast" : "border-brand"
|
||||
)}
|
||||
data-testid="time">
|
||||
{dayjs.tz(slot.time, timeZone()).format(timeFormat)}
|
||||
{seatsPerTimeSlot && (
|
||||
<p
|
||||
className={`${
|
||||
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
|
||||
? "text-rose-600"
|
||||
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
|
||||
? "text-yellow-500"
|
||||
: "text-emerald-400"
|
||||
} text-sm`}>
|
||||
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
|
||||
{seatsPerTimeSlot} {t("seats_available")}
|
||||
</p>
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -2,6 +2,7 @@ import {
|
|||
BanIcon,
|
||||
CheckIcon,
|
||||
ClockIcon,
|
||||
LocationMarkerIcon,
|
||||
PaperAirplaneIcon,
|
||||
PencilAltIcon,
|
||||
XIcon,
|
||||
|
@ -16,6 +17,7 @@ import { Frequency as RRuleFrequency } from "rrule";
|
|||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
import Button from "@calcom/ui/Button";
|
||||
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
||||
|
@ -23,9 +25,11 @@ import { TextArea } from "@calcom/ui/form/fields";
|
|||
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
import { LocationType } from "@lib/location";
|
||||
import { parseRecurringDates } from "@lib/parseDate";
|
||||
import { inferQueryInput, inferQueryOutput, trpc } from "@lib/trpc";
|
||||
|
||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
||||
import TableActions, { ActionType } from "@components/ui/TableActions";
|
||||
|
||||
|
@ -72,6 +76,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
if (!res.ok) {
|
||||
throw new HttpError({ statusCode: res.status });
|
||||
}
|
||||
setRejectionDialogIsOpen(false);
|
||||
},
|
||||
{
|
||||
async onSettled() {
|
||||
|
@ -89,8 +94,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
booking.listingStatus === "upcoming" && booking.recurringEventId !== null
|
||||
? t("reject_all")
|
||||
: t("reject"),
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
onClick: () => {
|
||||
setRejectionDialogIsOpen(true);
|
||||
},
|
||||
icon: BanIcon,
|
||||
|
@ -102,8 +106,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
booking.listingStatus === "upcoming" && booking.recurringEventId !== null
|
||||
? t("confirm_all")
|
||||
: t("confirm"),
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
onClick: () => {
|
||||
mutation.mutate(true);
|
||||
},
|
||||
icon: CheckIcon,
|
||||
|
@ -120,25 +123,33 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
icon: XIcon,
|
||||
},
|
||||
{
|
||||
id: "reschedule",
|
||||
label: t("reschedule"),
|
||||
icon: ClockIcon,
|
||||
id: "edit_booking",
|
||||
label: t("edit_booking"),
|
||||
icon: PencilAltIcon,
|
||||
actions: [
|
||||
{
|
||||
id: "edit",
|
||||
icon: PencilAltIcon,
|
||||
label: t("edit_booking"),
|
||||
id: "reschedule",
|
||||
icon: ClockIcon,
|
||||
label: t("reschedule_booking"),
|
||||
href: `/reschedule/${booking.uid}`,
|
||||
},
|
||||
{
|
||||
id: "reschedule_request",
|
||||
icon: ClockIcon,
|
||||
icon: PaperAirplaneIcon,
|
||||
iconClassName: "rotate-45 w-[18px] -ml-[2px]",
|
||||
label: t("send_reschedule_request"),
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
onClick: () => {
|
||||
setIsOpenRescheduleDialog(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "change_location",
|
||||
label: t("edit_location"),
|
||||
onClick: () => {
|
||||
setIsOpenLocationDialog(true);
|
||||
},
|
||||
icon: LocationMarkerIcon,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
@ -154,6 +165,26 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
|
||||
const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
|
||||
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
|
||||
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
|
||||
const setLocationMutation = trpc.useMutation("viewer.bookings.editLocation", {
|
||||
onSuccess: () => {
|
||||
showToast(t("location_updated"), "success");
|
||||
setIsOpenLocationDialog(false);
|
||||
utils.invalidateQueries("viewer.bookings");
|
||||
},
|
||||
});
|
||||
|
||||
const saveLocation = (newLocationType: LocationType, details: { [key: string]: string }) => {
|
||||
let newLocation = newLocationType as string;
|
||||
if (
|
||||
newLocationType === LocationType.InPerson ||
|
||||
newLocationType === LocationType.Link ||
|
||||
newLocationType === LocationType.UserPhone
|
||||
) {
|
||||
newLocation = details[Object.keys(details)[0]];
|
||||
}
|
||||
setLocationMutation.mutate({ bookingId: booking.id, newLocation });
|
||||
};
|
||||
|
||||
// Calculate the booking date(s)
|
||||
let recurringStrings: string[] = [];
|
||||
|
@ -168,6 +199,30 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
);
|
||||
}
|
||||
|
||||
const onClick = () => {
|
||||
router.push({
|
||||
pathname: "/success",
|
||||
query: {
|
||||
date: booking.startTime,
|
||||
type: booking.eventType.id,
|
||||
eventSlug: booking.eventType.slug,
|
||||
user: user?.username || "",
|
||||
name: booking.attendees[0] ? booking.attendees[0].name : undefined,
|
||||
email: booking.attendees[0] ? booking.attendees[0].email : undefined,
|
||||
location: booking.location
|
||||
? booking.location.includes("integration")
|
||||
? (t("web_conferencing_details_to_follow") as string)
|
||||
: booking.location
|
||||
: "",
|
||||
eventName: booking.eventType.eventName || "",
|
||||
bookingId: booking.id,
|
||||
recur: booking.recurringEventId,
|
||||
reschedule: booking.confirmed,
|
||||
listingStatus: booking.listingStatus,
|
||||
status: booking.status,
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<RescheduleDialog
|
||||
|
@ -175,6 +230,12 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
setIsOpenDialog={setIsOpenRescheduleDialog}
|
||||
bookingUId={booking.uid}
|
||||
/>
|
||||
<EditLocationDialog
|
||||
booking={booking}
|
||||
saveLocation={saveLocation}
|
||||
isOpenDialog={isOpenSetLocationDialog}
|
||||
setShowLocationModal={setIsOpenLocationDialog}
|
||||
/>
|
||||
|
||||
{/* NOTE: Should refactor this dialog component as is being rendered multiple times */}
|
||||
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
|
||||
|
@ -209,114 +270,103 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<tr
|
||||
className="flex cursor-pointer hover:bg-neutral-50"
|
||||
onClick={() =>
|
||||
router.push({
|
||||
pathname: "/success",
|
||||
query: {
|
||||
date: booking.startTime,
|
||||
type: booking.eventType.id,
|
||||
eventSlug: booking.eventType.slug,
|
||||
user: user?.username || "",
|
||||
name: booking.attendees[0].name,
|
||||
email: booking.attendees[0].email,
|
||||
location: booking.location
|
||||
? booking.location.includes("integration")
|
||||
? (t("web_conferencing_details_to_follow") as string)
|
||||
: booking.location
|
||||
: "",
|
||||
eventName: booking.eventType.eventName || "",
|
||||
bookingId: booking.id,
|
||||
recur: booking.recurringEventId,
|
||||
reschedule: booking.confirmed,
|
||||
},
|
||||
})
|
||||
}>
|
||||
<td className="hidden whitespace-nowrap py-4 align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:w-56">
|
||||
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{dayjs(booking.startTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")} -{" "}
|
||||
{dayjs(booking.endTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
{booking.recurringCount &&
|
||||
booking.eventType?.recurringEvent?.freq &&
|
||||
booking.listingStatus === "upcoming" && (
|
||||
<div className="underline decoration-gray-400 decoration-dashed underline-offset-2">
|
||||
<div className="flex">
|
||||
<Tooltip
|
||||
content={recurringStrings.map((aDate, key) => (
|
||||
<p key={key}>{aDate}</p>
|
||||
))}>
|
||||
<p className="text-gray-600 dark:text-white">
|
||||
<RefreshIcon className="mr-1 -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
{`${t("every_for_freq", {
|
||||
freq: t(
|
||||
<tr className="flex hover:bg-neutral-50">
|
||||
<td
|
||||
className="hidden whitespace-nowrap align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:w-56"
|
||||
onClick={onClick}>
|
||||
<div className="cursor-pointer py-4">
|
||||
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{dayjs(booking.startTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")} -{" "}
|
||||
{dayjs(booking.endTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
{booking.recurringCount &&
|
||||
booking.eventType?.recurringEvent?.freq &&
|
||||
booking.listingStatus === "upcoming" && (
|
||||
<div className="underline decoration-gray-400 decoration-dashed underline-offset-2">
|
||||
<div className="flex">
|
||||
<Tooltip
|
||||
content={recurringStrings.map((aDate, key) => (
|
||||
<p key={key}>{aDate}</p>
|
||||
))}>
|
||||
<p className="text-gray-600 dark:text-white">
|
||||
<RefreshIcon className="mr-1 -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
{`${t("every_for_freq", {
|
||||
freq: t(
|
||||
`${RRuleFrequency[booking.eventType.recurringEvent.freq]
|
||||
.toString()
|
||||
.toLowerCase()}`
|
||||
),
|
||||
})} ${booking.recurringCount} ${t(
|
||||
`${RRuleFrequency[booking.eventType.recurringEvent.freq]
|
||||
.toString()
|
||||
.toLowerCase()}`
|
||||
),
|
||||
})} ${booking.recurringCount} ${t(
|
||||
`${RRuleFrequency[booking.eventType.recurringEvent.freq].toString().toLowerCase()}`,
|
||||
{ count: booking.recurringCount }
|
||||
)}`}
|
||||
</p>
|
||||
</Tooltip>
|
||||
.toLowerCase()}`,
|
||||
{ count: booking.recurringCount }
|
||||
)}`}
|
||||
</p>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className={"flex-1 py-4 ltr:pl-4 rtl:pr-4" + (booking.rejected ? " line-through" : "")}>
|
||||
<div className="sm:hidden">
|
||||
{!booking.confirmed && !booking.rejected && (
|
||||
<Tag className="mb-2 ltr:mr-2 rtl:ml-2">{t("unconfirmed")}</Tag>
|
||||
)}
|
||||
{!!booking?.eventType?.price && !booking.paid && (
|
||||
<Tag className="mb-2 ltr:mr-2 rtl:ml-2">Pending payment</Tag>
|
||||
)}
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{startTime}:{" "}
|
||||
<small className="text-sm text-gray-500">
|
||||
{dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")}
|
||||
</small>
|
||||
<td
|
||||
className={"flex-1 ltr:pl-4 rtl:pr-4" + (booking.rejected ? " line-through" : "")}
|
||||
onClick={onClick}>
|
||||
<div className="cursor-pointer py-4">
|
||||
<div className="sm:hidden">
|
||||
{!booking.confirmed && !booking.rejected && (
|
||||
<Tag className="mb-2 ltr:mr-2 rtl:ml-2">{t("unconfirmed")}</Tag>
|
||||
)}
|
||||
{!!booking?.eventType?.price && !booking.paid && (
|
||||
<Tag className="mb-2 ltr:mr-2 rtl:ml-2">Pending payment</Tag>
|
||||
)}
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{startTime}:{" "}
|
||||
<small className="text-sm text-gray-500">
|
||||
{dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
title={booking.title}
|
||||
className={classNames(
|
||||
"max-w-56 truncate text-sm font-medium leading-6 text-neutral-900 md:max-w-max",
|
||||
isCancelled ? "line-through" : ""
|
||||
)}>
|
||||
{booking.eventType?.team && <strong>{booking.eventType.team.name}: </strong>}
|
||||
{booking.title}
|
||||
{!!booking?.eventType?.price && !booking.paid && (
|
||||
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">Pending payment</Tag>
|
||||
)}
|
||||
{!booking.confirmed && !booking.rejected && (
|
||||
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">{t("unconfirmed")}</Tag>
|
||||
)}
|
||||
</div>
|
||||
{booking.description && (
|
||||
<div className="max-w-52 md:max-w-96 truncate text-sm text-gray-500" title={booking.description}>
|
||||
"{booking.description}"
|
||||
<div
|
||||
title={booking.title}
|
||||
className={classNames(
|
||||
"max-w-56 truncate text-sm font-medium leading-6 text-neutral-900 md:max-w-max",
|
||||
isCancelled ? "line-through" : ""
|
||||
)}>
|
||||
{booking.eventType?.team && <strong>{booking.eventType.team.name}: </strong>}
|
||||
{booking.title}
|
||||
{!!booking?.eventType?.price && !booking.paid && (
|
||||
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">Pending payment</Tag>
|
||||
)}
|
||||
{!booking.confirmed && !booking.rejected && (
|
||||
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">{t("unconfirmed")}</Tag>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{booking.description && (
|
||||
<div
|
||||
className="max-w-52 md:max-w-96 truncate text-sm text-gray-500"
|
||||
title={booking.description}>
|
||||
"{booking.description}"
|
||||
</div>
|
||||
)}
|
||||
|
||||
{booking.attendees.length !== 0 && (
|
||||
<a
|
||||
className="text-sm text-gray-900 hover:text-blue-500"
|
||||
href={"mailto:" + booking.attendees[0].email}
|
||||
onClick={(e) => e.stopPropagation()}>
|
||||
{booking.attendees[0].email}
|
||||
</a>
|
||||
)}
|
||||
{isCancelled && booking.rescheduled && (
|
||||
<div className="mt-2 inline-block text-left text-sm md:hidden">
|
||||
<RequestSentMessage />
|
||||
</div>
|
||||
)}
|
||||
{booking.attendees.length !== 0 && (
|
||||
<a
|
||||
className="text-sm text-gray-900 hover:text-blue-500"
|
||||
href={"mailto:" + booking.attendees[0].email}
|
||||
onClick={(e) => e.stopPropagation()}>
|
||||
{booking.attendees[0].email}
|
||||
</a>
|
||||
)}
|
||||
{isCancelled && booking.rescheduled && (
|
||||
<div className="mt-2 inline-block text-left text-sm md:hidden">
|
||||
<RequestSentMessage />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="whitespace-nowrap py-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4">
|
||||
|
|
|
@ -7,7 +7,7 @@ import utc from "dayjs/plugin/utc";
|
|||
import { memoize } from "lodash";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useEmbedStyles } from "@calcom/embed-core";
|
||||
import { useEmbedStyles } from "@calcom/embed-core/embed-iframe";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
|
|
|
@ -8,27 +8,31 @@ import {
|
|||
CreditCardIcon,
|
||||
GlobeIcon,
|
||||
InformationCircleIcon,
|
||||
LocationMarkerIcon,
|
||||
RefreshIcon,
|
||||
VideoCameraIcon,
|
||||
} from "@heroicons/react/solid";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { useContracts } from "contexts/contractsContext";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { TFunction } from "next-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
import { Frequency as RRuleFrequency } from "rrule";
|
||||
|
||||
import { AppStoreLocationType, LocationObject, LocationType } from "@calcom/app-store/locations";
|
||||
import {
|
||||
useEmbedStyles,
|
||||
useIsEmbed,
|
||||
useIsBackgroundTransparent,
|
||||
sdkActionManager,
|
||||
useEmbedNonStylesConfig,
|
||||
} from "@calcom/embed-core";
|
||||
} from "@calcom/embed-core/embed-iframe";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
|
||||
|
@ -57,6 +61,35 @@ dayjs.extend(customParseFormat);
|
|||
|
||||
type Props = AvailabilityTeamPageProps | AvailabilityPageProps;
|
||||
|
||||
export const locationKeyToString = (location: LocationObject, t: TFunction) => {
|
||||
switch (location.type) {
|
||||
case LocationType.InPerson:
|
||||
return location.address || "In Person"; // If disabled address won't exist on the object
|
||||
case LocationType.Link:
|
||||
return location.link || "Link"; // If disabled link won't exist on the object
|
||||
case LocationType.Phone:
|
||||
return t("your_number");
|
||||
case LocationType.UserPhone:
|
||||
return t("phone_call");
|
||||
case LocationType.GoogleMeet:
|
||||
return "Google Meet";
|
||||
case LocationType.Zoom:
|
||||
return "Zoom";
|
||||
case LocationType.Daily:
|
||||
return "Cal Video";
|
||||
case LocationType.Jitsi:
|
||||
return "Jitsi";
|
||||
case LocationType.Huddle01:
|
||||
return "Huddle Video";
|
||||
case LocationType.Tandem:
|
||||
return "Tandem";
|
||||
case LocationType.Teams:
|
||||
return "Microsoft Teams";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage, booking }: Props) => {
|
||||
const router = useRouter();
|
||||
const isEmbed = useIsEmbed();
|
||||
|
@ -203,7 +236,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
|
|||
.filter((user) => user.name !== profile.name)
|
||||
.map((user) => ({
|
||||
title: user.name,
|
||||
image: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}/avatar.png`,
|
||||
image: `${CAL_URL}/${user.username}/avatar.png`,
|
||||
alt: user.name || undefined,
|
||||
})),
|
||||
].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[]
|
||||
|
@ -225,6 +258,25 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
|
|||
{eventType.description}
|
||||
</p>
|
||||
)}
|
||||
{eventType.locations.length === 1 && (
|
||||
<p className="text-bookinglight mb-2 dark:text-white">
|
||||
<LocationMarkerIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
{locationKeyToString(eventType.locations[0], t)}
|
||||
</p>
|
||||
)}
|
||||
{eventType.locations.length === 1 && (
|
||||
<p className="text-bookinglight mb-2 dark:text-white">
|
||||
{Object.values(AppStoreLocationType).includes(
|
||||
eventType.locations[0].type as unknown as AppStoreLocationType
|
||||
) ? (
|
||||
<VideoCameraIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
) : (
|
||||
<LocationMarkerIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
)}
|
||||
|
||||
{locationKeyToString(eventType.locations[0], t)}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-bookinglight mb-2 dark:text-white">
|
||||
<ClockIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4" />
|
||||
{eventType.length} {t("minutes")}
|
||||
|
@ -278,7 +330,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
|
|||
.map((user) => ({
|
||||
title: user.name,
|
||||
alt: user.name,
|
||||
image: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}/avatar.png`,
|
||||
image: `${CAL_URL}/${user.username}/avatar.png`,
|
||||
})),
|
||||
].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[]
|
||||
}
|
||||
|
@ -297,6 +349,38 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
|
|||
{eventType.description}
|
||||
</p>
|
||||
)}
|
||||
{eventType.locations.length === 1 && (
|
||||
<p className="text-bookinglight mb-2 dark:text-white">
|
||||
{Object.values(AppStoreLocationType).includes(
|
||||
eventType.locations[0].type as unknown as AppStoreLocationType
|
||||
) ? (
|
||||
<VideoCameraIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
) : (
|
||||
<LocationMarkerIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
)}
|
||||
|
||||
{locationKeyToString(eventType.locations[0], t)}
|
||||
</p>
|
||||
)}
|
||||
{eventType.locations.length > 1 && (
|
||||
<div className="text-bookinglight flex-warp mb-2 flex dark:text-white">
|
||||
<div className="mr-[10px] ml-[2px] -mt-1 ">
|
||||
<LocationMarkerIcon className="inline-block h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
<p>
|
||||
{eventType.locations.map((el, i, arr) => {
|
||||
return (
|
||||
<span key={el.type}>
|
||||
{locationKeyToString(el, t)}{" "}
|
||||
{arr.length - 1 !== i && (
|
||||
<span className="font-light"> {t("or_lowercase")} </span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-bookinglight mb-3 dark:text-white">
|
||||
<ClockIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
||||
{eventType.length} {t("minutes")}
|
||||
|
@ -340,12 +424,11 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
|
|||
</IntlProvider>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<TimezoneDropdown />
|
||||
{previousPage === `${WEBAPP_URL}/${profile.slug}` && (
|
||||
<div className="flex h-full flex-col justify-end">
|
||||
<ArrowLeftIcon
|
||||
className="h-4 w-4 text-black transition-opacity hover:cursor-pointer dark:text-white"
|
||||
className="h-4 w-4 text-black transition-opacity hover:cursor-pointer dark:text-white"
|
||||
onClick={() => router.back()}
|
||||
/>
|
||||
<p className="sr-only">Go Back</p>
|
||||
|
@ -398,6 +481,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
|
|||
schedulingType={eventType.schedulingType ?? null}
|
||||
beforeBufferTime={eventType.beforeEventBuffer}
|
||||
afterBufferTime={eventType.afterEventBuffer}
|
||||
seatsPerTimeSlot={eventType.seatsPerTimeSlot}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -24,7 +24,11 @@ import { Frequency as RRuleFrequency } from "rrule";
|
|||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useEmbedNonStylesConfig, useIsBackgroundTransparent, useIsEmbed } from "@calcom/embed-core";
|
||||
import {
|
||||
useEmbedNonStylesConfig,
|
||||
useIsBackgroundTransparent,
|
||||
useIsEmbed,
|
||||
} from "@calcom/embed-core/embed-iframe";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
@ -37,7 +41,7 @@ import { asStringOrNull } from "@lib/asStringOrNull";
|
|||
import { timeZone } from "@lib/clock";
|
||||
import { ensureArray } from "@lib/ensureArray";
|
||||
import useTheme from "@lib/hooks/useTheme";
|
||||
import { LocationType } from "@lib/location";
|
||||
import { LocationObject, LocationType } from "@lib/location";
|
||||
import createBooking from "@lib/mutations/bookings/create-booking";
|
||||
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
||||
import { parseDate, parseRecurringDates } from "@lib/parseDate";
|
||||
|
@ -79,6 +83,7 @@ type BookingFormValues = {
|
|||
customInputs?: {
|
||||
[key: string]: string | boolean;
|
||||
};
|
||||
rescheduleReason?: string;
|
||||
};
|
||||
|
||||
const BookingPage = ({
|
||||
|
@ -203,10 +208,9 @@ const BookingPage = ({
|
|||
|
||||
const eventTypeDetail = { isWeb3Active: false, ...eventType };
|
||||
|
||||
type Location = { type: LocationType; address?: string; link?: string; hostPhoneNumber?: string };
|
||||
// it would be nice if Prisma at some point in the future allowed for Json<Location>; as of now this is not the case.
|
||||
const locations: Location[] = useMemo(
|
||||
() => (eventType.locations as Location[]) || [],
|
||||
const locations: LocationObject[] = useMemo(
|
||||
() => (eventType.locations as LocationObject[]) || [],
|
||||
[eventType.locations]
|
||||
);
|
||||
|
||||
|
@ -252,6 +256,7 @@ const BookingPage = ({
|
|||
email: primaryAttendee.email || "",
|
||||
guests: guestListEmails,
|
||||
notes: booking.description || "",
|
||||
rescheduleReason: "",
|
||||
customInputs: eventType.customInputs.reduce(
|
||||
(customInputs, input) => ({
|
||||
...customInputs,
|
||||
|
@ -396,6 +401,7 @@ const BookingPage = ({
|
|||
timeZone: timeZone(),
|
||||
language: i18n.language,
|
||||
rescheduleUid,
|
||||
bookingUid: router.query.bookingUid as string,
|
||||
user: router.query.user,
|
||||
location: getLocationValue(
|
||||
booking.locationType ? booking : { ...booking, locationType: selectedLocation }
|
||||
|
@ -469,6 +475,21 @@ const BookingPage = ({
|
|||
<h1 className="text-bookingdark mb-4 text-xl font-semibold dark:text-white">
|
||||
{eventType.title}
|
||||
</h1>
|
||||
{eventType.seatsPerTimeSlot && (
|
||||
<p
|
||||
className={`${
|
||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||
? "text-rose-600"
|
||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||
? "text-yellow-500"
|
||||
: "text-emerald-400"
|
||||
} mb-2`}>
|
||||
{booking
|
||||
? eventType.seatsPerTimeSlot - booking.attendees.length
|
||||
: eventType.seatsPerTimeSlot}{" "}
|
||||
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
||||
</p>
|
||||
)}
|
||||
{eventType?.description && (
|
||||
<p className="text-bookinglight mb-2 dark:text-white">
|
||||
<InformationCircleIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
|
@ -767,18 +788,31 @@ const BookingPage = ({
|
|||
<label
|
||||
htmlFor="notes"
|
||||
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{t("additional_notes")}
|
||||
{rescheduleUid ? t("reschedule_optional") : t("additional_notes")}
|
||||
</label>
|
||||
<textarea
|
||||
{...bookingForm.register("notes")}
|
||||
id="notes"
|
||||
name="notes"
|
||||
rows={3}
|
||||
className={inputClassName}
|
||||
placeholder={t("share_additional_notes")}
|
||||
disabled={disabledExceptForOwner}
|
||||
/>
|
||||
{rescheduleUid ? (
|
||||
<textarea
|
||||
{...bookingForm.register("rescheduleReason")}
|
||||
id="rescheduleReason"
|
||||
name="rescheduleReason"
|
||||
rows={3}
|
||||
className={inputClassName}
|
||||
placeholder={t("reschedule_placeholder")}
|
||||
disabled={disabledExceptForOwner}
|
||||
/>
|
||||
) : (
|
||||
<textarea
|
||||
{...bookingForm.register("notes")}
|
||||
id="notes"
|
||||
name="notes"
|
||||
rows={3}
|
||||
className={inputClassName}
|
||||
placeholder={t("share_additional_notes")}
|
||||
disabled={disabledExceptForOwner}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-2 rtl:space-x-reverse">
|
||||
<Button
|
||||
type="submit"
|
||||
|
|
|
@ -0,0 +1,336 @@
|
|||
import { LocationMarkerIcon } from "@heroicons/react/solid";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
|
||||
import { Form } from "@calcom/ui/form/fields";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
import { linkValueToString } from "@lib/linkValueToString";
|
||||
import { LocationType } from "@lib/location";
|
||||
import { LocationOptionsToString } from "@lib/locationOptions";
|
||||
import { inferQueryOutput, trpc } from "@lib/trpc";
|
||||
|
||||
import CheckboxField from "@components/ui/form/CheckboxField";
|
||||
import type PhoneInputType from "@components/ui/form/PhoneInput";
|
||||
import Select from "@components/ui/form/Select";
|
||||
|
||||
const PhoneInput = dynamic(
|
||||
() => import("@components/ui/form/PhoneInput")
|
||||
) as unknown as typeof PhoneInputType;
|
||||
|
||||
type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
|
||||
|
||||
type OptionTypeBase = {
|
||||
label: string;
|
||||
value: LocationType;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type LocationFormValues = {
|
||||
locationType: LocationType;
|
||||
locationAddress?: string;
|
||||
locationLink?: string;
|
||||
locationPhoneNumber?: string;
|
||||
displayLocationPublicly?: boolean;
|
||||
};
|
||||
interface ISetLocationDialog {
|
||||
saveLocation: (newLocationType: LocationType, details: { [key: string]: string }) => void;
|
||||
selection?: OptionTypeBase;
|
||||
booking?: BookingItem;
|
||||
defaultValues?: {
|
||||
type: LocationType;
|
||||
address?: string | undefined;
|
||||
link?: string | undefined;
|
||||
hostPhoneNumber?: string | undefined;
|
||||
displayLocationPublicly?: boolean | undefined;
|
||||
}[];
|
||||
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isOpenDialog: boolean;
|
||||
setSelectedLocation?: (param: OptionTypeBase | undefined) => void;
|
||||
}
|
||||
|
||||
export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
const {
|
||||
saveLocation,
|
||||
selection,
|
||||
booking,
|
||||
setShowLocationModal,
|
||||
isOpenDialog,
|
||||
defaultValues,
|
||||
setSelectedLocation,
|
||||
} = props;
|
||||
const { t } = useLocale();
|
||||
const locationsQuery = trpc.useQuery(["viewer.locationOptions"]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selection) {
|
||||
locationFormMethods.setValue("locationType", selection?.value);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selection]);
|
||||
|
||||
const locationFormSchema = z.object({
|
||||
locationType: z.string(),
|
||||
locationAddress: z.string().optional(),
|
||||
locationLink: z.string().url().optional(),
|
||||
displayLocationPublicly: z.boolean().optional(),
|
||||
locationPhoneNumber: z
|
||||
.string()
|
||||
.refine((val) => isValidPhoneNumber(val))
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const locationFormMethods = useForm<LocationFormValues>({
|
||||
mode: "onSubmit",
|
||||
resolver: zodResolver(locationFormSchema),
|
||||
});
|
||||
|
||||
const selectedLocation = useWatch({
|
||||
control: locationFormMethods.control,
|
||||
name: "locationType",
|
||||
});
|
||||
|
||||
const LocationOptions =
|
||||
selectedLocation === LocationType.InPerson ? (
|
||||
<>
|
||||
<div>
|
||||
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
|
||||
{t("set_address_place")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
type="text"
|
||||
{...locationFormMethods.register("locationAddress")}
|
||||
id="address"
|
||||
required
|
||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
||||
defaultValue={
|
||||
defaultValues
|
||||
? defaultValues.find(
|
||||
(location: { type: LocationType }) => location.type === LocationType.InPerson
|
||||
)?.address
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{!booking && (
|
||||
<div className="mt-3">
|
||||
<Controller
|
||||
name="displayLocationPublicly"
|
||||
control={locationFormMethods.control}
|
||||
render={() => (
|
||||
<CheckboxField
|
||||
defaultChecked={
|
||||
defaultValues
|
||||
? defaultValues.find((location) => location.type === LocationType.InPerson)
|
||||
?.displayLocationPublicly
|
||||
: undefined
|
||||
}
|
||||
description={t("display_location_label")}
|
||||
onChange={(e) =>
|
||||
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
|
||||
}
|
||||
informationIconText={t("display_location_info_badge")}></CheckboxField>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : selectedLocation === LocationType.Link ? (
|
||||
<div>
|
||||
<label htmlFor="link" className="block text-sm font-medium text-gray-700">
|
||||
{t("set_link_meeting")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
type="text"
|
||||
{...locationFormMethods.register("locationLink")}
|
||||
id="link"
|
||||
required
|
||||
className="block w-full rounded-sm border-gray-300 sm:text-sm"
|
||||
defaultValue={
|
||||
defaultValues
|
||||
? defaultValues.find(
|
||||
(location: { type: LocationType }) => location.type === LocationType.Link
|
||||
)?.link
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{locationFormMethods.formState.errors.locationLink && (
|
||||
<p className="mt-1 text-sm text-red-500">{t("url_start_with_https")}</p>
|
||||
)}
|
||||
</div>
|
||||
{!booking && (
|
||||
<div className="mt-3">
|
||||
<Controller
|
||||
name="displayLocationPublicly"
|
||||
control={locationFormMethods.control}
|
||||
render={() => (
|
||||
<CheckboxField
|
||||
description={t("display_location_label")}
|
||||
defaultChecked={
|
||||
defaultValues
|
||||
? defaultValues.find((location) => location.type === LocationType.Link)
|
||||
?.displayLocationPublicly
|
||||
: undefined
|
||||
}
|
||||
onChange={(e) => locationFormMethods.setValue("displayLocationPublicly", e.target.checked)}
|
||||
informationIconText={t("display_location_info_badge")}></CheckboxField>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : selectedLocation === LocationType.UserPhone ? (
|
||||
<div>
|
||||
<label htmlFor="phonenumber" className="block text-sm font-medium text-gray-700">
|
||||
{t("set_your_phone_number")}
|
||||
{locationFormMethods.formState?.errors?.locationPhoneNumber?.message}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<PhoneInput<LocationFormValues>
|
||||
control={locationFormMethods.control}
|
||||
name="locationPhoneNumber"
|
||||
required
|
||||
id="locationPhoneNumber"
|
||||
placeholder={t("host_phone_number")}
|
||||
defaultValue={
|
||||
defaultValues
|
||||
? defaultValues.find(
|
||||
(location: { type: LocationType }) => location.type === LocationType.UserPhone
|
||||
)?.hostPhoneNumber
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{locationFormMethods.formState.errors.locationPhoneNumber && (
|
||||
<p className="mt-1 text-sm text-red-500">Invalid input</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm">{LocationOptionsToString(selectedLocation, t)}</p>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpenDialog}>
|
||||
<DialogContent asChild>
|
||||
<div className="inline-block transform rounded-sm bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle">
|
||||
<div className="mb-4 sm:flex sm:items-start">
|
||||
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
||||
<LocationMarkerIcon className="text-primary-600 h-6 w-6" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||
{t("edit_location")}
|
||||
</h3>
|
||||
{!booking && (
|
||||
<p className="text-sm text-gray-400">{t("this_input_will_shown_booking_this_event")}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"></div>
|
||||
</div>
|
||||
{booking && (
|
||||
<>
|
||||
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
|
||||
<p className="mb-2 ml-1 text-sm text-black">{linkValueToString(booking.location, t)}</p>
|
||||
</>
|
||||
)}
|
||||
<Form
|
||||
form={locationFormMethods}
|
||||
handleSubmit={async (values) => {
|
||||
const { locationType: newLocation, displayLocationPublicly } = values;
|
||||
|
||||
let details = {};
|
||||
if (newLocation === LocationType.InPerson) {
|
||||
details = {
|
||||
address: values.locationAddress,
|
||||
displayLocationPublicly,
|
||||
};
|
||||
}
|
||||
if (newLocation === LocationType.Link) {
|
||||
details = { link: values.locationLink, displayLocationPublicly };
|
||||
}
|
||||
if (newLocation === LocationType.UserPhone) {
|
||||
details = { hostPhoneNumber: values.locationPhoneNumber };
|
||||
}
|
||||
|
||||
saveLocation(newLocation, details);
|
||||
setShowLocationModal(false);
|
||||
setSelectedLocation?.(undefined);
|
||||
locationFormMethods.unregister([
|
||||
"locationType",
|
||||
"locationLink",
|
||||
"locationAddress",
|
||||
"locationPhoneNumber",
|
||||
]);
|
||||
}}>
|
||||
<QueryCell
|
||||
query={locationsQuery}
|
||||
success={({ data: locationOptions }) => {
|
||||
if (!locationOptions.length) return null;
|
||||
return (
|
||||
<Controller
|
||||
name="locationType"
|
||||
control={locationFormMethods.control}
|
||||
render={() => (
|
||||
<Select
|
||||
maxMenuHeight={150}
|
||||
name="location"
|
||||
defaultValue={selection}
|
||||
options={
|
||||
booking
|
||||
? locationOptions.filter((location) => location.value !== "phone")
|
||||
: locationOptions
|
||||
}
|
||||
isSearchable={false}
|
||||
className="my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
||||
onChange={(val) => {
|
||||
if (val) {
|
||||
locationFormMethods.setValue("locationType", val.value);
|
||||
locationFormMethods.unregister([
|
||||
"locationLink",
|
||||
"locationAddress",
|
||||
"locationPhoneNumber",
|
||||
]);
|
||||
locationFormMethods.clearErrors([
|
||||
"locationLink",
|
||||
"locationPhoneNumber",
|
||||
"locationAddress",
|
||||
]);
|
||||
setSelectedLocation?.(val);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{selectedLocation && LocationOptions}
|
||||
<div className="mt-4 flex justify-end space-x-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowLocationModal(false);
|
||||
setSelectedLocation?.(undefined);
|
||||
locationFormMethods.unregister("locationType");
|
||||
}}
|
||||
type="button"
|
||||
color="secondary">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button type="submit">{t("update")}</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,55 @@
|
|||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
import Button from "@calcom/ui/Button";
|
||||
|
||||
import { trpc } from "@lib/trpc";
|
||||
|
||||
import Badge from "@components/ui/Badge";
|
||||
|
||||
const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonation: boolean }) => {
|
||||
const utils = trpc.useContext();
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
const mutation = trpc.useMutation("viewer.updateProfile", {
|
||||
onSuccess: async () => {
|
||||
showToast(t("your_user_profile_updated_successfully"), "success");
|
||||
await utils.invalidateQueries(["viewer.me"]);
|
||||
},
|
||||
async onSettled() {
|
||||
await utils.invalidateQueries(["viewer.i18n"]);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col justify-between pt-9 pl-2 sm:flex-row">
|
||||
<div>
|
||||
<div className="flex flex-row items-center">
|
||||
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">
|
||||
{t("user_impersonation_heading")}
|
||||
</h2>
|
||||
<Badge className="ml-2 text-xs" variant={!disableImpersonation ? "success" : "gray"}>
|
||||
{!disableImpersonation ? t("enabled") : t("disabled")}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-500">{t("user_impersonation_description")}</p>
|
||||
</div>
|
||||
<div className="mt-5 sm:mt-0 sm:self-center">
|
||||
<Button
|
||||
type="submit"
|
||||
color="secondary"
|
||||
onClick={() =>
|
||||
!disableImpersonation
|
||||
? mutation.mutate({ disableImpersonation: true })
|
||||
: mutation.mutate({ disableImpersonation: false })
|
||||
}>
|
||||
{!disableImpersonation ? t("disable") : t("enable")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisableUserImpersonation;
|
|
@ -1,10 +1,10 @@
|
|||
import { UserIcon } from "@heroicons/react/outline";
|
||||
import { InformationCircleIcon } from "@heroicons/react/solid";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import React, { useState, useEffect, SyntheticEvent, useMemo } from "react";
|
||||
import React, { useState, SyntheticEvent, useMemo } from "react";
|
||||
|
||||
import Button from "@calcom/ui/Button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
||||
import { Dialog, DialogContent, DialogFooter } from "@calcom/ui/Dialog";
|
||||
import { TextField } from "@calcom/ui/form/fields";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
|
|
@ -4,7 +4,7 @@ import { MembershipRole } from "@prisma/client";
|
|||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
import Button from "@calcom/ui/Button";
|
||||
|
@ -74,7 +74,7 @@ export default function MemberListItem(props: Props) {
|
|||
<div className="flex w-full flex-col justify-between sm:flex-row">
|
||||
<div className="flex">
|
||||
<Avatar
|
||||
imageSrc={WEBSITE_URL + "/" + props.member.username + "/avatar.png"}
|
||||
imageSrc={WEBAPP_URL + "/" + props.member.username + "/avatar.png"}
|
||||
alt={name || ""}
|
||||
className="h-9 w-9 rounded-full"
|
||||
/>
|
||||
|
|
|
@ -5,13 +5,12 @@ import Link from "next/link";
|
|||
import { TeamPageProps } from "pages/team/[slug]";
|
||||
import React from "react";
|
||||
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import Button from "@calcom/ui/Button";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import Avatar from "@components/ui/Avatar";
|
||||
import Text from "@components/ui/Text";
|
||||
|
||||
type TeamType = TeamPageProps["team"];
|
||||
type MembersType = TeamType["members"];
|
||||
|
@ -52,14 +51,14 @@ const Team = ({ team }: TeamPageProps) => {
|
|||
<div>
|
||||
<Avatar
|
||||
alt={member.name || ""}
|
||||
imageSrc={WEBSITE_URL + "/" + member.username + "/avatar.png"}
|
||||
imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"}
|
||||
className="-mt-4 h-12 w-12"
|
||||
/>
|
||||
<section className="mt-2 w-full space-y-1">
|
||||
<Text variant="title">{member.name}</Text>
|
||||
<Text variant="subtitle" className="">
|
||||
<section className="line-clamp-4 mt-2 w-full space-y-1">
|
||||
<p className="font-medium text-neutral-900 dark:text-white">{member.name}</p>
|
||||
<p className="text-sm font-normal text-neutral-500 dark:text-white">
|
||||
{member.bio || t("user_from_team", { user: member.name, team: team.name })}
|
||||
</Text>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -18,7 +18,7 @@ export default function ModalContainer(props: Props) {
|
|||
<DialogContent>
|
||||
<div
|
||||
className={classNames(
|
||||
"inline-block transform bg-white text-left align-bottom transition-all sm:align-middle",
|
||||
"inline-block w-full transform bg-white text-left align-bottom transition-all sm:align-middle",
|
||||
{
|
||||
"sm:w-full sm:max-w-lg ": !props.wide,
|
||||
"sm:w-4xl sm:max-w-4xl": props.wide,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Link from "next/link";
|
||||
|
||||
import { useIsEmbed } from "@calcom/embed-core";
|
||||
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import { SVGComponent } from "@lib/types/SVGComponent";
|
|||
export type ActionType = {
|
||||
id: string;
|
||||
icon?: SVGComponent;
|
||||
iconClassName?: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
color?: "primary" | "secondary";
|
||||
|
@ -52,6 +53,7 @@ const DropdownActions = ({
|
|||
className="w-full rounded-none font-normal"
|
||||
href={action.href}
|
||||
StartIcon={action.icon}
|
||||
startIconClassName={action.iconClassName}
|
||||
onClick={action.onClick || defaultAction}
|
||||
data-testid={action.id}>
|
||||
{action.label}
|
||||
|
@ -81,6 +83,7 @@ const TableActions: FC<Props> = ({ actions }) => {
|
|||
href={action.href}
|
||||
onClick={action.onClick || defaultAction}
|
||||
StartIcon={action.icon}
|
||||
startIconClassName={action.iconClassName}
|
||||
{...(action?.actions ? { EndIcon: ChevronDownIcon } : null)}
|
||||
disabled={action.disabled}
|
||||
color={action.color || "secondary"}>
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
import classnames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
import { TextProps } from "../Text";
|
||||
|
||||
const Body: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||
const classes = classnames("text-gray-900 dark:text-white", props?.className);
|
||||
|
||||
return <p className={classes}>{props?.text || props.children}</p>;
|
||||
};
|
||||
|
||||
export default Body;
|
|
@ -1,3 +0,0 @@
|
|||
import Body from "./Body";
|
||||
|
||||
export default Body;
|
|
@ -1,12 +0,0 @@
|
|||
import classnames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
import { TextProps } from "../Text";
|
||||
|
||||
const Caption: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||
const classes = classnames("text-sm text-gray-500 dark:text-white leading-tight", props?.className);
|
||||
|
||||
return <p className={classes}>{props?.text || props.children}</p>;
|
||||
};
|
||||
|
||||
export default Caption;
|
|
@ -1,3 +0,0 @@
|
|||
import Caption from "./Caption";
|
||||
|
||||
export default Caption;
|
|
@ -1,12 +0,0 @@
|
|||
import classnames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
import { TextProps } from "../Text";
|
||||
|
||||
const Caption2: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||
const classes = classnames("text-xs italic text-gray-500 dark:text-white leading-tight", props?.className);
|
||||
|
||||
return <p className={classes}>{props?.text || props.children}</p>;
|
||||
};
|
||||
|
||||
export default Caption2;
|
|
@ -1,3 +0,0 @@
|
|||
import Caption2 from "./Caption2";
|
||||
|
||||
export default Caption2;
|
|
@ -1,12 +0,0 @@
|
|||
import classnames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
import { TextProps } from "../Text";
|
||||
|
||||
const Footnote: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||
const classes = classnames("text-xs font-medium text-gray-500 dark:text-white", props?.className);
|
||||
|
||||
return <p className={classes}>{props?.text || props.children}</p>;
|
||||
};
|
||||
|
||||
export default Footnote;
|
|
@ -1,3 +0,0 @@
|
|||
import Footnote from "./Footnote";
|
||||
|
||||
export default Footnote;
|
|
@ -1,12 +0,0 @@
|
|||
import classnames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
import { TextProps } from "../Text";
|
||||
|
||||
const Headline: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||
const classes = classnames("font-cal text-xl text-gray-900 dark:text-white", props?.className);
|
||||
|
||||
return <p className={classes}>{props?.text || props.children}</p>;
|
||||
};
|
||||
|
||||
export default Headline;
|
|
@ -1,3 +0,0 @@
|
|||
import Headline from "./Headline";
|
||||
|
||||
export default Headline;
|
|
@ -1,12 +0,0 @@
|
|||
import classnames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
import { TextProps } from "../Text";
|
||||
|
||||
const Largetitle: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||
const classes = classnames("font-cal tracking-wider text-3xl mb-2", props?.className);
|
||||
|
||||
return <p className={classes}>{props?.text || props.children}</p>;
|
||||
};
|
||||
|
||||
export default Largetitle;
|
|
@ -1,3 +0,0 @@
|
|||
import Largetitle from "./Largetitle";
|
||||
|
||||
export default Largetitle;
|
|
@ -1,15 +0,0 @@
|
|||
import classnames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
import { TextProps } from "../Text";
|
||||
|
||||
const Overline: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||
const classes = classnames(
|
||||
"text-sm capitalize font-medium text-gray-900 dark:text-white",
|
||||
props?.className
|
||||
);
|
||||
|
||||
return <p className={classes}>{props?.text || props.children}</p>;
|
||||
};
|
||||
|
||||
export default Overline;
|
|
@ -1,3 +0,0 @@
|
|||
import Overline from "./Overline";
|
||||
|
||||
export default Overline;
|
|
@ -1,12 +0,0 @@
|
|||
import classnames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
import { TextProps } from "../Text";
|
||||
|
||||
const Subheadline: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||
const classes = classnames("text-xl text-gray-500 dark:text-white leading-relaxed", props?.className);
|
||||
|
||||
return <p className={classes}>{props?.text || props.children}</p>;
|
||||
};
|
||||
|
||||
export default Subheadline;
|
|
@ -1,3 +0,0 @@
|
|||
import Subheadline from "./Subheadline";
|
||||
|
||||
export default Subheadline;
|
|
@ -1,12 +0,0 @@
|
|||
import classnames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
import { TextProps } from "../Text";
|
||||
|
||||
const Subtitle: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||
const classes = classnames("text-sm font-normal text-neutral-500 dark:text-white", props?.className);
|
||||
|
||||
return <p className={classes}>{props?.text || props.children}</p>;
|
||||
};
|
||||
|
||||
export default Subtitle;
|
|
@ -1,3 +0,0 @@
|
|||
import Subtitle from "./Subtitle";
|
||||
|
||||
export default Subtitle;
|
|
@ -1,164 +0,0 @@
|
|||
/**
|
||||
* @deprecated create new a new set of components, waiting for designs
|
||||
*/
|
||||
import React from "react";
|
||||
|
||||
import Body from "./Body";
|
||||
import Caption from "./Caption";
|
||||
import Caption2 from "./Caption2";
|
||||
import Footnote from "./Footnote";
|
||||
import Headline from "./Headline";
|
||||
import Largetitle from "./Largetitle";
|
||||
import Overline from "./Overline";
|
||||
import Subheadline from "./Subheadline";
|
||||
import Subtitle from "./Subtitle";
|
||||
import Title from "./Title";
|
||||
import Title2 from "./Title2";
|
||||
import Title3 from "./Title3";
|
||||
|
||||
type Props = {
|
||||
variant?:
|
||||
| "overline"
|
||||
| "caption"
|
||||
| "body"
|
||||
| "caption2"
|
||||
| "footnote"
|
||||
| "headline"
|
||||
| "largetitle"
|
||||
| "subheadline"
|
||||
| "subtitle"
|
||||
| "title"
|
||||
| "title2"
|
||||
| "title3";
|
||||
children: any;
|
||||
text?: string;
|
||||
tx?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export type TextProps = {
|
||||
children: any;
|
||||
text?: string;
|
||||
tx?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* static let largeTitle: Font
|
||||
* A font with the large title text style.
|
||||
*
|
||||
* static let title: Font
|
||||
* A font with the title text style.
|
||||
*
|
||||
* static let title2: Font
|
||||
* Create a font for second level hierarchical headings.
|
||||
*
|
||||
* static let title3: Font
|
||||
* Create a font for third level hierarchical headings.
|
||||
*
|
||||
* static let headline: Font
|
||||
* A font with the headline text style.
|
||||
*
|
||||
* static let subheadline: Font
|
||||
* A font with the subheadline text style.
|
||||
*
|
||||
* static let body: Font
|
||||
* A font with the body text style.
|
||||
*
|
||||
* static let callout: Font
|
||||
* A font with the callout text style.
|
||||
*
|
||||
* static let caption: Font
|
||||
* A font with the caption text style.
|
||||
*
|
||||
* static let caption2: Font
|
||||
* Create a font with the alternate caption text style.
|
||||
*
|
||||
* static let footnote: Font
|
||||
* A font with the footnote text style.
|
||||
*/
|
||||
|
||||
const Text: React.FunctionComponent<Props> = (props: Props) => {
|
||||
switch (props?.variant) {
|
||||
case "overline":
|
||||
return (
|
||||
<Overline text={props?.text} tx={props?.tx} className={props?.className}>
|
||||
{props.children}
|
||||
</Overline>
|
||||
);
|
||||
case "body":
|
||||
return (
|
||||
<Body text={props?.text} tx={props?.tx} className={props?.className}>
|
||||
{props.children}
|
||||
</Body>
|
||||
);
|
||||
case "caption":
|
||||
return (
|
||||
<Caption text={props?.text} tx={props?.tx} className={props?.className}>
|
||||
{props.children}
|
||||
</Caption>
|
||||
);
|
||||
case "caption2":
|
||||
return (
|
||||
<Caption2 text={props?.text} tx={props?.tx} className={props?.className}>
|
||||
{props.children}
|
||||
</Caption2>
|
||||
);
|
||||
case "footnote":
|
||||
return (
|
||||
<Footnote text={props?.text} tx={props?.tx} className={props?.className}>
|
||||
{props.children}
|
||||
</Footnote>
|
||||
);
|
||||
case "headline":
|
||||
return (
|
||||
<Headline text={props?.text} tx={props?.tx} className={props?.className}>
|
||||
{props.children}
|
||||
</Headline>
|
||||
);
|
||||
case "largetitle":
|
||||
return (
|
||||
<Largetitle text={props?.text} tx={props?.tx} className={props?.className}>
|
||||
{props.children}
|
||||
</Largetitle>
|
||||
);
|
||||
case "subheadline":
|
||||
return (
|
||||
<Subheadline text={props?.text} tx={props?.tx} className={props?.className}>
|
||||
{props.children}
|
||||
</Subheadline>
|
||||
);
|
||||
case "subtitle":
|
||||
return (
|
||||
<Subtitle text={props?.text} tx={props?.tx} className={props?.className}>
|
||||
{props.children}
|
||||
</Subtitle>
|
||||
);
|
||||
case "title":
|
||||
return (
|
||||
<Title text={props?.text} tx={props?.tx} className={props?.className}>
|
||||
{props.children}
|
||||
</Title>
|
||||
);
|
||||
case "title2":
|
||||
return (
|
||||
<Title2 text={props?.text} tx={props?.tx} className={props?.className}>
|
||||
{props.children}
|
||||
</Title2>
|
||||
);
|
||||
case "title3":
|
||||
return (
|
||||
<Title3 text={props?.text} tx={props?.tx} className={props?.className}>
|
||||
{props.children}
|
||||
</Title3>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Body text={props?.text} tx={props?.tx} className={props?.className}>
|
||||
{props.children}
|
||||
</Body>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Text;
|
|
@ -1,12 +0,0 @@
|
|||
import classnames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
import { TextProps } from "../Text";
|
||||
|
||||
const Title: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||
const classes = classnames("font-medium text-neutral-900 dark:text-white", props?.className);
|
||||
|
||||
return <p className={classes}>{props?.text || props.children}</p>;
|
||||
};
|
||||
|
||||
export default Title;
|
|
@ -1,3 +0,0 @@
|
|||
import Title from "./Title";
|
||||
|
||||
export default Title;
|
|
@ -1,12 +0,0 @@
|
|||
import classnames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
import { TextProps } from "../Text";
|
||||
|
||||
const Title2: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||
const classes = classnames("text-base font-normal text-gray-900 dark:text-white", props?.className);
|
||||
|
||||
return <p className={classes}>{props?.text || props.children}</p>;
|
||||
};
|
||||
|
||||
export default Title2;
|
|
@ -1,3 +0,0 @@
|
|||
import Title2 from "./Title2";
|
||||
|
||||
export default Title2;
|
|
@ -1,15 +0,0 @@
|
|||
import classnames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
import { TextProps } from "../Text";
|
||||
|
||||
const Title3: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
||||
const classes = classnames(
|
||||
"text-xs font-semibold leading-tight text-gray-900 dark:text-white",
|
||||
props?.className
|
||||
);
|
||||
|
||||
return <p className={classes}>{props?.text || props.children}</p>;
|
||||
};
|
||||
|
||||
export default Title3;
|
|
@ -1,3 +0,0 @@
|
|||
import Title3 from "./Title3";
|
||||
|
||||
export default Title3;
|
|
@ -1,40 +0,0 @@
|
|||
import Body from "./Body";
|
||||
import Caption from "./Caption";
|
||||
import Caption2 from "./Caption2";
|
||||
import Footnote from "./Footnote";
|
||||
import Headline from "./Headline";
|
||||
import Largetitle from "./Largetitle";
|
||||
import Overline from "./Overline";
|
||||
import Subheadline from "./Subheadline";
|
||||
import Subtitle from "./Subtitle";
|
||||
import Text from "./Text";
|
||||
import Title from "./Title";
|
||||
import Title2 from "./Title2";
|
||||
import Title3 from "./Title3";
|
||||
|
||||
export { Text };
|
||||
export default Text;
|
||||
|
||||
export { Title };
|
||||
|
||||
export { Title2 };
|
||||
|
||||
export { Title3 };
|
||||
|
||||
export { Largetitle };
|
||||
|
||||
export { Subtitle };
|
||||
|
||||
export { Headline };
|
||||
|
||||
export { Subheadline };
|
||||
|
||||
export { Caption };
|
||||
|
||||
export { Caption2 };
|
||||
|
||||
export { Footnote };
|
||||
|
||||
export { Overline };
|
||||
|
||||
export { Body };
|
|
@ -1,50 +1,60 @@
|
|||
import React, { forwardRef, InputHTMLAttributes } from "react";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
|
||||
import InfoBadge from "@components/ui/InfoBadge";
|
||||
|
||||
type Props = InputHTMLAttributes<HTMLInputElement> & {
|
||||
label?: React.ReactNode;
|
||||
description: string;
|
||||
descriptionAsLabel?: boolean;
|
||||
infomationIconText?: string;
|
||||
informationIconText?: string;
|
||||
};
|
||||
|
||||
const CheckboxField = forwardRef<HTMLInputElement, Props>(
|
||||
({ label, description, infomationIconText, descriptionAsLabel, ...rest }, ref) => {
|
||||
({ label, description, informationIconText, ...rest }, ref) => {
|
||||
const descriptionAsLabel = !label || rest.descriptionAsLabel;
|
||||
return (
|
||||
<div className="block items-center sm:flex">
|
||||
{label && !descriptionAsLabel && (
|
||||
{label && (
|
||||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
<label htmlFor={rest.id} className="flex text-sm font-medium text-neutral-700">
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{label && descriptionAsLabel && (
|
||||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
<span className="flex text-sm font-medium text-neutral-700">{label}</span>
|
||||
{React.createElement(
|
||||
descriptionAsLabel ? "div" : "label",
|
||||
{
|
||||
className: "flex text-sm font-medium text-neutral-700",
|
||||
...(!descriptionAsLabel
|
||||
? {
|
||||
htmlFor: rest.id,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
label
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full">
|
||||
<div className="relative flex items-start">
|
||||
<div className="flex h-5 items-center">
|
||||
<input
|
||||
{...rest}
|
||||
ref={ref}
|
||||
type="checkbox"
|
||||
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm ltr:ml-3 rtl:mr-3">
|
||||
{!label || descriptionAsLabel ? (
|
||||
<label htmlFor={rest.id} className="text-neutral-700">
|
||||
{description}
|
||||
</label>
|
||||
) : (
|
||||
<p className="text-neutral-900">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
{infomationIconText && <InfoBadge content={infomationIconText}></InfoBadge>}
|
||||
{React.createElement(
|
||||
descriptionAsLabel ? "label" : "div",
|
||||
{
|
||||
className: classNames(
|
||||
"relative flex items-start",
|
||||
descriptionAsLabel ? "text-neutral-700" : "text-neutral-900"
|
||||
),
|
||||
},
|
||||
<>
|
||||
<div className="flex h-5 items-center">
|
||||
<input
|
||||
{...rest}
|
||||
ref={ref}
|
||||
type="checkbox"
|
||||
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm ltr:ml-3 rtl:mr-3">{description}</span>
|
||||
</>
|
||||
)}
|
||||
{informationIconText && <InfoBadge content={informationIconText}></InfoBadge>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import { ExclamationIcon } from "@heroicons/react/solid";
|
||||
import { useSession } from "next-auth/react";
|
||||
import React, { AriaRole, ComponentType, FC, Fragment } from "react";
|
||||
|
||||
import { CONSOLE_URL } from "@calcom/lib/constants";
|
||||
|
||||
import EmptyScreen from "@components/EmptyScreen";
|
||||
|
||||
type LicenseRequiredProps = {
|
||||
as?: keyof JSX.IntrinsicElements | "";
|
||||
className?: string;
|
||||
role?: AriaRole | undefined;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* This component will only render it's children if the installation has a valid
|
||||
* license.
|
||||
*/
|
||||
const LicenseRequired: FC<LicenseRequiredProps> = ({ children, as = "", ...rest }) => {
|
||||
const session = useSession();
|
||||
const Component = as || Fragment;
|
||||
return (
|
||||
<Component {...rest}>
|
||||
{session.data?.hasValidLicense ? (
|
||||
children
|
||||
) : (
|
||||
<EmptyScreen
|
||||
Icon={ExclamationIcon}
|
||||
headline="This is an enterprise feature"
|
||||
description={
|
||||
<>
|
||||
To enable this feature, get a deployment key at{" "}
|
||||
<a href={CONSOLE_URL} target="_blank" rel="noopener noreferrer" className="underline">
|
||||
Cal.com console
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
export function withLicenseRequired<T>(Component: ComponentType<T>) {
|
||||
// eslint-disable-next-line react/display-name
|
||||
return (hocProps: T) => {
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<Component {...(hocProps as T)} />;
|
||||
</LicenseRequired>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default LicenseRequired;
|
|
@ -15,11 +15,12 @@ import { trpc } from "@lib/trpc";
|
|||
|
||||
import { DatePicker } from "@components/ui/form/DatePicker";
|
||||
|
||||
import { TApiKeys } from "./ApiKeyListItem";
|
||||
import LicenseRequired from "../LicenseRequired";
|
||||
import type { TApiKeys } from "./ApiKeyListItem";
|
||||
|
||||
export default function ApiKeyDialogForm(props: {
|
||||
title: string;
|
||||
defaultValues?: Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires: boolean };
|
||||
defaultValues?: Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires?: boolean };
|
||||
handleClose: () => void;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
|
@ -49,7 +50,7 @@ export default function ApiKeyDialogForm(props: {
|
|||
const watchNeverExpires = form.watch("neverExpires");
|
||||
|
||||
return (
|
||||
<>
|
||||
<LicenseRequired>
|
||||
{successfulNewApiKeyModal ? (
|
||||
<>
|
||||
<div className="mb-10">
|
||||
|
@ -92,12 +93,12 @@ export default function ApiKeyDialogForm(props: {
|
|||
</DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
<Form<Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires: boolean }>
|
||||
<Form<Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires?: boolean }>
|
||||
form={form}
|
||||
handleSubmit={async (event) => {
|
||||
const apiKey = await utils.client.mutation("viewer.apiKeys.create", event);
|
||||
setApiKey(apiKey);
|
||||
setApiKeyDetails({ ...event });
|
||||
setApiKeyDetails({ ...event, neverExpires: !!event.neverExpires });
|
||||
await utils.invalidateQueries(["viewer.apiKeys.list"]);
|
||||
setSuccessfulNewApiKeyModal(true);
|
||||
}}
|
||||
|
@ -146,6 +147,6 @@ export default function ApiKeyDialogForm(props: {
|
|||
</DialogFooter>
|
||||
</Form>
|
||||
)}
|
||||
</>
|
||||
</LicenseRequired>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,66 +12,76 @@ import { trpc } from "@lib/trpc";
|
|||
|
||||
import { List } from "@components/List";
|
||||
|
||||
export default function ApiKeyListContainer() {
|
||||
import LicenseRequired from "../LicenseRequired";
|
||||
|
||||
function ApiKeyListContainer() {
|
||||
const { t } = useLocale();
|
||||
const query = trpc.useQuery(["viewer.apiKeys.list"]);
|
||||
|
||||
const [newApiKeyModal, setNewApiKeyModal] = useState(false);
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
const [apiKeyToEdit, setApiKeyToEdit] = useState<(TApiKeys & { neverExpires: boolean }) | null>(null);
|
||||
const [apiKeyToEdit, setApiKeyToEdit] = useState<(TApiKeys & { neverExpires?: boolean }) | null>(null);
|
||||
return (
|
||||
<QueryCell
|
||||
query={query}
|
||||
success={({ data }) => (
|
||||
<>
|
||||
<div className="flex flex-col justify-between truncate pl-2 pr-1 sm:flex-row">
|
||||
<div className="mt-9">
|
||||
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("api_keys")}</h2>
|
||||
<p className="mt-1 mb-5 text-sm text-gray-500">{t("api_keys_subtitle")}</p>
|
||||
</div>
|
||||
<div className="mb-9 sm:self-center">
|
||||
<Button StartIcon={PlusIcon} color="secondary" onClick={() => setNewApiKeyModal(true)}>
|
||||
{t("generate_new_api_key")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data.length > 0 && (
|
||||
<List className="pb-6">
|
||||
{data.map((item: any) => (
|
||||
<ApiKeyListItem
|
||||
key={item.id}
|
||||
apiKey={item}
|
||||
onEditApiKey={() => {
|
||||
setApiKeyToEdit(item);
|
||||
setEditModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
|
||||
{/* New api key dialog */}
|
||||
<Dialog open={newApiKeyModal} onOpenChange={(isOpen) => !isOpen && setNewApiKeyModal(false)}>
|
||||
<DialogContent>
|
||||
<ApiKeyDialogForm title={t("create_api_key")} handleClose={() => setNewApiKeyModal(false)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/* Edit api key dialog */}
|
||||
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
|
||||
<DialogContent>
|
||||
{apiKeyToEdit && (
|
||||
<ApiKeyDialogForm
|
||||
title={t("edit_api_key")}
|
||||
key={apiKeyToEdit.id}
|
||||
handleClose={() => setEditModalOpen(false)}
|
||||
defaultValues={apiKeyToEdit}
|
||||
/>
|
||||
<>
|
||||
<div className="flex flex-col justify-between truncate pl-2 pr-1 sm:flex-row">
|
||||
<div className="mt-9">
|
||||
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("api_keys")}</h2>
|
||||
<p className="mt-1 mb-5 text-sm text-gray-500">{t("api_keys_subtitle")}</p>
|
||||
</div>
|
||||
<div className="mb-9 sm:self-center">
|
||||
<Button StartIcon={PlusIcon} color="secondary" onClick={() => setNewApiKeyModal(true)}>
|
||||
{t("generate_new_api_key")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<LicenseRequired>
|
||||
<QueryCell
|
||||
query={query}
|
||||
success={({ data }) => (
|
||||
<>
|
||||
{data.length > 0 && (
|
||||
<List className="pb-6">
|
||||
{data.map((item) => (
|
||||
<ApiKeyListItem
|
||||
key={item.id}
|
||||
apiKey={item}
|
||||
onEditApiKey={() => {
|
||||
setApiKeyToEdit(item);
|
||||
setEditModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* New api key dialog */}
|
||||
<Dialog open={newApiKeyModal} onOpenChange={(isOpen) => !isOpen && setNewApiKeyModal(false)}>
|
||||
<DialogContent>
|
||||
<ApiKeyDialogForm
|
||||
title={t("create_api_key")}
|
||||
handleClose={() => setNewApiKeyModal(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/* Edit api key dialog */}
|
||||
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
|
||||
<DialogContent>
|
||||
{apiKeyToEdit && (
|
||||
<ApiKeyDialogForm
|
||||
title={t("edit_api_key")}
|
||||
key={apiKeyToEdit.id}
|
||||
handleClose={() => setEditModalOpen(false)}
|
||||
defaultValues={apiKeyToEdit}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</LicenseRequired>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApiKeyListContainer;
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
import React, { useEffect, useState, useRef } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
import { Alert } from "@calcom/ui/Alert";
|
||||
import Button from "@calcom/ui/Button";
|
||||
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
|
||||
import { TextArea } from "@calcom/ui/form/fields";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
||||
import { trpc } from "@lib/trpc";
|
||||
|
||||
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
||||
import Badge from "@components/ui/Badge";
|
||||
|
||||
import LicenseRequired from "../LicenseRequired";
|
||||
|
||||
export default function SAMLConfiguration({
|
||||
teamsView,
|
||||
teamId,
|
||||
|
@ -92,7 +94,7 @@ export default function SAMLConfiguration({
|
|||
return (
|
||||
<>
|
||||
{isSAMLLoginEnabled ? (
|
||||
<>
|
||||
<LicenseRequired>
|
||||
<hr className="mt-8" />
|
||||
<div className="mt-6">
|
||||
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">
|
||||
|
@ -157,7 +159,7 @@ export default function SAMLConfiguration({
|
|||
</div>
|
||||
<hr className="mt-4" />
|
||||
</form>
|
||||
</>
|
||||
</LicenseRequired>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -6,10 +6,10 @@ import timezone from "dayjs/plugin/timezone";
|
|||
import toArray from "dayjs/plugin/toArray";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import Head from "next/head";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
|
||||
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core";
|
||||
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import getStripe from "@calcom/stripe/client";
|
||||
import PaymentComponent from "@ee/components/stripe/Payment";
|
||||
import { PaymentPageProps } from "@ee/pages/payment/[uid]";
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import HelpscoutMenuItem from "@ee/lib/helpscout/HelpscoutMenuItem";
|
||||
import IntercomMenuItem from "@ee/lib/intercom/IntercomMenuItem";
|
||||
import ZendeskMenuItem from "@ee/lib/zendesk/ZendeskMenuItem";
|
||||
|
||||
export default function HelpMenuItem() {
|
||||
return (
|
||||
<>
|
||||
<IntercomMenuItem />
|
||||
<ZendeskMenuItem />
|
||||
<HelpscoutMenuItem />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,13 +1,190 @@
|
|||
import HelpscoutMenuItem from "@ee/lib/helpscout/HelpscoutMenuItem";
|
||||
import IntercomMenuItem from "@ee/lib/intercom/IntercomMenuItem";
|
||||
import ZendeskMenuItem from "@ee/lib/zendesk/ZendeskMenuItem";
|
||||
import { ExternalLinkIcon, ExclamationIcon } from "@heroicons/react/solid";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
import Button from "@calcom/ui/Button";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
import { trpc } from "@lib/trpc";
|
||||
|
||||
import ContactMenuItem from "./ContactMenuItem";
|
||||
|
||||
export default function HelpMenuItem() {
|
||||
const [rating, setRating] = useState<null | string>(null);
|
||||
const [comment, setComment] = useState("");
|
||||
const [disableSubmit, setDisableSubmit] = useState(true);
|
||||
const { t } = useLocale();
|
||||
|
||||
const mutation = trpc.useMutation("viewer.submitFeedback");
|
||||
|
||||
const onRatingClick = (value: string) => {
|
||||
setRating(value);
|
||||
setDisableSubmit(false);
|
||||
};
|
||||
|
||||
const sendFeedback = async (rating: string, comment: string) => {
|
||||
mutation.mutate({ rating: rating, comment: comment });
|
||||
|
||||
if (mutation.isSuccess) {
|
||||
setDisableSubmit(true);
|
||||
showToast("Thank you, feedback submitted", "success");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntercomMenuItem />
|
||||
<ZendeskMenuItem />
|
||||
<HelpscoutMenuItem />
|
||||
</>
|
||||
<div className="w-full border-gray-300 bg-white shadow-sm md:w-[150%]">
|
||||
<div className=" w-full p-5">
|
||||
<p className="mb-1 text-neutral-500">{t("resources").toUpperCase()}</p>
|
||||
<a
|
||||
href="https://docs.cal.com/"
|
||||
target="_blank"
|
||||
className="flex w-full py-2 pr-4 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900"
|
||||
rel="noreferrer">
|
||||
{t("support_documentation")}
|
||||
<ExternalLinkIcon
|
||||
className={classNames(
|
||||
"text-neutral-400 group-hover:text-neutral-500",
|
||||
"ml-1 h-5 w-5 flex-shrink-0 ltr:mr-3"
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://developer.cal.com/"
|
||||
target="_blank"
|
||||
className="flex w-full py-2 pr-4 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900"
|
||||
rel="noreferrer">
|
||||
{t("developer_documentation")}
|
||||
<ExternalLinkIcon
|
||||
className={classNames(
|
||||
"text-neutral-400 group-hover:text-neutral-500",
|
||||
"ml-1 h-5 w-5 flex-shrink-0 ltr:mr-3"
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
<ContactMenuItem />
|
||||
</div>
|
||||
|
||||
<hr className=" bg-gray-200" />
|
||||
<div className="w-full p-5">
|
||||
<p className="mb-1 text-neutral-500">{t("feedback").toUpperCase()}</p>
|
||||
<p className="flex w-full py-2 text-sm font-medium text-gray-700">{t("comments")}</p>
|
||||
<textarea
|
||||
id="comment"
|
||||
name="comment"
|
||||
rows={3}
|
||||
onChange={(event) => setComment(event.target.value)}
|
||||
className="my-1 block w-full rounded-sm border-gray-300 py-2 pb-2 shadow-sm sm:text-sm"></textarea>
|
||||
|
||||
<div className="my-3 flex justify-end">
|
||||
<button
|
||||
className={classNames(
|
||||
"m-1 items-center justify-center p-1.5 grayscale hover:opacity-100 hover:grayscale-0",
|
||||
rating === "Extremely unsatisfied" ? "grayscale-0" : "opacity-50"
|
||||
)}
|
||||
onClick={() => onRatingClick("Extremely unsatisfied")}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="1.5em" height="1.5em">
|
||||
<path
|
||||
fill="#FFCC4D"
|
||||
d="M36 18c0 9.941-8.059 18-18 18S0 27.941 0 18 8.059 0 18 0s18 8.059 18 18"
|
||||
/>
|
||||
<path
|
||||
fill="#664500"
|
||||
d="M22 27c0 2.763-1.791 3-4 3-2.21 0-4-.237-4-3 0-2.761 1.79-6 4-6 2.209 0 4 3.239 4 6zm8-12c-.124 0-.25-.023-.371-.072-5.229-2.091-7.372-5.241-7.461-5.374-.307-.46-.183-1.081.277-1.387.459-.306 1.077-.184 1.385.274.019.027 1.93 2.785 6.541 4.629.513.206.763.787.558 1.3-.157.392-.533.63-.929.63zM6 15c-.397 0-.772-.238-.929-.629-.205-.513.044-1.095.557-1.3 4.612-1.844 6.523-4.602 6.542-4.629.308-.456.929-.577 1.387-.27.457.308.581.925.275 1.383-.089.133-2.232 3.283-7.46 5.374C6.25 14.977 6.124 15 6 15z"
|
||||
/>
|
||||
<path fill="#5DADEC" d="M24 16h4v19l-4-.046V16zM8 35l4-.046V16H8v19z" />
|
||||
<path
|
||||
fill="#664500"
|
||||
d="M14.999 18c-.15 0-.303-.034-.446-.105-3.512-1.756-7.07-.018-7.105 0-.495.249-1.095.046-1.342-.447-.247-.494-.047-1.095.447-1.342.182-.09 4.498-2.197 8.895 0 .494.247.694.848.447 1.342-.176.35-.529.552-.896.552zm14 0c-.15 0-.303-.034-.446-.105-3.513-1.756-7.07-.018-7.105 0-.494.248-1.094.047-1.342-.447-.247-.494-.047-1.095.447-1.342.182-.09 4.501-2.196 8.895 0 .494.247.694.848.447 1.342-.176.35-.529.552-.896.552z"
|
||||
/>
|
||||
<ellipse fill="#5DADEC" cx="18" cy="34" rx="18" ry="2" />
|
||||
<ellipse fill="#E75A70" cx="18" cy="27" rx="3" ry="2" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className={classNames(
|
||||
"m-1 items-center justify-center p-1.5 grayscale hover:opacity-100 hover:grayscale-0",
|
||||
rating === "Unsatisfied" ? "grayscale-0" : "opacity-50"
|
||||
)}
|
||||
onClick={() => onRatingClick("Unsatisfied")}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="1.5em" height="1.5em">
|
||||
<path
|
||||
fill="#FFCC4D"
|
||||
d="M36 18c0 9.941-8.059 18-18 18-9.94 0-18-8.059-18-18C0 8.06 8.06 0 18 0c9.941 0 18 8.06 18 18"
|
||||
/>
|
||||
<ellipse fill="#664500" cx="11.5" cy="14.5" rx="2.5" ry="3.5" />
|
||||
<ellipse fill="#664500" cx="24.5" cy="14.5" rx="2.5" ry="3.5" />
|
||||
<path
|
||||
fill="#664500"
|
||||
d="M8.665 27.871c.178.161.444.171.635.029.039-.029 3.922-2.9 8.7-2.9 4.766 0 8.662 2.871 8.7 2.9.191.142.457.13.635-.029.177-.16.217-.424.094-.628C27.3 27.029 24.212 22 18 22s-9.301 5.028-9.429 5.243c-.123.205-.084.468.094.628z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className={classNames(
|
||||
"m-1 items-center justify-center p-1.5 grayscale hover:opacity-100 hover:grayscale-0",
|
||||
rating === "Satisfied" ? "grayscale-0" : "opacity-50"
|
||||
)}
|
||||
onClick={() => onRatingClick("Satisfied")}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="1.5em" height="1.5em">
|
||||
<path
|
||||
fill="#FFCC4D"
|
||||
d="M36 18c0 9.941-8.059 18-18 18-9.94 0-18-8.059-18-18C0 8.06 8.06 0 18 0c9.941 0 18 8.06 18 18"
|
||||
/>
|
||||
<path
|
||||
fill="#664500"
|
||||
d="M28.457 17.797c-.06-.135-1.499-3.297-4.457-3.297-2.957 0-4.397 3.162-4.457 3.297-.092.207-.032.449.145.591.175.142.426.147.61.014.012-.009 1.262-.902 3.702-.902 2.426 0 3.674.881 3.702.901.088.066.194.099.298.099.11 0 .221-.037.312-.109.177-.142.238-.386.145-.594zm-12 0c-.06-.135-1.499-3.297-4.457-3.297-2.957 0-4.397 3.162-4.457 3.297-.092.207-.032.449.144.591.176.142.427.147.61.014.013-.009 1.262-.902 3.703-.902 2.426 0 3.674.881 3.702.901.088.066.194.099.298.099.11 0 .221-.037.312-.109.178-.142.237-.386.145-.594zM18 22c-3.623 0-6.027-.422-9-1-.679-.131-2 0-2 2 0 4 4.595 9 11 9 6.404 0 11-5 11-9 0-2-1.321-2.132-2-2-2.973.578-5.377 1-9 1z"
|
||||
/>
|
||||
<path fill="#FFF" d="M9 23s3 1 9 1 9-1 9-1-2 4-9 4-9-4-9-4z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className={classNames(
|
||||
"m-1 items-center justify-center p-1.5 grayscale hover:opacity-100 hover:grayscale-0",
|
||||
rating === "Extremely satisfied" ? "grayscale-0" : "opacity-50"
|
||||
)}
|
||||
onClick={() => onRatingClick("Extremely satisfied")}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="1.5em" height="1.5em">
|
||||
<path
|
||||
fill="#FFCC4D"
|
||||
d="M36 18c0 9.941-8.059 18-18 18S0 27.941 0 18 8.059 0 18 0s18 8.059 18 18"
|
||||
/>
|
||||
<path
|
||||
fill="#664500"
|
||||
d="M18 21c-3.623 0-6.027-.422-9-1-.679-.131-2 0-2 2 0 4 4.595 9 11 9 6.404 0 11-5 11-9 0-2-1.321-2.132-2-2-2.973.578-5.377 1-9 1z"
|
||||
/>
|
||||
<path fill="#FFF" d="M9 22s3 1 9 1 9-1 9-1-2 4-9 4-9-4-9-4z" />
|
||||
<path
|
||||
fill="#E95F28"
|
||||
d="M15.682 4.413l-4.542.801L8.8.961C8.542.492 8.012.241 7.488.333c-.527.093-.937.511-1.019 1.039l-.745 4.797-4.542.801c-.535.094-.948.525-1.021 1.064s.211 1.063.703 1.297l4.07 1.932-.748 4.812c-.083.536.189 1.064.673 1.309.179.09.371.133.562.133.327 0 .65-.128.891-.372l3.512-3.561 4.518 2.145c.49.232 1.074.123 1.446-.272.372-.395.446-.984.185-1.459L13.625 9.73l3.165-3.208c.382-.387.469-.977.217-1.459-.254-.482-.793-.743-1.325-.65zm4.636 0l4.542.801L27.2.961c.258-.469.788-.72 1.312-.628.526.093.936.511 1.018 1.039l.745 4.797 4.542.801c.536.094.949.524 1.021 1.063s-.211 1.063-.703 1.297l-4.07 1.932.748 4.812c.083.536-.189 1.064-.673 1.309-.179.09-.371.133-.562.133-.327 0-.65-.128-.891-.372l-3.512-3.561-4.518 2.145c-.49.232-1.074.123-1.446-.272-.372-.395-.446-.984-.185-1.459l2.348-4.267-3.165-3.208c-.382-.387-.469-.977-.217-1.459.255-.482.794-.743 1.326-.65z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="my-2 flex justify-end">
|
||||
<Button
|
||||
disabled={disableSubmit}
|
||||
loading={mutation.isLoading}
|
||||
onClick={async () => {
|
||||
if (rating && comment) {
|
||||
await sendFeedback(rating, comment);
|
||||
}
|
||||
}}>
|
||||
{t("submit")}
|
||||
</Button>
|
||||
</div>
|
||||
{mutation.isError && (
|
||||
<div className="mb-4 flex bg-red-100 p-4 text-sm text-red-700">
|
||||
<div className="flex-shrink-0">
|
||||
<ExclamationIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="ml-3 flex-grow">
|
||||
<p className="font-medium">{t("feedback_error")}</p>
|
||||
<p>{t("please_try_again")}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,9 +2,10 @@ import dayjs from "dayjs";
|
|||
import utc from "dayjs/plugin/utc";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import LicenseRequired from "@ee/components/LicenseRequired";
|
||||
|
||||
import { trpc, inferQueryOutput } from "@lib/trpc";
|
||||
import { inferQueryOutput, trpc } from "@lib/trpc";
|
||||
|
||||
import Avatar from "@components/ui/Avatar";
|
||||
import { DatePicker } from "@components/ui/form/DatePicker";
|
||||
|
@ -33,62 +34,66 @@ export default function TeamAvailabilityModal(props: Props) {
|
|||
}, [utils, selectedTimeZone, selectedDate]);
|
||||
|
||||
return (
|
||||
<div className="flex max-h-[500px] min-h-[500px] flex-row space-x-8 rtl:space-x-reverse">
|
||||
<div className="min-w-64 w-64 space-y-5 p-5 pr-0">
|
||||
<div className="flex">
|
||||
<Avatar
|
||||
imageSrc={WEBSITE_URL + "/" + props.member?.username + "/avatar.png"}
|
||||
alt={props.member?.name || ""}
|
||||
className="h-14 w-14 rounded-full"
|
||||
/>
|
||||
<div className="ml-3 inline-block pt-1">
|
||||
<span className="text-lg font-bold text-neutral-700">{props.member?.name}</span>
|
||||
<span className="-mt-1 block text-sm text-gray-400">{props.member?.email}</span>
|
||||
<LicenseRequired>
|
||||
<div className="flex max-h-[500px] min-h-[500px] flex-row space-x-8 rtl:space-x-reverse">
|
||||
<div className="min-w-64 w-64 space-y-5 p-5 pr-0">
|
||||
<div className="flex">
|
||||
<Avatar
|
||||
imageSrc={WEBAPP_URL + "/" + props.member?.username + "/avatar.png"}
|
||||
alt={props.member?.name || ""}
|
||||
className="h-14 w-14 rounded-full"
|
||||
/>
|
||||
<div className="ml-3 inline-block pt-1">
|
||||
<span className="text-lg font-bold text-neutral-700">{props.member?.name}</span>
|
||||
<span className="-mt-1 block text-sm text-gray-400">{props.member?.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-gray-600">Date</span>
|
||||
<DatePicker
|
||||
date={selectedDate.toDate()}
|
||||
onDatesChange={(newDate) => {
|
||||
setSelectedDate(dayjs(newDate));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold text-gray-600">Timezone</span>
|
||||
<TimezoneSelect
|
||||
id="timeZone"
|
||||
value={selectedTimeZone}
|
||||
onChange={(timezone) => setSelectedTimeZone(timezone.value)}
|
||||
classNamePrefix="react-select"
|
||||
className="react-select-container mt-1 block w-full rounded-sm border border-gray-300 shadow-sm focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold text-gray-600">Slot Length</span>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 15, label: "15 minutes" },
|
||||
{ value: 30, label: "30 minutes" },
|
||||
{ value: 60, label: "60 minutes" },
|
||||
]}
|
||||
isSearchable={false}
|
||||
classNamePrefix="react-select"
|
||||
className="react-select-container focus:ring-primary-500 focus:border-primary-500 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
||||
value={{ value: frequency, label: `${frequency} minutes` }}
|
||||
onChange={(newFrequency) => setFrequency(newFrequency?.value ?? 30)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-gray-600">Date</span>
|
||||
<DatePicker
|
||||
date={selectedDate.toDate()}
|
||||
onDatesChange={(newDate) => {
|
||||
setSelectedDate(dayjs(newDate));
|
||||
}}
|
||||
{props.team && props.member && (
|
||||
<TeamAvailabilityTimes
|
||||
className="overflow-scroll"
|
||||
teamId={props.team.id}
|
||||
memberId={props.member.id}
|
||||
frequency={frequency}
|
||||
selectedDate={selectedDate}
|
||||
selectedTimeZone={selectedTimeZone}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold text-gray-600">Timezone</span>
|
||||
<TimezoneSelect
|
||||
id="timeZone"
|
||||
value={selectedTimeZone}
|
||||
onChange={(timezone) => setSelectedTimeZone(timezone.value)}
|
||||
className="mt-1 block w-full rounded-sm border border-gray-300 shadow-sm sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold text-gray-600">Slot Length</span>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 15, label: "15 minutes" },
|
||||
{ value: 30, label: "30 minutes" },
|
||||
{ value: 60, label: "60 minutes" },
|
||||
]}
|
||||
isSearchable={false}
|
||||
className="block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
||||
value={{ value: frequency, label: `${frequency} minutes` }}
|
||||
onChange={(newFrequency) => setFrequency(newFrequency?.value ?? 30)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{props.team && props.member && (
|
||||
<TeamAvailabilityTimes
|
||||
className="overflow-auto"
|
||||
teamId={props.team.id}
|
||||
memberId={props.member.id}
|
||||
frequency={frequency}
|
||||
selectedDate={selectedDate}
|
||||
selectedTimeZone={selectedTimeZone}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</LicenseRequired>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import React, { useState, useEffect, CSSProperties } from "react";
|
|||
import AutoSizer from "react-virtualized-auto-sizer";
|
||||
import { FixedSizeList as List } from "react-window";
|
||||
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
|
||||
import { inferQueryOutput, trpc } from "@lib/trpc";
|
||||
|
||||
|
@ -46,7 +46,7 @@ export default function TeamAvailabilityScreen(props: Props) {
|
|||
HeaderComponent={
|
||||
<div className="mb-6 flex items-center">
|
||||
<Avatar
|
||||
imageSrc={WEBSITE_URL + "/" + member.username + "/avatar.png"}
|
||||
imageSrc={CAL_URL + "/" + member.username + "/avatar.png"}
|
||||
alt={member?.name || ""}
|
||||
className="min-w-10 min-h-10 mt-1 h-10 w-10 rounded-full"
|
||||
/>
|
||||
|
|
|
@ -11,6 +11,7 @@ import { Button } from "@calcom/ui/Button";
|
|||
import { useContracts } from "../../../contexts/contractsContext";
|
||||
import genericAbi from "../../../web3/abis/abiWithGetBalance.json";
|
||||
import verifyAccount, { AUTH_MESSAGE } from "../../../web3/utils/verifyAccount";
|
||||
import { withLicenseRequired } from "../LicenseRequired";
|
||||
|
||||
interface Window {
|
||||
ethereum: AbstractProvider & { selectedAddress: string };
|
||||
|
@ -150,4 +151,4 @@ const CryptoSection = (props: CryptoSectionProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default CryptoSection;
|
||||
export default withLicenseRequired(CryptoSection);
|
||||
|
|
|
@ -22,20 +22,12 @@ export default function HelpscoutMenuItem() {
|
|||
else
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuItem>
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="flex w-full px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-gray-100 hover:text-gray-900">
|
||||
<ChatAltIcon
|
||||
className={classNames(
|
||||
"text-neutral-400 group-hover:text-neutral-500",
|
||||
"h-5 w-5 flex-shrink-0 ltr:mr-3"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t("help")}
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="flex w-full py-2 pr-4 text-sm font-medium text-neutral-700 hover:bg-gray-100 hover:text-gray-900">
|
||||
{t("contact_support")}
|
||||
</button>
|
||||
|
||||
{active && <HelpScout color="#292929" icon="message" horizontalPosition="right" zIndex="1" />}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -32,6 +32,10 @@ const ImpersonationProvider = CredentialsProvider({
|
|||
throw new Error("This user does not exist");
|
||||
}
|
||||
|
||||
if (user.disableImpersonation) {
|
||||
throw new Error("This user has disabled Impersonation.");
|
||||
}
|
||||
|
||||
// Log impersonations for audit purposes
|
||||
await prisma.impersonations.create({
|
||||
data: {
|
||||
|
|
|
@ -13,22 +13,13 @@ export default function IntercomMenuItem() {
|
|||
if (!process.env.NEXT_PUBLIC_INTERCOM_APP_ID) return null;
|
||||
else
|
||||
return (
|
||||
<DropdownMenuItem>
|
||||
<button
|
||||
onClick={() => {
|
||||
boot();
|
||||
show();
|
||||
}}
|
||||
className="flex w-full px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-gray-100 hover:text-gray-900">
|
||||
<ChatAltIcon
|
||||
className={classNames(
|
||||
"text-neutral-400 group-hover:text-neutral-500",
|
||||
"h-5 w-5 flex-shrink-0 ltr:mr-3"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t("help")}
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
<button
|
||||
onClick={() => {
|
||||
boot();
|
||||
show();
|
||||
}}
|
||||
className="flex w-full py-2 pr-4 text-sm font-medium text-neutral-700 hover:bg-gray-100 hover:text-gray-900">
|
||||
{t("contact_support")}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,20 +17,11 @@ export default function ZendeskMenuItem() {
|
|||
else
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuItem>
|
||||
<button
|
||||
onClick={() => setActive(true)}
|
||||
className="flex w-full px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-gray-100 hover:text-gray-900">
|
||||
<ChatAltIcon
|
||||
className={classNames(
|
||||
"text-neutral-400 group-hover:text-neutral-500",
|
||||
"h-5 w-5 flex-shrink-0 ltr:mr-3"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t("help")}
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
<button
|
||||
onClick={() => setActive(true)}
|
||||
className="flex w-full py-2 pr-4 text-sm font-medium text-neutral-700 hover:bg-gray-100 hover:text-gray-900">
|
||||
{t("contact_support")}
|
||||
</button>
|
||||
{active && (
|
||||
<Script id="ze-snippet" src={"https://static.zdassets.com/ekr/snippet.js?key=" + ZENDESK_KEY} />
|
||||
)}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { useRouter } from "next/router";
|
|||
import { useMemo, useState } from "react";
|
||||
|
||||
import { Alert } from "@calcom/ui/Alert";
|
||||
import LicenseRequired from "@ee/components/LicenseRequired";
|
||||
import TeamAvailabilityScreen from "@ee/components/team/availability/TeamAvailabilityScreen";
|
||||
|
||||
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
|
||||
|
@ -49,17 +50,19 @@ export function TeamAvailabilityPage() {
|
|||
/>
|
||||
)
|
||||
}>
|
||||
{!!errorMessage && <Alert className="-mt-24 border" severity="error" title={errorMessage} />}
|
||||
{isLoading && <Loader />}
|
||||
{isFreeUser ? (
|
||||
<Alert
|
||||
className="-mt-24 border"
|
||||
severity="warning"
|
||||
title="This is a pro feature. Upgrade to pro to see your team's availability."
|
||||
/>
|
||||
) : (
|
||||
TeamAvailability
|
||||
)}
|
||||
<LicenseRequired>
|
||||
{!!errorMessage && <Alert className="-mt-24 border" severity="error" title={errorMessage} />}
|
||||
{isLoading && <Loader />}
|
||||
{isFreeUser ? (
|
||||
<Alert
|
||||
className="-mt-24 border"
|
||||
severity="warning"
|
||||
title="This is a pro feature. Upgrade to pro to see your team's availability."
|
||||
/>
|
||||
) : (
|
||||
TeamAvailability
|
||||
)}
|
||||
</LicenseRequired>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import React, { ErrorInfo } from "react";
|
||||
|
||||
class ErrorBoundary extends React.Component<
|
||||
{ children: React.ReactNode },
|
||||
{ error: Error | null; errorInfo: ErrorInfo | null }
|
||||
> {
|
||||
constructor(props: { children: React.ReactNode } | Readonly<{ children: React.ReactNode }>) {
|
||||
super(props);
|
||||
this.state = { error: null, errorInfo: null };
|
||||
}
|
||||
|
||||
componentDidCatch?(error: Error, errorInfo: ErrorInfo) {
|
||||
// Catch errors in any components below and re-render with error message
|
||||
this.setState({ error, errorInfo });
|
||||
// You can also log error messages to an error reporting service here
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.errorInfo) {
|
||||
// Error path
|
||||
return (
|
||||
<div>
|
||||
<h2>Something went wrong.</h2>
|
||||
<details style={{ whiteSpace: "pre-wrap" }}>
|
||||
{this.state.error && this.state.error.toString()}
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Normally, just render children
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
|
@ -16,7 +16,8 @@ const I18nextAdapter = appWithTranslation<NextJsAppProps & { children: React.Rea
|
|||
));
|
||||
|
||||
// Workaround for https://github.com/vercel/next.js/issues/8592
|
||||
export type AppProps = NextAppProps & {
|
||||
export type AppProps = Omit<NextAppProps, "Component"> & {
|
||||
Component: NextAppProps["Component"] & { requiresLicense?: boolean };
|
||||
/** Will be defined only is there was an error */
|
||||
err?: Error;
|
||||
};
|
||||
|
|
|
@ -3,12 +3,15 @@ import type { CalendarEvent, Person, RecurringEvent } from "@calcom/types/Calend
|
|||
import AttendeeAwaitingPaymentEmail from "@lib/emails/templates/attendee-awaiting-payment-email";
|
||||
import AttendeeCancelledEmail from "@lib/emails/templates/attendee-cancelled-email";
|
||||
import AttendeeDeclinedEmail from "@lib/emails/templates/attendee-declined-email";
|
||||
import AttendeeLocationChangeEmail from "@lib/emails/templates/attendee-location-change-email";
|
||||
import AttendeeRequestEmail from "@lib/emails/templates/attendee-request-email";
|
||||
import AttendeeRequestRescheduledEmail from "@lib/emails/templates/attendee-request-reschedule-email";
|
||||
import AttendeeRescheduledEmail from "@lib/emails/templates/attendee-rescheduled-email";
|
||||
import AttendeeScheduledEmail from "@lib/emails/templates/attendee-scheduled-email";
|
||||
import FeedbackEmail, { Feedback } from "@lib/emails/templates/feedback-email";
|
||||
import ForgotPasswordEmail, { PasswordReset } from "@lib/emails/templates/forgot-password-email";
|
||||
import OrganizerCancelledEmail from "@lib/emails/templates/organizer-cancelled-email";
|
||||
import OrganizerLocationChangeEmail from "@lib/emails/templates/organizer-location-change-email";
|
||||
import OrganizerPaymentRefundFailedEmail from "@lib/emails/templates/organizer-payment-refund-failed-email";
|
||||
import OrganizerRequestEmail from "@lib/emails/templates/organizer-request-email";
|
||||
import OrganizerRequestReminderEmail from "@lib/emails/templates/organizer-request-reminder-email";
|
||||
|
@ -266,3 +269,46 @@ export const sendRequestRescheduleEmail = async (
|
|||
|
||||
await Promise.all(emailsToSend);
|
||||
};
|
||||
|
||||
export const sendLocationChangeEmails = async (
|
||||
calEvent: CalendarEvent,
|
||||
recurringEvent: RecurringEvent = {}
|
||||
) => {
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
|
||||
emailsToSend.push(
|
||||
...calEvent.attendees.map((attendee) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const scheduledEmail = new AttendeeLocationChangeEmail(calEvent, attendee, recurringEvent);
|
||||
resolve(scheduledEmail.sendEmail());
|
||||
} catch (e) {
|
||||
reject(console.error("AttendeeLocationChangeEmail.sendEmail failed", e));
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
emailsToSend.push(
|
||||
new Promise((resolve, reject) => {
|
||||
try {
|
||||
const scheduledEmail = new OrganizerLocationChangeEmail(calEvent, recurringEvent);
|
||||
resolve(scheduledEmail.sendEmail());
|
||||
} catch (e) {
|
||||
reject(console.error("OrganizerLocationChangeEmail.sendEmail failed", e));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(emailsToSend);
|
||||
};
|
||||
export const sendFeedbackEmail = async (feedback: Feedback) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
try {
|
||||
const feedbackEmail = new FeedbackEmail(feedback);
|
||||
resolve(feedbackEmail.sendEmail());
|
||||
} catch (e) {
|
||||
reject(console.error("FeedbackEmail.sendEmail failed", e));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
import dayjs from "dayjs";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import toArray from "dayjs/plugin/toArray";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
|
||||
import { getCancelLink } from "@calcom/lib/CalEventParser";
|
||||
|
||||
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||
import {
|
||||
emailHead,
|
||||
emailSchedulingBodyHeader,
|
||||
emailBodyLogo,
|
||||
emailScheduledBodyHeaderContent,
|
||||
emailSchedulingBodyDivider,
|
||||
} from "./common";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(localizedFormat);
|
||||
dayjs.extend(toArray);
|
||||
|
||||
export default class AttendeeLocationChangeEmail extends AttendeeScheduledEmail {
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
return {
|
||||
icalEvent: {
|
||||
filename: "event.ics",
|
||||
content: this.getiCalEventAsString(),
|
||||
},
|
||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: this.calEvent.organizer.email,
|
||||
subject: `${this.attendee.language.translate("location_changed_event_type_subject", {
|
||||
eventType: this.calEvent.type,
|
||||
name: this.calEvent.team?.name || this.calEvent.organizer.name,
|
||||
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
|
||||
"h:mma"
|
||||
)}, ${this.attendee.language.translate(
|
||||
this.getInviteeStart().format("dddd").toLowerCase()
|
||||
)}, ${this.attendee.language.translate(
|
||||
this.getInviteeStart().format("MMMM").toLowerCase()
|
||||
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
|
||||
})}`,
|
||||
html: this.getHtmlBody(),
|
||||
text: this.getTextBody(),
|
||||
};
|
||||
}
|
||||
protected getTextBody(): string {
|
||||
// Only the original attendee can make changes to the event
|
||||
// Guests cannot
|
||||
if (this.attendee === this.calEvent.attendees[0]) {
|
||||
return `
|
||||
${this.attendee.language.translate("event_location_changed")}
|
||||
${this.attendee.language.translate("emailed_you_and_any_other_attendees")}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getLocation()}
|
||||
${this.getDescription()}
|
||||
${this.getAdditionalNotes()}
|
||||
${this.attendee.language.translate("need_to_reschedule_or_cancel")}
|
||||
${getCancelLink(this.calEvent)}
|
||||
`.replace(/(<([^>]+)>)/gi, "");
|
||||
}
|
||||
|
||||
return `
|
||||
${this.attendee.language.translate("event_location_changed")}
|
||||
${this.attendee.language.translate("emailed_you_and_any_other_attendees")}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
`.replace(/(<([^>]+)>)/gi, "");
|
||||
}
|
||||
|
||||
protected getHtmlBody(): string {
|
||||
const headerContent = this.attendee.language.translate("location_changed_event_type_subject", {
|
||||
eventType: this.calEvent.type,
|
||||
name: this.calEvent.team?.name || this.calEvent.organizer.name,
|
||||
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
|
||||
"h:mma"
|
||||
)}, ${this.attendee.language.translate(
|
||||
this.getInviteeStart().format("dddd").toLowerCase()
|
||||
)}, ${this.attendee.language.translate(
|
||||
this.getInviteeStart().format("MMMM").toLowerCase()
|
||||
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
|
||||
});
|
||||
|
||||
return `
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
${emailHead(headerContent)}
|
||||
<body style="word-spacing:normal;background-color:#F5F5F5;">
|
||||
<div style="background-color:#F5F5F5;">
|
||||
${emailSchedulingBodyHeader("calendarCircle")}
|
||||
${emailScheduledBodyHeaderContent(
|
||||
this.attendee.language.translate("event_location_changed"),
|
||||
this.attendee.language.translate("emailed_you_and_any_other_attendees")
|
||||
)}
|
||||
${emailSchedulingBodyDivider()}
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getWho()}
|
||||
${this.getLocation()}
|
||||
${this.getDescription()}
|
||||
${this.getAdditionalNotes()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
${emailSchedulingBodyDivider()}
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:0px;text-align:left;color:#3E3E3E;">
|
||||
${this.getManageLink()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
${emailBodyLogo()}
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
|
@ -102,6 +102,7 @@ ${this.calEvent.organizer.language.translate("request_reschedule_title_attendee"
|
|||
${this.calEvent.organizer.language.translate("request_reschedule_subtitle", {
|
||||
organizer: this.calEvent.organizer.name,
|
||||
})},
|
||||
${this.calEvent.cancellationReason && this.getReason()}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getAdditionalNotes()}
|
||||
|
@ -151,6 +152,7 @@ ${getCancelLink(this.calEvent)}
|
|||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
${this.calEvent.cancellationReason && this.getReason()}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getWho()}
|
||||
|
|
|
@ -52,6 +52,7 @@ export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
|
|||
return `
|
||||
${this.attendee.language.translate("event_has_been_rescheduled")}
|
||||
${this.attendee.language.translate("emailed_you_and_any_other_attendees")}
|
||||
${this.calEvent.cancellationReason && this.getReason()}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getLocation()}
|
||||
|
@ -112,6 +113,7 @@ ${this.getCustomInputs()}
|
|||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
${this.calEvent.cancellationReason && this.getReason()}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getWho()}
|
||||
|
|
|
@ -428,12 +428,13 @@ ${getRichDescription(this.calEvent)}
|
|||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
|
||||
providerName || this.calEvent.location
|
||||
}</p>
|
||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
|
||||
(providerName === "Zoom" || providerName === "Google") &&
|
||||
`
|
||||
${this.calEvent.organizer.language.translate("meeting_url_provided_after_confirmed")}
|
||||
`
|
||||
}</p>
|
||||
${
|
||||
providerName === "Zoom" || providerName === "Google"
|
||||
? `<p style="color: #494949; font-weight: 400; line-height: 24px;">
|
||||
${this.calEvent.organizer.language.translate("meeting_url_provided_after_confirmed")}
|
||||
</p>`
|
||||
: ``
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
@ -451,4 +452,12 @@ ${getRichDescription(this.calEvent)}
|
|||
protected getInviteeEnd(): Dayjs {
|
||||
return dayjs(this.calEvent.endTime).tz(this.getTimezone());
|
||||
}
|
||||
|
||||
protected getReason(): string {
|
||||
return `
|
||||
<div style="line-height: 6px; margin-bottom: 24px;">
|
||||
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("reschedule_reason")}</p>
|
||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.cancellationReason}</p>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
import BaseEmail from "@lib/emails/templates/_base-email";
|
||||
|
||||
import { emailHead, emailBodyLogo } from "./common";
|
||||
|
||||
export interface Feedback {
|
||||
userId: number;
|
||||
rating: string;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
export default class FeedbackEmail extends BaseEmail {
|
||||
feedback: Feedback;
|
||||
|
||||
constructor(feedback: Feedback) {
|
||||
super();
|
||||
this.feedback = feedback;
|
||||
}
|
||||
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
return {
|
||||
from: `Cal.com <${this.getMailerOptions().from}>`,
|
||||
to: process.env.SEND_FEEDBACK_EMAIL,
|
||||
subject: `User Feedback`,
|
||||
html: this.getHtmlBody(),
|
||||
text: this.getTextBody(),
|
||||
};
|
||||
}
|
||||
|
||||
protected getTextBody(): string {
|
||||
return `
|
||||
userId: ${this.feedback.userId}
|
||||
rating: ${this.feedback.rating}
|
||||
comment: ${this.feedback.comment}
|
||||
`;
|
||||
}
|
||||
|
||||
protected getHtmlBody(): string {
|
||||
return `
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
${emailHead("Feedback")}
|
||||
<body style="word-spacing:normal;background-color:#F5F5F5;">
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:24px;padding-bottom:0px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:24px;font-weight:700;line-height:24px;text-align:center;color:#292929;">Feedback</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="background-color:#F5F5F5;">
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
<div style="line-height: 6px;">
|
||||
<p style="color: #494949;">Used id</p>
|
||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
|
||||
this.feedback.userId
|
||||
}</p>
|
||||
</div>
|
||||
<div style="line-height: 6px;">
|
||||
<p style="color: #494949;">Rating</p>
|
||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
|
||||
this.feedback.rating
|
||||
}</p>
|
||||
</div>
|
||||
<div style="line-height: 6px;">
|
||||
<p style="color: #494949;">Comment</p>
|
||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
|
||||
this.feedback.comment
|
||||
}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
${emailBodyLogo()}
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
import dayjs from "dayjs";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import toArray from "dayjs/plugin/toArray";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
|
||||
import { getCancelLink } from "@calcom/lib/CalEventParser";
|
||||
|
||||
import {
|
||||
emailHead,
|
||||
emailSchedulingBodyHeader,
|
||||
emailBodyLogo,
|
||||
emailScheduledBodyHeaderContent,
|
||||
emailSchedulingBodyDivider,
|
||||
} from "./common";
|
||||
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(localizedFormat);
|
||||
dayjs.extend(toArray);
|
||||
|
||||
export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmail {
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
const toAddresses = [this.calEvent.organizer.email];
|
||||
if (this.calEvent.team) {
|
||||
this.calEvent.team.members.forEach((member) => {
|
||||
const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member);
|
||||
if (memberAttendee) {
|
||||
toAddresses.push(memberAttendee.email);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
icalEvent: {
|
||||
filename: "event.ics",
|
||||
content: this.getiCalEventAsString(),
|
||||
},
|
||||
from: `Cal.com <${this.getMailerOptions().from}>`,
|
||||
to: toAddresses.join(","),
|
||||
subject: `${this.calEvent.organizer.language.translate("location_changed_event_type_subject", {
|
||||
eventType: this.calEvent.type,
|
||||
name: this.calEvent.attendees[0].name,
|
||||
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
|
||||
"h:mma"
|
||||
)}, ${this.calEvent.organizer.language.translate(
|
||||
this.getOrganizerStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.organizer.language.translate(
|
||||
this.getOrganizerStart().format("MMMM").toLowerCase()
|
||||
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
|
||||
})}`,
|
||||
html: this.getHtmlBody(),
|
||||
text: this.getTextBody(),
|
||||
};
|
||||
}
|
||||
|
||||
protected getTextBody(): string {
|
||||
return `
|
||||
${this.calEvent.organizer.language.translate("event_location_changed")}
|
||||
${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getLocation()}
|
||||
${this.getDescription()}
|
||||
${this.getAdditionalNotes()}
|
||||
${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")}
|
||||
${getCancelLink(this.calEvent)}
|
||||
`.replace(/(<([^>]+)>)/gi, "");
|
||||
}
|
||||
|
||||
protected getHtmlBody(): string {
|
||||
const headerContent = this.calEvent.organizer.language.translate("location_changed_event_type_subject", {
|
||||
eventType: this.calEvent.type,
|
||||
name: this.calEvent.attendees[0].name,
|
||||
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
|
||||
"h:mma"
|
||||
)}, ${this.calEvent.organizer.language.translate(
|
||||
this.getOrganizerStart().format("dddd").toLowerCase()
|
||||
)}, ${this.calEvent.organizer.language.translate(
|
||||
this.getOrganizerStart().format("MMMM").toLowerCase()
|
||||
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
|
||||
});
|
||||
|
||||
return `
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
${emailHead(headerContent)}
|
||||
<body style="word-spacing:normal;background-color:#F5F5F5;">
|
||||
<div style="background-color:#F5F5F5;">
|
||||
${emailSchedulingBodyHeader("calendarCircle")}
|
||||
${emailScheduledBodyHeaderContent(
|
||||
this.calEvent.organizer.language.translate("event_location_changed"),
|
||||
this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")
|
||||
)}
|
||||
${emailSchedulingBodyDivider()}
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getWho()}
|
||||
${this.getLocation()}
|
||||
${this.getDescription()}
|
||||
${this.getAdditionalNotes()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
${emailSchedulingBodyDivider()}
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:0px;text-align:left;color:#3E3E3E;">
|
||||
${this.getManageLink()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
${emailBodyLogo()}
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
|
@ -111,6 +111,7 @@ ${this.calEvent.organizer.language.translate("request_reschedule_title_organizer
|
|||
${this.calEvent.organizer.language.translate("request_reschedule_subtitle_organizer", {
|
||||
attendee: this.calEvent.attendees[0].name,
|
||||
})},
|
||||
${this.calEvent.cancellationReason && this.getReason()}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getLocation()}
|
||||
|
@ -163,6 +164,7 @@ ${getCancelLink(this.calEvent)}
|
|||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
${this.calEvent.cancellationReason && this.getReason()}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getWho()}
|
||||
|
|
|
@ -59,6 +59,7 @@ export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
|
|||
return `
|
||||
${this.calEvent.organizer.language.translate("event_has_been_rescheduled")}
|
||||
${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")}
|
||||
${this.calEvent.cancellationReason && this.getReason()}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getLocation()}
|
||||
|
@ -108,6 +109,7 @@ ${getCancelLink(this.calEvent)}
|
|||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
${this.calEvent.cancellationReason && this.getReason()}
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getWho()}
|
||||
|
|
|
@ -417,12 +417,13 @@ ${getRichDescription(this.calEvent)}
|
|||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
|
||||
providerName || this.calEvent.location
|
||||
}</p>
|
||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
|
||||
(providerName === "Zoom" || providerName === "Google") &&
|
||||
`
|
||||
${this.calEvent.organizer.language.translate("meeting_url_provided_after_confirmed")}
|
||||
`
|
||||
}</p>
|
||||
${
|
||||
providerName === "Zoom" || providerName === "Google"
|
||||
? `<p style="color: #494949; font-weight: 400; line-height: 24px;">
|
||||
${this.calEvent.organizer.language.translate("meeting_url_provided_after_confirmed")}
|
||||
</p>`
|
||||
: ``
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
@ -438,4 +439,12 @@ ${getRichDescription(this.calEvent)}
|
|||
protected getOrganizerEnd(): Dayjs {
|
||||
return dayjs(this.calEvent.endTime).tz(this.getTimezone());
|
||||
}
|
||||
|
||||
protected getReason(): string {
|
||||
return `
|
||||
<div style="line-height: 6px; margin-bottom: 24px;">
|
||||
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("reschedule_reason")}</p>
|
||||
<p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.cancellationReason}</p>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { stringify } from "querystring";
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
import getSlots from "@lib/slots";
|
||||
import { TimeRange, WorkingHours } from "@lib/types/schedule";
|
||||
import { CurrentSeats, TimeRange, WorkingHours } from "@lib/types/schedule";
|
||||
|
||||
dayjs.extend(isBetween);
|
||||
dayjs.extend(utc);
|
||||
|
@ -15,11 +15,14 @@ type AvailabilityUserResponse = {
|
|||
busy: TimeRange[];
|
||||
timeZone: string;
|
||||
workingHours: WorkingHours[];
|
||||
currentSeats?: CurrentSeats[];
|
||||
};
|
||||
|
||||
type Slot = {
|
||||
time: Dayjs;
|
||||
users?: string[];
|
||||
bookingUid?: string;
|
||||
attendees?: number;
|
||||
};
|
||||
|
||||
type UseSlotsProps = {
|
||||
|
@ -40,10 +43,11 @@ type getFilteredTimesProps = {
|
|||
eventLength: number;
|
||||
beforeBufferTime: number;
|
||||
afterBufferTime: number;
|
||||
currentSeats?: CurrentSeats[];
|
||||
};
|
||||
|
||||
export const getFilteredTimes = (props: getFilteredTimesProps) => {
|
||||
const { times, busy, eventLength, beforeBufferTime, afterBufferTime } = props;
|
||||
const { times, busy, eventLength, beforeBufferTime, afterBufferTime, currentSeats } = props;
|
||||
const finalizationTime = times[times.length - 1]?.add(eventLength, "minutes");
|
||||
// Check for conflicts
|
||||
for (let i = times.length - 1; i >= 0; i -= 1) {
|
||||
|
@ -56,6 +60,10 @@ export const getFilteredTimes = (props: getFilteredTimesProps) => {
|
|||
const slotStartTime = times[i];
|
||||
const slotEndTime = times[i].add(eventLength, "minutes");
|
||||
const slotStartTimeWithBeforeBuffer = times[i].subtract(beforeBufferTime, "minutes");
|
||||
// If the event has seats then see if there is already a booking (want to show full bookings as well)
|
||||
if (currentSeats?.some((booking) => booking.startTime === slotStartTime.toISOString())) {
|
||||
break;
|
||||
}
|
||||
busy.every((busyTime): boolean => {
|
||||
const startTime = dayjs(busyTime.start);
|
||||
const endTime = dayjs(busyTime.end);
|
||||
|
@ -124,19 +132,21 @@ export const useSlots = (props: UseSlotsProps) => {
|
|||
|
||||
const handleAvailableSlots = async (res: Response) => {
|
||||
const responseBody: AvailabilityUserResponse = await res.json();
|
||||
const { workingHours, busy, currentSeats } = responseBody;
|
||||
const times = getSlots({
|
||||
frequency: slotInterval || eventLength,
|
||||
inviteeDate: date,
|
||||
workingHours: responseBody.workingHours,
|
||||
workingHours,
|
||||
minimumBookingNotice,
|
||||
eventLength,
|
||||
});
|
||||
const filterTimeProps = {
|
||||
times,
|
||||
busy: responseBody.busy,
|
||||
busy,
|
||||
eventLength,
|
||||
beforeBufferTime,
|
||||
afterBufferTime,
|
||||
currentSeats,
|
||||
};
|
||||
const filteredTimes = getFilteredTimes(filterTimeProps);
|
||||
// temporary
|
||||
|
@ -144,6 +154,14 @@ export const useSlots = (props: UseSlotsProps) => {
|
|||
return filteredTimes.map((time) => ({
|
||||
time,
|
||||
users: [user],
|
||||
// Conditionally add the attendees and booking id to slots object if there is already a booking during that time
|
||||
...(currentSeats?.some((booking) => booking.startTime === time.toISOString()) && {
|
||||
attendees:
|
||||
currentSeats[currentSeats.findIndex((booking) => booking.startTime === time.toISOString())]._count
|
||||
.attendees,
|
||||
bookingUid:
|
||||
currentSeats[currentSeats.findIndex((booking) => booking.startTime === time.toISOString())].uid,
|
||||
}),
|
||||
}));
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Head from "next/head";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useEmbedTheme } from "@calcom/embed-core";
|
||||
import { useEmbedTheme } from "@calcom/embed-core/embed-iframe";
|
||||
|
||||
import { Maybe } from "@trpc/server";
|
||||
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import { TFunction } from "next-i18next";
|
||||
|
||||
import { LocationType } from "./location";
|
||||
|
||||
/**
|
||||
* Use this function to translate booking location value to a readable string
|
||||
* @param linkValue
|
||||
* @param translationFunction
|
||||
* @returns
|
||||
*/
|
||||
export const linkValueToString = (
|
||||
linkValue: string | undefined | null,
|
||||
translationFunction: TFunction
|
||||
): string => {
|
||||
const t = translationFunction;
|
||||
if (!linkValue) {
|
||||
return translationFunction("no_location");
|
||||
}
|
||||
switch (linkValue) {
|
||||
case LocationType.InPerson:
|
||||
return t("in_person_meeting");
|
||||
case LocationType.UserPhone:
|
||||
return t("user_phone");
|
||||
case LocationType.GoogleMeet:
|
||||
return `Google Meet: ${t("meeting_url_in_conformation_email")}`;
|
||||
case LocationType.Zoom:
|
||||
return `Zoom: ${t("meeting_url_in_conformation_email")}`;
|
||||
case LocationType.Daily:
|
||||
return `Cal Video: ${t("meeting_url_in_conformation_email")}`;
|
||||
case LocationType.Jitsi:
|
||||
return `Jitsi: ${t("meeting_url_in_conformation_email")}`;
|
||||
case LocationType.Huddle01:
|
||||
return `Huddle01t: ${t("meeting_url_in_conformation_email")}`;
|
||||
case LocationType.Tandem:
|
||||
return `Tandem: ${t("meeting_url_in_conformation_email")}`;
|
||||
case LocationType.Teams:
|
||||
return `Teams: ${t("meeting_url_in_conformation_email")}`;
|
||||
default:
|
||||
return linkValue || "";
|
||||
}
|
||||
};
|
|
@ -2,6 +2,12 @@ import { TFunction } from "next-i18next";
|
|||
|
||||
import { LocationType } from "./location";
|
||||
|
||||
/**
|
||||
* Use this function for translating event location to a readable string
|
||||
* @param location
|
||||
* @param t
|
||||
* @returns string
|
||||
*/
|
||||
export const LocationOptionsToString = (location: string, t: TFunction) => {
|
||||
switch (location) {
|
||||
case LocationType.InPerson:
|
||||
|
|
|
@ -4,7 +4,7 @@ import isToday from "dayjs/plugin/isToday";
|
|||
import utc from "dayjs/plugin/utc";
|
||||
|
||||
import { getWorkingHours } from "./availability";
|
||||
import { WorkingHours } from "./types/schedule";
|
||||
import { WorkingHours, CurrentSeats } from "./types/schedule";
|
||||
|
||||
dayjs.extend(isToday);
|
||||
dayjs.extend(utc);
|
||||
|
@ -16,6 +16,7 @@ export type GetSlots = {
|
|||
workingHours: WorkingHours[];
|
||||
minimumBookingNotice: number;
|
||||
eventLength: number;
|
||||
currentSeats?: CurrentSeats[];
|
||||
};
|
||||
export type WorkingHoursTimeFrame = { startTime: number; endTime: number };
|
||||
|
||||
|
@ -42,7 +43,14 @@ const splitAvailableTime = (
|
|||
return result;
|
||||
};
|
||||
|
||||
const getSlots = ({ inviteeDate, frequency, minimumBookingNotice, workingHours, eventLength }: GetSlots) => {
|
||||
const getSlots = ({
|
||||
inviteeDate,
|
||||
frequency,
|
||||
minimumBookingNotice,
|
||||
workingHours,
|
||||
eventLength,
|
||||
currentSeats,
|
||||
}: GetSlots) => {
|
||||
// current date in invitee tz
|
||||
const startDate = dayjs().add(minimumBookingNotice, "minute");
|
||||
const startOfDay = dayjs.utc().startOf("day");
|
||||
|
|
|
@ -24,6 +24,7 @@ export type BookingCreateBody = {
|
|||
timeZone: string;
|
||||
user?: string | string[];
|
||||
language: string;
|
||||
bookingUid?: string;
|
||||
customInputs: { label: string; value: string | boolean }[];
|
||||
metadata: {
|
||||
[key: string]: string;
|
||||
|
|
|
@ -24,6 +24,7 @@ export type AdvancedOptions = {
|
|||
availability?: { openingHours: WorkingHours[]; dateOverrides: WorkingHours[] };
|
||||
customInputs?: EventTypeCustomInput[];
|
||||
timeZone?: string;
|
||||
seatsPerTimeSlot: number;
|
||||
destinationCalendar?: {
|
||||
userId?: number;
|
||||
eventTypeId?: number;
|
||||
|
|
|
@ -16,3 +16,11 @@ export type WorkingHours = {
|
|||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
|
||||
export type CurrentSeats = {
|
||||
uid: string;
|
||||
startTime: string;
|
||||
_count: {
|
||||
attendees: number;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "1.6.1",
|
||||
"version": "1.6.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
@ -76,6 +76,7 @@
|
|||
"jimp": "^0.16.1",
|
||||
"libphonenumber-js": "^1.9.53",
|
||||
"lodash": "^4.17.21",
|
||||
"memory-cache": "^0.2.0",
|
||||
"micro": "^9.3.4",
|
||||
"mime-types": "^2.1.35",
|
||||
"next": "^12.1.6",
|
||||
|
@ -125,6 +126,7 @@
|
|||
"@types/glidejs__glide": "^3.4.2",
|
||||
"@types/jest": "^27.5.1",
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@types/memory-cache": "^0.2.2",
|
||||
"@types/micro": "7.3.6",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/module-alias": "^2.0.1",
|
||||
|
|
|
@ -10,7 +10,12 @@ import { useEffect, useState } from "react";
|
|||
import { Toaster } from "react-hot-toast";
|
||||
import { JSONObject } from "superjson/dist/types";
|
||||
|
||||
import { sdkActionManager, useEmbedNonStylesConfig, useEmbedStyles, useIsEmbed } from "@calcom/embed-core";
|
||||
import {
|
||||
sdkActionManager,
|
||||
useEmbedNonStylesConfig,
|
||||
useEmbedStyles,
|
||||
useIsEmbed,
|
||||
} from "@calcom/embed-core/embed-iframe";
|
||||
import defaultEvents, {
|
||||
getDynamicEventDescription,
|
||||
getGroupName,
|
||||
|
@ -337,11 +342,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
weekStart: "Sunday",
|
||||
brandColor: "",
|
||||
darkBrandColor: "",
|
||||
allowDynamicBooking: users.some((user) => {
|
||||
allowDynamicBooking: !users.some((user) => {
|
||||
return !user.allowDynamicBooking;
|
||||
})
|
||||
? false
|
||||
: true,
|
||||
}),
|
||||
}
|
||||
: {
|
||||
name: user.name || user.username,
|
||||
|
|
|
@ -3,6 +3,7 @@ import { UserPlan } from "@prisma/client";
|
|||
import { GetServerSidePropsContext } from "next";
|
||||
import { JSONObject } from "superjson/dist/types";
|
||||
|
||||
import { locationHiddenFilter, LocationObject } from "@calcom/app-store/locations";
|
||||
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { RecurringEvent } from "@calcom/types/Calendar";
|
||||
|
@ -84,6 +85,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
periodEndDate: true,
|
||||
periodDays: true,
|
||||
periodCountCalendarDays: true,
|
||||
locations: true,
|
||||
schedulingType: true,
|
||||
recurringEvent: true,
|
||||
schedule: {
|
||||
|
@ -100,6 +102,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
timeZone: true,
|
||||
metadata: true,
|
||||
slotInterval: true,
|
||||
seatsPerTimeSlot: true,
|
||||
users: {
|
||||
select: {
|
||||
avatar: true,
|
||||
|
@ -253,12 +256,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
} as const;
|
||||
}
|
||||
}
|
||||
const locations = eventType.locations ? (eventType.locations as LocationObject[]) : [];
|
||||
|
||||
const eventTypeObject = Object.assign({}, eventType, {
|
||||
metadata: (eventType.metadata || {}) as JSONObject,
|
||||
periodStartDate: eventType.periodStartDate?.toString() ?? null,
|
||||
periodEndDate: eventType.periodEndDate?.toString() ?? null,
|
||||
recurringEvent: (eventType.recurringEvent || {}) as RecurringEvent,
|
||||
locations: locationHiddenFilter(locations),
|
||||
});
|
||||
|
||||
const schedule = eventType.schedule
|
||||
|
@ -302,11 +307,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
weekStart: "Sunday",
|
||||
brandColor: "",
|
||||
darkBrandColor: "",
|
||||
allowDynamicBooking: users.some((user) => {
|
||||
allowDynamicBooking: !users.some((user) => {
|
||||
return !user.allowDynamicBooking;
|
||||
})
|
||||
? false
|
||||
: true,
|
||||
}),
|
||||
}
|
||||
: {
|
||||
name: user.name || user.username,
|
||||
|
|
|
@ -126,6 +126,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
price: true,
|
||||
currency: true,
|
||||
disableGuests: true,
|
||||
seatsPerTimeSlot: true,
|
||||
users: {
|
||||
select: {
|
||||
id: true,
|
||||
|
@ -176,8 +177,13 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
})[0];
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (context.query.rescheduleUid) {
|
||||
booking = await getBooking(prisma, context.query.rescheduleUid as string);
|
||||
if (context.query.rescheduleUid || context.query.bookingUid) {
|
||||
booking = await getBooking(
|
||||
prisma,
|
||||
context.query.rescheduleUid
|
||||
? (context.query.rescheduleUid as string)
|
||||
: (context.query.bookingUid as string)
|
||||
);
|
||||
}
|
||||
|
||||
const dynamicNames = isDynamicGroupBooking
|
||||
|
@ -194,11 +200,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
theme: null,
|
||||
brandColor: "",
|
||||
darkBrandColor: "",
|
||||
allowDynamicBooking: users.some((user) => {
|
||||
allowDynamicBooking: !users.some((user) => {
|
||||
return !user.allowDynamicBooking;
|
||||
})
|
||||
? false
|
||||
: true,
|
||||
}),
|
||||
eventName: getDynamicEventName(dynamicNames, eventTypeSlug),
|
||||
}
|
||||
: {
|
||||
|
|
|
@ -3,6 +3,7 @@ import Head from "next/head";
|
|||
import superjson from "superjson";
|
||||
|
||||
import "@calcom/embed-core/src/embed-iframe";
|
||||
import LicenseRequired from "@ee/components/LicenseRequired";
|
||||
|
||||
import AppProviders, { AppProps } from "@lib/app-providers";
|
||||
import { seoConfig } from "@lib/config/next-seo.config";
|
||||
|
@ -37,7 +38,13 @@ function MyApp(props: AppProps) {
|
|||
<script dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}></script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
</Head>
|
||||
<Component {...pageProps} err={err} />
|
||||
{Component.requiresLicense ? (
|
||||
<LicenseRequired>
|
||||
<Component {...pageProps} err={err} />
|
||||
</LicenseRequired>
|
||||
) : (
|
||||
<Component {...pageProps} err={err} />
|
||||
)}
|
||||
</AppProviders>
|
||||
</ContractsProvider>
|
||||
);
|
||||
|
|
|
@ -10,6 +10,7 @@ import nodemailer, { TransportOptions } from "nodemailer";
|
|||
import { authenticator } from "otplib";
|
||||
import path from "path";
|
||||
|
||||
import checkLicense from "@calcom/ee/server/checkLicense";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import { defaultCookies } from "@calcom/lib/default-cookies";
|
||||
|
@ -276,8 +277,10 @@ export default NextAuth({
|
|||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
const hasValidLicense = await checkLicense(process.env.CALCOM_LICENSE_KEY || "");
|
||||
const calendsoSession: Session = {
|
||||
...session,
|
||||
hasValidLicense,
|
||||
user: {
|
||||
...session.user,
|
||||
id: token.id as number,
|
||||
|
|
|
@ -50,6 +50,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
seatsPerTimeSlot: true,
|
||||
timeZone: true,
|
||||
schedule: {
|
||||
select: {
|
||||
|
@ -107,9 +108,34 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
(eventType?.availability.length ? eventType.availability : currentUser.availability)
|
||||
);
|
||||
|
||||
/* Current logic is if a booking is in a time slot mark it as busy, but seats can have more than one attendee so grab
|
||||
current bookings with a seats event type and display them on the calendar, even if they are full */
|
||||
let currentSeats;
|
||||
if (eventType?.seatsPerTimeSlot) {
|
||||
currentSeats = await prisma.booking.findMany({
|
||||
where: {
|
||||
eventTypeId: eventTypeId,
|
||||
startTime: {
|
||||
gte: dateFrom.format(),
|
||||
lte: dateTo.format(),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
uid: true,
|
||||
startTime: true,
|
||||
_count: {
|
||||
select: {
|
||||
attendees: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
busy: bufferedBusyTimes,
|
||||
timeZone,
|
||||
workingHours,
|
||||
currentSeats,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -188,6 +188,7 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
|
|||
metadata: true,
|
||||
destinationCalendar: true,
|
||||
hideCalendarNotes: true,
|
||||
seatsPerTimeSlot: true,
|
||||
recurringEvent: true,
|
||||
},
|
||||
});
|
||||
|
@ -200,7 +201,11 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
|
|||
|
||||
type User = Prisma.UserGetPayload<typeof userSelect>;
|
||||
|
||||
type ExtendedBookingCreateBody = BookingCreateBody & { noEmail?: boolean; recurringCount?: number };
|
||||
type ExtendedBookingCreateBody = BookingCreateBody & {
|
||||
noEmail?: boolean;
|
||||
recurringCount?: number;
|
||||
rescheduleReason?: string;
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { recurringCount, noEmail, ...reqBody } = req.body as ExtendedBookingCreateBody;
|
||||
|
@ -294,6 +299,45 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
return g;
|
||||
});
|
||||
|
||||
// For seats, if the booking already exists then we want to add the new attendee to the existing booking
|
||||
if (reqBody.bookingUid) {
|
||||
if (!eventType.seatsPerTimeSlot)
|
||||
return res.status(404).json({ message: "Event type does not have seats" });
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: {
|
||||
uid: reqBody.bookingUid,
|
||||
},
|
||||
include: {
|
||||
attendees: true,
|
||||
},
|
||||
});
|
||||
if (!booking) return res.status(404).json({ message: "Booking not found" });
|
||||
|
||||
if (eventType.seatsPerTimeSlot <= booking.attendees.length)
|
||||
return res.status(409).json({ message: "Booking seats are full" });
|
||||
|
||||
if (booking.attendees.some((attendee) => attendee.email === invitee[0].email))
|
||||
return res.status(409).json({ message: "Already signed up for time slot" });
|
||||
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
uid: reqBody.bookingUid,
|
||||
},
|
||||
data: {
|
||||
attendees: {
|
||||
create: {
|
||||
email: invitee[0].email,
|
||||
name: invitee[0].name,
|
||||
timeZone: invitee[0].timeZone,
|
||||
locale: invitee[0].language.locale,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return res.status(201).json(booking);
|
||||
}
|
||||
|
||||
const teamMemberPromises =
|
||||
eventType.schedulingType === SchedulingType.COLLECTIVE
|
||||
? users.slice(1).map(async function (user) {
|
||||
|
@ -637,7 +681,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
|
||||
if (originalRescheduledBooking?.uid) {
|
||||
// Use EventManager to conditionally use all needed integrations.
|
||||
const updateManager = await eventManager.update(evt, originalRescheduledBooking.uid, booking?.id);
|
||||
const updateManager = await eventManager.update(
|
||||
evt,
|
||||
originalRescheduledBooking.uid,
|
||||
booking?.id,
|
||||
reqBody.rescheduleReason
|
||||
);
|
||||
// This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back
|
||||
// to the default description when we are sending the emails.
|
||||
evt.description = eventType.description;
|
||||
|
@ -671,7 +720,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
{
|
||||
...evt,
|
||||
additionInformation: metadata,
|
||||
additionalNotes, // Resets back to the addtionalNote input and not the overriden value
|
||||
additionalNotes, // Resets back to the additionalNote input and not the override value
|
||||
cancellationReason: reqBody.rescheduleReason,
|
||||
},
|
||||
reqBody.recurringEventId ? (eventType.recurringEvent as RecurringEvent) : {}
|
||||
);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user