feat: Enable Apps for Teams & Orgs [CAL-1782] (#9337)
* Initial commit * Adding feature flag * Add schema relation for teams and credentials * feat: Orgs Schema Changing `scopedMembers` to `orgUsers` (#9209) * Change scopedMembers to orgMembers * Change to orgUsers * Create getUserAdminTeams function & tRPC endpoint * Get user admin teams on app store page * Create UserAdminTeams type * Add user query to getUserAdminTeams * Letting duplicate slugs for teams to support orgs * Covering null on unique clauses * Add dropdown to install button on app store * Supporting having the orgId in the session cookie * On app page, only show dropdown if there are teams * Add teamId to OAuth state * Create team credential for OAuth flow * Create team credential for GCal * Add create user or team credential for Stripe * Create webex credentials for users or teams * Fix type error on useAddAppMutation * Hubspot create credential on user or team * Zoho create create credential for user or team * Zoom create credentials on user or team * Salesforce create credential on user or teams * OAuth create credentials for user or teams * Revert Outlook changes * Revert GCal changes * Default app instal, create credential on user or team * Add teamId to credential creation * Disable installing for teams for calendars * Include teams when querying installed apps * Render team credentials on installed page * Uninstall team apps * Type fix on app card * Add input to include user in teams query * Add dropdown to install app page for user or team * Type fixes on category page * Install app from eventType page to user or team * Render user and team apps on event type app page * feat: organization event type filter (#9253) Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * Missing changes to support orgs schema changes * Render user and team apps on event type app page * Add credentialOwner to eventTypeAppCard types * Type fixes * Create hook to check if app is enabled * Clean up console.logs * Fix useIsAppEnabled by returning not an array * Convert event type apps to useIsAppEnabled * Abstract credential owner type * Remove console.logs * On installed app page, show apps if only team credential is installed * Clean up commented lines * Handle installing app to just an team event from event type page * Fix early return when creating team app credential * Zoom add state to callback * Get team location credentials and save credential id to location * feat: Onboarding process to create an organization (#9184) * Desktop first banner, mobile pending * Removing dead code and img * WIP * Adds Email verification template+translations for organizations (#9202) * First step done * Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding * Step 2 done, avatar not working * Covering null on unique clauses * Onboarding admins step * Last step to create teams * Moving change password handler, improving verifying code flow * Clearing error before submitting * Reverting email testing api changes * Reverting having the banner for now * Consistent exported components * Remove unneeded files from banner * Removing uneeded code * Fixing avatar selector * Using meta component for head/descr * Missing i18n strings * Feedback * Making an org avatar (temp) * Check for subteams slug clashes with usernames * Fixing create teams onsuccess * feedback * Making sure we check requestedSlug now --------- Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> * Type fix * Grab team location credentials * Add isInstalled to eventType apps query * feat: [CAL-1816] Organization subdomain support (#9345) * Desktop first banner, mobile pending * Removing dead code and img * WIP * Adds Email verification template+translations for organizations (#9202) * First step done * Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding * Step 2 done, avatar not working * Covering null on unique clauses * Onboarding admins step * Last step to create teams * Moving change password handler, improving verifying code flow * Clearing error before submitting * Reverting email testing api changes * Reverting having the banner for now * Consistent exported components * Remove unneeded files from banner * Removing uneeded code * Fixing avatar selector * Using meta component for head/descr * Missing i18n strings * Feedback * Making an org avatar (temp) * Check for subteams slug clashes with usernames * Fixing create teams onsuccess * Covering users and subteams, excluding non-org users * Unpublished teams shows correctly * Create subdomain in Vercel * feedback * Renaming Vercel env vars * Vercel domain check before creation * Supporting cal-staging.com * Change to have vercel detect it * vercel domain check data message error * Remove check domain * Making sure we check requestedSlug now * Feedback and unneeded code * Reverting unneeded changes * Unneeded changes --------- Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> * Vercel subdomain creation in PROD only * Enable payment apps for team credentials * Fix for team-user apps for event types * Fix layout and add teamId to app card * Disable apps on managed event types * Add managed event type fields to event type apps * Include organizations in query * Change createAppCredential to createOAuthAppCredential * Show app installed on teams * Making sure we let localhost still work * UI show installed for which team * Type fixes * For team events move use host location to top * Add around to appStore * New team event types organizer default conf app * Fix app card bug * Clean up * Search for teamId or userId when deleting credential * Type fixes * Type fixes * Type fixes * Type fixes * Address feedback * Feedback * Type check fixes * feat: Organization branding in side menu (#9279) * Desktop first banner, mobile pending * Removing dead code and img * WIP * Adds Email verification template+translations for organizations (#9202) * First step done * Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding * Step 2 done, avatar not working * Covering null on unique clauses * Onboarding admins step * Last step to create teams * Moving change password handler, improving verifying code flow * Clearing error before submitting * Reverting email testing api changes * Reverting having the banner for now * Consistent exported components * Remove unneeded files from banner * Removing uneeded code * Fixing avatar selector * Org branding provider used in shell sidebar * Using meta component for head/descr * Missing i18n strings * Feedback * Making an org avatar (temp) * Using org avatar (temp) * Not showing org logo if not set * User onboarding with org branding (slug) * Check for subteams slug clashes with usernames * Fixing create teams onsuccess * feedback * Feedback * Org public profile * Public profiles for team event types * Added setup profile alert * Using org avatar on subteams avatar * Making sure we show the set up profile on org only * Profile username availability rely on org hook * Update apps/web/pages/team/[slug].tsx Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> * Update apps/web/pages/team/[slug].tsx Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> --------- Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> * feat: Organization support for event types page (#9449) * Desktop first banner, mobile pending * Removing dead code and img * WIP * Adds Email verification template+translations for organizations (#9202) * First step done * Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding * Step 2 done, avatar not working * Covering null on unique clauses * Onboarding admins step * Last step to create teams * Moving change password handler, improving verifying code flow * Clearing error before submitting * Reverting email testing api changes * Reverting having the banner for now * Consistent exported components * Remove unneeded files from banner * Removing uneeded code * Fixing avatar selector * Org branding provider used in shell sidebar * Using meta component for head/descr * Missing i18n strings * Feedback * Making an org avatar (temp) * Using org avatar (temp) * Not showing org logo if not set * User onboarding with org branding (slug) * Check for subteams slug clashes with usernames * Fixing create teams onsuccess * feedback * Feedback * Org public profile * Public profiles for team event types * Added setup profile alert * Using org avatar on subteams avatar * Processing orgs and children as profile options * Reverting change not belonging to this PR * Making sure we show the set up profile on org only * Removing console.log * Comparing memberships to choose the highest one --------- Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> * Type errors * Refactor and type fixes * Update orgDomains.ts * Feedback * Reverting * NIT * Address feedback * fix issue getting org slug from domain * Improving orgDomains util * Host comes with port * Update useRouterQuery.ts * Fix app card bug * Fix schema * Type fixes * Revert changes to location apps * Remove console.log * Fix app store test * Handle install app dropdown * Add CalendarApp to `getCalendar` * Add PaymentApp type fix * Payment type fix * Type fixes * Match with main * Change type to account for team * Fix app count for team events * Type fixes * More type fixes * Type fix? * Fix the type fix * Remove UserAdminTeams empty array union * Type fix * Type fix * Type fix * Uses type predicates * Use teamId. Fixes installation for teams after user installation * Fix Team Events not working * Get embed for org events working * Fix rewrites * Address feedback * Type fix * Fixes * Add useAppContextWithSchema in useIsAppEnabled * Type fix for apps using useIsAppEnabled * Integrations.handler change credentialIds to userCredentialIds * Remove apps endpoint * Add LockedIcon and disabled props to event type app context * Type fixes * Type fix * Type fixes * Show team installed apps for members * Type fix * Reverting findFirst * Revert findFirst * Avoid a possible 500 * Fix missing tanslation * Avoid possible 500 * Undo default app for teams * Type fix * Fix test * Update package.json * feat: Fix invite bug - added tests (#9945) Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> * chore: Button Component Tidy up (#9888) Co-authored-by: Peer Richelsen <peeroke@gmail.com> * feat: Make Team Private ## What does this PR do? Fixes https://github.com/calcom/cal.com/issues/8974 1) When user is admin <img width="1440" alt="Screenshot 2023-07-03 at 6 45 50 PM" src="https://github.com/calcom/cal.com/assets/53316345/ce15158f-d278-4f1a-ba2e-8b63e4274793"> 2) When user is not admin and team is private <img width="1440" alt="Screenshot 2023-07-03 at 6 47 15 PM" src="https://github.com/calcom/cal.com/assets/53316345/ce23560e-690a-4c42-a76d-49691260aa4d"> 3) <img width="1440" alt="Screenshot 2023-07-03 at 6 51 56 PM" src="https://github.com/calcom/cal.com/assets/53316345/13af38f8-5618-4dae-b359-b24dc91e4eb4"> ## Type of change <!-- Please delete bullets that are not relevant. --> - New feature (non-breaking change which adds functionality) ## How should this be tested? 1) go to Team members page and turn on switch Make Team Private. Now after making the team private only admin would be able to see all the members list in the settings. There will not be a button to Book a team member instead on the team page like before. ## Mandatory Tasks - [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected. --------- Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> Co-authored-by: Leo Giovanetti <hello@leog.me> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Alan <alannnc@gmail.com> Co-authored-by: zomars <zomars@me.com> Co-authored-by: Efraín Rochín <roae.85@gmail.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com>
This commit is contained in:
parent
54374cf43e
commit
5003ada671
|
@ -3,9 +3,10 @@ import type { ReactNode } from "react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import type { CredentialOwner } from "@calcom/app-store/types";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||||
import { Badge, ListItemText } from "@calcom/ui";
|
import { Badge, ListItemText, Avatar } from "@calcom/ui";
|
||||||
import { AlertCircle } from "@calcom/ui/components/icon";
|
import { AlertCircle } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
type ShouldHighlight = { slug: string; shouldHighlight: true } | { shouldHighlight?: never; slug?: never };
|
type ShouldHighlight = { slug: string; shouldHighlight: true } | { shouldHighlight?: never; slug?: never };
|
||||||
|
@ -19,6 +20,7 @@ type AppListCardProps = {
|
||||||
isTemplate?: boolean;
|
isTemplate?: boolean;
|
||||||
invalidCredential?: boolean;
|
invalidCredential?: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
credentialOwner?: CredentialOwner;
|
||||||
} & ShouldHighlight;
|
} & ShouldHighlight;
|
||||||
|
|
||||||
const schema = z.object({ hl: z.string().optional() });
|
const schema = z.object({ hl: z.string().optional() });
|
||||||
|
@ -36,6 +38,7 @@ export default function AppListCard(props: AppListCardProps) {
|
||||||
isTemplate,
|
isTemplate,
|
||||||
invalidCredential,
|
invalidCredential,
|
||||||
children,
|
children,
|
||||||
|
credentialOwner,
|
||||||
} = props;
|
} = props;
|
||||||
const {
|
const {
|
||||||
data: { hl },
|
data: { hl },
|
||||||
|
@ -65,7 +68,7 @@ export default function AppListCard(props: AppListCardProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${highlight ? "dark:bg-muted bg-yellow-100" : ""}`}>
|
<div className={`${highlight ? "dark:bg-muted bg-yellow-100" : ""}`}>
|
||||||
<div className="flex gap-x-3 px-5 py-4">
|
<div className="flex items-center gap-x-3 px-5 py-4">
|
||||||
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
|
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
|
||||||
<div className="flex grow flex-col gap-y-1 truncate">
|
<div className="flex grow flex-col gap-y-1 truncate">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
|
@ -85,6 +88,21 @@ export default function AppListCard(props: AppListCardProps) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{credentialOwner && (
|
||||||
|
<div>
|
||||||
|
<Badge variant="gray">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Avatar
|
||||||
|
className="mr-2"
|
||||||
|
alt={credentialOwner.name || "Nameless"}
|
||||||
|
size="xs"
|
||||||
|
imageSrc={credentialOwner.avatar}
|
||||||
|
/>
|
||||||
|
{credentialOwner.name}
|
||||||
|
</div>
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{actions}
|
{actions}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,18 +1,33 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { IframeHTMLAttributes } from "react";
|
import type { IframeHTMLAttributes } from "react";
|
||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
|
||||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||||
import { InstallAppButton, AppDependencyComponent } from "@calcom/app-store/components";
|
import { InstallAppButton, AppDependencyComponent } from "@calcom/app-store/components";
|
||||||
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
|
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { App as AppType } from "@calcom/types/App";
|
import type { App as AppType } from "@calcom/types/App";
|
||||||
|
import type { ButtonProps } from "@calcom/ui";
|
||||||
|
import type { AppFrontendPayload as App } from "@calcom/types/App";
|
||||||
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
|
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
|
||||||
|
import {
|
||||||
|
Dropdown,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownItem,
|
||||||
|
Avatar,
|
||||||
|
} from "@calcom/ui";
|
||||||
import { BookOpen, Check, ExternalLink, File, Flag, Mail, Plus, Shield } from "@calcom/ui/components/icon";
|
import { BookOpen, Check, ExternalLink, File, Flag, Mail, Plus, Shield } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
/* These app slugs all require Google Cal to be installed */
|
/* These app slugs all require Google Cal to be installed */
|
||||||
|
@ -60,11 +75,14 @@ const Component = ({
|
||||||
}).format(price);
|
}).format(price);
|
||||||
|
|
||||||
const [existingCredentials, setExistingCredentials] = useState<number[]>([]);
|
const [existingCredentials, setExistingCredentials] = useState<number[]>([]);
|
||||||
const appCredentials = trpc.viewer.appCredentialsByType.useQuery(
|
const [showDisconnectIntegration, setShowDisconnectIntegration] = useState(false);
|
||||||
|
const appDbQuery = trpc.viewer.appCredentialsByType.useQuery(
|
||||||
{ appType: type },
|
{ appType: type },
|
||||||
{
|
{
|
||||||
onSuccess(data) {
|
onSettled(data) {
|
||||||
setExistingCredentials(data);
|
const credentialsCount = data?.credentials.length || 0
|
||||||
|
setShowDisconnectIntegration(data?.userAdminTeams.length ? credentialsCount >= data?.userAdminTeams.length : credentialsCount > 0);
|
||||||
|
setExistingCredentials(data?.credentials.map(credential => credential.id) || []);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -141,7 +159,7 @@ const Component = ({
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
{!appCredentials.isLoading ? (
|
{!appDbQuery.isLoading ? (
|
||||||
isGlobal ||
|
isGlobal ||
|
||||||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
|
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
|
||||||
<div className="flex space-x-3">
|
<div className="flex space-x-3">
|
||||||
|
@ -166,28 +184,19 @@ const Component = ({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button
|
<InstallAppButtonChild appCategories={categories} userAdminTeams={appDbQuery.data?.userAdminTeams} addAppMutationInput={{ type, variant, slug }} multiInstall {...props} />
|
||||||
StartIcon={Plus}
|
|
||||||
{...props}
|
|
||||||
// @TODO: Overriding color and size prevent us from
|
|
||||||
// having to duplicate InstallAppButton for now.
|
|
||||||
color="primary"
|
|
||||||
size="base"
|
|
||||||
data-testid="install-app-button">
|
|
||||||
{t("install_another")}
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : existingCredentials.length > 0 ? (
|
) : showDisconnectIntegration ? (
|
||||||
<DisconnectIntegration
|
<DisconnectIntegration
|
||||||
buttonProps={{ color: "secondary" }}
|
buttonProps={{ color: "secondary" }}
|
||||||
label={t("disconnect")}
|
label={t("disconnect")}
|
||||||
credentialId={existingCredentials[0]}
|
credentialId={existingCredentials[0]}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
appCredentials.refetch();
|
appDbQuery.refetch();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
@ -206,15 +215,7 @@ const Component = ({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button
|
<InstallAppButtonChild appCategories={categories} userAdminTeams={appDbQuery.data?.userAdminTeams} addAppMutationInput={{type, variant, slug}} credentials={appDbQuery.data?.credentials} {...props} />
|
||||||
data-testid="install-app-button"
|
|
||||||
{...props}
|
|
||||||
// @TODO: Overriding color and size prevent us from
|
|
||||||
// having to duplicate InstallAppButton for now.
|
|
||||||
color="primary"
|
|
||||||
size="base">
|
|
||||||
{t("install_app")}
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -385,3 +386,98 @@ export default function App(props: {
|
||||||
</Shell>
|
</Shell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const InstallAppButtonChild = ({
|
||||||
|
userAdminTeams,
|
||||||
|
addAppMutationInput,
|
||||||
|
appCategories,
|
||||||
|
multiInstall,
|
||||||
|
credentials,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
userAdminTeams?: UserAdminTeams;
|
||||||
|
addAppMutationInput: { type: App["type"]; variant: string; slug: string };
|
||||||
|
appCategories: string[];
|
||||||
|
multiInstall?: boolean;
|
||||||
|
credentials?: RouterOutputs["viewer"]["appCredentialsByType"]["credentials"];
|
||||||
|
} & ButtonProps) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const mutation = useAddAppMutation(null, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data?.setupPending) return;
|
||||||
|
showToast(t("app_successfully_installed"), "success");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userAdminTeams?.length || appCategories.some((category) => category === "calendar")) {
|
||||||
|
return <Button
|
||||||
|
data-testid="install-app-button"
|
||||||
|
{...props}
|
||||||
|
// @TODO: Overriding color and size prevent us from
|
||||||
|
// having to duplicate InstallAppButton for now.
|
||||||
|
color="primary"
|
||||||
|
size="base">
|
||||||
|
{multiInstall ? t("install_another") : t("install_app")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
data-testid="install-app-button"
|
||||||
|
{...props}
|
||||||
|
// @TODO: Overriding color and size prevent us from
|
||||||
|
// having to duplicate InstallAppButton for now.
|
||||||
|
color="primary"
|
||||||
|
size="base">
|
||||||
|
{multiInstall ? t("install_another") : t("install_app")}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuPortal>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuLabel>{t("install_app_on")}</DropdownMenuLabel>
|
||||||
|
{userAdminTeams.map((team) => {
|
||||||
|
|
||||||
|
const isInstalled = credentials &&
|
||||||
|
credentials.some((credential) =>
|
||||||
|
credential?.teamId ? credential?.teamId === team.id : credential.userId === team.id
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownItem
|
||||||
|
type="button"
|
||||||
|
data-testid={team.isUser ? "install-app-button-personal" : "anything else"}
|
||||||
|
key={team.id}
|
||||||
|
disabled={
|
||||||
|
isInstalled
|
||||||
|
}
|
||||||
|
StartIcon={(props) => (
|
||||||
|
<Avatar
|
||||||
|
alt={team.logo || ""}
|
||||||
|
imageSrc={team.logo || `${CAL_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
|
||||||
|
size="sm"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
mutation.mutate(
|
||||||
|
team.isUser ? addAppMutationInput : { ...addAppMutationInput, teamId: team.id }
|
||||||
|
);
|
||||||
|
}}>
|
||||||
|
<p>{team.name}{" "}
|
||||||
|
{isInstalled &&
|
||||||
|
`(${t("installed")})`}</p>
|
||||||
|
</DropdownItem>
|
||||||
|
)})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -34,12 +34,12 @@ interface ISetLocationDialog {
|
||||||
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
|
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
|
||||||
selection?: LocationOption;
|
selection?: LocationOption;
|
||||||
booking?: BookingItem;
|
booking?: BookingItem;
|
||||||
isTeamEvent?: boolean;
|
|
||||||
defaultValues?: LocationObject[];
|
defaultValues?: LocationObject[];
|
||||||
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
isOpenDialog: boolean;
|
isOpenDialog: boolean;
|
||||||
setSelectedLocation?: (param: LocationOption | undefined) => void;
|
setSelectedLocation?: (param: LocationOption | undefined) => void;
|
||||||
setEditingLocationType?: (param: string) => void;
|
setEditingLocationType?: (param: string) => void;
|
||||||
|
teamId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LocationInput = (props: {
|
const LocationInput = (props: {
|
||||||
|
@ -79,15 +79,15 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||||
saveLocation,
|
saveLocation,
|
||||||
selection,
|
selection,
|
||||||
booking,
|
booking,
|
||||||
isTeamEvent,
|
|
||||||
setShowLocationModal,
|
setShowLocationModal,
|
||||||
isOpenDialog,
|
isOpenDialog,
|
||||||
defaultValues,
|
defaultValues,
|
||||||
setSelectedLocation,
|
setSelectedLocation,
|
||||||
setEditingLocationType,
|
setEditingLocationType,
|
||||||
|
teamId,
|
||||||
} = props;
|
} = props;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const locationsQuery = trpc.viewer.locationOptions.useQuery();
|
const locationsQuery = trpc.viewer.locationOptions.useQuery({ teamId });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selection) {
|
if (selection) {
|
||||||
|
@ -103,6 +103,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||||
locationType: z.string(),
|
locationType: z.string(),
|
||||||
phone: z.string().optional().nullable(),
|
phone: z.string().optional().nullable(),
|
||||||
locationAddress: z.string().optional(),
|
locationAddress: z.string().optional(),
|
||||||
|
credentialId: z.number().optional(),
|
||||||
locationLink: z
|
locationLink: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
|
@ -296,6 +297,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (values.credentialId) {
|
||||||
|
details = { ...details, credentialId: values.credentialId };
|
||||||
|
}
|
||||||
saveLocation(newLocation, details);
|
saveLocation(newLocation, details);
|
||||||
setShowLocationModal(false);
|
setShowLocationModal(false);
|
||||||
setSelectedLocation?.(undefined);
|
setSelectedLocation?.(undefined);
|
||||||
|
@ -311,7 +315,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||||
success={({ data }) => {
|
success={({ data }) => {
|
||||||
if (!data.length) return null;
|
if (!data.length) return null;
|
||||||
const locationOptions = [...data].map((option) => {
|
const locationOptions = [...data].map((option) => {
|
||||||
if (isTeamEvent) {
|
if (teamId) {
|
||||||
// Let host's Default conferencing App option show for Team Event
|
// Let host's Default conferencing App option show for Team Event
|
||||||
return option;
|
return option;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,12 +16,16 @@ export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
|
||||||
|
|
||||||
export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { data: eventTypeApps, isLoading } = trpc.viewer.apps.useQuery({
|
const { data: eventTypeApps, isLoading } = trpc.viewer.integrations.useQuery({
|
||||||
extendsFeature: "EventType",
|
extendsFeature: "EventType",
|
||||||
|
teamId: eventType.team?.id || eventType.parent?.teamId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const methods = useFormContext<FormValues>();
|
const methods = useFormContext<FormValues>();
|
||||||
const installedApps = eventTypeApps?.filter((app) => app.credentials.length);
|
const installedApps =
|
||||||
const notInstalledApps = eventTypeApps?.filter((app) => !app.credentials.length);
|
eventTypeApps?.items.filter((app) => app.userCredentialIds.length || app.teams.length) || [];
|
||||||
|
const notInstalledApps =
|
||||||
|
eventTypeApps?.items.filter((app) => !app.userCredentialIds.length && !app.teams.length) || [];
|
||||||
const allAppsData = methods.watch("metadata")?.apps || {};
|
const allAppsData = methods.watch("metadata")?.apps || {};
|
||||||
|
|
||||||
const setAllAppsData = (_allAppsData: typeof allAppsData) => {
|
const setAllAppsData = (_allAppsData: typeof allAppsData) => {
|
||||||
|
@ -62,11 +66,54 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||||
t("locked_fields_member_description")
|
t("locked_fields_member_description")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const appsWithTeamCredentials = eventTypeApps?.items.filter((app) => app.teams.length) || [];
|
||||||
|
const cardsForAppsWithTeams = appsWithTeamCredentials.map((app) => {
|
||||||
|
const appCards = [];
|
||||||
|
|
||||||
|
if (app.userCredentialIds.length) {
|
||||||
|
appCards.push(
|
||||||
|
<EventTypeAppCard
|
||||||
|
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||||
|
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
||||||
|
key={app.slug}
|
||||||
|
app={app}
|
||||||
|
eventType={eventType}
|
||||||
|
{...shouldLockDisableProps("apps")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const team of app.teams) {
|
||||||
|
if (team) {
|
||||||
|
appCards.push(
|
||||||
|
<EventTypeAppCard
|
||||||
|
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||||
|
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
||||||
|
key={app.slug + team?.credentialId}
|
||||||
|
app={{
|
||||||
|
...app,
|
||||||
|
// credentialIds: team?.credentialId ? [team.credentialId] : [],
|
||||||
|
credentialOwner: {
|
||||||
|
name: team.name,
|
||||||
|
avatar: team.logo,
|
||||||
|
teamId: team.teamId,
|
||||||
|
credentialId: team.credentialId,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
eventType={eventType}
|
||||||
|
{...shouldLockDisableProps("apps")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return appCards;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<div className="before:border-0">
|
<div className="before:border-0">
|
||||||
{!installedApps?.length && isManagedEventType && (
|
{isManagedEventType && (
|
||||||
<Alert
|
<Alert
|
||||||
severity="neutral"
|
severity="neutral"
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
|
@ -92,15 +139,20 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{installedApps?.map((app) => (
|
{cardsForAppsWithTeams.map((apps) => apps.map((cards) => cards))}
|
||||||
<EventTypeAppCard
|
{installedApps.map((app) => {
|
||||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
if (!app.teams.length)
|
||||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
return (
|
||||||
key={app.slug}
|
<EventTypeAppCard
|
||||||
app={app}
|
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||||
eventType={eventType}
|
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
||||||
/>
|
key={app.slug}
|
||||||
))}
|
app={app}
|
||||||
|
eventType={eventType}
|
||||||
|
{...shouldLockDisableProps("apps")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!shouldLockDisableProps("apps").disabled && (
|
{!shouldLockDisableProps("apps").disabled && (
|
||||||
|
|
|
@ -530,7 +530,6 @@ export const EventSetupTab = (
|
||||||
|
|
||||||
{/* We portal this modal so we can submit the form inside. Otherwise we get issues submitting two forms at once */}
|
{/* We portal this modal so we can submit the form inside. Otherwise we get issues submitting two forms at once */}
|
||||||
<EditLocationDialog
|
<EditLocationDialog
|
||||||
isTeamEvent={!!team}
|
|
||||||
isOpenDialog={showLocationModal}
|
isOpenDialog={showLocationModal}
|
||||||
setShowLocationModal={setShowLocationModal}
|
setShowLocationModal={setShowLocationModal}
|
||||||
saveLocation={saveLocation}
|
saveLocation={saveLocation}
|
||||||
|
@ -553,6 +552,7 @@ export const EventSetupTab = (
|
||||||
}
|
}
|
||||||
setSelectedLocation={setSelectedLocation}
|
setSelectedLocation={setSelectedLocation}
|
||||||
setEditingLocationType={setEditingLocationType}
|
setEditingLocationType={setEditingLocationType}
|
||||||
|
teamId={eventType.team?.id}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -210,7 +210,7 @@ function EventTypeSingleLayout({
|
||||||
}
|
}
|
||||||
if (isManagedEventType || isChildrenManagedEventType) {
|
if (isManagedEventType || isChildrenManagedEventType) {
|
||||||
// Removing apps and workflows for manageg event types by admins v1
|
// Removing apps and workflows for manageg event types by admins v1
|
||||||
navigation.splice(-2, 1);
|
navigation.splice(0, 1);
|
||||||
} else {
|
} else {
|
||||||
navigation.push({
|
navigation.push({
|
||||||
name: "webhooks",
|
name: "webhooks",
|
||||||
|
|
|
@ -20,7 +20,7 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const hasAnyInstalledVideoApps = queryConnectedVideoApps?.items.some(
|
const hasAnyInstalledVideoApps = queryConnectedVideoApps?.items.some(
|
||||||
(item) => item.credentialIds.length > 0
|
(item) => item.userCredentialIds.length > 0
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -38,7 +38,7 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
|
||||||
title={item.name}
|
title={item.name}
|
||||||
description={item.description}
|
description={item.description}
|
||||||
logo={item.logo}
|
logo={item.logo}
|
||||||
installed={item.credentialIds.length > 0}
|
installed={item.userCredentialIds.length > 0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -40,11 +40,13 @@ export default function LocationSelect(props: Props<LocationOption, false, Group
|
||||||
name="location"
|
name="location"
|
||||||
id="location-select"
|
id="location-select"
|
||||||
components={{
|
components={{
|
||||||
Option: (props) => (
|
Option: (props) => {
|
||||||
<components.Option {...props}>
|
return (
|
||||||
<OptionWithIcon icon={props.data.icon} label={props.data.label} />
|
<components.Option {...props}>
|
||||||
</components.Option>
|
<OptionWithIcon icon={props.data.icon} label={props.data.label} />
|
||||||
),
|
</components.Option>
|
||||||
|
);
|
||||||
|
},
|
||||||
SingleValue: (props) => (
|
SingleValue: (props) => (
|
||||||
<components.SingleValue {...props}>
|
<components.SingleValue {...props}>
|
||||||
<OptionWithIcon icon={props.data.icon} label={props.data.label} />
|
<OptionWithIcon icon={props.data.icon} label={props.data.label} />
|
||||||
|
|
|
@ -4,8 +4,17 @@ const os = require("os");
|
||||||
const englishTranslation = require("./public/static/locales/en/common.json");
|
const englishTranslation = require("./public/static/locales/en/common.json");
|
||||||
const { withAxiom } = require("next-axiom");
|
const { withAxiom } = require("next-axiom");
|
||||||
const { i18n } = require("./next-i18next.config");
|
const { i18n } = require("./next-i18next.config");
|
||||||
const { pages } = require("./pages");
|
const {
|
||||||
const { getSubdomainRegExp } = require("./getSubdomainRegExp");
|
userTypeRoutePath,
|
||||||
|
teamTypeRoutePath,
|
||||||
|
privateLinkRoutePath,
|
||||||
|
embedUserTypeRoutePath,
|
||||||
|
embedTeamTypeRoutePath,
|
||||||
|
orgHostPath,
|
||||||
|
orgUserRoutePath,
|
||||||
|
orgUserTypeRoutePath,
|
||||||
|
orgUserTypeEmbedRoutePath,
|
||||||
|
} = require("./pagesAndRewritePaths");
|
||||||
|
|
||||||
if (!process.env.NEXTAUTH_SECRET) throw new Error("Please set NEXTAUTH_SECRET");
|
if (!process.env.NEXTAUTH_SECRET) throw new Error("Please set NEXTAUTH_SECRET");
|
||||||
if (!process.env.CALENDSO_ENCRYPTION_KEY) throw new Error("Please set CALENDSO_ENCRYPTION_KEY");
|
if (!process.env.CALENDSO_ENCRYPTION_KEY) throw new Error("Please set CALENDSO_ENCRYPTION_KEY");
|
||||||
|
@ -81,52 +90,44 @@ if (process.env.ANALYZE === "true") {
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins.push(withAxiom);
|
plugins.push(withAxiom);
|
||||||
|
|
||||||
// .* matches / as well(Note: *(i.e wildcard) doesn't match / but .*(i.e. RegExp) does)
|
|
||||||
// It would match /free/30min but not /bookings/upcoming because 'bookings' is an item in pages
|
|
||||||
// It would also not match /free/30min/embed because we are ensuring just two slashes
|
|
||||||
// ?!book ensures it doesn't match /free/book page which doesn't have a corresponding new-booker page.
|
|
||||||
// [^/]+ makes the RegExp match the full path, it seems like a partial match doesn't work.
|
|
||||||
// book$ ensures that only /book is excluded from rewrite(which is at the end always) and not /booked
|
|
||||||
|
|
||||||
// Important Note: When modifying these RegExps update apps/web/test/lib/next-config.test.ts as well
|
|
||||||
const userTypeRouteRegExp = `/:user((?!${pages.join("/|")})[^/]*)/:type((?!book$)[^/]+)`;
|
|
||||||
const teamTypeRouteRegExp = "/team/:slug/:type((?!book$)[^/]+)";
|
|
||||||
const privateLinkRouteRegExp = "/d/:link/:slug((?!book$)[^/]+)";
|
|
||||||
const embedUserTypeRouteRegExp = `/:user((?!${pages.join("/|")})[^/]*)/:type/embed`;
|
|
||||||
const embedTeamTypeRouteRegExp = "/team/:slug/:type/embed";
|
|
||||||
const subdomainRegExp = getSubdomainRegExp(process.env.NEXT_PUBLIC_WEBAPP_URL);
|
|
||||||
// Important Note: Do update the RegExp in apps/web/test/lib/next-config.test.ts when changing it.
|
|
||||||
const orgHostRegExp = `^(?<orgSlug>${subdomainRegExp})\\..*`;
|
|
||||||
|
|
||||||
const matcherConfigRootPath = {
|
const matcherConfigRootPath = {
|
||||||
has: [
|
has: [
|
||||||
{
|
{
|
||||||
type: "host",
|
type: "host",
|
||||||
value: orgHostRegExp,
|
value: orgHostPath,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
source: "/",
|
source: "/",
|
||||||
};
|
};
|
||||||
|
|
||||||
const matcherConfigOrgMemberPath = {
|
const matcherConfigUserRoute = {
|
||||||
has: [
|
has: [
|
||||||
{
|
{
|
||||||
type: "host",
|
type: "host",
|
||||||
value: orgHostRegExp,
|
value: orgHostPath,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
source: `/:user((?!${pages.join("|")}|_next|public)[a-zA-Z0-9\-_]+)`,
|
source: orgUserRoutePath,
|
||||||
};
|
};
|
||||||
|
|
||||||
const matcherConfigUserPath = {
|
const matcherConfigUserTypeRoute = {
|
||||||
has: [
|
has: [
|
||||||
{
|
{
|
||||||
type: "host",
|
type: "host",
|
||||||
value: `^(?<orgSlug>${subdomainRegExp}[^.]+)\\..*`,
|
value: orgHostPath,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
source: `/:user((?!${pages.join("|")}|_next|public))/:path*`,
|
source: orgUserTypeRoutePath,
|
||||||
|
};
|
||||||
|
|
||||||
|
const matcherConfigUserTypeEmbedRoute = {
|
||||||
|
has: [
|
||||||
|
{
|
||||||
|
type: "host",
|
||||||
|
value: orgHostPath,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
source: orgUserTypeEmbedRoutePath,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
|
@ -220,6 +221,7 @@ const nextConfig = {
|
||||||
},
|
},
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
const beforeFiles = [
|
const beforeFiles = [
|
||||||
|
// These rewrites are other than booking pages rewrites and so that they aren't redirected to org pages ensure that they happen in beforeFiles
|
||||||
...(process.env.ORGANIZATIONS_ENABLED
|
...(process.env.ORGANIZATIONS_ENABLED
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
|
@ -227,12 +229,16 @@ const nextConfig = {
|
||||||
destination: "/team/:orgSlug",
|
destination: "/team/:orgSlug",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...matcherConfigOrgMemberPath,
|
...matcherConfigUserRoute,
|
||||||
destination: "/org/:orgSlug/:user",
|
destination: "/org/:orgSlug/:user",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...matcherConfigUserPath,
|
...matcherConfigUserTypeRoute,
|
||||||
destination: "/:user/:path*",
|
destination: "/org/:orgSlug/:user/:type",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...matcherConfigUserTypeEmbedRoute,
|
||||||
|
destination: "/org/:orgSlug/:user/:type/embed",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
@ -243,64 +249,70 @@ const nextConfig = {
|
||||||
source: "/org/:slug",
|
source: "/org/:slug",
|
||||||
destination: "/team/:slug",
|
destination: "/team/:slug",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
source: "/:user/avatar.png",
|
|
||||||
destination: "/api/user/avatar?username=:user",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
source: "/team/:teamname/avatar.png",
|
source: "/team/:teamname/avatar.png",
|
||||||
destination: "/api/user/avatar?teamname=:teamname",
|
destination: "/api/user/avatar?teamname=:teamname",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
source: "/forms/:formQuery*",
|
// When updating this also update pagesAndRewritePaths.js
|
||||||
destination: "/apps/routing-forms/routing-link/:formQuery*",
|
...[
|
||||||
},
|
{
|
||||||
{
|
source: "/:user/avatar.png",
|
||||||
source: "/router",
|
destination: "/api/user/avatar?username=:user",
|
||||||
destination: "/apps/routing-forms/router",
|
},
|
||||||
},
|
{
|
||||||
{
|
source: "/forms/:formQuery*",
|
||||||
source: "/success/:path*",
|
destination: "/apps/routing-forms/routing-link/:formQuery*",
|
||||||
has: [
|
},
|
||||||
{
|
{
|
||||||
type: "query",
|
source: "/router",
|
||||||
key: "uid",
|
destination: "/apps/routing-forms/router",
|
||||||
value: "(?<uid>.*)",
|
},
|
||||||
},
|
{
|
||||||
],
|
source: "/success/:path*",
|
||||||
destination: "/booking/:uid/:path*",
|
has: [
|
||||||
},
|
{
|
||||||
{
|
type: "query",
|
||||||
source: "/cancel/:path*",
|
key: "uid",
|
||||||
destination: "/booking/:path*",
|
value: "(?<uid>.*)",
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
destination: "/booking/:uid/:path*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/cancel/:path*",
|
||||||
|
destination: "/booking/:path*",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
// Keep cookie based booker enabled just in case we disable new-booker globally
|
// Keep cookie based booker enabled just in case we disable new-booker globally
|
||||||
...[
|
...[
|
||||||
{
|
{
|
||||||
source: userTypeRouteRegExp,
|
source: userTypeRoutePath,
|
||||||
destination: "/new-booker/:user/:type",
|
destination: "/new-booker/:user/:type",
|
||||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: teamTypeRouteRegExp,
|
source: teamTypeRoutePath,
|
||||||
destination: "/new-booker/team/:slug/:type",
|
destination: "/new-booker/team/:slug/:type",
|
||||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: privateLinkRouteRegExp,
|
source: privateLinkRoutePath,
|
||||||
destination: "/new-booker/d/:link/:slug",
|
destination: "/new-booker/d/:link/:slug",
|
||||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
// Keep cookie based booker enabled to test new-booker embed in production
|
// Keep cookie based booker enabled to test new-booker embed in production
|
||||||
...[
|
...[
|
||||||
{
|
{
|
||||||
source: embedUserTypeRouteRegExp,
|
source: embedUserTypeRoutePath,
|
||||||
destination: "/new-booker/:user/:type/embed",
|
destination: "/new-booker/:user/:type/embed",
|
||||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: embedTeamTypeRouteRegExp,
|
source: embedTeamTypeRoutePath,
|
||||||
destination: "/new-booker/team/:slug/:type/embed",
|
destination: "/new-booker/team/:slug/:type/embed",
|
||||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||||
},
|
},
|
||||||
|
@ -321,11 +333,11 @@ const nextConfig = {
|
||||||
afterFiles.push(
|
afterFiles.push(
|
||||||
...[
|
...[
|
||||||
{
|
{
|
||||||
source: embedUserTypeRouteRegExp,
|
source: embedUserTypeRoutePath,
|
||||||
destination: "/new-booker/:user/:type/embed",
|
destination: "/new-booker/:user/:type/embed",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: embedTeamTypeRouteRegExp,
|
source: embedTeamTypeRoutePath,
|
||||||
destination: "/new-booker/team/:slug/:type/embed",
|
destination: "/new-booker/team/:slug/:type/embed",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -338,15 +350,15 @@ const nextConfig = {
|
||||||
afterFiles.push(
|
afterFiles.push(
|
||||||
...[
|
...[
|
||||||
{
|
{
|
||||||
source: userTypeRouteRegExp,
|
source: userTypeRoutePath,
|
||||||
destination: "/new-booker/:user/:type",
|
destination: "/new-booker/:user/:type",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: teamTypeRouteRegExp,
|
source: teamTypeRoutePath,
|
||||||
destination: "/new-booker/team/:slug/:type",
|
destination: "/new-booker/team/:slug/:type",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: privateLinkRouteRegExp,
|
source: privateLinkRoutePath,
|
||||||
destination: "/new-booker/d/:link/:slug",
|
destination: "/new-booker/d/:link/:slug",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -401,7 +413,7 @@ const nextConfig = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...matcherConfigOrgMemberPath,
|
...matcherConfigUserRoute,
|
||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
key: "X-Cal-Org-path",
|
key: "X-Cal-Org-path",
|
||||||
|
@ -410,11 +422,20 @@ const nextConfig = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...matcherConfigUserPath,
|
...matcherConfigUserTypeRoute,
|
||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
key: "X-Cal-Org-path",
|
key: "X-Cal-Org-path",
|
||||||
value: "/:user/:path",
|
value: "/org/:orgSlug/:user/:type",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...matcherConfigUserTypeEmbedRoute,
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: "X-Cal-Org-path",
|
||||||
|
value: "/org/:orgSlug/:user/:type/embed",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
const glob = require("glob");
|
|
||||||
|
|
||||||
/** Needed to rewrite public booking page, gets all static pages but [user] */
|
|
||||||
const pages = glob
|
|
||||||
.sync("pages/**/[^_]*.{tsx,js,ts}", { cwd: __dirname })
|
|
||||||
.map((filename) =>
|
|
||||||
filename
|
|
||||||
.substr(6)
|
|
||||||
.replace(/(\.tsx|\.js|\.ts)/, "")
|
|
||||||
.replace(/\/.*/, "")
|
|
||||||
)
|
|
||||||
.filter((v, i, self) => self.indexOf(v) === i && !v.startsWith("[user]"));
|
|
||||||
|
|
||||||
exports.pages = pages;
|
|
|
@ -13,12 +13,14 @@ const defaultIntegrationAddHandler = async ({
|
||||||
supportsMultipleInstalls,
|
supportsMultipleInstalls,
|
||||||
appType,
|
appType,
|
||||||
user,
|
user,
|
||||||
|
teamId = undefined,
|
||||||
createCredential,
|
createCredential,
|
||||||
}: {
|
}: {
|
||||||
slug: string;
|
slug: string;
|
||||||
supportsMultipleInstalls: boolean;
|
supportsMultipleInstalls: boolean;
|
||||||
appType: string;
|
appType: string;
|
||||||
user?: Session["user"];
|
user?: Session["user"];
|
||||||
|
teamId?: number;
|
||||||
createCredential: AppDeclarativeHandler["createCredential"];
|
createCredential: AppDeclarativeHandler["createCredential"];
|
||||||
}) => {
|
}) => {
|
||||||
if (!user?.id) {
|
if (!user?.id) {
|
||||||
|
@ -28,21 +30,21 @@ const defaultIntegrationAddHandler = async ({
|
||||||
const alreadyInstalled = await prisma.credential.findFirst({
|
const alreadyInstalled = await prisma.credential.findFirst({
|
||||||
where: {
|
where: {
|
||||||
appId: slug,
|
appId: slug,
|
||||||
userId: user.id,
|
...(teamId ? { AND: [{ userId: user.id }, { teamId }] } : { userId: user.id }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (alreadyInstalled) {
|
if (alreadyInstalled) {
|
||||||
throw new Error("App is already installed");
|
throw new Error("App is already installed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await createCredential({ user: user, appType, slug });
|
await createCredential({ user: user, appType, slug, teamId });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
// Check that user is authenticated
|
// Check that user is authenticated
|
||||||
req.session = await getServerSession({ req, res });
|
req.session = await getServerSession({ req, res });
|
||||||
|
|
||||||
const { args } = req.query;
|
const { args, teamId } = req.query;
|
||||||
|
|
||||||
if (!Array.isArray(args)) {
|
if (!Array.isArray(args)) {
|
||||||
return res.status(404).json({ message: `API route not found` });
|
return res.status(404).json({ message: `API route not found` });
|
||||||
|
@ -62,7 +64,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
if (typeof handler === "function") {
|
if (typeof handler === "function") {
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
} else {
|
} else {
|
||||||
await defaultIntegrationAddHandler({ user: req.session?.user, ...handler });
|
await defaultIntegrationAddHandler({ user: req.session?.user, teamId: Number(teamId), ...handler });
|
||||||
redirectUrl = handler.redirect?.url || getInstalledAppPath(handler);
|
redirectUrl = handler.redirect?.url || getInstalledAppPath(handler);
|
||||||
res.json({ url: redirectUrl, newTab: handler.redirect?.newTab });
|
res.json({ url: redirectUrl, newTab: handler.redirect?.newTab });
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { useState } from "react";
|
||||||
|
|
||||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
|
import getUserAdminTeams from "@calcom/features/ee/teams/lib/getUserAdminTeams";
|
||||||
|
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { AppCategories } from "@calcom/prisma/enums";
|
import type { AppCategories } from "@calcom/prisma/enums";
|
||||||
|
@ -48,7 +50,11 @@ function AppsSearch({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Apps({ categories, appStore }: inferSSRProps<typeof getServerSideProps>) {
|
export default function Apps({
|
||||||
|
categories,
|
||||||
|
appStore,
|
||||||
|
userAdminTeams,
|
||||||
|
}: inferSSRProps<typeof getServerSideProps>) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [searchText, setSearchText] = useState<string | undefined>(undefined);
|
const [searchText, setSearchText] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
@ -80,6 +86,7 @@ export default function Apps({ categories, appStore }: inferSSRProps<typeof getS
|
||||||
apps={appStore}
|
apps={appStore}
|
||||||
searchText={searchText}
|
searchText={searchText}
|
||||||
categories={categories.map((category) => category.name)}
|
categories={categories.map((category) => category.name)}
|
||||||
|
userAdminTeams={userAdminTeams}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AppsLayout>
|
</AppsLayout>
|
||||||
|
@ -95,11 +102,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
|
|
||||||
const session = await getServerSession({ req, res });
|
const session = await getServerSession({ req, res });
|
||||||
|
|
||||||
let appStore;
|
let appStore, userAdminTeams: UserAdminTeams;
|
||||||
if (session?.user?.id) {
|
if (session?.user?.id) {
|
||||||
appStore = await getAppRegistryWithCredentials(session.user.id);
|
userAdminTeams = await getUserAdminTeams({ userId: session.user.id, getUserInfo: true });
|
||||||
|
appStore = await getAppRegistryWithCredentials(session.user.id, userAdminTeams);
|
||||||
} else {
|
} else {
|
||||||
appStore = await getAppRegistry();
|
appStore = await getAppRegistry();
|
||||||
|
userAdminTeams = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const categoryQuery = appStore.map(({ categories }) => ({
|
const categoryQuery = appStore.map(({ categories }) => ({
|
||||||
|
@ -111,6 +120,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
}
|
}
|
||||||
return c;
|
return c;
|
||||||
}, {} as Record<string, number>);
|
}, {} as Record<string, number>);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
categories: Object.entries(categories)
|
categories: Object.entries(categories)
|
||||||
|
@ -122,6 +132,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
return b.count - a.count;
|
return b.count - a.count;
|
||||||
}),
|
}),
|
||||||
appStore,
|
appStore,
|
||||||
|
userAdminTeams,
|
||||||
trpcState: ssr.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { AppSettings } from "@calcom/app-store/_components/AppSettings";
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import { getEventLocationTypeFromApp } from "@calcom/app-store/locations";
|
import { getEventLocationTypeFromApp } from "@calcom/app-store/locations";
|
||||||
|
import type { CredentialOwner } from "@calcom/app-store/types";
|
||||||
import { AppSetDefaultLinkDialog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
|
import { AppSetDefaultLinkDialog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
|
||||||
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
|
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
|
||||||
import { BulkEditDefaultConferencingModal } from "@calcom/features/eventtypes/components/BulkEditDefaultConferencingModal";
|
import { BulkEditDefaultConferencingModal } from "@calcom/features/eventtypes/components/BulkEditDefaultConferencingModal";
|
||||||
|
@ -52,16 +53,16 @@ import { CalendarListContainer } from "@components/apps/CalendarListContainer";
|
||||||
import InstalledAppsLayout from "@components/apps/layouts/InstalledAppsLayout";
|
import InstalledAppsLayout from "@components/apps/layouts/InstalledAppsLayout";
|
||||||
|
|
||||||
function ConnectOrDisconnectIntegrationMenuItem(props: {
|
function ConnectOrDisconnectIntegrationMenuItem(props: {
|
||||||
credentialIds: number[];
|
credentialId: number;
|
||||||
type: App["type"];
|
type: App["type"];
|
||||||
isGlobal?: boolean;
|
isGlobal?: boolean;
|
||||||
installed?: boolean;
|
installed?: boolean;
|
||||||
invalidCredentialIds?: number[];
|
invalidCredentialIds?: number[];
|
||||||
handleDisconnect: (credentialId: number) => void;
|
teamId?: number;
|
||||||
|
handleDisconnect: (credentialId: number, teamId?: number) => void;
|
||||||
}) {
|
}) {
|
||||||
const { type, credentialIds, isGlobal, installed, handleDisconnect } = props;
|
const { type, credentialId, isGlobal, installed, handleDisconnect, teamId } = props;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [credentialId] = credentialIds;
|
|
||||||
|
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const handleOpenChange = () => {
|
const handleOpenChange = () => {
|
||||||
|
@ -73,7 +74,7 @@ function ConnectOrDisconnectIntegrationMenuItem(props: {
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
color="destructive"
|
color="destructive"
|
||||||
onClick={() => handleDisconnect(credentialId)}
|
onClick={() => handleDisconnect(credentialId, teamId)}
|
||||||
disabled={isGlobal}
|
disabled={isGlobal}
|
||||||
StartIcon={Trash}>
|
StartIcon={Trash}>
|
||||||
{t("remove_app")}
|
{t("remove_app")}
|
||||||
|
@ -138,72 +139,113 @@ const IntegrationsList = ({ data, handleDisconnect, variant }: IntegrationsListP
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ChildAppCard = ({
|
||||||
|
item,
|
||||||
|
}: {
|
||||||
|
item: RouterOutputs["viewer"]["integrations"]["items"][number] & {
|
||||||
|
credentialOwner?: CredentialOwner;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
|
const appSlug = item?.slug;
|
||||||
|
const appIsDefault =
|
||||||
|
appSlug === defaultConferencingApp?.appSlug ||
|
||||||
|
(appSlug === "daily-video" && !defaultConferencingApp?.appSlug);
|
||||||
|
return (
|
||||||
|
<AppListCard
|
||||||
|
key={item.name}
|
||||||
|
description={item.description}
|
||||||
|
title={item.name}
|
||||||
|
logo={item.logo}
|
||||||
|
isDefault={appIsDefault}
|
||||||
|
shouldHighlight
|
||||||
|
slug={item.slug}
|
||||||
|
invalidCredential={item?.invalidCredentialIds ? item.invalidCredentialIds.length > 0 : false}
|
||||||
|
credentialOwner={item?.credentialOwner}
|
||||||
|
actions={
|
||||||
|
!item.credentialOwner?.readOnly ? (
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Dropdown modal={false}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button StartIcon={MoreHorizontal} variant="icon" color="secondary" />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
{!appIsDefault && variant === "conferencing" && (
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem
|
||||||
|
type="button"
|
||||||
|
color="secondary"
|
||||||
|
StartIcon={Video}
|
||||||
|
onClick={() => {
|
||||||
|
const locationType = getEventLocationTypeFromApp(item?.locationOption?.value ?? "");
|
||||||
|
if (locationType?.linkType === "static") {
|
||||||
|
setLocationType({ ...locationType, slug: appSlug });
|
||||||
|
} else {
|
||||||
|
updateDefaultAppMutation.mutate({
|
||||||
|
appSlug,
|
||||||
|
});
|
||||||
|
setBulkUpdateModal(true);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{t("set_as_default")}
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
<ConnectOrDisconnectIntegrationMenuItem
|
||||||
|
credentialId={item.credentialOwner?.credentialId || item.userCredentialIds[0]}
|
||||||
|
type={item.type}
|
||||||
|
isGlobal={item.isGlobal}
|
||||||
|
installed
|
||||||
|
invalidCredentialIds={item.invalidCredentialIds}
|
||||||
|
handleDisconnect={handleDisconnect}
|
||||||
|
teamId={item.credentialOwner ? item.credentialOwner?.teamId : undefined}
|
||||||
|
/>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}>
|
||||||
|
<AppSettings slug={item.slug} />
|
||||||
|
</AppListCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const appsWithTeamCredentials = data.items.filter((app) => app.teams.length);
|
||||||
|
const cardsForAppsWithTeams = appsWithTeamCredentials.map((app) => {
|
||||||
|
const appCards = [];
|
||||||
|
|
||||||
|
if (app.userCredentialIds.length) {
|
||||||
|
appCards.push(<ChildAppCard item={app} />);
|
||||||
|
}
|
||||||
|
for (const team of app.teams) {
|
||||||
|
if (team) {
|
||||||
|
appCards.push(
|
||||||
|
<ChildAppCard
|
||||||
|
item={{
|
||||||
|
...app,
|
||||||
|
credentialOwner: {
|
||||||
|
name: team.name,
|
||||||
|
avatar: team.logo,
|
||||||
|
teamId: team.teamId,
|
||||||
|
credentialId: team.credentialId,
|
||||||
|
readOnly: !team.isAdmin,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return appCards;
|
||||||
|
});
|
||||||
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<List>
|
<List>
|
||||||
|
{cardsForAppsWithTeams.map((apps) => apps.map((cards) => cards))}
|
||||||
{data.items
|
{data.items
|
||||||
.filter((item) => item.invalidCredentialIds)
|
.filter((item) => item.invalidCredentialIds)
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const appSlug = item?.slug;
|
if (!item.teams.length) return <ChildAppCard item={item} />;
|
||||||
const appIsDefault =
|
|
||||||
appSlug === defaultConferencingApp?.appSlug ||
|
|
||||||
(appSlug === "daily-video" && !defaultConferencingApp?.appSlug);
|
|
||||||
return (
|
|
||||||
<AppListCard
|
|
||||||
key={item.name}
|
|
||||||
description={item.description}
|
|
||||||
title={item.name}
|
|
||||||
logo={item.logo}
|
|
||||||
isDefault={appIsDefault}
|
|
||||||
shouldHighlight
|
|
||||||
slug={item.slug}
|
|
||||||
invalidCredential={item.invalidCredentialIds.length > 0}
|
|
||||||
actions={
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Dropdown modal={false}>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button StartIcon={MoreHorizontal} variant="icon" color="secondary" />
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent>
|
|
||||||
{!appIsDefault && variant === "conferencing" && (
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<DropdownItem
|
|
||||||
type="button"
|
|
||||||
color="secondary"
|
|
||||||
StartIcon={Video}
|
|
||||||
onClick={() => {
|
|
||||||
const locationType = getEventLocationTypeFromApp(
|
|
||||||
item?.locationOption?.value ?? ""
|
|
||||||
);
|
|
||||||
if (locationType?.linkType === "static") {
|
|
||||||
setLocationType({ ...locationType, slug: appSlug });
|
|
||||||
} else {
|
|
||||||
updateDefaultAppMutation.mutate({
|
|
||||||
appSlug,
|
|
||||||
});
|
|
||||||
setBulkUpdateModal(true);
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
{t("set_as_default")}
|
|
||||||
</DropdownItem>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
<ConnectOrDisconnectIntegrationMenuItem
|
|
||||||
credentialIds={item.credentialIds}
|
|
||||||
type={item.type}
|
|
||||||
isGlobal={item.isGlobal}
|
|
||||||
installed
|
|
||||||
invalidCredentialIds={item.invalidCredentialIds}
|
|
||||||
handleDisconnect={handleDisconnect}
|
|
||||||
/>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
}>
|
|
||||||
<AppSettings slug={item.slug} />
|
|
||||||
</AppListCard>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</List>
|
</List>
|
||||||
{locationType && (
|
{locationType && (
|
||||||
|
@ -227,7 +269,12 @@ const IntegrationsContainer = ({
|
||||||
handleDisconnect,
|
handleDisconnect,
|
||||||
}: IntegrationsContainerProps): JSX.Element => {
|
}: IntegrationsContainerProps): JSX.Element => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const query = trpc.viewer.integrations.useQuery({ variant, exclude, onlyInstalled: true });
|
const query = trpc.viewer.integrations.useQuery({
|
||||||
|
variant,
|
||||||
|
exclude,
|
||||||
|
onlyInstalled: true,
|
||||||
|
includeTeamInstalledApps: true,
|
||||||
|
});
|
||||||
|
|
||||||
// TODO: Refactor and reuse getAppCategories?
|
// TODO: Refactor and reuse getAppCategories?
|
||||||
const emptyIcon: Record<AppCategories, LucideIcon> = {
|
const emptyIcon: Record<AppCategories, LucideIcon> = {
|
||||||
|
@ -299,6 +346,7 @@ type querySchemaType = z.infer<typeof querySchema>;
|
||||||
type ModalState = {
|
type ModalState = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
credentialId: null | number;
|
credentialId: null | number;
|
||||||
|
teamId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function InstalledApps() {
|
export default function InstalledApps() {
|
||||||
|
@ -323,8 +371,8 @@ export default function InstalledApps() {
|
||||||
updateData({ isOpen: false, credentialId: null });
|
updateData({ isOpen: false, credentialId: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDisconnect = (credentialId: number) => {
|
const handleDisconnect = (credentialId: number, teamId?: number) => {
|
||||||
updateData({ isOpen: true, credentialId });
|
updateData({ isOpen: true, credentialId, teamId });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -346,6 +394,7 @@ export default function InstalledApps() {
|
||||||
handleModelClose={handleModelClose}
|
handleModelClose={handleModelClose}
|
||||||
isOpen={data.isOpen}
|
isOpen={data.isOpen}
|
||||||
credentialId={data.credentialId}
|
credentialId={data.credentialId}
|
||||||
|
teamId={data.teamId}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -101,6 +101,7 @@ export type FormValues = {
|
||||||
displayLocationPublicly?: boolean;
|
displayLocationPublicly?: boolean;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
hostDefault?: string;
|
hostDefault?: string;
|
||||||
|
credentialId?: number;
|
||||||
}[];
|
}[];
|
||||||
customInputs: CustomInputParsed[];
|
customInputs: CustomInputParsed[];
|
||||||
schedule: number | null;
|
schedule: number | null;
|
||||||
|
@ -161,8 +162,10 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||||
data: { tabName },
|
data: { tabName },
|
||||||
} = useTypedQuery(querySchema);
|
} = useTypedQuery(querySchema);
|
||||||
|
|
||||||
const { data: eventTypeApps } = trpc.viewer.apps.useQuery({
|
const { data: eventTypeApps } = trpc.viewer.integrations.useQuery({
|
||||||
extendsFeature: "EventType",
|
extendsFeature: "EventType",
|
||||||
|
teamId: props.eventType.team?.id || props.eventType.parent?.teamId,
|
||||||
|
onlyInstalled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { eventType, locationOptions, team, teamMembers, currentUserMembership, destinationCalendar } = props;
|
const { eventType, locationOptions, team, teamMembers, currentUserMembership, destinationCalendar } = props;
|
||||||
|
@ -306,12 +309,12 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||||
|
|
||||||
const appsMetadata = formMethods.getValues("metadata")?.apps;
|
const appsMetadata = formMethods.getValues("metadata")?.apps;
|
||||||
const availability = formMethods.watch("availability");
|
const availability = formMethods.watch("availability");
|
||||||
const numberOfInstalledApps = eventTypeApps?.filter((app) => app.isInstalled).length || 0;
|
|
||||||
let numberOfActiveApps = 0;
|
let numberOfActiveApps = 0;
|
||||||
|
|
||||||
if (appsMetadata) {
|
if (appsMetadata) {
|
||||||
numberOfActiveApps = Object.entries(appsMetadata).filter(
|
numberOfActiveApps = Object.entries(appsMetadata).filter(
|
||||||
([appId, appData]) => eventTypeApps?.find((app) => app.slug === appId)?.isInstalled && appData.enabled
|
([appId, appData]) =>
|
||||||
|
eventTypeApps?.items.find((app) => app.slug === appId)?.isInstalled && appData.enabled
|
||||||
).length;
|
).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -423,7 +426,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||||
<>
|
<>
|
||||||
<EventTypeSingleLayout
|
<EventTypeSingleLayout
|
||||||
enabledAppsNumber={numberOfActiveApps}
|
enabledAppsNumber={numberOfActiveApps}
|
||||||
installedAppsNumber={numberOfInstalledApps}
|
installedAppsNumber={eventTypeApps?.items.length || 0}
|
||||||
enabledWorkflowsNumber={eventType.workflows.length}
|
enabledWorkflowsNumber={eventType.workflows.length}
|
||||||
eventType={eventType}
|
eventType={eventType}
|
||||||
team={team}
|
team={team}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
type PageProps = inferSSRProps<typeof getServerSideProps>;
|
export type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||||
|
|
||||||
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
|
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
|
||||||
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
|
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
|
||||||
|
|
|
@ -13,7 +13,7 @@ import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
type PageProps = inferSSRProps<typeof getServerSideProps>;
|
export type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||||
|
|
||||||
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
|
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
|
||||||
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
|
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
|
||||||
|
@ -94,6 +94,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
booking,
|
booking,
|
||||||
away: false,
|
away: false,
|
||||||
user: teamSlug,
|
user: teamSlug,
|
||||||
|
teamId: team.id,
|
||||||
slug: meetingSlug,
|
slug: meetingSlug,
|
||||||
trpcState: ssr.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
isBrandingHidden: team?.hideBranding,
|
isBrandingHidden: team?.hideBranding,
|
||||||
|
|
|
@ -1 +1,62 @@
|
||||||
export { getServerSideProps, default } from "../../../[user]/[type]";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
|
import type { PageProps as UserTypePageProps } from "../../../new-booker/[user]/[type]";
|
||||||
|
import UserTypePage, { getServerSideProps as GSSUserTypePage } from "../../../new-booker/[user]/[type]";
|
||||||
|
import TeamTypePage, { getServerSideProps as GSSTeamTypePage } from "../../../new-booker/team/[slug]/[type]";
|
||||||
|
import type { PageProps as TeamTypePageProps } from "../../../new-booker/team/[slug]/[type]";
|
||||||
|
|
||||||
|
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||||
|
const team = await prisma.team.findFirst({
|
||||||
|
where: {
|
||||||
|
slug: ctx.query.user as string,
|
||||||
|
parentId: {
|
||||||
|
not: null,
|
||||||
|
},
|
||||||
|
parent: {
|
||||||
|
slug: ctx.query.orgSlug as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (team) {
|
||||||
|
const params = { slug: ctx.query.user, type: ctx.query.type };
|
||||||
|
return GSSTeamTypePage({
|
||||||
|
...ctx,
|
||||||
|
params: {
|
||||||
|
...ctx.params,
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
...ctx.query,
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const params = { user: ctx.query.user, type: ctx.query.type };
|
||||||
|
return GSSUserTypePage({
|
||||||
|
...ctx,
|
||||||
|
params: {
|
||||||
|
...ctx.params,
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
...ctx.query,
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = UserTypePageProps | TeamTypePageProps;
|
||||||
|
|
||||||
|
export default function Page(props: Props) {
|
||||||
|
if ((props as TeamTypePageProps)?.teamId) return TeamTypePage(props as TeamTypePageProps);
|
||||||
|
return UserTypePage(props as UserTypePageProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
Page.PageWrapper = PageWrapper;
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import { getServerSideProps as _getServerSideProps } from "../[type]";
|
||||||
|
|
||||||
|
export { default } from "../[type]";
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssrResponse = await _getServerSideProps(context);
|
||||||
|
if (ssrResponse.notFound) {
|
||||||
|
return ssrResponse;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...ssrResponse,
|
||||||
|
props: {
|
||||||
|
...ssrResponse.props,
|
||||||
|
isEmbed: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -160,7 +160,7 @@ const ConferencingLayout = () => {
|
||||||
disabled={app.isGlobal}
|
disabled={app.isGlobal}
|
||||||
StartIcon={Trash}
|
StartIcon={Trash}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDeleteCredentialId(app.credentialIds[0]);
|
setDeleteCredentialId(app.userCredentialIds[0]);
|
||||||
setDeleteAppModal(true);
|
setDeleteAppModal(true);
|
||||||
}}>
|
}}>
|
||||||
{t("remove_app")}
|
{t("remove_app")}
|
||||||
|
|
|
@ -65,7 +65,7 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
|
||||||
}
|
}
|
||||||
|
|
||||||
// slug is a route parameter, we don't want to forward it to the next route
|
// slug is a route parameter, we don't want to forward it to the next route
|
||||||
const { slug: _slug, ...queryParamsToForward } = router.query;
|
const { slug: _slug, orgSlug: _orgSlug, user: _user, ...queryParamsToForward } = router.query;
|
||||||
|
|
||||||
const EventTypes = () => (
|
const EventTypes = () => (
|
||||||
<ul className="border-subtle rounded-md border">
|
<ul className="border-subtle rounded-md border">
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
const glob = require("glob");
|
||||||
|
const { getSubdomainRegExp } = require("./getSubdomainRegExp");
|
||||||
|
/** Needed to rewrite public booking page, gets all static pages but [user] */
|
||||||
|
let pages = (exports.pages = glob
|
||||||
|
.sync("pages/**/[^_]*.{tsx,js,ts}", { cwd: __dirname })
|
||||||
|
.map((filename) =>
|
||||||
|
filename
|
||||||
|
.substr(6)
|
||||||
|
.replace(/(\.tsx|\.js|\.ts)/, "")
|
||||||
|
.replace(/\/.*/, "")
|
||||||
|
)
|
||||||
|
.filter((v, i, self) => self.indexOf(v) === i && !v.startsWith("[user]")));
|
||||||
|
|
||||||
|
// Following routes don't exist but they work by doing rewrite. Thus they need to be excluded from matching the orgRewrite patterns
|
||||||
|
// Make sure to keep it upto date as more nonExistingRouteRewrites are added.
|
||||||
|
const otherNonExistingRoutePrefixes = ["forms", "router", "success", "cancel"];
|
||||||
|
|
||||||
|
// .* matches / as well(Note: *(i.e wildcard) doesn't match / but .*(i.e. RegExp) does)
|
||||||
|
// It would match /free/30min but not /bookings/upcoming because 'bookings' is an item in pages
|
||||||
|
// It would also not match /free/30min/embed because we are ensuring just two slashes
|
||||||
|
// ?!book ensures it doesn't match /free/book page which doesn't have a corresponding new-booker page.
|
||||||
|
// [^/]+ makes the RegExp match the full path, it seems like a partial match doesn't work.
|
||||||
|
// book$ ensures that only /book is excluded from rewrite(which is at the end always) and not /booked
|
||||||
|
|
||||||
|
const afterFilesRewriteExcludePages = pages;
|
||||||
|
exports.userTypeRoutePath = `/:user((?!${afterFilesRewriteExcludePages.join(
|
||||||
|
"/|"
|
||||||
|
)})[^/]*)/:type((?!book$)[^/]+)`;
|
||||||
|
exports.teamTypeRoutePath = "/team/:slug/:type((?!book$)[^/]+)";
|
||||||
|
exports.privateLinkRoutePath = "/d/:link/:slug((?!book$)[^/]+)";
|
||||||
|
exports.embedUserTypeRoutePath = `/:user((?!${afterFilesRewriteExcludePages.join("/|")})[^/]*)/:type/embed`;
|
||||||
|
exports.embedTeamTypeRoutePath = "/team/:slug/:type/embed";
|
||||||
|
let subdomainRegExp = (exports.subdomainRegExp = getSubdomainRegExp(process.env.NEXT_PUBLIC_WEBAPP_URL));
|
||||||
|
exports.orgHostPath = `^(?<orgSlug>${subdomainRegExp})\\..*`;
|
||||||
|
|
||||||
|
let beforeRewriteExcludePages = pages.concat(otherNonExistingRoutePrefixes);
|
||||||
|
exports.orgUserRoutePath = `/:user((?!${beforeRewriteExcludePages.join("|")}|_next|public)[a-zA-Z0-9\-_]+)`;
|
||||||
|
exports.orgUserTypeRoutePath = `/:user((?!${beforeRewriteExcludePages.join(
|
||||||
|
"/|"
|
||||||
|
)}|_next/|public/)[^/]+)/:type((?!avatar\.png)[^/]+)`;
|
||||||
|
exports.orgUserTypeEmbedRoutePath = `/:user((?!${beforeRewriteExcludePages.join(
|
||||||
|
"/|"
|
||||||
|
)}|_next/|public/)[^/]+)/:type/embed`;
|
|
@ -414,6 +414,7 @@ test.describe("FORM_SUBMITTED", async () => {
|
||||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||||
|
|
||||||
await page.click('[data-testid="install-app-button"]');
|
await page.click('[data-testid="install-app-button"]');
|
||||||
|
await page.click('[data-testid="install-app-button-personal"]');
|
||||||
|
|
||||||
await page.waitForLoadState("networkidle");
|
await page.waitForLoadState("networkidle");
|
||||||
await page.goto(`/settings/developer/webhooks/new`);
|
await page.goto(`/settings/developer/webhooks/new`);
|
||||||
|
|
|
@ -118,7 +118,7 @@
|
||||||
"team_info": "Team Info",
|
"team_info": "Team Info",
|
||||||
"request_another_invitation_email": "If you prefer not to use {{toEmail}} as your {{appName}} email or already have a {{appName}} account, please request another invitation to that email.",
|
"request_another_invitation_email": "If you prefer not to use {{toEmail}} as your {{appName}} email or already have a {{appName}} account, please request another invitation to that email.",
|
||||||
"you_have_been_invited": "You have been invited to join the team {{teamName}}",
|
"you_have_been_invited": "You have been invited to join the team {{teamName}}",
|
||||||
"user_invited_you": "{{user}} invited you to join the team {{team}} on {{appName}}",
|
"user_invited_you": "{{user}} invited you to join the {{entity}} {{team}} on {{appName}}",
|
||||||
"hidden_team_member_title": "You are hidden in this team",
|
"hidden_team_member_title": "You are hidden in this team",
|
||||||
"hidden_team_member_message": "Your seat is not paid for, either Upgrade to PRO or let the team owner know they can pay for your seat.",
|
"hidden_team_member_message": "Your seat is not paid for, either Upgrade to PRO or let the team owner know they can pay for your seat.",
|
||||||
"hidden_team_owner_message": "You need a pro account to use teams, you are hidden until you upgrade.",
|
"hidden_team_owner_message": "You need a pro account to use teams, you are hidden until you upgrade.",
|
||||||
|
@ -1875,6 +1875,7 @@
|
||||||
"connect_google_workspace": "Connect Google Workspace",
|
"connect_google_workspace": "Connect Google Workspace",
|
||||||
"google_workspace_admin_tooltip": "You must be a Workspace Admin to use this feature",
|
"google_workspace_admin_tooltip": "You must be a Workspace Admin to use this feature",
|
||||||
"first_event_type_webhook_description": "Create your first webhook for this event type",
|
"first_event_type_webhook_description": "Create your first webhook for this event type",
|
||||||
|
"install_app_on": "Install app on",
|
||||||
"create_for": "Create for",
|
"create_for": "Create for",
|
||||||
"setup_organization": "Setup an Organization",
|
"setup_organization": "Setup an Organization",
|
||||||
"organization_banner_description": "Create an environments where your teams can create shared apps, workflows and event types with round-robin and collective scheduling.",
|
"organization_banner_description": "Create an environments where your teams can create shared apps, workflows and event types with round-robin and collective scheduling.",
|
||||||
|
|
|
@ -1,165 +1,171 @@
|
||||||
|
|
||||||
import { it, expect, describe, beforeAll, afterAll } from "vitest";
|
import { it, expect, describe, beforeAll } from "vitest";
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const { getSubdomainRegExp } = require("../../getSubdomainRegExp");
|
const { getSubdomainRegExp } = require("../../getSubdomainRegExp");
|
||||||
let userTypeRouteRegExp: RegExp;
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
let teamTypeRouteRegExp:RegExp;
|
const {match, pathToRegexp} = require("next/dist/compiled/path-to-regexp");
|
||||||
let privateLinkRouteRegExp:RegExp
|
type MatcherRes = (path: string) => {params: Record<string, string>}
|
||||||
let embedUserTypeRouteRegExp:RegExp
|
let userTypeRouteMatch: MatcherRes;
|
||||||
let embedTeamTypeRouteRegExp:RegExp
|
let teamTypeRouteMatch:MatcherRes;
|
||||||
|
let privateLinkRouteMatch:MatcherRes
|
||||||
|
let embedUserTypeRouteMatch:MatcherRes
|
||||||
|
let embedTeamTypeRouteMatch:MatcherRes
|
||||||
|
let orgUserTypeRouteMatch:MatcherRes
|
||||||
|
let orgUserRouteMatch: MatcherRes
|
||||||
|
|
||||||
|
|
||||||
const getRegExpFromNextJsRewriteRegExp = (nextJsRegExp:string) => {
|
beforeAll(async()=>{
|
||||||
// const parts = nextJsRegExp.split(':');
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
process.env.NEXT_PUBLIC_WEBAPP_URL = "http://example.com"
|
||||||
|
const {
|
||||||
|
userTypeRoutePath,
|
||||||
|
teamTypeRoutePath,
|
||||||
|
privateLinkRoutePath,
|
||||||
|
embedUserTypeRoutePath,
|
||||||
|
embedTeamTypeRoutePath,
|
||||||
|
orgUserRoutePath,
|
||||||
|
orgUserTypeRoutePath,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
} = require("../../pagesAndRewritePaths")
|
||||||
|
|
||||||
|
userTypeRouteMatch = match(userTypeRoutePath);
|
||||||
|
|
||||||
// const validNamedGroupRegExp = parts.map((part, index)=>{
|
teamTypeRouteMatch = match(teamTypeRoutePath);
|
||||||
// if (index === 0) {
|
|
||||||
// return part;
|
|
||||||
// }
|
|
||||||
// if (part.match(/^[a-zA-Z0-9]+$/)) {
|
|
||||||
// return `(?<${part}>[^/]+)`
|
|
||||||
// }
|
|
||||||
// part = part.replace(new RegExp('([^(]+)(.*)'), '(?<$1>$2)');
|
|
||||||
// return part
|
|
||||||
// }).join('');
|
|
||||||
|
|
||||||
// TODO: If we can easily convert the exported rewrite regexes from next.config.js to a valid named capturing group regex, it would be best
|
privateLinkRouteMatch = match(privateLinkRoutePath);
|
||||||
// Next.js does an exact match as per my testing.
|
|
||||||
return new RegExp(`^${nextJsRegExp}$`)
|
embedUserTypeRouteMatch = match(embedUserTypeRoutePath);
|
||||||
}
|
|
||||||
|
embedTeamTypeRouteMatch = match(embedTeamTypeRoutePath);
|
||||||
|
|
||||||
|
orgUserTypeRouteMatch = match(orgUserTypeRoutePath)
|
||||||
|
|
||||||
|
orgUserRouteMatch = match(orgUserRoutePath)
|
||||||
|
console.log({
|
||||||
|
regExps: {
|
||||||
|
userTypeRouteMatch: pathToRegexp(userTypeRoutePath),
|
||||||
|
|
||||||
|
teamTypeRouteMatch:pathToRegexp(teamTypeRoutePath),
|
||||||
|
|
||||||
|
privateLinkRouteMatch:pathToRegexp(privateLinkRoutePath),
|
||||||
|
|
||||||
|
embedUserTypeRouteMatch:pathToRegexp(embedUserTypeRoutePath),
|
||||||
|
|
||||||
|
embedTeamTypeRouteMatch:pathToRegexp(embedTeamTypeRoutePath),
|
||||||
|
|
||||||
|
orgUserTypeRouteMatch:pathToRegexp(orgUserTypeRoutePath),
|
||||||
|
|
||||||
|
orgUserRouteMatch:pathToRegexp(orgUserRoutePath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
describe('next.config.js - RegExp', ()=>{
|
describe('next.config.js - RegExp', ()=>{
|
||||||
beforeAll(async()=>{
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
// process.env.NEXTAUTH_SECRET = process.env.NEXTAUTH_URL = process.env.CALENDSO_ENCRYPTION_KEY = 1
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
const pages = require("../../pages").pages
|
|
||||||
|
|
||||||
// How to convert a Next.js rewrite RegExp/wildcard to a valid JS named capturing Group RegExp?
|
|
||||||
// - /:user/ -> (?<user>[^/]+)
|
|
||||||
// - /:user(?!404)[^/]+/ -> (?<user>((?!404)[^/]+))
|
|
||||||
|
|
||||||
// userTypeRouteRegExp = `/:user((?!${pages.join("/|")})[^/]*)/:type((?!book$)[^/]+)`;
|
|
||||||
userTypeRouteRegExp = getRegExpFromNextJsRewriteRegExp(`/(?<user>((?!${pages.join("/|")})[^/]*))/(?<type>((?!book$)[^/]+))`);
|
|
||||||
|
|
||||||
// teamTypeRouteRegExp = "/team/:slug/:type((?!book$)[^/]+)";
|
|
||||||
teamTypeRouteRegExp = getRegExpFromNextJsRewriteRegExp("/team/(?<slug>[^/]+)/(?<type>((?!book$)[^/]+))");
|
|
||||||
|
|
||||||
// privateLinkRouteRegExp = "/d/:link/:slug((?!book$)[^/]+)";
|
|
||||||
privateLinkRouteRegExp = getRegExpFromNextJsRewriteRegExp("/d/(?<link>[^/]+)/(?<slug>((?!book$)[^/]+))");
|
|
||||||
|
|
||||||
// embedUserTypeRouteRegExp = `/:user((?!${pages.join("/|")})[^/]*)/:type/embed`;
|
|
||||||
embedUserTypeRouteRegExp = getRegExpFromNextJsRewriteRegExp(`/(?<user>((?!${pages.join("/|")})[^/]*))/(?<type>[^/]+)/embed`);
|
|
||||||
|
|
||||||
// embedTeamTypeRouteRegExp = "/team/:slug/:type/embed";
|
|
||||||
embedTeamTypeRouteRegExp = getRegExpFromNextJsRewriteRegExp("/team/(?<slug>[^/]+)/(?<type>[^/]+)/embed");
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(()=>{
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
//@ts-ignore
|
|
||||||
process.env.NEXTAUTH_SECRET = process.env.NEXTAUTH_URL = process.env.CALENDSO_ENCRYPTION_KEY = undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
it("Booking Urls", async () => {
|
it("Booking Urls", async () => {
|
||||||
expect(userTypeRouteRegExp.exec('/free/30')?.groups).toContain({
|
expect(userTypeRouteMatch('/free/30')?.params).toContain({
|
||||||
user: 'free',
|
user: 'free',
|
||||||
type: '30'
|
type: '30'
|
||||||
})
|
})
|
||||||
|
|
||||||
// Edgecase of username starting with team also works
|
// Edgecase of username starting with team also works
|
||||||
expect(userTypeRouteRegExp.exec('/teampro/30')?.groups).toContain({
|
expect(userTypeRouteMatch('/teampro/30')?.params).toContain({
|
||||||
user: 'teampro',
|
user: 'teampro',
|
||||||
type: '30'
|
type: '30'
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(userTypeRouteRegExp.exec('/teampro+pro/30')?.groups).toContain({
|
// Edgecase of username starting with team also works
|
||||||
|
expect(userTypeRouteMatch('/workflowteam/30')?.params).toContain({
|
||||||
|
user: 'workflowteam',
|
||||||
|
type: '30'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(userTypeRouteMatch('/teampro+pro/30')?.params).toContain({
|
||||||
user: 'teampro+pro',
|
user: 'teampro+pro',
|
||||||
type: '30'
|
type: '30'
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(userTypeRouteRegExp.exec('/teampro+pro/book')).toEqual(null)
|
expect(userTypeRouteMatch('/teampro+pro/book')).toEqual(false)
|
||||||
|
|
||||||
// Because /book doesn't have a corresponding new-booker route.
|
// Because /book doesn't have a corresponding new-booker route.
|
||||||
expect(userTypeRouteRegExp.exec('/free/book')).toEqual(null)
|
expect(userTypeRouteMatch('/free/book')).toEqual(false)
|
||||||
|
|
||||||
// Because /booked is a normal event name
|
// Because /booked is a normal event name
|
||||||
expect(userTypeRouteRegExp.exec('/free/booked')?.groups).toEqual({
|
expect(userTypeRouteMatch('/free/booked')?.params).toEqual({
|
||||||
user: 'free',
|
user: 'free',
|
||||||
type: 'booked'
|
type: 'booked'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
expect(embedUserTypeRouteRegExp.exec('/free/30/embed')?.groups).toContain({
|
expect(embedUserTypeRouteMatch('/free/30/embed')?.params).toContain({
|
||||||
user: 'free',
|
user: 'free',
|
||||||
type:'30'
|
type:'30'
|
||||||
})
|
})
|
||||||
|
|
||||||
// Edgecase of username starting with team also works
|
// Edgecase of username starting with team also works
|
||||||
expect(embedUserTypeRouteRegExp.exec('/teampro/30/embed')?.groups).toContain({
|
expect(embedUserTypeRouteMatch('/teampro/30/embed')?.params).toContain({
|
||||||
user: 'teampro',
|
user: 'teampro',
|
||||||
type: '30'
|
type: '30'
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(teamTypeRouteRegExp.exec('/team/seeded/30')?.groups).toContain({
|
expect(teamTypeRouteMatch('/team/seeded/30')?.params).toContain({
|
||||||
slug: 'seeded',
|
slug: 'seeded',
|
||||||
type: '30'
|
type: '30'
|
||||||
})
|
})
|
||||||
|
|
||||||
// Because /book doesn't have a corresponding new-booker route.
|
// Because /book doesn't have a corresponding new-booker route.
|
||||||
expect(teamTypeRouteRegExp.exec('/team/seeded/book')).toEqual(null)
|
expect(teamTypeRouteMatch('/team/seeded/book')).toEqual(false)
|
||||||
|
|
||||||
expect(teamTypeRouteRegExp.exec('/team/seeded/30/embed')).toEqual(null)
|
expect(teamTypeRouteMatch('/team/seeded/30/embed')).toEqual(false)
|
||||||
|
|
||||||
expect(embedTeamTypeRouteRegExp.exec('/team/seeded/30/embed')?.groups).toContain({
|
expect(embedTeamTypeRouteMatch('/team/seeded/30/embed')?.params).toContain({
|
||||||
slug: 'seeded',
|
slug: 'seeded',
|
||||||
type:'30'
|
type:'30'
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(privateLinkRouteRegExp.exec('/d/3v4s321CXRJZx5TFxkpPvd/30min')?.groups).toContain({
|
expect(privateLinkRouteMatch('/d/3v4s321CXRJZx5TFxkpPvd/30min')?.params).toContain({
|
||||||
link: '3v4s321CXRJZx5TFxkpPvd',
|
link: '3v4s321CXRJZx5TFxkpPvd',
|
||||||
slug: '30min'
|
slug: '30min'
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(privateLinkRouteRegExp.exec('/d/3v4s321CXRJZx5TFxkpPvd/30min')?.groups).toContain({
|
expect(privateLinkRouteMatch('/d/3v4s321CXRJZx5TFxkpPvd/30min')?.params).toContain({
|
||||||
link: '3v4s321CXRJZx5TFxkpPvd',
|
link: '3v4s321CXRJZx5TFxkpPvd',
|
||||||
slug: '30min'
|
slug: '30min'
|
||||||
})
|
})
|
||||||
|
|
||||||
// Because /book doesn't have a corresponding new-booker route.
|
// Because /book doesn't have a corresponding new-booker route.
|
||||||
expect(privateLinkRouteRegExp.exec('/d/3v4s321CXRJZx5TFxkpPvd/book')).toEqual(null)
|
expect(privateLinkRouteMatch('/d/3v4s321CXRJZx5TFxkpPvd/book')).toEqual(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Non booking Urls', ()=>{
|
it('Non booking Urls', ()=>{
|
||||||
|
|
||||||
expect(userTypeRouteRegExp.exec('/404')).toEqual(null)
|
expect(userTypeRouteMatch('/404/')).toEqual(false)
|
||||||
expect(teamTypeRouteRegExp.exec('/404')).toEqual(null)
|
expect(teamTypeRouteMatch('/404/')).toEqual(false)
|
||||||
|
|
||||||
expect(userTypeRouteRegExp.exec('/404/30')).toEqual(null)
|
expect(userTypeRouteMatch('/404/30')).toEqual(false)
|
||||||
expect(teamTypeRouteRegExp.exec('/404/30')).toEqual(null)
|
expect(teamTypeRouteMatch('/404/30')).toEqual(false)
|
||||||
|
|
||||||
expect(userTypeRouteRegExp.exec('/api')).toEqual(null)
|
expect(userTypeRouteMatch('/api')).toEqual(false)
|
||||||
expect(teamTypeRouteRegExp.exec('/api')).toEqual(null)
|
expect(teamTypeRouteMatch('/api')).toEqual(false)
|
||||||
|
|
||||||
expect(userTypeRouteRegExp.exec('/api/30')).toEqual(null)
|
expect(userTypeRouteMatch('/api/30')).toEqual(false)
|
||||||
expect(teamTypeRouteRegExp.exec('/api/30')).toEqual(null)
|
expect(teamTypeRouteMatch('/api/30')).toEqual(false)
|
||||||
|
|
||||||
expect(userTypeRouteRegExp.exec('/workflows/30')).toEqual(null)
|
expect(userTypeRouteMatch('/workflows/30')).toEqual(false)
|
||||||
expect(teamTypeRouteRegExp.exec('/workflows/30')).toEqual(null)
|
expect(teamTypeRouteMatch('/workflows/30')).toEqual(false)
|
||||||
|
|
||||||
expect(userTypeRouteRegExp.exec('/event-types/30')).toEqual(null)
|
expect(userTypeRouteMatch('/event-types/30')).toEqual(false)
|
||||||
expect(teamTypeRouteRegExp.exec('/event-types/30')).toEqual(null)
|
expect(teamTypeRouteMatch('/event-types/30')).toEqual(false)
|
||||||
|
|
||||||
expect(userTypeRouteRegExp.exec('/teams/1')).toEqual(null)
|
expect(userTypeRouteMatch('/teams/1')).toEqual(false)
|
||||||
expect(teamTypeRouteRegExp.exec('/teams/1')).toEqual(null)
|
expect(teamTypeRouteMatch('/teams/1')).toEqual(false)
|
||||||
|
|
||||||
expect(userTypeRouteRegExp.exec('/teams')).toEqual(null)
|
expect(userTypeRouteMatch('/teams')).toEqual(false)
|
||||||
expect(teamTypeRouteRegExp.exec('/teams')).toEqual(null)
|
expect(teamTypeRouteMatch('/teams')).toEqual(false)
|
||||||
|
|
||||||
// Note that even though it matches /embed/embed.js, but it's served from /public and the regexes are in afterEach, it won't hit the flow.
|
// Note that even though it matches /embed/embed.js, but it's served from /public and the regexes are in afterEach, it won't hit the flow.
|
||||||
// expect(userTypeRouteRegExp.exec('/embed/embed.js')).toEqual(null)
|
// expect(userTypeRouteRegExp('/embed/embed.js')).toEqual(false)
|
||||||
// expect(teamTypeRouteRegExp.exec('/embed/embed.js')).toEqual(null)
|
// expect(teamTypeRouteRegExp('/embed/embed.js')).toEqual(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -167,7 +173,7 @@ describe('next.config.js - RegExp', ()=>{
|
||||||
describe('next.config.js - Org Rewrite', ()=> {
|
describe('next.config.js - Org Rewrite', ()=> {
|
||||||
// RegExp copied from next.config.js
|
// RegExp copied from next.config.js
|
||||||
const orgHostRegExp = (subdomainRegExp:string)=> new RegExp(`^(?<orgSlug>${subdomainRegExp})\\..*`)
|
const orgHostRegExp = (subdomainRegExp:string)=> new RegExp(`^(?<orgSlug>${subdomainRegExp})\\..*`)
|
||||||
describe('SubDomain Retrieval from NEXT_PUBLIC_WEBAPP_URL', ()=>{
|
describe('Host matching based on NEXT_PUBLIC_WEBAPP_URL', ()=>{
|
||||||
it('https://app.cal.com', ()=>{
|
it('https://app.cal.com', ()=>{
|
||||||
const subdomainRegExp = getSubdomainRegExp('https://app.cal.com');
|
const subdomainRegExp = getSubdomainRegExp('https://app.cal.com');
|
||||||
expect(orgHostRegExp(subdomainRegExp).exec('app.cal.com')).toEqual(null)
|
expect(orgHostRegExp(subdomainRegExp).exec('app.cal.com')).toEqual(null)
|
||||||
|
@ -195,4 +201,42 @@ describe('next.config.js - Org Rewrite', ()=> {
|
||||||
expect(orgHostRegExp(subdomainRegExp).exec('some-other.company.com')?.groups?.orgSlug).toEqual('some-other')
|
expect(orgHostRegExp(subdomainRegExp).exec('some-other.company.com')?.groups?.orgSlug).toEqual('some-other')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Rewrite', () =>{
|
||||||
|
it('booking pages', () => {
|
||||||
|
expect(orgUserTypeRouteMatch('/user/type')?.params).toContain({
|
||||||
|
user: 'user',
|
||||||
|
type: 'type'
|
||||||
|
})
|
||||||
|
|
||||||
|
// User slug starting with 404(which is a page route) will work
|
||||||
|
expect(orgUserTypeRouteMatch('/404a/def')?.params).toEqual({
|
||||||
|
user: '404a',
|
||||||
|
type: 'def'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Team Page won't match - There is no /team prefix required for Org team event pages
|
||||||
|
expect(orgUserTypeRouteMatch('/team/abc')).toEqual(false)
|
||||||
|
|
||||||
|
expect(orgUserTypeRouteMatch('/abc')).toEqual(false)
|
||||||
|
|
||||||
|
expect(orgUserRouteMatch('/abc')?.params).toContain({
|
||||||
|
user: 'abc'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Non booking pages', () => {
|
||||||
|
expect(orgUserTypeRouteMatch('/_next/def')).toEqual(false)
|
||||||
|
expect(orgUserTypeRouteMatch('/public/def')).toEqual(false)
|
||||||
|
expect(orgUserRouteMatch('/_next/')).toEqual(false)
|
||||||
|
expect(orgUserRouteMatch('/public/')).toEqual(false)
|
||||||
|
expect(orgUserRouteMatch('/event-types')).toEqual(false)
|
||||||
|
expect(orgUserTypeRouteMatch('/event-types')).toEqual(false)
|
||||||
|
expect(orgUserTypeRouteMatch('/john/avatar.png')).toEqual(false)
|
||||||
|
expect(orgUserTypeRouteMatch('/cancel/abcd')).toEqual(false)
|
||||||
|
expect(orgUserTypeRouteMatch('/success/abcd')).toEqual(false)
|
||||||
|
expect(orgUserRouteMatch('/forms/xdsdf-sd')).toEqual(false)
|
||||||
|
expect(orgUserRouteMatch('/router?form=')).toEqual(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
|
@ -3,8 +3,15 @@ import type { z, ZodType } from "zod";
|
||||||
|
|
||||||
export type GetAppData = (key: string) => unknown;
|
export type GetAppData = (key: string) => unknown;
|
||||||
export type SetAppData = (key: string, value: unknown) => void;
|
export type SetAppData = (key: string, value: unknown) => void;
|
||||||
|
type LockedIcon = JSX.Element | false | undefined;
|
||||||
|
type Disabled = boolean | undefined;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
const EventTypeAppContext = React.createContext<[GetAppData, SetAppData]>([() => ({}), () => {}]);
|
const EventTypeAppContext = React.createContext<[GetAppData, SetAppData, LockedIcon, Disabled]>([
|
||||||
|
() => ({}),
|
||||||
|
() => ({}),
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
]);
|
||||||
|
|
||||||
export type SetAppDataGeneric<TAppData extends ZodType> = <
|
export type SetAppDataGeneric<TAppData extends ZodType> = <
|
||||||
TKey extends keyof z.infer<TAppData>,
|
TKey extends keyof z.infer<TAppData>,
|
||||||
|
@ -22,7 +29,7 @@ export const useAppContextWithSchema = <TAppData extends ZodType>() => {
|
||||||
type GetAppData = GetAppDataGeneric<TAppData>;
|
type GetAppData = GetAppDataGeneric<TAppData>;
|
||||||
type SetAppData = SetAppDataGeneric<TAppData>;
|
type SetAppData = SetAppDataGeneric<TAppData>;
|
||||||
// TODO: Not able to do it without type assertion here
|
// TODO: Not able to do it without type assertion here
|
||||||
const context = React.useContext(EventTypeAppContext) as [GetAppData, SetAppData];
|
const context = React.useContext(EventTypeAppContext) as [GetAppData, SetAppData, LockedIcon, Disabled];
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
export default EventTypeAppContext;
|
export default EventTypeAppContext;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { z } from "zod";
|
||||||
|
|
||||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||||
import { getAppFromSlug } from "@calcom/app-store/utils";
|
import { getAppFromSlug } from "@calcom/app-store/utils";
|
||||||
|
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
|
||||||
import prisma, { safeAppSelect, safeCredentialSelect } from "@calcom/prisma";
|
import prisma, { safeAppSelect, safeCredentialSelect } from "@calcom/prisma";
|
||||||
import { userMetadata } from "@calcom/prisma/zod-utils";
|
import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||||
import type { AppFrontendPayload as App } from "@calcom/types/App";
|
import type { AppFrontendPayload as App } from "@calcom/types/App";
|
||||||
|
@ -56,13 +57,21 @@ export async function getAppRegistry() {
|
||||||
return apps;
|
return apps;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAppRegistryWithCredentials(userId: number) {
|
export async function getAppRegistryWithCredentials(userId: number, userAdminTeams: UserAdminTeams = []) {
|
||||||
|
// Get teamIds to grab existing credentials
|
||||||
|
const teamIds = [];
|
||||||
|
for (const team of userAdminTeams) {
|
||||||
|
if (!team.isUser) {
|
||||||
|
teamIds.push(team.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const dbApps = await prisma.app.findMany({
|
const dbApps = await prisma.app.findMany({
|
||||||
where: { enabled: true },
|
where: { enabled: true },
|
||||||
select: {
|
select: {
|
||||||
...safeAppSelect,
|
...safeAppSelect,
|
||||||
credentials: {
|
credentials: {
|
||||||
where: { userId },
|
where: { OR: [{ userId }, { teamId: { in: teamIds } }] },
|
||||||
select: safeCredentialSelect,
|
select: safeCredentialSelect,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,10 +3,11 @@ import Link from "next/link";
|
||||||
|
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { Switch } from "@calcom/ui";
|
import { Switch, Badge, Avatar } from "@calcom/ui";
|
||||||
|
|
||||||
import type { SetAppDataGeneric } from "../EventTypeAppContext";
|
import type { SetAppDataGeneric } from "../EventTypeAppContext";
|
||||||
import type { eventTypeAppCardZod } from "../eventTypeAppCardZod";
|
import type { eventTypeAppCardZod } from "../eventTypeAppCardZod";
|
||||||
|
import type { CredentialOwner } from "../types";
|
||||||
import OmniInstallAppButton from "./OmniInstallAppButton";
|
import OmniInstallAppButton from "./OmniInstallAppButton";
|
||||||
|
|
||||||
export default function AppCard({
|
export default function AppCard({
|
||||||
|
@ -17,14 +18,20 @@ export default function AppCard({
|
||||||
children,
|
children,
|
||||||
setAppData,
|
setAppData,
|
||||||
returnTo,
|
returnTo,
|
||||||
|
teamId,
|
||||||
|
disableSwitch,
|
||||||
|
LockedIcon,
|
||||||
}: {
|
}: {
|
||||||
app: RouterOutputs["viewer"]["apps"][number];
|
app: RouterOutputs["viewer"]["integrations"]["items"][number] & { credentialOwner?: CredentialOwner };
|
||||||
description?: React.ReactNode;
|
description?: React.ReactNode;
|
||||||
switchChecked?: boolean;
|
switchChecked?: boolean;
|
||||||
switchOnClick?: (e: boolean) => void;
|
switchOnClick?: (e: boolean) => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
setAppData: SetAppDataGeneric<typeof eventTypeAppCardZod>;
|
setAppData: SetAppDataGeneric<typeof eventTypeAppCardZod>;
|
||||||
returnTo?: string;
|
returnTo?: string;
|
||||||
|
teamId?: number;
|
||||||
|
disableSwitch?: boolean;
|
||||||
|
LockedIcon?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const [animationRef] = useAutoAnimate<HTMLDivElement>();
|
const [animationRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
|
|
||||||
|
@ -55,26 +62,45 @@ export default function AppCard({
|
||||||
{description || app?.description}
|
{description || app?.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{app?.isInstalled ? (
|
<div className="ml-auto flex items-center space-x-2">
|
||||||
<div className="ml-auto flex items-center">
|
{app.credentialOwner && (
|
||||||
<Switch
|
<div className="ml-auto">
|
||||||
disabled={!app.enabled}
|
<Badge variant="gray">
|
||||||
onCheckedChange={(enabled) => {
|
<div className="flex items-center">
|
||||||
if (switchOnClick) {
|
<Avatar
|
||||||
switchOnClick(enabled);
|
className="mr-2"
|
||||||
}
|
alt={app.credentialOwner.name || "Credential Owner Name"}
|
||||||
setAppData("enabled", enabled);
|
size="sm"
|
||||||
}}
|
imageSrc={app.credentialOwner.avatar}
|
||||||
checked={switchChecked}
|
/>
|
||||||
|
{app.credentialOwner.name}
|
||||||
|
</div>
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{app?.isInstalled || app.credentialOwner ? (
|
||||||
|
<div className="ml-auto flex items-center">
|
||||||
|
<Switch
|
||||||
|
disabled={!app.enabled || disableSwitch}
|
||||||
|
onCheckedChange={(enabled) => {
|
||||||
|
if (switchOnClick) {
|
||||||
|
switchOnClick(enabled);
|
||||||
|
}
|
||||||
|
setAppData("enabled", enabled);
|
||||||
|
}}
|
||||||
|
checked={switchChecked}
|
||||||
|
LockedIcon={LockedIcon}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<OmniInstallAppButton
|
||||||
|
className="ml-auto flex items-center"
|
||||||
|
appId={app.slug}
|
||||||
|
returnTo={returnTo}
|
||||||
|
teamId={teamId}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
) : (
|
</div>
|
||||||
<OmniInstallAppButton
|
|
||||||
className="ml-auto flex items-center"
|
|
||||||
appId={app.slug}
|
|
||||||
returnTo={returnTo}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref={animationRef}>
|
<div ref={animationRef}>
|
||||||
|
|
|
@ -4,19 +4,22 @@ import { EventTypeAddonMap } from "@calcom/app-store/apps.browser.generated";
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { ErrorBoundary } from "@calcom/ui";
|
import { ErrorBoundary } from "@calcom/ui";
|
||||||
|
|
||||||
import type { EventTypeAppCardComponentProps } from "../types";
|
import type { EventTypeAppCardComponentProps, CredentialOwner } from "../types";
|
||||||
import { DynamicComponent } from "./DynamicComponent";
|
import { DynamicComponent } from "./DynamicComponent";
|
||||||
|
|
||||||
export const EventTypeAppCard = (props: {
|
export const EventTypeAppCard = (props: {
|
||||||
app: RouterOutputs["viewer"]["apps"][number];
|
app: RouterOutputs["viewer"]["integrations"]["items"][number] & { credentialOwner?: CredentialOwner };
|
||||||
eventType: EventTypeAppCardComponentProps["eventType"];
|
eventType: EventTypeAppCardComponentProps["eventType"];
|
||||||
getAppData: GetAppData;
|
getAppData: GetAppData;
|
||||||
setAppData: SetAppData;
|
setAppData: SetAppData;
|
||||||
|
// For event type apps, get these props from shouldLockDisableProps
|
||||||
|
LockedIcon?: JSX.Element | false;
|
||||||
|
disabled?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { app, getAppData, setAppData } = props;
|
const { app, getAppData, setAppData, LockedIcon, disabled } = props;
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary message={`There is some problem with ${app.name} App`}>
|
<ErrorBoundary message={`There is some problem with ${app.name} App`}>
|
||||||
<EventTypeAppContext.Provider value={[getAppData, setAppData]}>
|
<EventTypeAppContext.Provider value={[getAppData, setAppData, LockedIcon, disabled]}>
|
||||||
<DynamicComponent
|
<DynamicComponent
|
||||||
slug={app.slug === "stripe" ? "stripepayment" : app.slug}
|
slug={app.slug === "stripe" ? "stripepayment" : app.slug}
|
||||||
componentMap={EventTypeAddonMap}
|
componentMap={EventTypeAddonMap}
|
||||||
|
|
|
@ -16,10 +16,12 @@ export default function OmniInstallAppButton({
|
||||||
appId,
|
appId,
|
||||||
className,
|
className,
|
||||||
returnTo,
|
returnTo,
|
||||||
|
teamId,
|
||||||
}: {
|
}: {
|
||||||
appId: string;
|
appId: string;
|
||||||
className: string;
|
className: string;
|
||||||
returnTo?: string;
|
returnTo?: string;
|
||||||
|
teamId?: number;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { data: app } = useApp(appId);
|
const { data: app } = useApp(appId);
|
||||||
|
@ -30,7 +32,10 @@ export default function OmniInstallAppButton({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
//TODO: viewer.appById might be replaced with viewer.apps so that a single query needs to be invalidated.
|
//TODO: viewer.appById might be replaced with viewer.apps so that a single query needs to be invalidated.
|
||||||
utils.viewer.appById.invalidate({ appId });
|
utils.viewer.appById.invalidate({ appId });
|
||||||
utils.viewer.apps.invalidate({ extendsFeature: "EventType" });
|
utils.viewer.integrations.invalidate({
|
||||||
|
extendsFeature: "EventType",
|
||||||
|
...(teamId && { teamId }),
|
||||||
|
});
|
||||||
if (data?.setupPending) return;
|
if (data?.setupPending) return;
|
||||||
showToast(t("app_successfully_installed"), "success");
|
showToast(t("app_successfully_installed"), "success");
|
||||||
},
|
},
|
||||||
|
@ -53,7 +58,13 @@ export default function OmniInstallAppButton({
|
||||||
props = {
|
props = {
|
||||||
...props,
|
...props,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
mutation.mutate({ type: app.type, variant: app.variant, slug: app.slug, isOmniInstall: true });
|
mutation.mutate({
|
||||||
|
type: app.type,
|
||||||
|
variant: app.variant,
|
||||||
|
slug: app.slug,
|
||||||
|
isOmniInstall: true,
|
||||||
|
...(teamId && { teamId }),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import { decodeOAuthState } from "./decodeOAuthState";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is used to create app credentials for either a user or a team
|
||||||
|
*
|
||||||
|
* @param appData information about the app
|
||||||
|
* @param appData.type the app slug
|
||||||
|
* @param appData.appId the app slug
|
||||||
|
* @param key the keys for the app's credentials
|
||||||
|
* @param req the request object from the API call. Used to determine if the credential belongs to a user or a team
|
||||||
|
*/
|
||||||
|
const createOAuthAppCredential = async (
|
||||||
|
appData: { type: string; appId: string },
|
||||||
|
key: unknown,
|
||||||
|
req: NextApiRequest
|
||||||
|
) => {
|
||||||
|
const userId = req.session?.user.id;
|
||||||
|
// For OAuth flows, see if a teamId was passed through the state
|
||||||
|
const state = decodeOAuthState(req);
|
||||||
|
if (state?.teamId) {
|
||||||
|
// Check that the user belongs to the team
|
||||||
|
const team = await prisma.team.findFirst({
|
||||||
|
where: {
|
||||||
|
id: state.teamId,
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId: req.session?.user.id,
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: { id: true, members: { select: { userId: true } } },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!team) throw new Error("User does not belong to the team");
|
||||||
|
|
||||||
|
await prisma.credential.create({
|
||||||
|
data: {
|
||||||
|
type: appData.type,
|
||||||
|
key: key || {},
|
||||||
|
teamId: state.teamId,
|
||||||
|
appId: appData.appId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.credential.create({
|
||||||
|
data: {
|
||||||
|
type: appData.type,
|
||||||
|
key: key || {},
|
||||||
|
userId,
|
||||||
|
appId: appData.appId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createOAuthAppCredential;
|
|
@ -1,11 +1,28 @@
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import type { Calendar } from "@calcom/types/Calendar";
|
import type { Calendar, CalendarClass } from "@calcom/types/Calendar";
|
||||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||||
|
|
||||||
import appStore from "..";
|
import appStore from "..";
|
||||||
|
|
||||||
|
interface CalendarApp {
|
||||||
|
lib: {
|
||||||
|
CalendarService: CalendarClass;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const log = logger.getChildLogger({ prefix: ["CalendarManager"] });
|
const log = logger.getChildLogger({ prefix: ["CalendarManager"] });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [Using type predicates](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates)
|
||||||
|
*/
|
||||||
|
const isCalendarService = (x: unknown): x is CalendarApp =>
|
||||||
|
!!x &&
|
||||||
|
typeof x === "object" &&
|
||||||
|
"lib" in x &&
|
||||||
|
typeof x.lib === "object" &&
|
||||||
|
!!x.lib &&
|
||||||
|
"CalendarService" in x.lib;
|
||||||
|
|
||||||
export const getCalendar = async (credential: CredentialPayload | null): Promise<Calendar | null> => {
|
export const getCalendar = async (credential: CredentialPayload | null): Promise<Calendar | null> => {
|
||||||
if (!credential || !credential.key) return null;
|
if (!credential || !credential.key) return null;
|
||||||
let { type: calendarType } = credential;
|
let { type: calendarType } = credential;
|
||||||
|
@ -20,7 +37,8 @@ export const getCalendar = async (credential: CredentialPayload | null): Promise
|
||||||
}
|
}
|
||||||
|
|
||||||
const calendarApp = await calendarAppImportFn();
|
const calendarApp = await calendarAppImportFn();
|
||||||
if (!(calendarApp && "lib" in calendarApp && "CalendarService" in calendarApp.lib)) {
|
|
||||||
|
if (!isCalendarService(calendarApp)) {
|
||||||
log.warn(`calendar of type ${calendarType} is not implemented`);
|
log.warn(`calendar of type ${calendarType} is not implemented`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,17 +20,19 @@ export async function createDefaultInstallation({
|
||||||
userId,
|
userId,
|
||||||
slug,
|
slug,
|
||||||
key = {},
|
key = {},
|
||||||
|
teamId,
|
||||||
}: {
|
}: {
|
||||||
appType: string;
|
appType: string;
|
||||||
userId: number;
|
userId: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
key?: Prisma.InputJsonValue;
|
key?: Prisma.InputJsonValue;
|
||||||
|
teamId?: number;
|
||||||
}) {
|
}) {
|
||||||
const installation = await prisma.credential.create({
|
const installation = await prisma.credential.create({
|
||||||
data: {
|
data: {
|
||||||
type: appType,
|
type: appType,
|
||||||
key,
|
key,
|
||||||
userId,
|
...(teamId ? { teamId } : { userId }),
|
||||||
appId: slug,
|
appId: slug,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -31,10 +31,11 @@ function useAddAppMutation(_type: App["type"] | null, allOptions?: UseAddAppMuta
|
||||||
const mutation = useMutation<
|
const mutation = useMutation<
|
||||||
AddAppMutationData,
|
AddAppMutationData,
|
||||||
Error,
|
Error,
|
||||||
{ type?: App["type"]; variant?: string; slug?: string; isOmniInstall?: boolean } | ""
|
{ type?: App["type"]; variant?: string; slug?: string; isOmniInstall?: boolean; teamId?: number } | ""
|
||||||
>(async (variables) => {
|
>(async (variables) => {
|
||||||
let type: string | null | undefined;
|
let type: string | null | undefined;
|
||||||
let isOmniInstall;
|
let isOmniInstall;
|
||||||
|
const teamId = variables && variables.teamId ? variables.teamId : undefined;
|
||||||
if (variables === "") {
|
if (variables === "") {
|
||||||
type = _type;
|
type = _type;
|
||||||
} else {
|
} else {
|
||||||
|
@ -57,9 +58,11 @@ function useAddAppMutation(_type: App["type"] | null, allOptions?: UseAddAppMuta
|
||||||
location.search
|
location.search
|
||||||
),
|
),
|
||||||
...(type === "google_calendar" && { installGoogleVideo: options?.installGoogleVideo }),
|
...(type === "google_calendar" && { installGoogleVideo: options?.installGoogleVideo }),
|
||||||
|
...(teamId && { teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateStr = encodeURIComponent(JSON.stringify(state));
|
const stateStr = encodeURIComponent(JSON.stringify(state));
|
||||||
const searchParams = `?state=${stateStr}`;
|
const searchParams = `?state=${stateStr}${teamId ? `&teamId=${teamId}` : ""}`;
|
||||||
|
|
||||||
const res = await fetch(`/api/integrations/${type}/add` + searchParams);
|
const res = await fetch(`/api/integrations/${type}/add` + searchParams);
|
||||||
|
|
||||||
|
@ -70,7 +73,6 @@ function useAddAppMutation(_type: App["type"] | null, allOptions?: UseAddAppMuta
|
||||||
|
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
const externalUrl = /https?:\/\//.test(json.url) && !json.url.startsWith(window.location.origin);
|
const externalUrl = /https?:\/\//.test(json.url) && !json.url.startsWith(window.location.origin);
|
||||||
|
|
||||||
if (!isOmniInstall) {
|
if (!isOmniInstall) {
|
||||||
gotoUrl(json.url, json.newTab);
|
gotoUrl(json.url, json.newTab);
|
||||||
return { setupPending: externalUrl || json.url.endsWith("/setup") };
|
return { setupPending: externalUrl || json.url.endsWith("/setup") };
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||||
|
|
||||||
|
import type { EventTypeAppCardApp } from "../types";
|
||||||
|
|
||||||
|
function useIsAppEnabled(app: EventTypeAppCardApp) {
|
||||||
|
const [getAppData, setAppData] = useAppContextWithSchema();
|
||||||
|
const [enabled, setEnabled] = useState(() => {
|
||||||
|
if (!app.credentialOwner) {
|
||||||
|
return getAppData("enabled");
|
||||||
|
}
|
||||||
|
const credentialId = getAppData("credentialId");
|
||||||
|
const isAppEnabledForCredential =
|
||||||
|
app.userCredentialIds.some((id) => id === credentialId) ||
|
||||||
|
app.credentialOwner.credentialId === credentialId;
|
||||||
|
return isAppEnabledForCredential;
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateEnabled = (newValue: boolean) => {
|
||||||
|
if (!newValue) {
|
||||||
|
setAppData("credentialId", undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newValue && (app.userCredentialIds?.length || app.credentialOwner?.credentialId)) {
|
||||||
|
setAppData("credentialId", app.credentialOwner?.credentialId || app.userCredentialIds[0]);
|
||||||
|
}
|
||||||
|
setEnabled(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { enabled, updateEnabled };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useIsAppEnabled;
|
|
@ -13,8 +13,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
newTab: true,
|
newTab: true,
|
||||||
url: "https://amie.so/signup",
|
url: "https://amie.so/signup",
|
||||||
},
|
},
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -12,6 +12,7 @@ export async function getHandler(req: NextApiRequest) {
|
||||||
const slug = appConfig.slug;
|
const slug = appConfig.slug;
|
||||||
const variant = appConfig.variant;
|
const variant = appConfig.variant;
|
||||||
const appType = appConfig.type;
|
const appType = appConfig.type;
|
||||||
|
const teamId = req.query.teamId ? Number(req.query.teamId) : undefined;
|
||||||
|
|
||||||
await checkInstalled(slug, session.user.id);
|
await checkInstalled(slug, session.user.id);
|
||||||
await createDefaultInstallation({
|
await createDefaultInstallation({
|
||||||
|
@ -19,6 +20,7 @@ export async function getHandler(req: NextApiRequest) {
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
slug,
|
slug,
|
||||||
key: {},
|
key: {},
|
||||||
|
teamId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { url: getInstalledAppPath({ variant, slug }) };
|
return { url: getInstalledAppPath({ variant, slug }) };
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -13,8 +13,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
newTab: true,
|
newTab: true,
|
||||||
url: "https://cron.com",
|
url: "https://cron.com",
|
||||||
},
|
},
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -62,6 +62,7 @@ export const FAKE_DAILY_CREDENTIAL: CredentialPayload & { invalid: boolean } = {
|
||||||
userId: +new Date().getTime(),
|
userId: +new Date().getTime(),
|
||||||
appId: "daily-video",
|
appId: "daily-video",
|
||||||
invalid: false,
|
invalid: false,
|
||||||
|
teamId: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
|
export const fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { z } from "zod";
|
||||||
|
|
||||||
export const eventTypeAppCardZod = z.object({
|
export const eventTypeAppCardZod = z.object({
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
|
credentialId: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const appKeysSchema = z.object({});
|
export const appKeysSchema = z.object({});
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -1,32 +1,31 @@
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||||
|
import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled";
|
||||||
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||||
import { TextField } from "@calcom/ui";
|
import { TextField } from "@calcom/ui";
|
||||||
|
|
||||||
import type { appDataSchema } from "../zod";
|
import type { appDataSchema } from "../zod";
|
||||||
|
|
||||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app }) {
|
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
||||||
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>();
|
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>();
|
||||||
const trackingId = getAppData("trackingId");
|
const trackingId = getAppData("trackingId");
|
||||||
const [enabled, setEnabled] = useState(getAppData("enabled"));
|
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppCard
|
<AppCard
|
||||||
setAppData={setAppData}
|
setAppData={setAppData}
|
||||||
app={app}
|
app={app}
|
||||||
|
disableSwitch={disabled}
|
||||||
|
LockedIcon={LockedIcon}
|
||||||
switchOnClick={(e) => {
|
switchOnClick={(e) => {
|
||||||
if (!e) {
|
updateEnabled(e);
|
||||||
setEnabled(false);
|
|
||||||
} else {
|
|
||||||
setEnabled(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
switchChecked={enabled}>
|
switchChecked={enabled}
|
||||||
|
teamId={eventType.team?.id || undefined}>
|
||||||
<TextField
|
<TextField
|
||||||
name="Tracking ID"
|
name="Tracking ID"
|
||||||
value={trackingId}
|
value={trackingId}
|
||||||
|
disabled={disabled}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setAppData("trackingId", e.target.value);
|
setAppData("trackingId", e.target.value);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -1,32 +1,31 @@
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||||
|
import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled";
|
||||||
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||||
import { TextField } from "@calcom/ui";
|
import { TextField } from "@calcom/ui";
|
||||||
|
|
||||||
import type { appDataSchema } from "../zod";
|
import type { appDataSchema } from "../zod";
|
||||||
|
|
||||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app }) {
|
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
||||||
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>();
|
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>();
|
||||||
const trackingId = getAppData("trackingId");
|
const trackingId = getAppData("trackingId");
|
||||||
const [enabled, setEnabled] = useState(getAppData("enabled"));
|
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppCard
|
<AppCard
|
||||||
setAppData={setAppData}
|
setAppData={setAppData}
|
||||||
app={app}
|
app={app}
|
||||||
|
disableSwitch={disabled}
|
||||||
|
LockedIcon={LockedIcon}
|
||||||
switchOnClick={(e) => {
|
switchOnClick={(e) => {
|
||||||
if (!e) {
|
updateEnabled(e);
|
||||||
setEnabled(false);
|
|
||||||
} else {
|
|
||||||
setEnabled(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
switchChecked={enabled}>
|
switchChecked={enabled}
|
||||||
|
teamId={eventType.team?.id || undefined}>
|
||||||
<TextField
|
<TextField
|
||||||
name="Tracking ID"
|
name="Tracking ID"
|
||||||
value={trackingId}
|
value={trackingId}
|
||||||
|
disabled={disabled}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setAppData("trackingId", e.target.value);
|
setAppData("trackingId", e.target.value);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -14,11 +14,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
return res.status(401).json({ message: "You must be logged in to do this" });
|
return res.status(401).json({ message: "You must be logged in to do this" });
|
||||||
}
|
}
|
||||||
const appType = "giphy_other";
|
const appType = "giphy_other";
|
||||||
|
const credentialOwner = req.query.teamId
|
||||||
|
? { teamId: Number(req.query.teamId) }
|
||||||
|
: { userId: req.session.user.id };
|
||||||
try {
|
try {
|
||||||
const alreadyInstalled = await prisma.credential.findFirst({
|
const alreadyInstalled = await prisma.credential.findFirst({
|
||||||
where: {
|
where: {
|
||||||
type: appType,
|
type: appType,
|
||||||
userId: req.session.user.id,
|
...credentialOwner,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (alreadyInstalled) {
|
if (alreadyInstalled) {
|
||||||
|
@ -28,7 +31,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
data: {
|
data: {
|
||||||
type: appType,
|
type: appType,
|
||||||
key: {},
|
key: {},
|
||||||
userId: req.session.user.id,
|
...credentialOwner,
|
||||||
appId: "giphy",
|
appId: "giphy",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||||
|
import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled";
|
||||||
import { SelectGifInput } from "@calcom/app-store/giphy/components";
|
import { SelectGifInput } from "@calcom/app-store/giphy/components";
|
||||||
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|
||||||
import type { appDataSchema } from "../zod";
|
import type { appDataSchema } from "../zod";
|
||||||
|
|
||||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app }) {
|
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
||||||
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>();
|
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>();
|
||||||
const thankYouPage = getAppData("thankYouPage");
|
const thankYouPage = getAppData("thankYouPage");
|
||||||
const [showGifSelection, setShowGifSelection] = useState(getAppData("enabled"));
|
const { enabled: showGifSelection, updateEnabled: setShowGifSelection } = useIsAppEnabled(app);
|
||||||
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -19,13 +19,17 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||||
setAppData={setAppData}
|
setAppData={setAppData}
|
||||||
app={app}
|
app={app}
|
||||||
description={t("confirmation_page_gif")}
|
description={t("confirmation_page_gif")}
|
||||||
|
disableSwitch={disabled}
|
||||||
|
LockedIcon={LockedIcon}
|
||||||
switchOnClick={(e) => {
|
switchOnClick={(e) => {
|
||||||
setShowGifSelection(e);
|
setShowGifSelection(e);
|
||||||
}}
|
}}
|
||||||
switchChecked={showGifSelection}>
|
switchChecked={showGifSelection}
|
||||||
|
teamId={eventType.team?.id || undefined}>
|
||||||
{showGifSelection && (
|
{showGifSelection && (
|
||||||
<SelectGifInput
|
<SelectGifInput
|
||||||
defaultValue={thankYouPage}
|
defaultValue={thankYouPage}
|
||||||
|
disabled={disabled}
|
||||||
onChange={(url: string) => {
|
onChange={(url: string) => {
|
||||||
setAppData("thankYouPage", url);
|
setAppData("thankYouPage", url);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { SearchDialog } from "./SearchDialog";
|
||||||
interface ISelectGifInput {
|
interface ISelectGifInput {
|
||||||
defaultValue?: string | null;
|
defaultValue?: string | null;
|
||||||
onChange: (url: string) => void;
|
onChange: (url: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
export default function SelectGifInput(props: ISelectGifInput) {
|
export default function SelectGifInput(props: ISelectGifInput) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
@ -24,11 +25,21 @@ export default function SelectGifInput(props: ISelectGifInput) {
|
||||||
)}
|
)}
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
{selectedGif ? (
|
{selectedGif ? (
|
||||||
<Button color="minimal" type="button" StartIcon={Edit} onClick={() => setShowDialog(true)}>
|
<Button
|
||||||
|
color="minimal"
|
||||||
|
type="button"
|
||||||
|
StartIcon={Edit}
|
||||||
|
onClick={() => setShowDialog(true)}
|
||||||
|
disabled={props.disabled}>
|
||||||
Change
|
Change
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button color="minimal" type="button" StartIcon={Plus} onClick={() => setShowDialog(true)}>
|
<Button
|
||||||
|
color="minimal"
|
||||||
|
type="button"
|
||||||
|
StartIcon={Plus}
|
||||||
|
onClick={() => setShowDialog(true)}
|
||||||
|
disabled={props.disabled}>
|
||||||
Add from Giphy
|
Add from Giphy
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
@ -41,7 +52,8 @@ export default function SelectGifInput(props: ISelectGifInput) {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedGif("");
|
setSelectedGif("");
|
||||||
props.onChange("");
|
props.onChange("");
|
||||||
}}>
|
}}
|
||||||
|
disabled={props.disabled}>
|
||||||
{t("remove")}
|
{t("remove")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -1,32 +1,31 @@
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||||
|
import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled";
|
||||||
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||||
import { TextField } from "@calcom/ui";
|
import { TextField } from "@calcom/ui";
|
||||||
|
|
||||||
import type { appDataSchema } from "../zod";
|
import type { appDataSchema } from "../zod";
|
||||||
|
|
||||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app }) {
|
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
||||||
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>();
|
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>();
|
||||||
const trackingId = getAppData("trackingId");
|
const trackingId = getAppData("trackingId");
|
||||||
const [enabled, setEnabled] = useState(getAppData("enabled"));
|
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppCard
|
<AppCard
|
||||||
setAppData={setAppData}
|
setAppData={setAppData}
|
||||||
app={app}
|
app={app}
|
||||||
|
disableSwitch={disabled}
|
||||||
|
LockedIcon={LockedIcon}
|
||||||
switchOnClick={(e) => {
|
switchOnClick={(e) => {
|
||||||
if (!e) {
|
updateEnabled(e);
|
||||||
setEnabled(false);
|
|
||||||
} else {
|
|
||||||
setEnabled(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
switchChecked={enabled}>
|
switchChecked={enabled}
|
||||||
|
teamId={eventType.team?.id || undefined}>
|
||||||
<TextField
|
<TextField
|
||||||
name="Tracking ID"
|
name="Tracking ID"
|
||||||
value={trackingId}
|
value={trackingId}
|
||||||
|
disabled={disabled}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setAppData("trackingId", e.target.value);
|
setAppData("trackingId", e.target.value);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -4,8 +4,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
|
|
||||||
|
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
|
||||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||||
|
@ -46,15 +46,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
|
|
||||||
// set expiry date as offset from current time.
|
// set expiry date as offset from current time.
|
||||||
hubspotToken.expiryDate = Math.round(Date.now() + hubspotToken.expiresIn * 1000);
|
hubspotToken.expiryDate = Math.round(Date.now() + hubspotToken.expiresIn * 1000);
|
||||||
await prisma.credential.create({
|
|
||||||
data: {
|
createOAuthAppCredential({ appId: "hubspot", type: "hubspot_other_calendar" }, hubspotToken as any, req);
|
||||||
type: "hubspot_other_calendar",
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
key: hubspotToken as any,
|
|
||||||
userId: req.session.user.id,
|
|
||||||
appId: "hubspot",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const state = decodeOAuthState(req);
|
const state = decodeOAuthState(req);
|
||||||
res.redirect(
|
res.redirect(
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const appStore = {
|
const appStore = {
|
||||||
// example: () => import("./example"),
|
// example: () => import("./example"),
|
||||||
applecalendar: () => import("./applecalendar"),
|
applecalendar: () => import("./applecalendar"),
|
||||||
|
aroundvideo: () => import("./around"),
|
||||||
caldavcalendar: () => import("./caldavcalendar"),
|
caldavcalendar: () => import("./caldavcalendar"),
|
||||||
closecom: () => import("./closecom"),
|
closecom: () => import("./closecom"),
|
||||||
dailyvideo: () => import("./dailyvideo"),
|
dailyvideo: () => import("./dailyvideo"),
|
||||||
|
|
|
@ -165,6 +165,7 @@ export type LocationObject = {
|
||||||
type: string;
|
type: string;
|
||||||
address?: string;
|
address?: string;
|
||||||
displayLocationPublicly?: boolean;
|
displayLocationPublicly?: boolean;
|
||||||
|
credentialId?: number;
|
||||||
} & Partial<
|
} & Partial<
|
||||||
Record<"address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "hostDefault" | "phone", string>
|
Record<"address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "hostDefault" | "phone", string>
|
||||||
>;
|
>;
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -1,22 +1,29 @@
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||||
|
import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled";
|
||||||
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||||
import { TextField } from "@calcom/ui";
|
import { TextField } from "@calcom/ui";
|
||||||
|
|
||||||
import type { appDataSchema } from "../zod";
|
import type { appDataSchema } from "../zod";
|
||||||
|
|
||||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app }) {
|
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
||||||
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>();
|
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>();
|
||||||
const trackingId = getAppData("trackingId");
|
const trackingId = getAppData("trackingId");
|
||||||
const [enabled, setEnabled] = useState(getAppData("enabled"));
|
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppCard setAppData={setAppData} app={app} switchOnClick={setEnabled} switchChecked={enabled}>
|
<AppCard
|
||||||
|
setAppData={setAppData}
|
||||||
|
app={app}
|
||||||
|
disableSwitch={disabled}
|
||||||
|
LockedIcon={LockedIcon}
|
||||||
|
switchOnClick={updateEnabled}
|
||||||
|
switchChecked={enabled}
|
||||||
|
teamId={eventType.team?.id || undefined}>
|
||||||
<TextField
|
<TextField
|
||||||
name="Pixel ID"
|
name="Pixel ID"
|
||||||
value={trackingId}
|
value={trackingId}
|
||||||
|
disabled={disabled}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setAppData("trackingId", e.target.value);
|
setAppData("trackingId", e.target.value);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -13,8 +13,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
url: "https://n8n.io/integrations/cal-trigger/",
|
url: "https://n8n.io/integrations/cal-trigger/",
|
||||||
newTab: true,
|
newTab: true,
|
||||||
},
|
},
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
|
||||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||||
|
@ -92,14 +93,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
await prisma.credential.deleteMany({ where: { id: { in: credentialIdsToDelete }, userId } });
|
await prisma.credential.deleteMany({ where: { id: { in: credentialIdsToDelete }, userId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.credential.create({
|
createOAuthAppCredential({ appId: "msteams", type: "office365_video" }, responseBody, req);
|
||||||
data: {
|
|
||||||
type: "office365_video",
|
|
||||||
key: responseBody,
|
|
||||||
userId,
|
|
||||||
appId: "msteams",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const state = decodeOAuthState(req);
|
const state = decodeOAuthState(req);
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
|
|
|
@ -13,8 +13,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
newTab: true,
|
newTab: true,
|
||||||
url: "https://pipedream.com/apps/cal-com",
|
url: "https://pipedream.com/apps/cal-com",
|
||||||
},
|
},
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -1,40 +1,40 @@
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||||
|
import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled";
|
||||||
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||||
import { TextField } from "@calcom/ui";
|
import { TextField } from "@calcom/ui";
|
||||||
|
|
||||||
import type { appDataSchema } from "../zod";
|
import type { appDataSchema } from "../zod";
|
||||||
|
|
||||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app }) {
|
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
||||||
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>();
|
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>();
|
||||||
const plausibleUrl = getAppData("PLAUSIBLE_URL");
|
const plausibleUrl = getAppData("PLAUSIBLE_URL");
|
||||||
const trackingId = getAppData("trackingId");
|
const trackingId = getAppData("trackingId");
|
||||||
const [enabled, setEnabled] = useState(getAppData("enabled"));
|
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppCard
|
<AppCard
|
||||||
setAppData={setAppData}
|
setAppData={setAppData}
|
||||||
app={app}
|
app={app}
|
||||||
|
disableSwitch={disabled}
|
||||||
|
LockedIcon={LockedIcon}
|
||||||
switchOnClick={(e) => {
|
switchOnClick={(e) => {
|
||||||
if (!e) {
|
updateEnabled(e);
|
||||||
setEnabled(false);
|
|
||||||
} else {
|
|
||||||
setEnabled(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
switchChecked={enabled}>
|
switchChecked={enabled}
|
||||||
|
teamId={eventType.team?.id || undefined}>
|
||||||
<TextField
|
<TextField
|
||||||
name="Plausible URL"
|
name="Plausible URL"
|
||||||
defaultValue="https://plausible.io/js/script.js"
|
defaultValue="https://plausible.io/js/script.js"
|
||||||
placeholder="https://plausible.io/js/script.js"
|
placeholder="https://plausible.io/js/script.js"
|
||||||
value={plausibleUrl}
|
value={plausibleUrl}
|
||||||
|
disabled={disabled}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setAppData("PLAUSIBLE_URL", e.target.value);
|
setAppData("PLAUSIBLE_URL", e.target.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
|
disabled={disabled}
|
||||||
name="Tracked Domain"
|
name="Tracked Domain"
|
||||||
placeholder="yourdomain.com"
|
placeholder="yourdomain.com"
|
||||||
value={trackingId}
|
value={trackingId}
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { useState } from "react";
|
||||||
|
|
||||||
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||||
|
import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled";
|
||||||
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Tooltip, TextField } from "@calcom/ui";
|
import { Tooltip, TextField } from "@calcom/ui";
|
||||||
|
@ -10,9 +11,9 @@ import type { appDataSchema } from "../zod";
|
||||||
|
|
||||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ eventType, app }) {
|
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ eventType, app }) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>();
|
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>();
|
||||||
const [enabled, setEnabled] = useState(getAppData("enabled"));
|
|
||||||
const [additionalParameters, setAdditionalParameters] = useState("");
|
const [additionalParameters, setAdditionalParameters] = useState("");
|
||||||
|
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||||
|
|
||||||
const query = additionalParameters !== "" ? `?${additionalParameters}` : "";
|
const query = additionalParameters !== "" ? `?${additionalParameters}` : "";
|
||||||
const eventTypeURL = eventType.URL + query;
|
const eventTypeURL = eventType.URL + query;
|
||||||
|
@ -38,18 +39,18 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||||
<AppCard
|
<AppCard
|
||||||
setAppData={setAppData}
|
setAppData={setAppData}
|
||||||
app={app}
|
app={app}
|
||||||
|
disableSwitch={disabled}
|
||||||
|
LockedIcon={LockedIcon}
|
||||||
switchOnClick={(e) => {
|
switchOnClick={(e) => {
|
||||||
if (!e) {
|
updateEnabled(e);
|
||||||
setEnabled(false);
|
|
||||||
} else {
|
|
||||||
setEnabled(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
switchChecked={enabled}>
|
switchChecked={enabled}
|
||||||
|
teamId={eventType.team?.id || undefined}>
|
||||||
<div className="flex w-full flex-col gap-2 text-sm">
|
<div className="flex w-full flex-col gap-2 text-sm">
|
||||||
<div className="flex w-full">
|
<div className="flex w-full">
|
||||||
<TextField
|
<TextField
|
||||||
name="hello"
|
name="hello"
|
||||||
|
disabled={disabled}
|
||||||
value={additionalParameters}
|
value={additionalParameters}
|
||||||
onChange={(e) => setAdditionalParameters(e.target.value)}
|
onChange={(e) => setAdditionalParameters(e.target.value)}
|
||||||
label={t("additional_url_parameters")}
|
label={t("additional_url_parameters")}
|
||||||
|
|
|
@ -12,8 +12,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
redirect: {
|
redirect: {
|
||||||
url: "raycast://extensions/eluce2/cal-com-share-meeting-links?source=webstore",
|
url: "raycast://extensions/eluce2/cal-com-share-meeting-links?source=webstore",
|
||||||
},
|
},
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -9,12 +9,12 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: async ({ user, appType, slug }) => {
|
createCredential: async ({ user, appType, slug, teamId }) => {
|
||||||
return await prisma.credential.create({
|
return await prisma.credential.create({
|
||||||
data: {
|
data: {
|
||||||
type: appType,
|
type: appType,
|
||||||
key: {},
|
key: {},
|
||||||
userId: user.id,
|
...(teamId ? { teamId } : { userId: user.id }),
|
||||||
appId: slug,
|
appId: slug,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { v4 as uuidv4 } from "uuid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import getOrgAwareUrlOnClient from "@calcom/lib/getOrgAwareUrl";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { ButtonProps } from "@calcom/ui";
|
import type { ButtonProps } from "@calcom/ui";
|
||||||
|
@ -195,7 +195,6 @@ function Dialogs({
|
||||||
deleteDialogFormId: string | null;
|
deleteDialogFormId: string | null;
|
||||||
}) {
|
}) {
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const router = useRouter();
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const deleteMutation = trpc.viewer.appRoutingForms.deleteForm.useMutation({
|
const deleteMutation = trpc.viewer.appRoutingForms.deleteForm.useMutation({
|
||||||
onMutate: async ({ id: formId }) => {
|
onMutate: async ({ id: formId }) => {
|
||||||
|
@ -402,11 +401,11 @@ export const FormAction = forwardRef(function FormAction<T extends typeof Button
|
||||||
const dropdownCtxValue = useContext(dropdownCtx);
|
const dropdownCtxValue = useContext(dropdownCtx);
|
||||||
const dropdown = dropdownCtxValue?.dropdown;
|
const dropdown = dropdownCtxValue?.dropdown;
|
||||||
const embedLink = `forms/${routingForm?.id}`;
|
const embedLink = `forms/${routingForm?.id}`;
|
||||||
const formLink = `${CAL_URL}/${embedLink}`;
|
const formRelativeLink = `/${embedLink}`;
|
||||||
let redirectUrl = `${CAL_URL}/router?form=${routingForm?.id}`;
|
let relativeRedirectUrl = `/router?form=${routingForm?.id}`;
|
||||||
|
|
||||||
routingForm?.fields?.forEach((field) => {
|
routingForm?.fields?.forEach((field) => {
|
||||||
redirectUrl += `&${getFieldIdentifier(field)}={Recalled_Response_For_This_Field}`;
|
relativeRedirectUrl += `&${getFieldIdentifier(field)}={Recalled_Response_For_This_Field}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
@ -416,12 +415,12 @@ export const FormAction = forwardRef(function FormAction<T extends typeof Button
|
||||||
ButtonProps & { as?: React.ElementType; render?: FormActionProps<unknown>["render"] }
|
ButtonProps & { as?: React.ElementType; render?: FormActionProps<unknown>["render"] }
|
||||||
> = {
|
> = {
|
||||||
preview: {
|
preview: {
|
||||||
href: formLink,
|
href: formRelativeLink,
|
||||||
},
|
},
|
||||||
copyLink: {
|
copyLink: {
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
showToast(t("link_copied"), "success");
|
showToast(t("link_copied"), "success");
|
||||||
navigator.clipboard.writeText(formLink);
|
navigator.clipboard.writeText(getOrgAwareUrlOnClient(formRelativeLink));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
duplicate: {
|
duplicate: {
|
||||||
|
@ -448,7 +447,7 @@ export const FormAction = forwardRef(function FormAction<T extends typeof Button
|
||||||
},
|
},
|
||||||
copyRedirectUrl: {
|
copyRedirectUrl: {
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
navigator.clipboard.writeText(redirectUrl);
|
navigator.clipboard.writeText(getOrgAwareUrlOnClient(relativeRedirectUrl));
|
||||||
showToast(t("typeform_redirect_url_copied"), "success");
|
showToast(t("typeform_redirect_url_copied"), "success");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -186,6 +186,7 @@ test.describe("Routing Forms", () => {
|
||||||
// Install app
|
// Install app
|
||||||
await page.goto(`/apps/routing-forms`);
|
await page.goto(`/apps/routing-forms`);
|
||||||
await page.click('[data-testid="install-app-button"]');
|
await page.click('[data-testid="install-app-button"]');
|
||||||
|
(await page.waitForSelector('[data-testid="install-app-button-personal"]')).click();
|
||||||
await page.waitForURL((url) => url.pathname === `/apps/routing-forms/forms`);
|
await page.waitForURL((url) => url.pathname === `/apps/routing-forms/forms`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -217,6 +218,7 @@ test.describe("Routing Forms", () => {
|
||||||
// Install app
|
// Install app
|
||||||
await page.goto(`/apps/routing-forms`);
|
await page.goto(`/apps/routing-forms`);
|
||||||
await page.click('[data-testid="install-app-button"]');
|
await page.click('[data-testid="install-app-button"]');
|
||||||
|
(await page.waitForSelector('[data-testid="install-app-button-personal"]')).click();
|
||||||
await page.waitForURL((url) => url.pathname === `/apps/routing-forms/forms`);
|
await page.waitForURL((url) => url.pathname === `/apps/routing-forms/forms`);
|
||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,8 +3,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
|
|
||||||
|
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
|
||||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||||
|
@ -38,15 +38,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
|
|
||||||
const salesforceTokenInfo = await conn.oauth2.requestToken(code as string);
|
const salesforceTokenInfo = await conn.oauth2.requestToken(code as string);
|
||||||
|
|
||||||
await prisma.credential.create({
|
createOAuthAppCredential(
|
||||||
data: {
|
{ appId: "salesforce", type: "salesforce_other_calendar" },
|
||||||
type: "salesforce_other_calendar",
|
salesforceTokenInfo as any,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
req
|
||||||
key: salesforceTokenInfo as any,
|
);
|
||||||
userId: req.session.user.id,
|
|
||||||
appId: "salesforce",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const state = decodeOAuthState(req);
|
const state = decodeOAuthState(req);
|
||||||
res.redirect(
|
res.redirect(
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -2,8 +2,8 @@ import type { Prisma } from "@prisma/client";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { stringify } from "querystring";
|
import { stringify } from "querystring";
|
||||||
|
|
||||||
import prisma from "@calcom/prisma";
|
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
|
||||||
|
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||||
import type { StripeData } from "../lib/server";
|
import type { StripeData } from "../lib/server";
|
||||||
import stripe from "../lib/server";
|
import stripe from "../lib/server";
|
||||||
|
@ -31,6 +31,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
return res.status(401).json({ message: "You must be logged in to do this" });
|
return res.status(401).json({ message: "You must be logged in to do this" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const state = decodeOAuthState(req);
|
||||||
|
|
||||||
const response = await stripe.oauth.token({
|
const response = await stripe.oauth.token({
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code: code?.toString(),
|
code: code?.toString(),
|
||||||
|
@ -42,14 +44,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
data["default_currency"] = account.default_currency;
|
data["default_currency"] = account.default_currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.credential.create({
|
createOAuthAppCredential(
|
||||||
data: {
|
{ appId: "stripe", type: "stripe_payment" },
|
||||||
type: "stripe_payment",
|
data as unknown as Prisma.InputJsonObject,
|
||||||
key: data as unknown as Prisma.InputJsonObject,
|
req
|
||||||
userId: req.session.user.id,
|
);
|
||||||
appId: "stripe",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const returnTo = getReturnToValueFromQueryState(req);
|
const returnTo = getReturnToValueFromQueryState(req);
|
||||||
res.redirect(returnTo || getInstalledAppPath({ variant: "payment", slug: "stripe" }));
|
res.redirect(returnTo || getInstalledAppPath({ variant: "payment", slug: "stripe" }));
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||||
|
import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled";
|
||||||
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
@ -15,12 +15,13 @@ type Option = { value: string; label: string };
|
||||||
|
|
||||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
||||||
const { asPath } = useRouter();
|
const { asPath } = useRouter();
|
||||||
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>();
|
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>();
|
||||||
const price = getAppData("price");
|
const price = getAppData("price");
|
||||||
const currency = getAppData("currency");
|
const currency = getAppData("currency");
|
||||||
const paymentOption = getAppData("paymentOption");
|
const paymentOption = getAppData("paymentOption");
|
||||||
const paymentOptionSelectValue = paymentOptions.find((option) => paymentOption === option.value);
|
const paymentOptionSelectValue = paymentOptions.find((option) => paymentOption === option.value);
|
||||||
const [requirePayment, setRequirePayment] = useState(getAppData("enabled"));
|
const { enabled: requirePayment, updateEnabled: setRequirePayment } = useIsAppEnabled(app);
|
||||||
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const recurringEventDefined = eventType.recurringEvent?.count !== undefined;
|
const recurringEventDefined = eventType.recurringEvent?.count !== undefined;
|
||||||
const seatsEnabled = !!eventType.seatsPerTimeSlot;
|
const seatsEnabled = !!eventType.seatsPerTimeSlot;
|
||||||
|
@ -39,6 +40,8 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||||
returnTo={WEBAPP_URL + asPath}
|
returnTo={WEBAPP_URL + asPath}
|
||||||
setAppData={setAppData}
|
setAppData={setAppData}
|
||||||
app={app}
|
app={app}
|
||||||
|
disableSwitch={disabled}
|
||||||
|
LockedIcon={LockedIcon}
|
||||||
switchChecked={requirePayment}
|
switchChecked={requirePayment}
|
||||||
switchOnClick={(enabled) => {
|
switchOnClick={(enabled) => {
|
||||||
setRequirePayment(enabled);
|
setRequirePayment(enabled);
|
||||||
|
@ -55,48 +58,48 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||||
</>
|
</>
|
||||||
}>
|
}>
|
||||||
<>
|
<>
|
||||||
{recurringEventDefined ? (
|
{recurringEventDefined && (
|
||||||
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
|
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
|
||||||
) : (
|
)}
|
||||||
requirePayment && (
|
{!recurringEventDefined && requirePayment && (
|
||||||
<>
|
<>
|
||||||
<div className="mt-2 block items-center justify-start sm:flex sm:space-x-2">
|
<div className="mt-2 block items-center justify-start sm:flex sm:space-x-2">
|
||||||
<TextField
|
<TextField
|
||||||
label=""
|
label=""
|
||||||
className="h-[38px]"
|
className="h-[38px]"
|
||||||
addOnLeading={<>{currency ? getCurrencySymbol("en", currency) : ""}</>}
|
addOnLeading={<>{currency ? getCurrencySymbol("en", currency) : ""}</>}
|
||||||
addOnClassname="h-[38px]"
|
addOnClassname="h-[38px]"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
min="0.5"
|
min="0.5"
|
||||||
type="number"
|
type="number"
|
||||||
required
|
required
|
||||||
placeholder="Price"
|
placeholder="Price"
|
||||||
onChange={(e) => {
|
disabled={disabled}
|
||||||
setAppData("price", Number(e.target.value) * 100);
|
onChange={(e) => {
|
||||||
}}
|
setAppData("price", Number(e.target.value) * 100);
|
||||||
value={price > 0 ? price / 100 : undefined}
|
}}
|
||||||
/>
|
value={price > 0 ? price / 100 : undefined}
|
||||||
<Select<Option>
|
/>
|
||||||
defaultValue={
|
<Select<Option>
|
||||||
paymentOptionSelectValue
|
defaultValue={
|
||||||
? { ...paymentOptionSelectValue, label: t(paymentOptionSelectValue.label) }
|
paymentOptionSelectValue
|
||||||
: { ...paymentOptions[0], label: t(paymentOptions[0].label) }
|
? { ...paymentOptionSelectValue, label: t(paymentOptionSelectValue.label) }
|
||||||
}
|
: { ...paymentOptions[0], label: t(paymentOptions[0].label) }
|
||||||
options={paymentOptions.map((option) => {
|
}
|
||||||
return { ...option, label: t(option.label) || option.label };
|
options={paymentOptions.map((option) => {
|
||||||
})}
|
return { ...option, label: t(option.label) || option.label };
|
||||||
onChange={(input) => {
|
})}
|
||||||
if (input) setAppData("paymentOption", input.value);
|
onChange={(input) => {
|
||||||
}}
|
if (input) setAppData("paymentOption", input.value);
|
||||||
className="mb-1 h-[38px] w-full"
|
}}
|
||||||
isDisabled={seatsEnabled}
|
className="mb-1 h-[38px] w-full"
|
||||||
/>
|
isDisabled={seatsEnabled || disabled}
|
||||||
</div>
|
/>
|
||||||
{seatsEnabled && paymentOption === "HOLD" && (
|
</div>
|
||||||
<Alert className="mt-2" severity="warning" title={t("seats_and_no_show_fee_error")} />
|
{seatsEnabled && paymentOption === "HOLD" && (
|
||||||
)}
|
<Alert className="mt-2" severity="warning" title={t("seats_and_no_show_fee_error")} />
|
||||||
</>
|
)}
|
||||||
)
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
|
||||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||||
|
|
||||||
|
@ -59,21 +60,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
responseBody.expiry_date = Math.round(Date.now() + responseBody.expires_in * 1000);
|
responseBody.expiry_date = Math.round(Date.now() + responseBody.expires_in * 1000);
|
||||||
delete responseBody.expires_in;
|
delete responseBody.expires_in;
|
||||||
|
|
||||||
await prisma.user.update({
|
createOAuthAppCredential({ appId: "tandem", type: "tandem_video" }, responseBody, req);
|
||||||
where: {
|
|
||||||
id: req.session?.user.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
credentials: {
|
|
||||||
create: {
|
|
||||||
type: "tandem_video",
|
|
||||||
key: responseBody,
|
|
||||||
appId: "tandem",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.redirect(getInstalledAppPath({ variant: "conferencing", slug: "tandem" }));
|
res.redirect(getInstalledAppPath({ variant: "conferencing", slug: "tandem" }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { TextField } from "@calcom/ui";
|
||||||
|
|
||||||
import type { appDataSchema } from "../zod";
|
import type { appDataSchema } from "../zod";
|
||||||
|
|
||||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app }) {
|
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
||||||
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>();
|
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>();
|
||||||
const trackingId = getAppData("trackingId");
|
const trackingId = getAppData("trackingId");
|
||||||
const [enabled, setEnabled] = useState(getAppData("enabled"));
|
const [enabled, setEnabled] = useState(getAppData("enabled"));
|
||||||
|
@ -23,7 +23,8 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||||
setEnabled(true);
|
setEnabled(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
switchChecked={enabled}>
|
switchChecked={enabled}
|
||||||
|
teamId={eventType.team?.id || undefined}>
|
||||||
<TextField
|
<TextField
|
||||||
name="Tracking ID"
|
name="Tracking ID"
|
||||||
value={trackingId}
|
value={trackingId}
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -1,31 +1,33 @@
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||||
|
import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled";
|
||||||
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||||
import { Sunrise, Sunset } from "@calcom/ui/components/icon";
|
import { Sunrise, Sunset } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import type { appDataSchema } from "../zod";
|
import type { appDataSchema } from "../zod";
|
||||||
|
|
||||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ eventType, app }) {
|
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ eventType, app }) {
|
||||||
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>();
|
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>();
|
||||||
const isSunrise = getAppData("isSunrise");
|
const isSunrise = getAppData("isSunrise");
|
||||||
const [enabled, setEnabled] = useState(getAppData("enabled"));
|
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppCard
|
<AppCard
|
||||||
setAppData={setAppData}
|
setAppData={setAppData}
|
||||||
app={app}
|
app={app}
|
||||||
|
disableSwitch={disabled}
|
||||||
|
LockedIcon={LockedIcon}
|
||||||
switchOnClick={(e) => {
|
switchOnClick={(e) => {
|
||||||
if (!e) {
|
if (!e) {
|
||||||
setEnabled(false);
|
updateEnabled(false);
|
||||||
setAppData("isSunrise", false);
|
setAppData("isSunrise", false);
|
||||||
} else {
|
} else {
|
||||||
setEnabled(true);
|
updateEnabled(true);
|
||||||
setAppData("isSunrise", true);
|
setAppData("isSunrise", true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
switchChecked={enabled}>
|
switchChecked={enabled}
|
||||||
|
teamId={eventType.team?.id || undefined}>
|
||||||
<div className="mt-2 text-sm">
|
<div className="mt-2 text-sm">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<span className="ltr:mr-2 rtl:ml-2">{isSunrise ? <Sunrise /> : <Sunset />}</span>I am an AppCard for
|
<span className="ltr:mr-2 rtl:ml-2">{isSunrise ? <Sunrise /> : <Sunset />}</span>I am an AppCard for
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -13,8 +13,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
newTab: true,
|
newTab: true,
|
||||||
url: "https://example.com/link",
|
url: "https://example.com/link",
|
||||||
},
|
},
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -12,8 +12,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
redirect: {
|
redirect: {
|
||||||
url: "/apps/typeform/how-to-use",
|
url: "/apps/typeform/how-to-use",
|
||||||
},
|
},
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -19,9 +19,11 @@ const installApps = async (page: Page, users: Fixtures["users"]) => {
|
||||||
await user.login();
|
await user.login();
|
||||||
await page.goto(`/apps/routing-forms`);
|
await page.goto(`/apps/routing-forms`);
|
||||||
await page.click('[data-testid="install-app-button"]');
|
await page.click('[data-testid="install-app-button"]');
|
||||||
|
(await page.waitForSelector('[data-testid="install-app-button-personal"]')).click();
|
||||||
await page.waitForURL((url) => url.pathname === `/apps/routing-forms/forms`);
|
await page.waitForURL((url) => url.pathname === `/apps/routing-forms/forms`);
|
||||||
await page.goto(`/apps/typeform`);
|
await page.goto(`/apps/typeform`);
|
||||||
await page.click('[data-testid="install-app-button"]');
|
await page.click('[data-testid="install-app-button"]');
|
||||||
|
(await page.waitForSelector('[data-testid="install-app-button-personal"]')).click();
|
||||||
await page.waitForURL((url) => url.pathname === `/apps/typeform/how-to-use`);
|
await page.waitForURL((url) => url.pathname === `/apps/typeform/how-to-use`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,26 @@
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
|
|
||||||
import type { _EventTypeModel } from "@calcom/prisma/zod";
|
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import type { ButtonProps } from "@calcom/ui";
|
import type { ButtonProps } from "@calcom/ui";
|
||||||
|
|
||||||
export type IntegrationOAuthCallbackState = {
|
export type IntegrationOAuthCallbackState = {
|
||||||
returnTo: string;
|
returnTo: string;
|
||||||
installGoogleVideo?: boolean;
|
installGoogleVideo?: boolean;
|
||||||
|
teamId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CredentialOwner = {
|
||||||
|
name: string | null;
|
||||||
|
avatar?: string | null;
|
||||||
|
teamId?: number;
|
||||||
|
credentialId?: number;
|
||||||
|
readOnly?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EventTypeAppCardApp = RouterOutputs["viewer"]["integrations"]["items"][number] & {
|
||||||
|
credentialOwner?: CredentialOwner;
|
||||||
|
credentialIds?: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type AppScript = { attrs?: Record<string, string> } & { src?: string; content?: string };
|
type AppScript = { attrs?: Record<string, string> } & { src?: string; content?: string };
|
||||||
|
@ -29,11 +42,13 @@ export interface InstallAppButtonProps {
|
||||||
export type EventTypeAppCardComponentProps = {
|
export type EventTypeAppCardComponentProps = {
|
||||||
// Limit what data should be accessible to apps
|
// Limit what data should be accessible to apps
|
||||||
eventType: Pick<
|
eventType: Pick<
|
||||||
z.infer<typeof _EventTypeModel>,
|
z.infer<typeof EventTypeModel>,
|
||||||
"id" | "title" | "description" | "teamId" | "length" | "recurringEvent" | "seatsPerTimeSlot"
|
"id" | "title" | "description" | "teamId" | "length" | "recurringEvent" | "seatsPerTimeSlot" | "team"
|
||||||
> & {
|
> & {
|
||||||
URL: string;
|
URL: string;
|
||||||
};
|
};
|
||||||
app: RouterOutputs["viewer"]["apps"][number];
|
app: EventTypeAppCardApp;
|
||||||
|
disabled?: boolean;
|
||||||
|
LockedIcon?: JSX.Element | false;
|
||||||
};
|
};
|
||||||
export type EventTypeAppCardComponent = React.FC<EventTypeAppCardComponentProps>;
|
export type EventTypeAppCardComponent = React.FC<EventTypeAppCardComponentProps>;
|
||||||
|
|
|
@ -13,8 +13,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
newTab: true,
|
newTab: true,
|
||||||
url: "https://cal.com/blog/cal-plus-vimcal",
|
url: "https://cal.com/blog/cal-plus-vimcal",
|
||||||
},
|
},
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
|
||||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||||
import config from "../config.json";
|
import config from "../config.json";
|
||||||
import { getWebexAppKeys } from "../lib/getWebexAppKeys";
|
import { getWebexAppKeys } from "../lib/getWebexAppKeys";
|
||||||
|
@ -80,20 +81,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
await prisma.credential.deleteMany({ where: { id: { in: credentialIdsToDelete }, userId } });
|
await prisma.credential.deleteMany({ where: { id: { in: credentialIdsToDelete }, userId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.user.update({
|
createOAuthAppCredential({ appId: config.slug, type: config.type }, responseBody, req);
|
||||||
where: {
|
|
||||||
id: req.session?.user.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
credentials: {
|
|
||||||
create: {
|
|
||||||
type: config.type,
|
|
||||||
key: responseBody,
|
|
||||||
appId: config.slug,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
res.redirect(getInstalledAppPath({ variant: config.variant, slug: config.slug }));
|
res.redirect(getInstalledAppPath({ variant: config.variant, slug: config.slug }));
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
slug: appConfig.slug,
|
slug: appConfig.slug,
|
||||||
supportsMultipleInstalls: false,
|
supportsMultipleInstalls: false,
|
||||||
handlerType: "add",
|
handlerType: "add",
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -23,7 +23,7 @@ const WipeMyCalActionButton = (props: IWipeMyCalActionButtonProps) => {
|
||||||
}
|
}
|
||||||
const wipeMyCalCredentials = data?.items.find((item: { type: string }) => item.type === "wipemycal_other");
|
const wipeMyCalCredentials = data?.items.find((item: { type: string }) => item.type === "wipemycal_other");
|
||||||
|
|
||||||
const [credentialId] = wipeMyCalCredentials?.credentialIds || [false];
|
const [credentialId] = wipeMyCalCredentials?.userCredentialIds || [false];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -13,8 +13,8 @@ const handler: AppDeclarativeHandler = {
|
||||||
newTab: true,
|
newTab: true,
|
||||||
url: "https://wordpress.org/plugins/cal-com/",
|
url: "https://wordpress.org/plugins/cal-com/",
|
||||||
},
|
},
|
||||||
createCredential: ({ appType, user, slug }) =>
|
createCredential: ({ appType, user, slug, teamId }) =>
|
||||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
@ -22,10 +22,10 @@ export default function ZapierSetup(props: IZapierSetupProps) {
|
||||||
const oldApiKey = trpc.viewer.apiKeys.findKeyOfType.useQuery({ appId: ZAPIER });
|
const oldApiKey = trpc.viewer.apiKeys.findKeyOfType.useQuery({ appId: ZAPIER });
|
||||||
|
|
||||||
const deleteApiKey = trpc.viewer.apiKeys.delete.useMutation();
|
const deleteApiKey = trpc.viewer.apiKeys.delete.useMutation();
|
||||||
const zapierCredentials: { credentialIds: number[] } | undefined = integrations.data?.items.find(
|
const zapierCredentials: { userCredentialIds: number[] } | undefined = integrations.data?.items.find(
|
||||||
(item: { type: string }) => item.type === "zapier_automation"
|
(item: { type: string }) => item.type === "zapier_automation"
|
||||||
);
|
);
|
||||||
const [credentialId] = zapierCredentials?.credentialIds || [false];
|
const [credentialId] = zapierCredentials?.userCredentialIds || [false];
|
||||||
const showContent = integrations.data && integrations.isSuccess && credentialId;
|
const showContent = integrations.data && integrations.isSuccess && credentialId;
|
||||||
const isCalDev = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.dev";
|
const isCalDev = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.dev";
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ import qs from "qs";
|
||||||
|
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
|
|
||||||
|
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
|
||||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||||
|
@ -52,14 +52,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
tokenInfo.data.expiryDate = Math.round(Date.now() + tokenInfo.data.expires_in);
|
tokenInfo.data.expiryDate = Math.round(Date.now() + tokenInfo.data.expires_in);
|
||||||
tokenInfo.data.accountServer = accountsServer;
|
tokenInfo.data.accountServer = accountsServer;
|
||||||
|
|
||||||
await prisma.credential.create({
|
createOAuthAppCredential({ appId: appConfig.slug, type: appConfig.type }, tokenInfo.data, req);
|
||||||
data: {
|
|
||||||
type: appConfig.type,
|
|
||||||
key: tokenInfo.data,
|
|
||||||
userId: req.session.user.id,
|
|
||||||
appId: appConfig.slug,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const state = decodeOAuthState(req);
|
const state = decodeOAuthState(req);
|
||||||
res.redirect(
|
res.redirect(
|
||||||
|
|
|
@ -4,8 +4,8 @@ import qs from "qs";
|
||||||
|
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
|
|
||||||
|
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
|
||||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||||
|
@ -51,15 +51,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
zohoCrmTokenInfo.data.expiryDate = Math.round(Date.now() + 60 * 60);
|
zohoCrmTokenInfo.data.expiryDate = Math.round(Date.now() + 60 * 60);
|
||||||
zohoCrmTokenInfo.data.accountServer = req.query["accounts-server"];
|
zohoCrmTokenInfo.data.accountServer = req.query["accounts-server"];
|
||||||
|
|
||||||
await prisma.credential.create({
|
createOAuthAppCredential(
|
||||||
data: {
|
{ appId: "zohocrm", type: "zohocrm_other_calendar" },
|
||||||
type: "zohocrm_other_calendar",
|
zohoCrmTokenInfo.data as any,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
req
|
||||||
key: zohoCrmTokenInfo.data as any,
|
);
|
||||||
userId: req.session.user.id,
|
|
||||||
appId: "zohocrm",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const state = decodeOAuthState(req);
|
const state = decodeOAuthState(req);
|
||||||
res.redirect(
|
res.redirect(
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import { encodeOAuthState } from "../../_utils/encodeOAuthState";
|
||||||
import { getZoomAppKeys } from "../lib";
|
import { getZoomAppKeys } from "../lib";
|
||||||
|
|
||||||
async function handler(req: NextApiRequest) {
|
async function handler(req: NextApiRequest) {
|
||||||
|
@ -19,11 +20,13 @@ async function handler(req: NextApiRequest) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const { client_id } = await getZoomAppKeys();
|
const { client_id } = await getZoomAppKeys();
|
||||||
|
const state = encodeOAuthState(req);
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
response_type: "code",
|
response_type: "code",
|
||||||
client_id,
|
client_id,
|
||||||
redirect_uri: WEBAPP_URL + "/api/integrations/zoomvideo/callback",
|
redirect_uri: WEBAPP_URL + "/api/integrations/zoomvideo/callback",
|
||||||
|
state,
|
||||||
};
|
};
|
||||||
const query = stringify(params);
|
const query = stringify(params);
|
||||||
const url = `https://zoom.us/oauth/authorize?${query}`;
|
const url = `https://zoom.us/oauth/authorize?${query}`;
|
||||||
|
|
|
@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
|
||||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||||
import { getZoomAppKeys } from "../lib";
|
import { getZoomAppKeys } from "../lib";
|
||||||
|
|
||||||
|
@ -69,20 +70,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
await prisma.credential.deleteMany({ where: { id: { in: credentialIdsToDelete }, userId } });
|
await prisma.credential.deleteMany({ where: { id: { in: credentialIdsToDelete }, userId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.user.update({
|
createOAuthAppCredential({ appId: "zoom", type: "zoom_video" }, responseBody, req);
|
||||||
where: {
|
|
||||||
id: req.session?.user.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
credentials: {
|
|
||||||
create: {
|
|
||||||
type: "zoom_video",
|
|
||||||
key: responseBody,
|
|
||||||
appId: "zoom",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
res.redirect(getInstalledAppPath({ variant: "conferencing", slug: "zoom" }));
|
res.redirect(getInstalledAppPath({ variant: "conferencing", slug: "zoom" }));
|
||||||
}
|
}
|
||||||
|
|
|
@ -404,7 +404,7 @@ export default class EventManager {
|
||||||
* @param event
|
* @param event
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private createVideoEvent(event: CalendarEvent) {
|
private async createVideoEvent(event: CalendarEvent) {
|
||||||
const credential = this.getVideoCredential(event);
|
const credential = this.getVideoCredential(event);
|
||||||
|
|
||||||
if (credential) {
|
if (credential) {
|
||||||
|
@ -538,7 +538,7 @@ export default class EventManager {
|
||||||
* @param booking
|
* @param booking
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private updateVideoEvent(event: CalendarEvent, booking: PartialBooking) {
|
private async updateVideoEvent(event: CalendarEvent, booking: PartialBooking) {
|
||||||
const credential = this.getVideoCredential(event);
|
const credential = this.getVideoCredential(event);
|
||||||
|
|
||||||
if (credential) {
|
if (credential) {
|
||||||
|
|
|
@ -6,12 +6,14 @@ interface DisconnectIntegrationModalProps {
|
||||||
credentialId: number | null;
|
credentialId: number | null;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
handleModelClose: () => void;
|
handleModelClose: () => void;
|
||||||
|
teamId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DisconnectIntegrationModal({
|
export default function DisconnectIntegrationModal({
|
||||||
credentialId,
|
credentialId,
|
||||||
isOpen,
|
isOpen,
|
||||||
handleModelClose,
|
handleModelClose,
|
||||||
|
teamId,
|
||||||
}: DisconnectIntegrationModalProps) {
|
}: DisconnectIntegrationModalProps) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
|
@ -31,17 +33,17 @@ export default function DisconnectIntegrationModal({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={handleModelClose}>
|
<Dialog open={isOpen} onOpenChange={handleModelClose}>
|
||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
variety="danger"
|
variety="danger"
|
||||||
title={t("remove_app")}
|
title={t("remove_app")}
|
||||||
confirmBtnText={t("yes_remove_app")}
|
confirmBtnText={t("yes_remove_app")}
|
||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
if (credentialId) {
|
if (credentialId) {
|
||||||
mutation.mutate({ id: credentialId });
|
mutation.mutate({ id: credentialId, teamId });
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<p className="mt-5">{t("are_you_sure_you_want_to_remove_this_app")}</p>
|
<p className="mt-5">{t("are_you_sure_you_want_to_remove_this_app")}</p>
|
||||||
</ConfirmationDialogContent>
|
</ConfirmationDialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user